Microversion 2.50 - fix quota class sets resource usage

This adds support for the 2.50 microversion which does the following:

* Adds the server_groups and server_groups_members resources to the output
  for the 'nova quota-class-show' and 'nova quota-class-update' CLIs.
* Removes the ability to show or update network-related resource quota class
  values, specifically floating_ips, fixed_ips, security_groups and
  security_group_members.
* Defines explicit kwargs for the update() method in the python API binding.

This also fixes a problem where the 'nova quota-class-update' CLI was
incorrectly capped at the 2.35 microversion for updating network-related
resources. That was true for the os-quota-sets API which is tenant-specific,
but not for the os-quota-class-sets API which is global.

Functional tests are added for the 2.1 and 2.50 microversion behavior for
both commands.

Part of blueprint fix-quota-classes-api

Change-Id: I2531f9094d92e1b9ed36ab03bc43ae1be5290790
This commit is contained in:
Matt Riedemann 2017-07-12 14:10:07 -04:00
parent 77f940c534
commit 5bfa57a433
7 changed files with 295 additions and 5 deletions

View File

@ -25,4 +25,4 @@ API_MIN_VERSION = api_versions.APIVersion("2.1")
# when client supported the max version, and bumped sequentially, otherwise # when client supported the max version, and bumped sequentially, otherwise
# the client may break due to server side new version may include some # the client may break due to server side new version may include some
# backward incompatible change. # backward incompatible change.
API_MAX_VERSION = api_versions.APIVersion("2.49") API_MAX_VERSION = api_versions.APIVersion("2.50")

View File

@ -0,0 +1,133 @@
# Copyright 2017 Huawei Technologies Co.,LTD.
#
# 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 tempest.lib import exceptions
from novaclient.tests.functional import base
class TestQuotaClassesNovaClient(base.ClientTestBase):
"""Nova quota classes functional tests for the v2.1 microversion."""
COMPUTE_API_VERSION = '2.1'
# The list of quota class resources we expect in the output table.
_included_resources = ['instances', 'cores', 'ram',
'floating_ips', 'fixed_ips', 'metadata_items',
'injected_files', 'injected_file_content_bytes',
'injected_file_path_bytes', 'key_pairs',
'security_groups', 'security_group_rules']
# The list of quota class resources we do not expect in the output table.
_excluded_resources = ['server_groups', 'server_group_members']
# Any resources that are not shown but can be updated. For example, before
# microversion 2.50 you can update server_groups and server_groups_members
# quota class values but they are not shown in the GET response.
_extra_update_resources = _excluded_resources
# The list of resources which are blocked from being updated.
_blocked_update_resources = []
def _get_quota_class_name(self):
"""Returns a fake quota class name specific to this test class."""
return 'fake-class-%s' % self.COMPUTE_API_VERSION.replace('.', '-')
def _verify_qouta_class_show_output(self, output, expected_values):
# Assert that the expected key/value pairs are in the output table
for quota_name in self._included_resources:
# First make sure the resource is actually in expected quota.
self.assertIn(quota_name, expected_values)
expected_value = expected_values[quota_name]
actual_value = self._get_value_from_the_table(output, quota_name)
self.assertEqual(expected_value, actual_value)
# Now make sure anything that we don't expect in the output table is
# actually not showing up.
for quota_name in self._excluded_resources:
# ValueError is raised when the key isn't found in the table.
self.assertRaises(ValueError,
self._get_value_from_the_table,
output, quota_name)
def test_quota_class_show(self):
"""Tests showing quota class values for a fake non-existing quota
class. The API will return the defaults if the quota class does not
actually exist. We use a fake class to avoid any interaction with the
real default quota class values.
"""
default_quota_class_set = self.client.quota_classes.get('default')
default_values = {
quota_name: str(getattr(default_quota_class_set, quota_name))
for quota_name in self._included_resources
}
output = self.nova('quota-class-show %s' %
self._get_quota_class_name())
self._verify_qouta_class_show_output(output, default_values)
def test_quota_class_update(self):
"""Tests updating a fake quota class. The way this works in the API
is that if the quota class is not found, it is created. So in this
test we can use a fake quota class with fake values and they will all
get set. We don't use the default quota class because it is global
and we don't want to interfere with other tests.
"""
class_name = self._get_quota_class_name()
params = [class_name]
expected_values = {}
for quota_name in (
self._included_resources + self._extra_update_resources):
params.append("--%s 99" % quota_name.replace("_", "-"))
expected_values[quota_name] = '99'
# Note that the quota-class-update CLI doesn't actually output any
# information from the response.
self.nova("quota-class-update", params=" ".join(params))
# Assert the results using the quota-class-show output.
output = self.nova('quota-class-show %s' % class_name)
self._verify_qouta_class_show_output(output, expected_values)
# Assert that attempting to update resources that are blocked will
# result in a failure.
for quota_name in self._blocked_update_resources:
self.assertRaises(
exceptions.CommandFailed,
self.nova, "quota-class-update %s --%s 99" %
(class_name, quota_name.replace("_", "-")))
class TestQuotasNovaClient2_50(TestQuotaClassesNovaClient):
"""Nova quota classes functional tests for the v2.50 microversion."""
COMPUTE_API_VERSION = '2.50'
# The 2.50 microversion added the server_groups and server_group_members
# to the response, and filtered out floating_ips, fixed_ips,
# security_groups and security_group_members, similar to the 2.36
# microversion in the os-qouta-sets API.
_included_resources = ['instances', 'cores', 'ram', 'metadata_items',
'injected_files', 'injected_file_content_bytes',
'injected_file_path_bytes', 'key_pairs',
'server_groups', 'server_group_members']
# The list of quota class resources we do not expect in the output table.
_excluded_resources = ['floating_ips', 'fixed_ips',
'security_groups', 'security_group_rules']
# In 2.50, server_groups and server_group_members can be both updated
# in a PUT request and shown in a GET response.
_extra_update_resources = []
# In 2.50, you can't update the network-related resources.
_blocked_update_resources = _excluded_resources

