b26bc7fd7a
This microversion implements below API cleanups: 1. 400 for unknown param for query param and for request body. 2. Making server representation always consistent among all APIs returning the complete server representation. 3. Change the default return value of ``swap`` field from the empty string to 0 (integer) in flavor APIs. 4. Return ``servers`` field always in the response of GET hypervisors API even there are no servers on hypervisor Details: https://specs.openstack.org/openstack/nova-specs/specs/train/approved/api-consistency-cleanup.html Partial-Implements: blueprint api-consistency-cleanup Change-Id: I9d257a003d315b84b937dcef91f3cb41f3e24b53
524 lines
19 KiB
Python
524 lines
19 KiB
Python
# Copyright 2011 OpenStack Foundation
|
|
# All Rights Reserved.
|
|
#
|
|
# 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.
|
|
|
|
"""
|
|
Tests dealing with HTTP rate-limiting.
|
|
"""
|
|
|
|
import mock
|
|
from oslo_serialization import jsonutils
|
|
from oslo_utils import encodeutils
|
|
from six.moves import http_client as httplib
|
|
from six.moves import StringIO
|
|
|
|
from nova.api.openstack.compute import limits as limits_v21
|
|
from nova.api.openstack.compute import views
|
|
from nova.api.openstack import wsgi
|
|
import nova.context
|
|
from nova import exception
|
|
from nova.policies import used_limits as ul_policies
|
|
from nova import quota
|
|
from nova import test
|
|
from nova.tests.unit.api.openstack import fakes
|
|
from nova.tests.unit import matchers
|
|
|
|
|
|
class BaseLimitTestSuite(test.NoDBTestCase):
|
|
"""Base test suite which provides relevant stubs and time abstraction."""
|
|
|
|
def setUp(self):
|
|
super(BaseLimitTestSuite, self).setUp()
|
|
self.time = 0.0
|
|
self.absolute_limits = {}
|
|
|
|
def stub_get_project_quotas(context, project_id, usages=True):
|
|
return {k: dict(limit=v, in_use=v // 2)
|
|
for k, v in self.absolute_limits.items()}
|
|
|
|
mock_get_project_quotas = mock.patch.object(
|
|
nova.quota.QUOTAS,
|
|
"get_project_quotas",
|
|
side_effect = stub_get_project_quotas)
|
|
mock_get_project_quotas.start()
|
|
self.addCleanup(mock_get_project_quotas.stop)
|
|
patcher = self.mock_can = mock.patch('nova.context.RequestContext.can')
|
|
self.mock_can = patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
def _get_time(self):
|
|
"""Return the "time" according to this test suite."""
|
|
return self.time
|
|
|
|
|
|
class LimitsControllerTestV21(BaseLimitTestSuite):
|
|
"""Tests for `limits.LimitsController` class."""
|
|
limits_controller = limits_v21.LimitsController
|
|
|
|
def setUp(self):
|
|
"""Run before each test."""
|
|
super(LimitsControllerTestV21, self).setUp()
|
|
self.controller = wsgi.Resource(self.limits_controller())
|
|
self.ctrler = self.limits_controller()
|
|
|
|
def _get_index_request(self, accept_header="application/json",
|
|
tenant_id=None, user_id='testuser',
|
|
project_id='testproject'):
|
|
"""Helper to set routing arguments."""
|
|
request = fakes.HTTPRequest.blank('', version='2.1')
|
|
if tenant_id:
|
|
request = fakes.HTTPRequest.blank('/?tenant_id=%s' % tenant_id,
|
|
version='2.1')
|
|
|
|
request.accept = accept_header
|
|
request.environ["wsgiorg.routing_args"] = (None, {
|
|
"action": "index",
|
|
"controller": "",
|
|
})
|
|
context = nova.context.RequestContext(user_id, project_id)
|
|
request.environ["nova.context"] = context
|
|
return request
|
|
|
|
def test_empty_index_json(self):
|
|
# Test getting empty limit details in JSON.
|
|
request = self._get_index_request()
|
|
response = request.get_response(self.controller)
|
|
expected = {
|
|
"limits": {
|
|
"rate": [],
|
|
"absolute": {},
|
|
},
|
|
}
|
|
body = jsonutils.loads(response.body)
|
|
self.assertEqual(expected, body)
|
|
|
|
def test_index_json(self):
|
|
self._test_index_json()
|
|
|
|
def test_index_json_by_tenant(self):
|
|
self._test_index_json('faketenant')
|
|
|
|
def _test_index_json(self, tenant_id=None):
|
|
# Test getting limit details in JSON.
|
|
request = self._get_index_request(tenant_id=tenant_id)
|
|
context = request.environ["nova.context"]
|
|
if tenant_id is None:
|
|
tenant_id = context.project_id
|
|
|
|
self.absolute_limits = {
|
|
'ram': 512,
|
|
'instances': 5,
|
|
'cores': 21,
|
|
'key_pairs': 10,
|
|
'floating_ips': 10,
|
|
'security_groups': 10,
|
|
'security_group_rules': 20,
|
|
}
|
|
expected = {
|
|
"limits": {
|
|
"rate": [],
|
|
"absolute": {
|
|
"maxTotalRAMSize": 512,
|
|
"maxTotalInstances": 5,
|
|
"maxTotalCores": 21,
|
|
"maxTotalKeypairs": 10,
|
|
"maxTotalFloatingIps": 10,
|
|
"maxSecurityGroups": 10,
|
|
"maxSecurityGroupRules": 20,
|
|
"totalRAMUsed": 256,
|
|
"totalCoresUsed": 10,
|
|
"totalInstancesUsed": 2,
|
|
"totalFloatingIpsUsed": 5,
|
|
"totalSecurityGroupsUsed": 5,
|
|
},
|
|
},
|
|
}
|
|
|
|
def _get_project_quotas(context, project_id, usages=True):
|
|
return {k: dict(limit=v, in_use=v // 2)
|
|
for k, v in self.absolute_limits.items()}
|
|
|
|
with mock.patch('nova.quota.QUOTAS.get_project_quotas') as \
|
|
get_project_quotas:
|
|
get_project_quotas.side_effect = _get_project_quotas
|
|
|
|
response = request.get_response(self.controller)
|
|
|
|
body = jsonutils.loads(response.body)
|
|
self.assertEqual(expected, body)
|
|
get_project_quotas.assert_called_once_with(context, tenant_id,
|
|
usages=True)
|
|
|
|
def _do_test_used_limits(self, reserved):
|
|
request = self._get_index_request(tenant_id=None)
|
|
quota_map = {
|
|
'totalRAMUsed': 'ram',
|
|
'totalCoresUsed': 'cores',
|
|
'totalInstancesUsed': 'instances',
|
|
'totalFloatingIpsUsed': 'floating_ips',
|
|
'totalSecurityGroupsUsed': 'security_groups',
|
|
'totalServerGroupsUsed': 'server_groups',
|
|
}
|
|
limits = {}
|
|
expected_abs_limits = []
|
|
for display_name, q in quota_map.items():
|
|
limits[q] = {'limit': len(display_name),
|
|
'in_use': len(display_name) // 2,
|
|
'reserved': 0}
|
|
expected_abs_limits.append(display_name)
|
|
|
|
def stub_get_project_quotas(context, project_id, usages=True):
|
|
return limits
|
|
|
|
self.stub_out('nova.quota.QUOTAS.get_project_quotas',
|
|
stub_get_project_quotas)
|
|
|
|
res = request.get_response(self.controller)
|
|
body = jsonutils.loads(res.body)
|
|
abs_limits = body['limits']['absolute']
|
|
for limit in expected_abs_limits:
|
|
value = abs_limits[limit]
|
|
r = limits[quota_map[limit]]['reserved'] if reserved else 0
|
|
self.assertEqual(limits[quota_map[limit]]['in_use'] + r, value)
|
|
|
|
def test_used_limits_basic(self):
|
|
self._do_test_used_limits(False)
|
|
|
|
def test_used_limits_with_reserved(self):
|
|
self._do_test_used_limits(True)
|
|
|
|
def test_admin_can_fetch_limits_for_a_given_tenant_id(self):
|
|
project_id = "123456"
|
|
user_id = "A1234"
|
|
tenant_id = 'abcd'
|
|
target = {
|
|
"project_id": tenant_id,
|
|
"user_id": user_id
|
|
}
|
|
fake_req = self._get_index_request(tenant_id=tenant_id,
|
|
user_id=user_id,
|
|
project_id=project_id)
|
|
context = fake_req.environ["nova.context"]
|
|
with mock.patch.object(quota.QUOTAS, 'get_project_quotas',
|
|
return_value={}) as mock_get_quotas:
|
|
fake_req.get_response(self.controller)
|
|
self.assertEqual(2, self.mock_can.call_count)
|
|
self.mock_can.assert_called_with(ul_policies.BASE_POLICY_NAME,
|
|
target)
|
|
mock_get_quotas.assert_called_once_with(context,
|
|
tenant_id, usages=True)
|
|
|
|
def _test_admin_can_fetch_used_limits_for_own_project(self, req_get):
|
|
project_id = "123456"
|
|
if 'tenant_id' in req_get:
|
|
project_id = req_get['tenant_id']
|
|
|
|
user_id = "A1234"
|
|
fake_req = self._get_index_request(user_id=user_id,
|
|
project_id=project_id)
|
|
context = fake_req.environ["nova.context"]
|
|
|
|
with mock.patch.object(quota.QUOTAS, 'get_project_quotas',
|
|
return_value={}) as mock_get_quotas:
|
|
fake_req.get_response(self.controller)
|
|
mock_get_quotas.assert_called_once_with(context,
|
|
project_id, usages=True)
|
|
|
|
def test_admin_can_fetch_used_limits_for_own_project(self):
|
|
req_get = {}
|
|
self._test_admin_can_fetch_used_limits_for_own_project(req_get)
|
|
|
|
def test_admin_can_fetch_used_limits_for_dummy_only(self):
|
|
# for back compatible we allow additional param to be send to req.GET
|
|
# it can be removed when we add restrictions to query param later
|
|
req_get = {'dummy': 'dummy'}
|
|
self._test_admin_can_fetch_used_limits_for_own_project(req_get)
|
|
|
|
def test_admin_can_fetch_used_limits_with_positive_int(self):
|
|
req_get = {'tenant_id': 123}
|
|
self._test_admin_can_fetch_used_limits_for_own_project(req_get)
|
|
|
|
def test_admin_can_fetch_used_limits_with_negative_int(self):
|
|
req_get = {'tenant_id': -1}
|
|
self._test_admin_can_fetch_used_limits_for_own_project(req_get)
|
|
|
|
def test_admin_can_fetch_used_limits_with_unkown_param(self):
|
|
req_get = {'tenant_id': '123', 'unknown': 'unknown'}
|
|
self._test_admin_can_fetch_used_limits_for_own_project(req_get)
|
|
|
|
def test_used_limits_fetched_for_context_project_id(self):
|
|
project_id = "123456"
|
|
fake_req = self._get_index_request(project_id=project_id)
|
|
context = fake_req.environ["nova.context"]
|
|
with mock.patch.object(quota.QUOTAS, 'get_project_quotas',
|
|
return_value={}) as mock_get_quotas:
|
|
fake_req.get_response(self.controller)
|
|
|
|
mock_get_quotas.assert_called_once_with(context,
|
|
project_id, usages=True)
|
|
|
|
def test_used_ram_added(self):
|
|
fake_req = self._get_index_request()
|
|
|
|
def stub_get_project_quotas(context, project_id, usages=True):
|
|
return {'ram': {'limit': 512, 'in_use': 256}}
|
|
|
|
with mock.patch.object(quota.QUOTAS, 'get_project_quotas',
|
|
side_effect=stub_get_project_quotas
|
|
) as mock_get_quotas:
|
|
|
|
res = fake_req.get_response(self.controller)
|
|
body = jsonutils.loads(res.body)
|
|
abs_limits = body['limits']['absolute']
|
|
self.assertIn('totalRAMUsed', abs_limits)
|
|
self.assertEqual(256, abs_limits['totalRAMUsed'])
|
|
self.assertEqual(1, mock_get_quotas.call_count)
|
|
|
|
def test_no_ram_quota(self):
|
|
fake_req = self._get_index_request()
|
|
|
|
with mock.patch.object(quota.QUOTAS, 'get_project_quotas',
|
|
return_value={}) as mock_get_quotas:
|
|
|
|
res = fake_req.get_response(self.controller)
|
|
body = jsonutils.loads(res.body)
|
|
abs_limits = body['limits']['absolute']
|
|
self.assertNotIn('totalRAMUsed', abs_limits)
|
|
self.assertEqual(1, mock_get_quotas.call_count)
|
|
|
|
|
|
class FakeHttplibSocket(object):
|
|
"""Fake `httplib.HTTPResponse` replacement."""
|
|
|
|
def __init__(self, response_string):
|
|
"""Initialize new `FakeHttplibSocket`."""
|
|
self._buffer = StringIO(response_string)
|
|
|
|
def makefile(self, _mode, _other):
|
|
"""Returns the socket's internal buffer."""
|
|
return self._buffer
|
|
|
|
|
|
class FakeHttplibConnection(object):
|
|
"""Fake `httplib.HTTPConnection`."""
|
|
|
|
def __init__(self, app, host):
|
|
"""Initialize `FakeHttplibConnection`."""
|
|
self.app = app
|
|
self.host = host
|
|
|
|
def request(self, method, path, body="", headers=None):
|
|
"""Requests made via this connection actually get translated and routed
|
|
into our WSGI app, we then wait for the response and turn it back into
|
|
an `httplib.HTTPResponse`.
|
|
"""
|
|
if not headers:
|
|
headers = {}
|
|
|
|
req = fakes.HTTPRequest.blank(path)
|
|
req.method = method
|
|
req.headers = headers
|
|
req.host = self.host
|
|
req.body = encodeutils.safe_encode(body)
|
|
|
|
resp = str(req.get_response(self.app))
|
|
resp = "HTTP/1.0 %s" % resp
|
|
sock = FakeHttplibSocket(resp)
|
|
self.http_response = httplib.HTTPResponse(sock)
|
|
self.http_response.begin()
|
|
|
|
def getresponse(self):
|
|
"""Return our generated response from the request."""
|
|
return self.http_response
|
|
|
|
|
|
class LimitsViewBuilderTest(test.NoDBTestCase):
|
|
def setUp(self):
|
|
super(LimitsViewBuilderTest, self).setUp()
|
|
self.view_builder = views.limits.ViewBuilder()
|
|
self.req = fakes.HTTPRequest.blank('/?tenant_id=None')
|
|
self.rate_limits = []
|
|
patcher = self.mock_can = mock.patch('nova.context.RequestContext.can')
|
|
self.mock_can = patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
self.absolute_limits = {"metadata_items": {'limit': 1, 'in_use': 1},
|
|
"injected_files": {'limit': 5, 'in_use': 1},
|
|
"injected_file_content_bytes":
|
|
{'limit': 5, 'in_use': 1}}
|
|
|
|
def test_build_limits(self):
|
|
expected_limits = {"limits": {
|
|
"rate": [],
|
|
"absolute": {"maxServerMeta": 1,
|
|
"maxImageMeta": 1,
|
|
"maxPersonality": 5,
|
|
"maxPersonalitySize": 5}}}
|
|
|
|
output = self.view_builder.build(self.req, self.absolute_limits)
|
|
self.assertThat(output, matchers.DictMatches(expected_limits))
|
|
|
|
def test_build_limits_empty_limits(self):
|
|
expected_limits = {"limits": {"rate": [],
|
|
"absolute": {}}}
|
|
|
|
quotas = {}
|
|
output = self.view_builder.build(self.req, quotas)
|
|
self.assertThat(output, matchers.DictMatches(expected_limits))
|
|
|
|
def test_non_admin_cannot_fetch_used_limits_for_any_other_project(self):
|
|
project_id = "123456"
|
|
user_id = "A1234"
|
|
tenant_id = "abcd"
|
|
target = {
|
|
"project_id": tenant_id,
|
|
"user_id": user_id
|
|
}
|
|
req = fakes.HTTPRequest.blank('/?tenant_id=%s' % tenant_id)
|
|
context = nova.context.RequestContext(user_id, project_id)
|
|
req.environ["nova.context"] = context
|
|
|
|
self.mock_can.side_effect = exception.PolicyNotAuthorized(
|
|
action="os_compute_api:os-used-limits")
|
|
self.assertRaises(exception.PolicyNotAuthorized,
|
|
self.view_builder.build,
|
|
req, self.absolute_limits)
|
|
|
|
self.mock_can.assert_called_with(ul_policies.BASE_POLICY_NAME,
|
|
target)
|
|
|
|
|
|
class LimitsPolicyEnforcementV21(test.NoDBTestCase):
|
|
|
|
def setUp(self):
|
|
super(LimitsPolicyEnforcementV21, self).setUp()
|
|
self.controller = limits_v21.LimitsController()
|
|
|
|
def test_limits_index_policy_failed(self):
|
|
rule_name = "os_compute_api:limits"
|
|
self.policy.set_rules({rule_name: "project:non_fake"})
|
|
req = fakes.HTTPRequest.blank('')
|
|
exc = self.assertRaises(
|
|
exception.PolicyNotAuthorized,
|
|
self.controller.index, req=req)
|
|
self.assertEqual(
|
|
"Policy doesn't allow %s to be performed." % rule_name,
|
|
exc.format_message())
|
|
|
|
|
|
class LimitsControllerTestV236(BaseLimitTestSuite):
|
|
|
|
def setUp(self):
|
|
super(LimitsControllerTestV236, self).setUp()
|
|
self.controller = limits_v21.LimitsController()
|
|
self.req = fakes.HTTPRequest.blank("/?tenant_id=faketenant",
|
|
version='2.36')
|
|
|
|
def test_index_filtered(self):
|
|
absolute_limits = {
|
|
'ram': 512,
|
|
'instances': 5,
|
|
'cores': 21,
|
|
'key_pairs': 10,
|
|
'floating_ips': 10,
|
|
'security_groups': 10,
|
|
'security_group_rules': 20,
|
|
}
|
|
|
|
def _get_project_quotas(context, project_id, usages=True):
|
|
return {k: dict(limit=v, in_use=v // 2)
|
|
for k, v in absolute_limits.items()}
|
|
|
|
with mock.patch('nova.quota.QUOTAS.get_project_quotas') as \
|
|
get_project_quotas:
|
|
get_project_quotas.side_effect = _get_project_quotas
|
|
response = self.controller.index(self.req)
|
|
expected_response = {
|
|
"limits": {
|
|
"rate": [],
|
|
"absolute": {
|
|
"maxTotalRAMSize": 512,
|
|
"maxTotalInstances": 5,
|
|
"maxTotalCores": 21,
|
|
"maxTotalKeypairs": 10,
|
|
"totalRAMUsed": 256,
|
|
"totalCoresUsed": 10,
|
|
"totalInstancesUsed": 2,
|
|
},
|
|
},
|
|
}
|
|
self.assertEqual(expected_response, response)
|
|
|
|
|
|
class LimitsControllerTestV239(BaseLimitTestSuite):
|
|
|
|
def setUp(self):
|
|
super(LimitsControllerTestV239, self).setUp()
|
|
self.controller = limits_v21.LimitsController()
|
|
self.req = fakes.HTTPRequest.blank("/?tenant_id=faketenant",
|
|
version='2.39')
|
|
|
|
def test_index_filtered_no_max_image_meta(self):
|
|
absolute_limits = {
|
|
"metadata_items": 1,
|
|
}
|
|
|
|
def _get_project_quotas(context, project_id, usages=True):
|
|
return {k: dict(limit=v, in_use=v // 2)
|
|
for k, v in absolute_limits.items()}
|
|
|
|
with mock.patch('nova.quota.QUOTAS.get_project_quotas') as \
|
|
get_project_quotas:
|
|
get_project_quotas.side_effect = _get_project_quotas
|
|
response = self.controller.index(self.req)
|
|
# staring from version 2.39 there is no 'maxImageMeta' field
|
|
# in response after removing 'image-metadata' proxy API
|
|
expected_response = {
|
|
"limits": {
|
|
"rate": [],
|
|
"absolute": {
|
|
"maxServerMeta": 1,
|
|
},
|
|
},
|
|
}
|
|
self.assertEqual(expected_response, response)
|
|
|
|
|
|
class LimitsControllerTestV275(BaseLimitTestSuite):
|
|
def setUp(self):
|
|
super(LimitsControllerTestV275, self).setUp()
|
|
self.controller = limits_v21.LimitsController()
|
|
|
|
def test_index_additional_query_param_old_version(self):
|
|
absolute_limits = {
|
|
"metadata_items": 1,
|
|
}
|
|
req = fakes.HTTPRequest.blank("/?unkown=fake",
|
|
version='2.74')
|
|
|
|
def _get_project_quotas(context, project_id, usages=True):
|
|
return {k: dict(limit=v, in_use=v // 2)
|
|
for k, v in absolute_limits.items()}
|
|
|
|
with mock.patch('nova.quota.QUOTAS.get_project_quotas') as \
|
|
get_project_quotas:
|
|
get_project_quotas.side_effect = _get_project_quotas
|
|
self.controller.index(req)
|
|
|
|
def test_index_additional_query_param(self):
|
|
req = fakes.HTTPRequest.blank("/?unkown=fake",
|
|
version='2.75')
|
|
self.assertRaises(
|
|
exception.ValidationError,
|
|
self.controller.index, req=req)
|