Stop guessing mime types based on URLs
Currently we have a pecan feature enabled that strips extensions from the end of the URL and treat it like requested content type. E.g. /v1/nodes.json is treated as /v1/nodes with requested content type Application/Json. However, this prevents certain node names: e.g. /v1/nodes/small.1 is treated like /v1/nodes/small with content type of a man page. It does not make any sense for ironic API, as we only support Application/Json content type (and .json suffix). This change disabled this pecan feature. To keep backward compability a new middleware stips the .json prefix and saves a flag in the environment about its presence. API accepting names try to find their resource first without, then with .json suffix. The following endpoints are special-cased to support names with .json: * Node GET, PATCH and DELETE * Ramdisk heartbeat * Port group GET, PATCH and DELETE VIF API is not updated, so VIF IDs still cannot have .json suffix. Change-Id: I789ecfeac9b64a9c4105a20619f7bf5dfc133189 Closes-Bug: #1643995
This commit is contained in:
parent
12d3157a96
commit
cfc167eadf
@ -26,6 +26,7 @@ from ironic.api.controllers import base
|
|||||||
from ironic.api import hooks
|
from ironic.api import hooks
|
||||||
from ironic.api import middleware
|
from ironic.api import middleware
|
||||||
from ironic.api.middleware import auth_token
|
from ironic.api.middleware import auth_token
|
||||||
|
from ironic.api.middleware import json_ext
|
||||||
from ironic.common import exception
|
from ironic.common import exception
|
||||||
from ironic.conf import CONF
|
from ironic.conf import CONF
|
||||||
|
|
||||||
@ -73,6 +74,11 @@ def setup_app(pecan_config=None, extra_hooks=None):
|
|||||||
force_canonical=getattr(pecan_config.app, 'force_canonical', True),
|
force_canonical=getattr(pecan_config.app, 'force_canonical', True),
|
||||||
hooks=app_hooks,
|
hooks=app_hooks,
|
||||||
wrap_app=middleware.ParsableErrorMiddleware,
|
wrap_app=middleware.ParsableErrorMiddleware,
|
||||||
|
# NOTE(dtantsur): enabling this causes weird issues with nodes named
|
||||||
|
# as if they had a known mime extension, e.g. "mynode.1". We do
|
||||||
|
# simulate the same behaviour for .json extensions for backward
|
||||||
|
# compatibility through JsonExtensionMiddleware.
|
||||||
|
guess_content_type_from_ext=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
if CONF.audit.enabled:
|
if CONF.audit.enabled:
|
||||||
@ -106,6 +112,8 @@ def setup_app(pecan_config=None, extra_hooks=None):
|
|||||||
base.Version.string]
|
base.Version.string]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app = json_ext.JsonExtensionMiddleware(app)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
@ -1788,7 +1788,7 @@ class NodesController(rest.RestController):
|
|||||||
api_utils.check_allow_specify_fields(fields)
|
api_utils.check_allow_specify_fields(fields)
|
||||||
api_utils.check_allowed_fields(fields)
|
api_utils.check_allowed_fields(fields)
|
||||||
|
|
||||||
rpc_node = api_utils.get_rpc_node(node_ident)
|
rpc_node = api_utils.get_rpc_node_with_suffix(node_ident)
|
||||||
return Node.convert_with_links(rpc_node, fields=fields)
|
return Node.convert_with_links(rpc_node, fields=fields)
|
||||||
|
|
||||||
@METRICS.timer('NodesController.post')
|
@METRICS.timer('NodesController.post')
|
||||||
@ -1913,7 +1913,7 @@ class NodesController(rest.RestController):
|
|||||||
if r_interface and not api_utils.allow_rescue_interface():
|
if r_interface and not api_utils.allow_rescue_interface():
|
||||||
raise exception.NotAcceptable()
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
rpc_node = api_utils.get_rpc_node(node_ident)
|
rpc_node = api_utils.get_rpc_node_with_suffix(node_ident)
|
||||||
|
|
||||||
remove_inst_uuid_patch = [{'op': 'remove', 'path': '/instance_uuid'}]
|
remove_inst_uuid_patch = [{'op': 'remove', 'path': '/instance_uuid'}]
|
||||||
if rpc_node.maintenance and patch == remove_inst_uuid_patch:
|
if rpc_node.maintenance and patch == remove_inst_uuid_patch:
|
||||||
@ -1988,7 +1988,7 @@ class NodesController(rest.RestController):
|
|||||||
if self.from_chassis:
|
if self.from_chassis:
|
||||||
raise exception.OperationNotPermitted()
|
raise exception.OperationNotPermitted()
|
||||||
|
|
||||||
rpc_node = api_utils.get_rpc_node(node_ident)
|
rpc_node = api_utils.get_rpc_node_with_suffix(node_ident)
|
||||||
chassis_uuid = _get_chassis_uuid(rpc_node)
|
chassis_uuid = _get_chassis_uuid(rpc_node)
|
||||||
notify.emit_start_notification(context, rpc_node, 'delete',
|
notify.emit_start_notification(context, rpc_node, 'delete',
|
||||||
chassis_uuid=chassis_uuid)
|
chassis_uuid=chassis_uuid)
|
||||||
|
@ -422,7 +422,8 @@ class PortgroupsController(pecan.rest.RestController):
|
|||||||
|
|
||||||
api_utils.check_allowed_portgroup_fields(fields)
|
api_utils.check_allowed_portgroup_fields(fields)
|
||||||
|
|
||||||
rpc_portgroup = api_utils.get_rpc_portgroup(portgroup_ident)
|
rpc_portgroup = api_utils.get_rpc_portgroup_with_suffix(
|
||||||
|
portgroup_ident)
|
||||||
return Portgroup.convert_with_links(rpc_portgroup, fields=fields)
|
return Portgroup.convert_with_links(rpc_portgroup, fields=fields)
|
||||||
|
|
||||||
@METRICS.timer('PortgroupsController.post')
|
@METRICS.timer('PortgroupsController.post')
|
||||||
@ -502,7 +503,8 @@ class PortgroupsController(pecan.rest.RestController):
|
|||||||
api_utils.is_path_updated(patch, '/properties'))):
|
api_utils.is_path_updated(patch, '/properties'))):
|
||||||
raise exception.NotAcceptable()
|
raise exception.NotAcceptable()
|
||||||
|
|
||||||
rpc_portgroup = api_utils.get_rpc_portgroup(portgroup_ident)
|
rpc_portgroup = api_utils.get_rpc_portgroup_with_suffix(
|
||||||
|
portgroup_ident)
|
||||||
|
|
||||||
names = api_utils.get_patch_values(patch, '/name')
|
names = api_utils.get_patch_values(patch, '/name')
|
||||||
for name in names:
|
for name in names:
|
||||||
@ -573,7 +575,8 @@ class PortgroupsController(pecan.rest.RestController):
|
|||||||
if self.parent_node_ident:
|
if self.parent_node_ident:
|
||||||
raise exception.OperationNotPermitted()
|
raise exception.OperationNotPermitted()
|
||||||
|
|
||||||
rpc_portgroup = api_utils.get_rpc_portgroup(portgroup_ident)
|
rpc_portgroup = api_utils.get_rpc_portgroup_with_suffix(
|
||||||
|
portgroup_ident)
|
||||||
rpc_node = objects.Node.get_by_id(pecan.request.context,
|
rpc_node = objects.Node.get_by_id(pecan.request.context,
|
||||||
rpc_portgroup.node_id)
|
rpc_portgroup.node_id)
|
||||||
|
|
||||||
|
@ -180,7 +180,7 @@ class HeartbeatController(rest.RestController):
|
|||||||
cdict = pecan.request.context.to_policy_values()
|
cdict = pecan.request.context.to_policy_values()
|
||||||
policy.authorize('baremetal:node:ipa_heartbeat', cdict, cdict)
|
policy.authorize('baremetal:node:ipa_heartbeat', cdict, cdict)
|
||||||
|
|
||||||
rpc_node = api_utils.get_rpc_node(node_ident)
|
rpc_node = api_utils.get_rpc_node_with_suffix(node_ident)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
|
topic = pecan.request.rpcapi.get_topic_for(rpc_node)
|
||||||
|
@ -164,6 +164,20 @@ def allow_node_logical_names():
|
|||||||
return pecan.request.version.minor >= versions.MINOR_5_NODE_NAME
|
return pecan.request.version.minor >= versions.MINOR_5_NODE_NAME
|
||||||
|
|
||||||
|
|
||||||
|
def _get_with_suffix(get_func, ident, exc_class):
|
||||||
|
"""Helper to get a resource taking into account API .json suffix."""
|
||||||
|
try:
|
||||||
|
return get_func(ident)
|
||||||
|
except exc_class:
|
||||||
|
if not pecan.request.environ['HAS_JSON_SUFFIX']:
|
||||||
|
raise
|
||||||
|
|
||||||
|
# NOTE(dtantsur): strip .json prefix to maintain compatibility
|
||||||
|
# with the guess_content_type_from_ext feature. Try to return it
|
||||||
|
# back if the resulting resource was not found.
|
||||||
|
return get_func(ident + '.json')
|
||||||
|
|
||||||
|
|
||||||
def get_rpc_node(node_ident):
|
def get_rpc_node(node_ident):
|
||||||
"""Get the RPC node from the node uuid or logical name.
|
"""Get the RPC node from the node uuid or logical name.
|
||||||
|
|
||||||
@ -188,6 +202,21 @@ def get_rpc_node(node_ident):
|
|||||||
raise exception.NodeNotFound(node=node_ident)
|
raise exception.NodeNotFound(node=node_ident)
|
||||||
|
|
||||||
|
|
||||||
|
def get_rpc_node_with_suffix(node_ident):
|
||||||
|
"""Get the RPC node from the node uuid or logical name.
|
||||||
|
|
||||||
|
If HAS_JSON_SUFFIX flag is set in the pecan environment, try also looking
|
||||||
|
for node_ident with '.json' suffix. Otherwise identical to get_rpc_node.
|
||||||
|
|
||||||
|
:param node_ident: the UUID or logical name of a node.
|
||||||
|
|
||||||
|
:returns: The RPC Node.
|
||||||
|
:raises: InvalidUuidOrName if the name or uuid provided is not valid.
|
||||||
|
:raises: NodeNotFound if the node is not found.
|
||||||
|
"""
|
||||||
|
return _get_with_suffix(get_rpc_node, node_ident, exception.NodeNotFound)
|
||||||
|
|
||||||
|
|
||||||
def get_rpc_portgroup(portgroup_ident):
|
def get_rpc_portgroup(portgroup_ident):
|
||||||
"""Get the RPC portgroup from the portgroup UUID or logical name.
|
"""Get the RPC portgroup from the portgroup UUID or logical name.
|
||||||
|
|
||||||
@ -210,6 +239,23 @@ def get_rpc_portgroup(portgroup_ident):
|
|||||||
raise exception.InvalidUuidOrName(name=portgroup_ident)
|
raise exception.InvalidUuidOrName(name=portgroup_ident)
|
||||||
|
|
||||||
|
|
||||||
|
def get_rpc_portgroup_with_suffix(portgroup_ident):
|
||||||
|
"""Get the RPC portgroup from the portgroup UUID or logical name.
|
||||||
|
|
||||||
|
If HAS_JSON_SUFFIX flag is set in the pecan environment, try also looking
|
||||||
|
for portgroup_ident with '.json' suffix. Otherwise identical
|
||||||
|
to get_rpc_portgroup.
|
||||||
|
|
||||||
|
:param portgroup_ident: the UUID or logical name of a portgroup.
|
||||||
|
|
||||||
|
:returns: The RPC portgroup.
|
||||||
|
:raises: InvalidUuidOrName if the name or uuid provided is not valid.
|
||||||
|
:raises: PortgroupNotFound if the portgroup is not found.
|
||||||
|
"""
|
||||||
|
return _get_with_suffix(get_rpc_portgroup, portgroup_ident,
|
||||||
|
exception.PortgroupNotFound)
|
||||||
|
|
||||||
|
|
||||||
def is_valid_node_name(name):
|
def is_valid_node_name(name):
|
||||||
"""Determine if the provided name is a valid node name.
|
"""Determine if the provided name is a valid node name.
|
||||||
|
|
||||||
|
@ -13,11 +13,14 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from ironic.api.middleware import auth_token
|
from ironic.api.middleware import auth_token
|
||||||
|
from ironic.api.middleware import json_ext
|
||||||
from ironic.api.middleware import parsable_error
|
from ironic.api.middleware import parsable_error
|
||||||
|
|
||||||
|
|
||||||
ParsableErrorMiddleware = parsable_error.ParsableErrorMiddleware
|
ParsableErrorMiddleware = parsable_error.ParsableErrorMiddleware
|
||||||
AuthTokenMiddleware = auth_token.AuthTokenMiddleware
|
AuthTokenMiddleware = auth_token.AuthTokenMiddleware
|
||||||
|
JsonExtensionMiddleware = json_ext.JsonExtensionMiddleware
|
||||||
|
|
||||||
__all__ = ('ParsableErrorMiddleware',
|
__all__ = ('ParsableErrorMiddleware',
|
||||||
'AuthTokenMiddleware')
|
'AuthTokenMiddleware',
|
||||||
|
'JsonExtensionMiddleware')
|
||||||
|
43
ironic/api/middleware/json_ext.py
Normal file
43
ironic/api/middleware/json_ext.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from ironic.common import utils
|
||||||
|
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class JsonExtensionMiddleware(object):
|
||||||
|
"""Simplified processing of .json extension.
|
||||||
|
|
||||||
|
Previously Ironic API used the "guess_content_type_from_ext" feature.
|
||||||
|
It was never needed, as we never allowed non-JSON content types anyway.
|
||||||
|
Now that it is removed, this middleware strips .json extension for
|
||||||
|
backward compatibility.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
def __call__(self, env, start_response):
|
||||||
|
path = utils.safe_rstrip(env.get('PATH_INFO'), '/')
|
||||||
|
if path and path.endswith('.json'):
|
||||||
|
LOG.debug('Stripping .json prefix from %s for compatibility '
|
||||||
|
'with pecan', path)
|
||||||
|
env['PATH_INFO'] = path[:-5]
|
||||||
|
env['HAS_JSON_SUFFIX'] = True
|
||||||
|
else:
|
||||||
|
env['HAS_JSON_SUFFIX'] = False
|
||||||
|
|
||||||
|
return self.app(env, start_response)
|
@ -157,6 +157,45 @@ class TestListNodes(test_api_base.BaseApiTest):
|
|||||||
# never expose the chassis_id
|
# never expose the chassis_id
|
||||||
self.assertNotIn('chassis_id', data)
|
self.assertNotIn('chassis_id', data)
|
||||||
|
|
||||||
|
def test_get_one_with_json(self):
|
||||||
|
# Test backward compatibility with guess_content_type_from_ext
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
chassis_id=self.chassis.id)
|
||||||
|
data = self.get_json(
|
||||||
|
'/nodes/%s.json' % node.uuid,
|
||||||
|
headers={api_base.Version.string: str(api_v1.max_version())})
|
||||||
|
self.assertEqual(node.uuid, data['uuid'])
|
||||||
|
|
||||||
|
def test_get_one_with_json_in_name(self):
|
||||||
|
# Test that it is possible to name a node ending with .json
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
name='node.json',
|
||||||
|
chassis_id=self.chassis.id)
|
||||||
|
data = self.get_json(
|
||||||
|
'/nodes/%s' % node.name,
|
||||||
|
headers={api_base.Version.string: str(api_v1.max_version())})
|
||||||
|
self.assertEqual(node.uuid, data['uuid'])
|
||||||
|
|
||||||
|
def test_get_one_with_suffix(self):
|
||||||
|
# This tests that we don't mess with mime-like suffixes
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
name='test.1',
|
||||||
|
chassis_id=self.chassis.id)
|
||||||
|
data = self.get_json(
|
||||||
|
'/nodes/%s' % node.name,
|
||||||
|
headers={api_base.Version.string: str(api_v1.max_version())})
|
||||||
|
self.assertEqual(node.uuid, data['uuid'])
|
||||||
|
|
||||||
|
def test_get_one_with_double_json(self):
|
||||||
|
# Check that .json is only stripped once
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
name='node.json',
|
||||||
|
chassis_id=self.chassis.id)
|
||||||
|
data = self.get_json(
|
||||||
|
'/nodes/%s.json' % node.name,
|
||||||
|
headers={api_base.Version.string: str(api_v1.max_version())})
|
||||||
|
self.assertEqual(node.uuid, data['uuid'])
|
||||||
|
|
||||||
def test_node_states_field_hidden_in_lower_version(self):
|
def test_node_states_field_hidden_in_lower_version(self):
|
||||||
node = obj_utils.create_test_node(self.context,
|
node = obj_utils.create_test_node(self.context,
|
||||||
chassis_id=self.chassis.id)
|
chassis_id=self.chassis.id)
|
||||||
@ -1381,7 +1420,7 @@ class TestPatch(test_api_base.BaseApiTest):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestPatch, self).setUp()
|
super(TestPatch, self).setUp()
|
||||||
self.chassis = obj_utils.create_test_chassis(self.context)
|
self.chassis = obj_utils.create_test_chassis(self.context)
|
||||||
self.node = obj_utils.create_test_node(self.context, name='node-57',
|
self.node = obj_utils.create_test_node(self.context, name='node-57.1',
|
||||||
chassis_id=self.chassis.id)
|
chassis_id=self.chassis.id)
|
||||||
self.node_no_name = obj_utils.create_test_node(
|
self.node_no_name = obj_utils.create_test_node(
|
||||||
self.context, uuid='deadbeef-0000-1111-2222-333333333333',
|
self.context, uuid='deadbeef-0000-1111-2222-333333333333',
|
||||||
@ -1458,6 +1497,25 @@ class TestPatch(test_api_base.BaseApiTest):
|
|||||||
self.mock_update_node.assert_called_once_with(
|
self.mock_update_node.assert_called_once_with(
|
||||||
mock.ANY, mock.ANY, 'test-topic')
|
mock.ANY, mock.ANY, 'test-topic')
|
||||||
|
|
||||||
|
def test_update_ok_by_name_with_json(self):
|
||||||
|
self.mock_update_node.return_value = self.node
|
||||||
|
(self
|
||||||
|
.mock_update_node
|
||||||
|
.return_value
|
||||||
|
.updated_at) = "2013-12-03T06:20:41.184720+00:00"
|
||||||
|
response = self.patch_json(
|
||||||
|
'/nodes/%s.json' % self.node.name,
|
||||||
|
[{'path': '/instance_uuid',
|
||||||
|
'value': 'aaaaaaaa-1111-bbbb-2222-cccccccccccc',
|
||||||
|
'op': 'replace'}],
|
||||||
|
headers={api_base.Version.string: "1.5"})
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.OK, response.status_code)
|
||||||
|
self.assertEqual(self.mock_update_node.return_value.updated_at,
|
||||||
|
timeutils.parse_isotime(response.json['updated_at']))
|
||||||
|
self.mock_update_node.assert_called_once_with(
|
||||||
|
mock.ANY, mock.ANY, 'test-topic')
|
||||||
|
|
||||||
def test_update_state(self):
|
def test_update_state(self):
|
||||||
response = self.patch_json('/nodes/%s' % self.node.uuid,
|
response = self.patch_json('/nodes/%s' % self.node.uuid,
|
||||||
[{'power_state': 'new state'}],
|
[{'power_state': 'new state'}],
|
||||||
@ -2780,11 +2838,18 @@ class TestDelete(test_api_base.BaseApiTest):
|
|||||||
|
|
||||||
@mock.patch.object(rpcapi.ConductorAPI, 'destroy_node')
|
@mock.patch.object(rpcapi.ConductorAPI, 'destroy_node')
|
||||||
def test_delete_node_by_name(self, mock_dn):
|
def test_delete_node_by_name(self, mock_dn):
|
||||||
node = obj_utils.create_test_node(self.context, name='foo')
|
node = obj_utils.create_test_node(self.context, name='foo.1')
|
||||||
self.delete('/nodes/%s' % node.name,
|
self.delete('/nodes/%s' % node.name,
|
||||||
headers={api_base.Version.string: "1.5"})
|
headers={api_base.Version.string: "1.5"})
|
||||||
mock_dn.assert_called_once_with(mock.ANY, node.uuid, 'test-topic')
|
mock_dn.assert_called_once_with(mock.ANY, node.uuid, 'test-topic')
|
||||||
|
|
||||||
|
@mock.patch.object(rpcapi.ConductorAPI, 'destroy_node')
|
||||||
|
def test_delete_node_by_name_with_json(self, mock_dn):
|
||||||
|
node = obj_utils.create_test_node(self.context, name='foo')
|
||||||
|
self.delete('/nodes/%s.json' % node.name,
|
||||||
|
headers={api_base.Version.string: "1.5"})
|
||||||
|
mock_dn.assert_called_once_with(mock.ANY, node.uuid, 'test-topic')
|
||||||
|
|
||||||
@mock.patch.object(objects.Node, 'get_by_uuid')
|
@mock.patch.object(objects.Node, 'get_by_uuid')
|
||||||
def test_delete_node_not_found(self, mock_gbu):
|
def test_delete_node_not_found(self, mock_gbu):
|
||||||
node = obj_utils.get_test_node(self.context)
|
node = obj_utils.get_test_node(self.context)
|
||||||
|
@ -86,6 +86,29 @@ class TestListPortgroups(test_api_base.BaseApiTest):
|
|||||||
# never expose the node_id
|
# never expose the node_id
|
||||||
self.assertNotIn('node_id', data)
|
self.assertNotIn('node_id', data)
|
||||||
|
|
||||||
|
def test_get_one_with_json(self):
|
||||||
|
portgroup = obj_utils.create_test_portgroup(self.context,
|
||||||
|
node_id=self.node.id)
|
||||||
|
data = self.get_json('/portgroups/%s.json' % portgroup.uuid,
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertEqual(portgroup.uuid, data['uuid'])
|
||||||
|
|
||||||
|
def test_get_one_with_json_in_name(self):
|
||||||
|
portgroup = obj_utils.create_test_portgroup(self.context,
|
||||||
|
name='pg.json',
|
||||||
|
node_id=self.node.id)
|
||||||
|
data = self.get_json('/portgroups/%s' % portgroup.uuid,
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertEqual(portgroup.uuid, data['uuid'])
|
||||||
|
|
||||||
|
def test_get_one_with_suffix(self):
|
||||||
|
portgroup = obj_utils.create_test_portgroup(self.context,
|
||||||
|
name='pg.1',
|
||||||
|
node_id=self.node.id)
|
||||||
|
data = self.get_json('/portgroups/%s' % portgroup.uuid,
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertEqual(portgroup.uuid, data['uuid'])
|
||||||
|
|
||||||
def test_get_one_custom_fields(self):
|
def test_get_one_custom_fields(self):
|
||||||
portgroup = obj_utils.create_test_portgroup(self.context,
|
portgroup = obj_utils.create_test_portgroup(self.context,
|
||||||
node_id=self.node.id)
|
node_id=self.node.id)
|
||||||
@ -477,6 +500,7 @@ class TestPatch(test_api_base.BaseApiTest):
|
|||||||
super(TestPatch, self).setUp()
|
super(TestPatch, self).setUp()
|
||||||
self.node = obj_utils.create_test_node(self.context)
|
self.node = obj_utils.create_test_node(self.context)
|
||||||
self.portgroup = obj_utils.create_test_portgroup(self.context,
|
self.portgroup = obj_utils.create_test_portgroup(self.context,
|
||||||
|
name='pg.1',
|
||||||
node_id=self.node.id)
|
node_id=self.node.id)
|
||||||
|
|
||||||
p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for')
|
p = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for')
|
||||||
@ -522,6 +546,19 @@ class TestPatch(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual(http_client.OK, response.status_code)
|
self.assertEqual(http_client.OK, response.status_code)
|
||||||
self.assertEqual(extra, response.json['extra'])
|
self.assertEqual(extra, response.json['extra'])
|
||||||
|
|
||||||
|
def test_update_byname_with_json(self, mock_upd):
|
||||||
|
extra = {'foo': 'bar'}
|
||||||
|
mock_upd.return_value = self.portgroup
|
||||||
|
mock_upd.return_value.extra = extra
|
||||||
|
response = self.patch_json('/portgroups/%s.json' % self.portgroup.name,
|
||||||
|
[{'path': '/extra/foo',
|
||||||
|
'value': 'bar',
|
||||||
|
'op': 'add'}],
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.OK, response.status_code)
|
||||||
|
self.assertEqual(extra, response.json['extra'])
|
||||||
|
|
||||||
def test_update_invalid_name(self, mock_upd):
|
def test_update_invalid_name(self, mock_upd):
|
||||||
mock_upd.return_value = self.portgroup
|
mock_upd.return_value = self.portgroup
|
||||||
response = self.patch_json('/portgroups/%s' % self.portgroup.name,
|
response = self.patch_json('/portgroups/%s' % self.portgroup.name,
|
||||||
@ -1211,6 +1248,7 @@ class TestDelete(test_api_base.BaseApiTest):
|
|||||||
super(TestDelete, self).setUp()
|
super(TestDelete, self).setUp()
|
||||||
self.node = obj_utils.create_test_node(self.context)
|
self.node = obj_utils.create_test_node(self.context)
|
||||||
self.portgroup = obj_utils.create_test_portgroup(self.context,
|
self.portgroup = obj_utils.create_test_portgroup(self.context,
|
||||||
|
name='pg.1',
|
||||||
node_id=self.node.id)
|
node_id=self.node.id)
|
||||||
|
|
||||||
gtf = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for')
|
gtf = mock.patch.object(rpcapi.ConductorAPI, 'get_topic_for')
|
||||||
@ -1269,6 +1307,11 @@ class TestDelete(test_api_base.BaseApiTest):
|
|||||||
headers=self.headers)
|
headers=self.headers)
|
||||||
self.assertTrue(mock_dpt.called)
|
self.assertTrue(mock_dpt.called)
|
||||||
|
|
||||||
|
def test_delete_portgroup_byname_with_json(self, mock_dpt):
|
||||||
|
self.delete('/portgroups/%s.json' % self.portgroup.name,
|
||||||
|
headers=self.headers)
|
||||||
|
self.assertTrue(mock_dpt.called)
|
||||||
|
|
||||||
def test_delete_portgroup_byname_not_existed(self, mock_dpt):
|
def test_delete_portgroup_byname_not_existed(self, mock_dpt):
|
||||||
res = self.delete('/portgroups/%s' % 'blah', expect_errors=True,
|
res = self.delete('/portgroups/%s' % 'blah', expect_errors=True,
|
||||||
headers=self.headers)
|
headers=self.headers)
|
||||||
|
@ -188,6 +188,32 @@ class TestHeartbeat(test_api_base.BaseApiTest):
|
|||||||
node.uuid, 'url', None,
|
node.uuid, 'url', None,
|
||||||
topic='test-topic')
|
topic='test-topic')
|
||||||
|
|
||||||
|
@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
|
||||||
|
def test_ok_with_json(self, mock_heartbeat):
|
||||||
|
node = obj_utils.create_test_node(self.context)
|
||||||
|
response = self.post_json(
|
||||||
|
'/heartbeat/%s.json' % node.uuid,
|
||||||
|
{'callback_url': 'url'},
|
||||||
|
headers={api_base.Version.string: str(api_v1.max_version())})
|
||||||
|
self.assertEqual(http_client.ACCEPTED, response.status_int)
|
||||||
|
self.assertEqual(b'', response.body)
|
||||||
|
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
|
||||||
|
node.uuid, 'url', None,
|
||||||
|
topic='test-topic')
|
||||||
|
|
||||||
|
@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
|
||||||
|
def test_ok_by_name(self, mock_heartbeat):
|
||||||
|
node = obj_utils.create_test_node(self.context, name='test.1')
|
||||||
|
response = self.post_json(
|
||||||
|
'/heartbeat/%s' % node.name,
|
||||||
|
{'callback_url': 'url'},
|
||||||
|
headers={api_base.Version.string: str(api_v1.max_version())})
|
||||||
|
self.assertEqual(http_client.ACCEPTED, response.status_int)
|
||||||
|
self.assertEqual(b'', response.body)
|
||||||
|
mock_heartbeat.assert_called_once_with(mock.ANY, mock.ANY,
|
||||||
|
node.uuid, 'url', None,
|
||||||
|
topic='test-topic')
|
||||||
|
|
||||||
@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
|
@mock.patch.object(rpcapi.ConductorAPI, 'heartbeat', autospec=True)
|
||||||
def test_ok_agent_version(self, mock_heartbeat):
|
def test_ok_agent_version(self, mock_heartbeat):
|
||||||
node = obj_utils.create_test_node(self.context)
|
node = obj_utils.create_test_node(self.context)
|
||||||
|
19
releasenotes/notes/name-suffix-47aea2d265fa75ae.yaml
Normal file
19
releasenotes/notes/name-suffix-47aea2d265fa75ae.yaml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
fixes:
|
||||||
|
- |
|
||||||
|
Nodes and port groups with names ending with known file extensions are now
|
||||||
|
correctly handled by the API. See `bug 1643995
|
||||||
|
<https://bugs.launchpad.net/bugs/1643995>`_ for more details.
|
||||||
|
issues:
|
||||||
|
- |
|
||||||
|
If you have two nodes or port groups with names that only differ in
|
||||||
|
a ``.json`` suffix (for example, ``test`` and ``test.json``) you won't be
|
||||||
|
able to get, update or delete the one with the suffix via the
|
||||||
|
``/v1/nodes/<node>`` endpoint (``/v1/portgroups/<portgroup>`` for port
|
||||||
|
groups). Similarly, the ``/v1/heartbeat/<node>`` endpoint won't work
|
||||||
|
for the node with the suffix.
|
||||||
|
|
||||||
|
To work around it, add one more ``.json`` suffix (for example, use
|
||||||
|
``/v1/nodes/test`` for node ``test`` and ``/v1/nodes/test.json.json``
|
||||||
|
for ``test.json``). This issue will be addressed in one of the future
|
||||||
|
API revisions.
|
Loading…
x
Reference in New Issue
Block a user