View File

@ -1289,6 +1289,20 @@ class FakeSessionClient(base_client.SessionClient):
# #
def get_os_quota_class_sets_test(self, **kw): def get_os_quota_class_sets_test(self, **kw):
if self.api_version >= api_versions.APIVersion('2.50'):
return (200, FAKE_RESPONSE_HEADERS, {
'quota_class_set': {
'id': 'test',
'metadata_items': 1,
'injected_file_content_bytes': 1,
'injected_file_path_bytes': 1,
'ram': 1,
'instances': 1,
'injected_files': 1,
'cores': 1,
'key_pairs': 1,
'server_groups': 1,
'server_group_members': 1}})
return (200, FAKE_RESPONSE_HEADERS, { return (200, FAKE_RESPONSE_HEADERS, {
'quota_class_set': { 'quota_class_set': {
'id': 'test', 'id': 'test',
@ -1297,6 +1311,7 @@ class FakeSessionClient(base_client.SessionClient):
'injected_file_path_bytes': 1, 'injected_file_path_bytes': 1,
'ram': 1, 'ram': 1,
'floating_ips': 1, 'floating_ips': 1,
'fixed_ips': -1,
'instances': 1, 'instances': 1,
'injected_files': 1, 'injected_files': 1,
'cores': 1, 'cores': 1,
@ -1306,6 +1321,19 @@ class FakeSessionClient(base_client.SessionClient):
def put_os_quota_class_sets_test(self, body, **kw): def put_os_quota_class_sets_test(self, body, **kw):
assert list(body) == ['quota_class_set'] assert list(body) == ['quota_class_set']
if self.api_version >= api_versions.APIVersion('2.50'):
return (200, {}, {
'quota_class_set': {
'metadata_items': 1,
'injected_file_content_bytes': 1,
'injected_file_path_bytes': 1,
'ram': 1,
'instances': 1,
'injected_files': 1,
'cores': 1,
'key_pairs': 1,
'server_groups': 1,
'server_group_members': 1}})
return (200, {}, { return (200, {}, {
'quota_class_set': { 'quota_class_set': {
'metadata_items': 1, 'metadata_items': 1,
@ -1313,6 +1341,7 @@ class FakeSessionClient(base_client.SessionClient):
'injected_file_path_bytes': 1, 'injected_file_path_bytes': 1,
'ram': 1, 'ram': 1,
'floating_ips': 1, 'floating_ips': 1,
'fixed_ips': -1,
'instances': 1, 'instances': 1,
'injected_files': 1, 'injected_files': 1,
'cores': 1, 'cores': 1,

View File

@ -28,12 +28,14 @@ class QuotaClassSetsTest(utils.TestCase):
q = self.cs.quota_classes.get(class_name) q = self.cs.quota_classes.get(class_name)
self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST)
self.cs.assert_called('GET', '/os-quota-class-sets/%s' % class_name) self.cs.assert_called('GET', '/os-quota-class-sets/%s' % class_name)
return q
def test_update_quota(self): def test_update_quota(self):
q = self.cs.quota_classes.get('test') q = self.cs.quota_classes.get('test')
self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST)
q.update(cores=2) q.update(cores=2)
self.cs.assert_called('PUT', '/os-quota-class-sets/test') self.cs.assert_called('PUT', '/os-quota-class-sets/test')
return q
def test_refresh_quota(self): def test_refresh_quota(self):
q = self.cs.quota_classes.get('test') q = self.cs.quota_classes.get('test')
@ -43,3 +45,53 @@ class QuotaClassSetsTest(utils.TestCase):
self.assertNotEqual(q.cores, q2.cores) self.assertNotEqual(q.cores, q2.cores)
q2.get() q2.get()
self.assertEqual(q.cores, q2.cores) self.assertEqual(q.cores, q2.cores)
class QuotaClassSetsTest2_50(QuotaClassSetsTest):
"""Tests the quota classes API binding using the 2.50 microversion."""
def setUp(self):
super(QuotaClassSetsTest2_50, self).setUp()
self.cs = fakes.FakeClient(api_versions.APIVersion("2.50"))
def test_class_quotas_get(self):
"""Tests that network-related resources aren't in a 2.50 response
and server group related resources are in the response.
"""
q = super(QuotaClassSetsTest2_50, self).test_class_quotas_get()
for invalid_resource in ('floating_ips', 'fixed_ips', 'networks',
'security_groups', 'security_group_rules'):
self.assertFalse(hasattr(q, invalid_resource),
'%s should not be in %s' % (invalid_resource, q))
# Also make sure server_groups and server_group_members are in the
# response.
for valid_resource in ('server_groups', 'server_group_members'):
self.assertTrue(hasattr(q, valid_resource),
'%s should be in %s' % (invalid_resource, q))
def test_update_quota(self):
"""Tests that network-related resources aren't in a 2.50 response
and server group related resources are in the response.
"""
q = super(QuotaClassSetsTest2_50, self).test_update_quota()
for invalid_resource in ('floating_ips', 'fixed_ips', 'networks',
'security_groups', 'security_group_rules'):
self.assertFalse(hasattr(q, invalid_resource),
'%s should not be in %s' % (invalid_resource, q))
# Also make sure server_groups and server_group_members are in the
# response.
for valid_resource in ('server_groups', 'server_group_members'):
self.assertTrue(hasattr(q, valid_resource),
'%s should be in %s' % (invalid_resource, q))
def test_update_quota_invalid_resources(self):
"""Tests trying to update quota class values for invalid resources.
This will fail with TypeError because the network-related resource
kwargs aren't defined.
"""
q = self.cs.quota_classes.get('test')
self.assertRaises(TypeError, q.update, floating_ips=1)
self.assertRaises(TypeError, q.update, fixed_ips=1)
self.assertRaises(TypeError, q.update, security_groups=1)
self.assertRaises(TypeError, q.update, security_group_rules=1)
self.assertRaises(TypeError, q.update, networks=1)

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from novaclient import api_versions
from novaclient import base from novaclient import base
@ -32,6 +33,10 @@ class QuotaClassSetManager(base.Manager):
def _update_body(self, **kwargs): def _update_body(self, **kwargs):
return {'quota_class_set': kwargs} return {'quota_class_set': kwargs}
# NOTE(mriedem): Before 2.50 the resources you could update was just a
# kwargs dict and not validated on the client-side, only on the API server
# side.
@api_versions.wraps("2.0", "2.49")
def update(self, class_name, **kwargs): def update(self, class_name, **kwargs):
body = self._update_body(**kwargs) body = self._update_body(**kwargs)
@ -42,3 +47,37 @@ class QuotaClassSetManager(base.Manager):
return self._update('/os-quota-class-sets/%s' % (class_name), return self._update('/os-quota-class-sets/%s' % (class_name),
body, body,
'quota_class_set') 'quota_class_set')
# NOTE(mriedem): 2.50 does strict validation of the resources you can
# specify since the network-related resources are blocked in 2.50.
@api_versions.wraps("2.50")
def update(self, class_name, instances=None, cores=None, ram=None,
metadata_items=None, injected_files=None,
injected_file_content_bytes=None, injected_file_path_bytes=None,
key_pairs=None, server_groups=None, server_group_members=None):
resources = {}
if instances is not None:
resources['instances'] = instances
if cores is not None:
resources['cores'] = cores
if ram is not None:
resources['ram'] = ram
if metadata_items is not None:
resources['metadata_items'] = metadata_items
if injected_files is not None:
resources['injected_files'] = injected_files
if injected_file_content_bytes is not None:
resources['injected_file_content_bytes'] = (
injected_file_content_bytes)
if injected_file_path_bytes is not None:
resources['injected_file_path_bytes'] = injected_file_path_bytes
if key_pairs is not None:
resources['key_pairs'] = key_pairs
if server_groups is not None:
resources['server_groups'] = server_groups
if server_group_members is not None:
resources['server_group_members'] = server_group_members
body = {'quota_class_set': resources}
return self._update('/os-quota-class-sets/%s' % class_name, body,
'quota_class_set')

View File

@ -3827,6 +3827,11 @@ def do_ssh(cs, args):
os.system(cmd) os.system(cmd)
# NOTE(mriedem): In the 2.50 microversion, the os-quota-class-sets API
# will return the server_groups and server_group_members, but no longer
# return floating_ips, fixed_ips, security_groups or security_group_members
# as those are deprecated as networking service proxies and/or because
# nova-network is deprecated. Similar to the 2.36 microversion.
_quota_resources = ['instances', 'cores', 'ram', _quota_resources = ['instances', 'cores', 'ram',
'floating_ips', 'fixed_ips', 'metadata_items', 'floating_ips', 'fixed_ips', 'metadata_items',
'injected_files', 'injected_file_content_bytes', 'injected_files', 'injected_file_content_bytes',
@ -4137,7 +4142,7 @@ def do_quota_class_show(cs, args):
_quota_show(cs.quota_classes.get(args.class_name)) _quota_show(cs.quota_classes.get(args.class_name))
@api_versions.wraps("2.0", "2.35") @api_versions.wraps("2.0", "2.49")
@utils.arg( @utils.arg(
'class_name', 'class_name',
metavar='<class>', metavar='<class>',
@ -4233,9 +4238,9 @@ def do_quota_class_update(cs, args):
_quota_update(cs.quota_classes, args.class_name, args) _quota_update(cs.quota_classes, args.class_name, args)
# 2.36 does not support updating quota for floating IPs, fixed IPs, security # 2.50 does not support updating quota class values for floating IPs,
# groups or security group rules. # fixed IPs, security groups or security group rules.
@api_versions.wraps("2.36") @api_versions.wraps("2.50")
@utils.arg( @utils.arg(
'class_name', 'class_name',
metavar='<class>', metavar='<class>',

View File

@ -0,0 +1,32 @@
---
fixes:
- |
Adds support for the ``2.50`` microversion which fixes the
``nova quota-class-show`` and ``nova quota-class-update`` commands in the
following ways:
* The ``server_groups`` and ``server_group_members`` quota resources will
now be shown in the output table for ``nova quota-class-show``.
* The ``floating_ips``, ``fixed_ips``, ``security_groups`` and
``security_group_rules`` quota resources will no longer be able to
be updated using ``nova quota-class-update`` nor will they be shown in
the output of ``nova quota-class-show``. Use python-openstackclient or
python-neutronclient to work with quotas for network resources.
In addition, the ``nova quota-class-update`` CLI was previously incorrectly
limiting the ability to update quota class values for ``floating_ips``,
``fixed_ips``, ``security_groups`` and ``security_group_rules`` based on
the 2.36 microversion. That has been changed to limit based on the ``2.50``
microversion.
upgrade:
- |
The ``novaclient.v2.quota_classes.QuotaClassSetManager.update`` method
now defines specific kwargs starting with microversion ``2.50`` since
updating network-related resource quota class values is not supported on
the server with microversion ``2.50``. The list of excluded resources is:
- ``fixed_ips``
- ``floating_ips``
- ``networks``
- ``security_groups``
- ``security_group_rules``