Add config option to override url for links

The versions url returns the wrong data when Ironic API is behind a
proxy. This adds a new config option called "public_endpoint" so it can
be set properly.

Closes-Bug: #1384379
Change-Id: I6d1b59db3ce09aba7bca5a71edcf97eb79f0b17b
This commit is contained in:
Lucas Alvares Gomes
2015-09-15 13:58:30 +01:00
parent f38cfb1ee2
commit eec96136be
17 changed files with 142 additions and 21 deletions

View File

@@ -447,6 +447,14 @@
# from a collection resource. (integer value)
#max_limit=1000
# Public URL to use when building the links to the API
# resources (for example, "https://ironic.rocks:6384"). If
# None the links will be built using the request's host URL.
# If the API is operating behind a proxy, you will want to
# change this to represent the proxy's URL. Defaults to None.
# (string value)
#public_endpoint=<None>
[cisco_ucs]

View File

@@ -29,6 +29,14 @@ API_SERVICE_OPTS = [
default=1000,
help=_('The maximum number of items returned in a single '
'response from a collection resource.')),
cfg.StrOpt('public_endpoint',
default=None,
help=_("Public URL to use when building the links to the API "
"resources (for example, \"https://ironic.rocks:6384\")."
" If None the links will be built using the request's "
"host URL. If the API is operating behind a proxy, you "
"will want to change this to represent the proxy's URL. "
"Defaults to None.")),
]
CONF = cfg.CONF

View File

@@ -54,7 +54,8 @@ def setup_app(pecan_config=None, extra_hooks=None):
hooks.DBHook(),
hooks.ContextHook(pecan_config.app.acl_public_routes),
hooks.RPCHook(),
hooks.NoExceptionTracebackHook()]
hooks.NoExceptionTracebackHook(),
hooks.PublicUrlHook()]
if extra_hooks:
app_hooks.extend(extra_hooks)

View File

@@ -21,7 +21,7 @@ from ironic.api.controllers import base
def build_url(resource, resource_args, bookmark=False, base_url=None):
if base_url is None:
base_url = pecan.request.host_url
base_url = pecan.request.public_url
template = '%(url)s/%(res)s' if bookmark else '%(url)s/v1/%(res)s'
# FIXME(lucasagomes): I'm getting a 404 when doing a GET on

View File

