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

View File

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

View File

@ -13,6 +13,9 @@
# under the License. # under the License.
import six
def build_v3_resource_relation(resource_name): def build_v3_resource_relation(resource_name):
return ('http://docs.openstack.org/api/openstack-identity/3/rel/%s' % return ('http://docs.openstack.org/api/openstack-identity/3/rel/%s' %
resource_name) resource_name)
@ -49,3 +52,13 @@ class Parameters(object):
ROLE_ID = build_v3_parameter_relation('role_id') ROLE_ID = build_v3_parameter_relation('role_id')
SERVICE_ID = build_v3_parameter_relation('service_id') SERVICE_ID = build_v3_parameter_relation('service_id')
USER_ID = build_v3_parameter_relation('user_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 # License for the specific language governing permissions and limitations
# under the License. # under the License.
import webob
from keystone.common import extension from keystone.common import extension
from keystone.common import json_home
from keystone.common import wsgi from keystone.common import wsgi
from keystone import exception from keystone import exception
from keystone.openstack.common import jsonutils
from keystone.openstack.common import log from keystone.openstack.common import log
@ -25,6 +29,31 @@ MEDIA_TYPE_XML = 'application/vnd.openstack.identity-%s+xml'
_VERSIONS = [] _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): class Extensions(wsgi.Application):
"""Base extensions controller to be extended by public and admin API's.""" """Base extensions controller to be extended by public and admin API's."""
@ -144,6 +173,14 @@ class Version(wsgi.Application):
return versions return versions
def get_versions(self, context): 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) versions = self._get_versions_list(context)
return wsgi.render_response(status=(300, 'Multiple Choices'), body={ return wsgi.render_response(status=(300, 'Multiple Choices'), body={
'versions': { 'versions': {

View File

@ -15,6 +15,7 @@
import functools import functools
import sys import sys
from paste import deploy
import routes import routes
from keystone import assignment from keystone import assignment
@ -36,6 +37,14 @@ CONF = config.CONF
LOG = log.getLogger(__name__) 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): def fail_gracefully(f):
"""Logs exceptions and aborts.""" """Logs exceptions and aborts."""
@functools.wraps(f) @functools.wraps(f)

View File

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

View File

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

View File

@ -13,6 +13,8 @@
# under the License. # under the License.
import copy
from testtools import matchers from testtools import matchers
from keystone.common import json_home 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/' 'http://docs.openstack.org/api/openstack-identity/3/ext/%s/%s/'
'param/%s' % (extension_name, extension_version, parameter_name)) 'param/%s' % (extension_name, extension_version, parameter_name))
self.assertThat(relation, matchers.Equals(exp_relation)) 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))