nova/nova/tests/unit/api/openstack/compute/test_limits.py

633 lines
24 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.
"""
from http import client as httplib
from io import StringIO
import mock
from oslo_limit import fixture as limit_fixture
from oslo_serialization import jsonutils
from oslo_utils import encodeutils
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.limit import local as local_limit
from nova.limit import placement as placement_limit
from nova import objects
from nova.policies import limits as l_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'
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(
l_policies.OTHER_PROJECT_LIMIT_POLICY_NAME)
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_unknown_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 = []
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))
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("/?unknown=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("/?unknown=fake",
version='2.75')
self.assertRaises(
exception.ValidationError,
self.controller.index, req=req)
class NoopLimitsControllerTest(test.NoDBTestCase):
quota_driver = "nova.quota.NoopQuotaDriver"
def setUp(self):
super(NoopLimitsControllerTest, self).setUp()
self.flags(driver=self.quota_driver, group="quota")
self.controller = limits_v21.LimitsController()
# remove policy checks
patcher = self.mock_can = mock.patch('nova.context.RequestContext.can')
self.mock_can = patcher.start()
self.addCleanup(patcher.stop)
def test_index_v21(self):
req = fakes.HTTPRequest.blank("/")
response = self.controller.index(req)
expected_response = {
"limits": {
"rate": [],
"absolute": {
'maxImageMeta': -1,
'maxPersonality': -1,
'maxPersonalitySize': -1,
'maxSecurityGroupRules': -1,
'maxSecurityGroups': -1,
'maxServerGroupMembers': -1,
'maxServerGroups': -1,
'maxServerMeta': -1,
'maxTotalCores': -1,
'maxTotalFloatingIps': -1,
'maxTotalInstances': -1,
'maxTotalKeypairs': -1,
'maxTotalRAMSize': -1,
'totalCoresUsed': -1,
'totalFloatingIpsUsed': -1,
'totalInstancesUsed': -1,
'totalRAMUsed': -1,
'totalSecurityGroupsUsed': -1,
'totalServerGroupsUsed': -1,
},
},
}
self.assertEqual(expected_response, response)
def test_index_v275(self):
req = fakes.HTTPRequest.blank("/?tenant_id=faketenant",
version='2.75')
response = self.controller.index(req)
expected_response = {
"limits": {
"rate": [],
"absolute": {
'maxServerGroupMembers': -1,
'maxServerGroups': -1,
'maxServerMeta': -1,
'maxTotalCores': -1,
'maxTotalInstances': -1,
'maxTotalKeypairs': -1,
'maxTotalRAMSize': -1,
'totalCoresUsed': -1,
'totalInstancesUsed': -1,
'totalRAMUsed': -1,
'totalServerGroupsUsed': -1,
},
},
}
self.assertEqual(expected_response, response)
class UnifiedLimitsControllerTest(NoopLimitsControllerTest):
quota_driver = "nova.quota.UnifiedLimitsDriver"
def setUp(self):
super(UnifiedLimitsControllerTest, self).setUp()
reglimits = {local_limit.SERVER_METADATA_ITEMS: 128,
local_limit.INJECTED_FILES: 5,
local_limit.INJECTED_FILES_CONTENT: 10 * 1024,
local_limit.INJECTED_FILES_PATH: 255,
local_limit.KEY_PAIRS: 100,
local_limit.SERVER_GROUPS: 12,
local_limit.SERVER_GROUP_MEMBERS: 10}
self.useFixture(limit_fixture.LimitFixture(reglimits, {}))
@mock.patch.object(placement_limit, "get_legacy_counts")
@mock.patch.object(placement_limit, "get_legacy_project_limits")
@mock.patch.object(objects.InstanceGroupList, "get_counts")
def test_index_v21(self, mock_count, mock_proj, mock_kcount):
mock_proj.return_value = {"instances": 1, "cores": 2, "ram": 3}
mock_kcount.return_value = {"instances": 4, "cores": 5, "ram": 6}
mock_count.return_value = {'project': {'server_groups': 9}}
req = fakes.HTTPRequest.blank("/")
response = self.controller.index(req)
expected_response = {
"limits": {
"rate": [],
"absolute": {
'maxImageMeta': 128,
'maxPersonality': 5,
'maxPersonalitySize': 10240,
'maxSecurityGroupRules': -1,
'maxSecurityGroups': -1,
'maxServerGroupMembers': 10,
'maxServerGroups': 12,
'maxServerMeta': 128,
'maxTotalCores': 2,
'maxTotalFloatingIps': -1,
'maxTotalInstances': 1,
'maxTotalKeypairs': 100,
'maxTotalRAMSize': 3,
'totalCoresUsed': 5,
'totalFloatingIpsUsed': 0,
'totalInstancesUsed': 4,
'totalRAMUsed': 6,
'totalSecurityGroupsUsed': 0,
'totalServerGroupsUsed': 9,
},
},
}
self.assertEqual(expected_response, response)
@mock.patch.object(placement_limit, "get_legacy_counts")
@mock.patch.object(placement_limit, "get_legacy_project_limits")
@mock.patch.object(objects.InstanceGroupList, "get_counts")
def test_index_v275(self, mock_count, mock_proj, mock_kcount):
mock_proj.return_value = {"instances": 1, "cores": 2, "ram": 3}
mock_kcount.return_value = {"instances": 4, "cores": 5, "ram": 6}
mock_count.return_value = {'project': {'server_groups': 9}}
req = fakes.HTTPRequest.blank("/?tenant_id=faketenant",
version='2.75')
response = self.controller.index(req)
expected_response = {
"limits": {
"rate": [],
"absolute": {
'maxServerGroupMembers': 10,
'maxServerGroups': 12,
'maxServerMeta': 128,
'maxTotalCores': 2,
'maxTotalInstances': 1,
'maxTotalKeypairs': 100,
'maxTotalRAMSize': 3,
'totalCoresUsed': 5,
'totalInstancesUsed': 4,
'totalRAMUsed': 6,
'totalServerGroupsUsed': 9,
},
},
}
self.assertEqual(expected_response, response)