@@ -37,7 +37,7 @@ class Version(base.APIBase):
def convert(id):
version = Version()
version.id = id
version.links = [link.Link.make_link('self', pecan.request.host_url,
version.links = [link.Link.make_link('self', pecan.request.public_url,
id, '', bookmark=True)]
return version

View File

@@ -86,7 +86,7 @@ class V1(base.APIBase):
def convert():
v1 = V1()
v1.id = "v1"
v1.links = [link.Link.make_link('self', pecan.request.host_url,
v1.links = [link.Link.make_link('self', pecan.request.public_url,
'v1', '', bookmark=True),
link.Link.make_link('describedby',
'http://docs.openstack.org',
@@ -96,31 +96,31 @@ class V1(base.APIBase):
]
v1.media_types = [MediaType('application/json',
'application/vnd.openstack.ironic.v1+json')]
v1.chassis = [link.Link.make_link('self', pecan.request.host_url,
v1.chassis = [link.Link.make_link('self', pecan.request.public_url,
'chassis', ''),
link.Link.make_link('bookmark',
pecan.request.host_url,
pecan.request.public_url,
'chassis', '',
bookmark=True)
]
v1.nodes = [link.Link.make_link('self', pecan.request.host_url,
v1.nodes = [link.Link.make_link('self', pecan.request.public_url,
'nodes', ''),
link.Link.make_link('bookmark',
pecan.request.host_url,
pecan.request.public_url,
'nodes', '',
bookmark=True)
]
v1.ports = [link.Link.make_link('self', pecan.request.host_url,
v1.ports = [link.Link.make_link('self', pecan.request.public_url,
'ports', ''),
link.Link.make_link('bookmark',
pecan.request.host_url,
pecan.request.public_url,
'ports', '',
bookmark=True)
]
v1.drivers = [link.Link.make_link('self', pecan.request.host_url,
v1.drivers = [link.Link.make_link('self', pecan.request.public_url,
'drivers', ''),
link.Link.make_link('bookmark',
pecan.request.host_url,
pecan.request.public_url,
'drivers', '',
bookmark=True)
]

View File

@@ -108,7 +108,7 @@ class Chassis(base.APIBase):
if fields is not None:
api_utils.check_for_invalid_fields(fields, chassis.as_dict())
return cls._convert_with_links(chassis, pecan.request.host_url,
return cls._convert_with_links(chassis, pecan.request.public_url,
fields)
@classmethod

View File

@@ -44,5 +44,5 @@ class Collection(base.APIBase):
'args': q_args, 'limit': limit,
'marker': self.collection[-1].uuid}
return link.Link.make_link('next', pecan.request.host_url,
return link.Link.make_link('next', pecan.request.public_url,
resource_url, next_args).href

View File

@@ -77,10 +77,10 @@ class Driver(base.APIBase):
driver.hosts = hosts
driver.links = [
link.Link.make_link('self',
pecan.request.host_url,
pecan.request.public_url,
'drivers', name),
link.Link.make_link('bookmark',
pecan.request.host_url,
pecan.request.public_url,
'drivers', name,
bookmark=True)
]

View File

@@ -684,7 +684,7 @@ class Node(base.APIBase):
assert_juno_provision_state_name(node)
hide_fields_in_newer_versions(node)
show_password = pecan.request.context.show_password
return cls._convert_with_links(node, pecan.request.host_url,
return cls._convert_with_links(node, pecan.request.public_url,
fields=fields,
show_password=show_password)

View File

@@ -137,7 +137,7 @@ class Port(base.APIBase):
if fields is not None:
api_utils.check_for_invalid_fields(fields, port.as_dict())
return cls._convert_with_links(port, pecan.request.host_url,
return cls._convert_with_links(port, pecan.request.public_url,
fields=fields)
@classmethod

View File

@@ -153,3 +153,16 @@ class NoExceptionTracebackHook(hooks.PecanHook):
# Replace the whole json. Cannot change original one beacause it's
# generated on the fly.
state.response.json = json_body
class PublicUrlHook(hooks.PecanHook):
"""Attach the right public_url to the request.
Attach the right public_url to the request so resources can create
links even when the API service is behind a proxy or SSL terminator.
"""
def before(self, state):
state.request.public_url = (cfg.CONF.api.public_endpoint or
state.request.host_url)

View File

@@ -36,6 +36,7 @@ class FakeRequest(object):
self.context = context
self.environ = environ or {}
self.version = (1, 0)
self.host_url = 'http://127.0.0.1:6385'
class FakeRequestState(object):
@@ -281,3 +282,22 @@ class TestTrustedCallHookCompatJuno(TestTrustedCallHook):
def test_trusted_call_hook_public_api(self):
self.skipTest('no public_api trusted call policy in juno')
class TestPublicUrlHook(base.FunctionalTest):
def test_before_host_url(self):
headers = fake_headers()
reqstate = FakeRequestState(headers=headers)
trusted_call_hook = hooks.PublicUrlHook()
trusted_call_hook.before(reqstate)
self.assertEqual(reqstate.request.host_url,
reqstate.request.public_url)
def test_before_public_endpoint(self):
cfg.CONF.set_override('public_endpoint', 'http://foo', 'api')
headers = fake_headers()
reqstate = FakeRequestState(headers=headers)
trusted_call_hook = hooks.PublicUrlHook()
trusted_call_hook.before(reqstate)
self.assertEqual('http://foo', reqstate.request.public_url)

View File

@@ -132,7 +132,8 @@ class TestListChassis(test_api_base.FunctionalTest):
uuids = [n['uuid'] for n in data['chassis']]
six.assertCountEqual(self, ch_list, uuids)
def test_links(self):
def _test_links(self, public_url=None):
cfg.CONF.set_override('public_endpoint', public_url, 'api')
uuid = uuidutils.generate_uuid()
obj_utils.create_test_chassis(self.context, uuid=uuid)
data = self.get_json('/chassis/%s' % uuid)
@@ -143,6 +144,20 @@ class TestListChassis(test_api_base.FunctionalTest):
bookmark = l['rel'] == 'bookmark'
self.assertTrue(self.validate_link(l['href'], bookmark=bookmark))
if public_url is not None:
expected = [{'href': '%s/v1/chassis/%s' % (public_url, uuid),
'rel': 'self'},
{'href': '%s/chassis/%s' % (public_url, uuid),
'rel': 'bookmark'}]
for i in expected:
self.assertIn(i, data['links'])
def test_links(self):
self._test_links()
def test_links_public_url(self):
self._test_links(public_url='http://foo')
def test_collection_links(self):
for id in range(5):
obj_utils.create_test_chassis(self.context,

View File

@@ -16,6 +16,7 @@
import json
import mock
from oslo_config import cfg
from six.moves import http_client
from testtools.matchers import HasLength
@@ -75,6 +76,31 @@ class TestListDrivers(base.FunctionalTest):
response = self.get_json('/drivers/%s' % self.d1, expect_errors=True)
self.assertEqual(http_client.NOT_FOUND, response.status_int)
def _test_links(self, public_url=None):
cfg.CONF.set_override('public_endpoint', public_url, 'api')
self.register_fake_conductors()
data = self.get_json('/drivers/%s' % self.d1)
self.assertIn('links', data.keys())
self.assertEqual(2, len(data['links']))
self.assertIn(self.d1, data['links'][0]['href'])
for l in data['links']:
bookmark = l['rel'] == 'bookmark'
self.assertTrue(self.validate_link(l['href'], bookmark=bookmark))
if public_url is not None:
expected = [{'href': '%s/v1/drivers/%s' % (public_url, self.d1),
'rel': 'self'},
{'href': '%s/drivers/%s' % (public_url, self.d1),
'rel': 'bookmark'}]
for i in expected:
self.assertIn(i, data['links'])
def test_links(self):
self._test_links()
def test_links_public_url(self):
self._test_links(public_url='http://foo')
@mock.patch.object(rpcapi.ConductorAPI, 'driver_vendor_passthru')
def test_driver_vendor_passthru_sync(self, mocked_driver_vendor_passthru):
self.register_fake_conductors()

View File

@@ -322,7 +322,8 @@ class TestListNodes(test_api_base.FunctionalTest):
self.assertEqual(len(nodes), len(data['nodes']))
self.assertEqual(sorted(node_names), sorted(names))
def test_links(self):
def _test_links(self, public_url=None):
cfg.CONF.set_override('public_endpoint', public_url, 'api')
uuid = uuidutils.generate_uuid()
obj_utils.create_test_node(self.context, uuid=uuid)
data = self.get_json('/nodes/%s' % uuid)
@@ -333,6 +334,20 @@ class TestListNodes(test_api_base.FunctionalTest):
bookmark = l['rel'] == 'bookmark'
self.assertTrue(self.validate_link(l['href'], bookmark=bookmark))
if public_url is not None:
expected = [{'href': '%s/v1/nodes/%s' % (public_url, uuid),
'rel': 'self'},
{'href': '%s/nodes/%s' % (public_url, uuid),
'rel': 'bookmark'}]
for i in expected:
self.assertIn(i, data['links'])
def test_links(self):
self._test_links()
def test_links_public_url(self):
self._test_links(public_url='http://foo')
def test_collection_links(self):
nodes = []
for id in range(5):

View File

@@ -161,7 +161,8 @@ class TestListPorts(test_api_base.FunctionalTest):
uuids = [n['uuid'] for n in data['ports']]
six.assertCountEqual(self, ports, uuids)
def test_links(self):
def _test_links(self, public_url=None):
cfg.CONF.set_override('public_endpoint', public_url, 'api')
uuid = uuidutils.generate_uuid()
obj_utils.create_test_port(self.context,
uuid=uuid,
@@ -174,6 +175,20 @@ class TestListPorts(test_api_base.FunctionalTest):
bookmark = l['rel'] == 'bookmark'
self.assertTrue(self.validate_link(l['href'], bookmark=bookmark))
if public_url is not None:
expected = [{'href': '%s/v1/ports/%s' % (public_url, uuid),
'rel': 'self'},
{'href': '%s/ports/%s' % (public_url, uuid),
'rel': 'bookmark'}]
for i in expected:
self.assertIn(i, data['links'])
def test_links(self):
self._test_links()
def test_links_public_url(self):
self._test_links(public_url='http://foo')
def test_collection_links(self):
ports = []
for id_ in range(5):