Merge "Add functional tests for nested quotas"
This commit is contained in:
commit
42f416462d
@ -22,40 +22,37 @@ from cinder.tests.unit import fake_constants as fake
|
||||
|
||||
|
||||
class OpenStackApiException(Exception):
|
||||
def __init__(self, message=None, response=None):
|
||||
message = 'Unspecified error'
|
||||
|
||||
def __init__(self, response=None, msg=None):
|
||||
self.response = response
|
||||
if not message:
|
||||
message = 'Unspecified error'
|
||||
# Give chance to override default message
|
||||
if msg:
|
||||
self.message = msg
|
||||
|
||||
if response:
|
||||
message = _('%(message)s\nStatus Code: %(_status)s\n'
|
||||
'Body: %(_body)s') % {'_status': response.status_code,
|
||||
'_body': response.text}
|
||||
self.message = _(
|
||||
'%(message)s\nStatus Code: %(_status)s\nBody: %(_body)s') % {
|
||||
'_status': response.status_code, '_body': response.text,
|
||||
'message': self.message}
|
||||
|
||||
super(OpenStackApiException, self).__init__(message)
|
||||
super(OpenStackApiException, self).__init__(self.message)
|
||||
|
||||
|
||||
class OpenStackApiAuthenticationException(OpenStackApiException):
|
||||
def __init__(self, response=None, message=None):
|
||||
if not message:
|
||||
message = _("Authentication error")
|
||||
super(OpenStackApiAuthenticationException, self).__init__(message,
|
||||
response)
|
||||
class OpenStackApiException401(OpenStackApiException):
|
||||
message = _("401 Unauthorized Error")
|
||||
|
||||
|
||||
class OpenStackApiAuthorizationException(OpenStackApiException):
|
||||
def __init__(self, response=None, message=None):
|
||||
if not message:
|
||||
message = _("Authorization error")
|
||||
super(OpenStackApiAuthorizationException, self).__init__(message,
|
||||
response)
|
||||
class OpenStackApiException404(OpenStackApiException):
|
||||
message = _("404 Not Found Error")
|
||||
|
||||
|
||||
class OpenStackApiNotFoundException(OpenStackApiException):
|
||||
def __init__(self, response=None, message=None):
|
||||
if not message:
|
||||
message = _("Item not found")
|
||||
super(OpenStackApiNotFoundException, self).__init__(message, response)
|
||||
class OpenStackApiException413(OpenStackApiException):
|
||||
message = _("413 Request entity too large")
|
||||
|
||||
|
||||
class OpenStackApiException400(OpenStackApiException):
|
||||
message = _("400 Bad Request")
|
||||
|
||||
|
||||
class TestOpenStackClient(object):
|
||||
@ -102,8 +99,8 @@ class TestOpenStackClient(object):
|
||||
|
||||
return response
|
||||
|
||||
def _authenticate(self):
|
||||
if self.auth_result:
|
||||
def _authenticate(self, reauthenticate=False):
|
||||
if self.auth_result and not reauthenticate:
|
||||
return self.auth_result
|
||||
|
||||
auth_uri = self.auth_uri
|
||||
@ -116,11 +113,15 @@ class TestOpenStackClient(object):
|
||||
http_status = response.status_code
|
||||
|
||||
if http_status == 401:
|
||||
raise OpenStackApiAuthenticationException(response=response)
|
||||
raise OpenStackApiException401(response=response)
|
||||
|
||||
self.auth_result = response.headers
|
||||
return self.auth_result
|
||||
|
||||
def update_project(self, new_project_id):
|
||||
self.project_id = new_project_id
|
||||
self._authenticate(True)
|
||||
|
||||
def api_request(self, relative_uri, check_response_status=None, **kwargs):
|
||||
auth_result = self._authenticate()
|
||||
|
||||
@ -135,17 +136,15 @@ class TestOpenStackClient(object):
|
||||
response = self.request(full_uri, **kwargs)
|
||||
|
||||
http_status = response.status_code
|
||||
|
||||
if check_response_status:
|
||||
if http_status not in check_response_status:
|
||||
if http_status == 404:
|
||||
raise OpenStackApiNotFoundException(response=response)
|
||||
elif http_status == 401:
|
||||
raise OpenStackApiAuthorizationException(response=response)
|
||||
else:
|
||||
raise OpenStackApiException(
|
||||
message=_("Unexpected status code"),
|
||||
response=response)
|
||||
message = None
|
||||
try:
|
||||
exc = globals()["OpenStackApiException%s" % http_status]
|
||||
except KeyError:
|
||||
exc = OpenStackApiException
|
||||
message = _("Unexpected status code")
|
||||
raise exc(response, message)
|
||||
|
||||
return response
|
||||
|
||||
@ -204,6 +203,16 @@ class TestOpenStackClient(object):
|
||||
def put_volume(self, volume_id, volume):
|
||||
return self.api_put('/volumes/%s' % volume_id, volume)['volume']
|
||||
|
||||
def quota_set(self, project_id, quota_update):
|
||||
return self.api_put(
|
||||
'os-quota-sets/%s' % project_id,
|
||||
{'quota_set': quota_update})['quota_set']
|
||||
|
||||
def quota_get(self, project_id, usage=True):
|
||||
|
||||
return self.api_get('os-quota-sets/%s?usage=%s'
|
||||
% (project_id, usage))['quota_set']
|
||||
|
||||
def create_type(self, type_name, extra_specs=None):
|
||||
type = {"volume_type": {"name": type_name}}
|
||||
if extra_specs:
|
||||
|
@ -19,6 +19,7 @@ Provides common functionality for functional tests
|
||||
import os.path
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import fixtures
|
||||
@ -80,6 +81,9 @@ class _FunctionalTestBase(test.TestCase):
|
||||
self.api = client.TestOpenStackClient(fake.USER_ID,
|
||||
fake.PROJECT_ID, self.auth_url)
|
||||
|
||||
def _update_project(self, new_project_id):
|
||||
self.api.update_project(new_project_id)
|
||||
|
||||
def _start_api_service(self):
|
||||
default_conf = os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), '..', '..', '..',
|
||||
@ -138,3 +142,28 @@ class _FunctionalTestBase(test.TestCase):
|
||||
server_name = self.get_unused_server_name()
|
||||
server['name'] = server_name
|
||||
return server
|
||||
|
||||
def _poll_volume_while(self, volume_id, continue_states,
|
||||
expected_end_status=None, max_retries=5):
|
||||
"""Poll (briefly) while the state is in continue_states.
|
||||
|
||||
Continues until the state changes from continue_states or max_retries
|
||||
are hit. If expected_end_status is specified, we assert that the end
|
||||
status of the volume is expected_end_status.
|
||||
"""
|
||||
retries = 0
|
||||
while retries <= max_retries:
|
||||
try:
|
||||
found_volume = self.api.get_volume(volume_id)
|
||||
except client.OpenStackApiException404:
|
||||
return None
|
||||
|
||||
self.assertEqual(volume_id, found_volume['id'])
|
||||
vol_status = found_volume['status']
|
||||
if vol_status not in continue_states:
|
||||
if expected_end_status:
|
||||
self.assertEqual(expected_end_status, vol_status)
|
||||
return found_volume
|
||||
|
||||
time.sleep(1)
|
||||
retries += 1
|
||||
|
170
cinder/tests/functional/test_quotas.py
Normal file
170
cinder/tests/functional/test_quotas.py
Normal file
@ -0,0 +1,170 @@
|
||||
# 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.
|
||||
import mock
|
||||
import uuid
|
||||
|
||||
from cinder import quota
|
||||
from cinder.tests.functional.api import client
|
||||
from cinder.tests.functional import functional_helpers
|
||||
from cinder.tests.unit import fake_driver
|
||||
|
||||
|
||||
class NestedQuotasTest(functional_helpers._FunctionalTestBase):
|
||||
_vol_type_name = 'functional_test_type'
|
||||
|
||||
def setUp(self):
|
||||
super(NestedQuotasTest, self).setUp()
|
||||
self.api.create_type(self._vol_type_name)
|
||||
fake_driver.LoggingVolumeDriver.clear_logs()
|
||||
self._create_project_hierarchy()
|
||||
# Need to mock out Keystone so the functional tests don't require other
|
||||
# services
|
||||
_keystone_client = mock.MagicMock()
|
||||
_keystone_client.version = 'v3'
|
||||
_keystone_client.projects.get.side_effect = self._get_project
|
||||
_keystone_client_get = mock.patch(
|
||||
'cinder.quota_utils._keystone_client',
|
||||
lambda *args, **kwargs: _keystone_client)
|
||||
_keystone_client_get.start()
|
||||
self.addCleanup(_keystone_client_get.stop)
|
||||
# The QUOTA engine in Cinder is a global variable that lazy loads the
|
||||
# quota driver, so even if we change the config for the quota driver,
|
||||
# we won't reliably change the driver being used (or change it back)
|
||||
# unless the global variables get cleaned up, so using mock instead to
|
||||
# simulate this change
|
||||
nested_driver = quota.NestedDbQuotaDriver()
|
||||
_driver_patcher = mock.patch(
|
||||
'cinder.quota.QuotaEngine._driver', new=nested_driver)
|
||||
_driver_patcher.start()
|
||||
self.addCleanup(_driver_patcher.stop)
|
||||
# Default to using the top parent in the hierarchy
|
||||
self._update_project(self.A.id)
|
||||
|
||||
def _get_flags(self):
|
||||
f = super(NestedQuotasTest, self)._get_flags()
|
||||
f['volume_driver'] = \
|
||||
'cinder.tests.unit.fake_driver.LoggingVolumeDriver'
|
||||
f['default_volume_type'] = self._vol_type_name
|
||||
return f
|
||||
|
||||
# Currently we use 413 error for over quota
|
||||
over_quota_exception = client.OpenStackApiException413
|
||||
|
||||
def _create_project_hierarchy(self):
|
||||
"""Sets up the nested hierarchy show below.
|
||||
|
||||
+-----------+
|
||||
| A |
|
||||
| / \ |
|
||||
| B C |
|
||||
| / |
|
||||
| D |
|
||||
+-----------+
|
||||
"""
|
||||
self.A = self.FakeProject()
|
||||
self.B = self.FakeProject(parent_id=self.A.id)
|
||||
self.C = self.FakeProject(parent_id=self.A.id)
|
||||
self.D = self.FakeProject(parent_id=self.B.id)
|
||||
|
||||
self.B.subtree = {self.D.id: self.D.subtree}
|
||||
self.A.subtree = {self.B.id: self.B.subtree, self.C.id: self.C.subtree}
|
||||
|
||||
self.A.parents = None
|
||||
self.B.parents = {self.A.id: None}
|
||||
self.C.parents = {self.A.id: None}
|
||||
self.D.parents = {self.B.id: self.B.parents}
|
||||
|
||||
# project_by_id attribute is used to recover a project based on its id.
|
||||
self.project_by_id = {self.A.id: self.A, self.B.id: self.B,
|
||||
self.C.id: self.C, self.D.id: self.D}
|
||||
|
||||
class FakeProject(object):
|
||||
_dom_id = uuid.uuid4().hex
|
||||
|
||||
def __init__(self, parent_id=None):
|
||||
self.id = uuid.uuid4().hex
|
||||
self.parent_id = parent_id
|
||||
self.domain_id = self._dom_id
|
||||
self.subtree = None
|
||||
self.parents = None
|
||||
|
||||
def _get_project(self, project_id, *args, **kwargs):
|
||||
return self.project_by_id[project_id]
|
||||
|
||||
def _create_volume(self):
|
||||
return self.api.post_volume({'volume': {'size': 1}})
|
||||
|
||||
def test_default_quotas_enforced(self):
|
||||
# Should be able to create volume on parent project by default
|
||||
created_vol = self._create_volume()
|
||||
self._poll_volume_while(created_vol['id'], ['creating'], 'available')
|
||||
self._update_project(self.B.id)
|
||||
# Shouldn't be able to create volume on child project by default
|
||||
self.assertRaises(self.over_quota_exception, self._create_volume)
|
||||
|
||||
def test_update_child_with_parent_default_quota(self):
|
||||
# Make sure we can update to a reasonable value
|
||||
self.api.quota_set(self.B.id, {'volumes': 5})
|
||||
# Ensure that the update took and we can create a volume
|
||||
self._poll_volume_while(
|
||||
self._create_volume()['id'], ['creating'], 'available')
|
||||
|
||||
def test_quota_update_child_greater_than_parent(self):
|
||||
self.assertRaises(
|
||||
client.OpenStackApiException400,
|
||||
self.api.quota_set, self.B.id, {'volumes': 11})
|
||||
|
||||
def test_child_soft_limit_propagates_to_parent(self):
|
||||
self.api.quota_set(self.B.id, {'volumes': 0})
|
||||
self.api.quota_set(self.D.id, {'volumes': -1})
|
||||
self._update_project(self.D.id)
|
||||
self.assertRaises(self.over_quota_exception, self._create_volume)
|
||||
|
||||
def test_child_quota_hard_limits_affects_parents_allocated(self):
|
||||
self.api.quota_set(self.B.id, {'volumes': 5})
|
||||
self.api.quota_set(self.C.id, {'volumes': 3})
|
||||
alloc = self.api.quota_get(self.A.id)['volumes']['allocated']
|
||||
self.assertEqual(8, alloc)
|
||||
self.assertRaises(client.OpenStackApiException400,
|
||||
self.api.quota_set, self.C.id, {'volumes': 6})
|
||||
|
||||
def _update_quota_and_def_type(self, project_id, quota):
|
||||
self.api.quota_set(project_id, quota)
|
||||
type_updates = {'%s_%s' % (key, self._vol_type_name): val for key, val
|
||||
in quota.items() if key != 'per_volume_gigabytes'}
|
||||
return self.api.quota_set(project_id, type_updates)
|
||||
|
||||
def test_grandchild_soft_limit_propogates_up(self):
|
||||
quota = {'volumes': -1, 'gigabytes': -1, 'per_volume_gigabytes': -1}
|
||||
self._update_quota_and_def_type(self.B.id, quota)
|
||||
self._update_quota_and_def_type(self.D.id, quota)
|
||||
self._update_project(self.D.id)
|
||||
# Create two volumes in the grandchild project and ensure grandparent's
|
||||
# allocated is updated accordingly
|
||||
vol = self._create_volume()
|
||||
self._create_volume()
|
||||
self._update_project(self.A.id)
|
||||
alloc = self.api.quota_get(self.A.id)['volumes']['allocated']
|
||||
self.assertEqual(2, alloc)
|
||||
alloc = self.api.quota_get(self.B.id)['volumes']['allocated']
|
||||
self.assertEqual(2, alloc)
|
||||
# Ensure delete reduces the quota
|
||||
self._update_project(self.D.id)
|
||||
self.api.delete_volume(vol['id'])
|
||||
self._poll_volume_while(vol['id'], ['deleting'])
|
||||
self._update_project(self.A.id)
|
||||
alloc = self.api.quota_get(self.A.id)['volumes']['allocated']
|
||||
self.assertEqual(1, alloc)
|
||||
alloc = self.api.quota_get(self.B.id)['volumes']['allocated']
|
||||
self.assertEqual(1, alloc)
|
@ -13,10 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import time
|
||||
|
||||
from cinder.tests import fake_driver
|
||||
from cinder.tests.functional.api import client
|
||||
from cinder.tests.functional import functional_helpers
|
||||
|
||||
|
||||
@ -45,27 +42,6 @@ class VolumesTest(functional_helpers._FunctionalTestBase):
|
||||
volumes = self.api.get_volumes()
|
||||
self.assertIsNotNone(volumes)
|
||||
|
||||
def _poll_while(self, volume_id, continue_states, max_retries=5):
|
||||
"""Poll (briefly) while the state is in continue_states."""
|
||||
retries = 0
|
||||
while True:
|
||||
try:
|
||||
found_volume = self.api.get_volume(volume_id)
|
||||
except client.OpenStackApiNotFoundException:
|
||||
found_volume = None
|
||||
break
|
||||
|
||||
self.assertEqual(volume_id, found_volume['id'])
|
||||
|
||||
if found_volume['status'] not in continue_states:
|
||||
break
|
||||
|
||||
time.sleep(1)
|
||||
retries = retries + 1
|
||||
if retries > max_retries:
|
||||
break
|
||||
return found_volume
|
||||
|
||||
def test_create_and_delete_volume(self):
|
||||
"""Creates and deletes a volume."""
|
||||
|
||||
@ -85,7 +61,7 @@ class VolumesTest(functional_helpers._FunctionalTestBase):
|
||||
self.assertIn(created_volume_id, volume_names)
|
||||
|
||||
# Wait (briefly) for creation. Delay is due to the 'message queue'
|
||||
found_volume = self._poll_while(created_volume_id, ['creating'])
|
||||
found_volume = self._poll_volume_while(created_volume_id, ['creating'])
|
||||
|
||||
# It should be available...
|
||||
self.assertEqual('available', found_volume['status'])
|
||||
@ -94,7 +70,7 @@ class VolumesTest(functional_helpers._FunctionalTestBase):
|
||||
self.api.delete_volume(created_volume_id)
|
||||
|
||||
# Wait (briefly) for deletion. Delay is due to the 'message queue'
|
||||
found_volume = self._poll_while(created_volume_id, ['deleting'])
|
||||
found_volume = self._poll_volume_while(created_volume_id, ['deleting'])
|
||||
|
||||
# Should be gone
|
||||
self.assertFalse(found_volume)
|
||||
|
@ -52,4 +52,6 @@ def set_defaults(conf):
|
||||
conf.set_default('state_path', os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), '..', '..', '..')))
|
||||
conf.set_default('policy_dirs', [], group='oslo_policy')
|
||||
# This is where we don't authenticate
|
||||
conf.set_default('auth_strategy', 'noauth')
|
||||
conf.set_default('auth_uri', 'fake', 'keystone_authtoken')
|
||||
|
Loading…
x
Reference in New Issue
Block a user