diff --git a/doc/source/cli/osc/v2/load-balancer.rst b/doc/source/cli/osc/v2/load-balancer.rst index 9c065c9..ed6b2ef 100644 --- a/doc/source/cli/osc/v2/load-balancer.rst +++ b/doc/source/cli/osc/v2/load-balancer.rst @@ -67,3 +67,10 @@ l7rule .. autoprogram-cliff:: openstack.load_balancer.v2 :command: loadbalancer l7rule * + +===== +quota +===== + +.. autoprogram-cliff:: openstack.load_balancer.v2 + :command: loadbalancer quota * diff --git a/octaviaclient/api/constants.py b/octaviaclient/api/constants.py index 41848b8..59bed04 100644 --- a/octaviaclient/api/constants.py +++ b/octaviaclient/api/constants.py @@ -31,3 +31,7 @@ BASE_L7POLICY_URL = '/l7policies' BASE_SINGLE_L7POLICY_URL = BASE_L7POLICY_URL + '/{policy_uuid}' BASE_L7RULE_URL = BASE_SINGLE_L7POLICY_URL + '/rules' BASE_SINGLE_L7RULE_URL = BASE_SINGLE_L7POLICY_URL + '/rules/{rule_uuid}' + +BASE_QUOTA_URL = '/quotas' +BASE_SINGLE_QUOTA_URL = BASE_QUOTA_URL + '/{uuid}' +BASE_QUOTA_DEFAULT_URL = BASE_QUOTA_URL + '/defaults' diff --git a/octaviaclient/api/load_balancer_v2.py b/octaviaclient/api/load_balancer_v2.py index 1c0d38d..396874d 100644 --- a/octaviaclient/api/load_balancer_v2.py +++ b/octaviaclient/api/load_balancer_v2.py @@ -580,6 +580,72 @@ class APIv2(api.BaseAPI): return response + def quota_list(self, **params): + """List all quotas + + :param params: + Parameters to filter on (not implemented) + :return: + A ``dict`` representing a list of quotas for the project + """ + url = const.BASE_QUOTA_URL + response = self.list(url, **params) + + return response + + def quota_show(self, project_id): + """Show a quota + + :param string project_id: + ID of the project to show + :return: + A ``dict`` representing the quota for the project + """ + response = self.find(path=const.BASE_QUOTA_URL, value=project_id) + + return response + + @correct_return_codes + def quota_reset(self, project_id): + """Reset a quota + + :param string project_id: + The ID of the project to reset quotas + :return: + ``None`` + """ + url = const.BASE_SINGLE_QUOTA_URL.format(uuid=project_id) + response = self.delete(url) + + return response + + @correct_return_codes + def quota_set(self, project_id, **params): + """Update a quota's settings + + :param string project_id: + The ID of the project to update + :param params: + A ``dict`` of arguments to update project quota + :return: + A ``dict`` representing the updated quota + """ + url = const.BASE_SINGLE_QUOTA_URL.format(uuid=project_id) + response = self.create(url, method='PUT', **params) + + return response + + def quota_defaults_show(self): + """Show quota defaults + + :return: + A ``dict`` representing a list of quota defaults + """ + url = const.BASE_QUOTA_DEFAULT_URL + response = self.list(url) + + return response + class OctaviaClientException(Exception): """The base exception class for all exceptions this library raises.""" diff --git a/octaviaclient/osc/v2/constants.py b/octaviaclient/osc/v2/constants.py index a015ce7..c3ca91f 100644 --- a/octaviaclient/osc/v2/constants.py +++ b/octaviaclient/osc/v2/constants.py @@ -206,3 +206,20 @@ MONITOR_COLUMNS = ( 'type', 'admin_state_up', ) + +QUOTA_ROWS = ( + 'load_balancer', + 'listener', + 'pool', + 'health_monitor', + 'member' +) + +QUOTA_COLUMNS = ( + 'project_id', + 'load_balancer', + 'listener', + 'pool', + 'health_monitor', + 'member' +) diff --git a/octaviaclient/osc/v2/quota.py b/octaviaclient/osc/v2/quota.py new file mode 100644 index 0000000..20253a1 --- /dev/null +++ b/octaviaclient/osc/v2/quota.py @@ -0,0 +1,185 @@ +# 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. +# + +"""Quota action implementation""" + + +from cliff import lister +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils + +from octaviaclient.osc.v2 import constants as const +from octaviaclient.osc.v2 import utils as v2_utils + + +class ListQuota(lister.Lister): + """List quotas""" + + def get_parser(self, prog_name): + parser = super(ListQuota, self).get_parser(prog_name) + + parser.add_argument( + '--project', + metavar='', + help="Name or UUID of the project." + ) + + return parser + + def take_action(self, parsed_args): + columns = const.QUOTA_COLUMNS + attrs = v2_utils.get_listener_attrs(self.app.client_manager, + parsed_args) + data = self.app.client_manager.load_balancer.quota_list(**attrs) + formatters = {'quotas': v2_utils.format_list} + return (columns, + (utils.get_dict_properties(s, columns, formatters=formatters) + for s in data['quotas'])) + + +class ShowQuota(command.ShowOne): + """Show the quota details for a project""" + + def get_parser(self, prog_name): + parser = super(ShowQuota, self).get_parser(prog_name) + + parser.add_argument( + 'project', + metavar='', + help="Name or UUID of the project." + ) + + return parser + + def take_action(self, parsed_args): + rows = const.QUOTA_ROWS + attrs = v2_utils.get_quota_attrs(self.app.client_manager, + parsed_args) + project_id = attrs.pop('project_id') + + data = self.app.client_manager.load_balancer.quota_show( + project_id=project_id + ) + + return (rows, (utils.get_dict_properties(data, rows))) + + +class ShowQuotaDefaults(command.ShowOne): + """Show quota defaults""" + + def take_action(self, parsed_args): + rows = const.QUOTA_ROWS + data = self.app.client_manager.load_balancer.quota_defaults_show() + return (rows, (utils.get_dict_properties(data['quota'], rows))) + + +class SetQuota(command.ShowOne): + """Update a quota""" + + @staticmethod + def _check_attrs(attrs): + args = ['health_monitor', 'listener', 'load_balancer', 'member', + 'pool'] + + if not any(arg in attrs for arg in args): + args = [arg.replace('_', '') for arg in args] + msg = ('Missing required argument. Requires at least one of:%s' % + ','.join((' --%s' % arg) for arg in args)) + raise exceptions.CommandError(msg) + + def get_parser(self, prog_name): + parser = super(SetQuota, self).get_parser(prog_name) + + quota_group = parser.add_argument_group( + "Quota limits", + description='At least one of the following arguments is required.' + ) + + quota_group.add_argument( + '--healthmonitor', + dest='health_monitor', + metavar='', + help=('New value for the health monitor quota. Value -1 means ' + 'unlimited.') + ) + quota_group.add_argument( + '--listener', + metavar='', + help=('New value for the listener quota. Value -1 means ' + 'unlimited.') + ) + quota_group.add_argument( + '--loadbalancer', + dest='load_balancer', + metavar='', + help=('New value for the load balancer quota limit. Value -1 ' + 'means unlimited.') + ) + quota_group.add_argument( + '--member', + metavar='', + help=('New value for the member quota limit. Value -1 means ' + 'unlimited.') + ) + quota_group.add_argument( + '--pool', + metavar='', + help=('New value for the pool quota limit. Value -1 means ' + 'unlimited.') + ) + + parser.add_argument( + 'project', + metavar='', + help="Name or UUID of the project." + ) + + return parser + + def take_action(self, parsed_args): + rows = const.QUOTA_ROWS + attrs = v2_utils.get_quota_attrs(self.app.client_manager, + parsed_args) + self._check_attrs(attrs) + project_id = attrs.pop('project_id') + body = {'quota': attrs} + + data = self.app.client_manager.load_balancer.quota_set(project_id, + json=body) + + return (rows, (utils.get_dict_properties(data['quota'], rows))) + + +class ResetQuota(command.Command): + """Resets quotas to default quotas""" + + def get_parser(self, prog_name): + parser = super(ResetQuota, self).get_parser(prog_name) + + parser.add_argument( + 'project', + metavar="", + help="Project to reset quotas (name or ID)" + ) + + return parser + + def take_action(self, parsed_args): + attrs = v2_utils.get_quota_attrs(self.app.client_manager, + parsed_args) + + project_id = attrs.pop('project_id') + + self.app.client_manager.load_balancer.quota_reset( + project_id=project_id) diff --git a/octaviaclient/osc/v2/utils.py b/octaviaclient/osc/v2/utils.py index 047d190..249908e 100644 --- a/octaviaclient/osc/v2/utils.py +++ b/octaviaclient/osc/v2/utils.py @@ -376,6 +376,26 @@ def get_health_monitor_attrs(client_manager, parsed_args): return attrs +def get_quota_attrs(client_manager, parsed_args): + attr_map = { + 'health_monitor': ('health_monitor', int), + 'listener': ('listener', int), + 'load_balancer': ('load_balancer', int), + 'member': ('member', int), + 'pool': ('pool', int), + 'project': ( + 'project_id', + 'project', + client_manager.identity + ), + } + + _attrs = vars(parsed_args) + attrs = _map_attrs(_attrs, attr_map) + + return attrs + + def format_list(data): return '\n'.join(i['id'] for i in data) diff --git a/octaviaclient/tests/unit/api/test_load_balancer.py b/octaviaclient/tests/unit/api/test_load_balancer.py index 1512490..af77b25 100644 --- a/octaviaclient/tests/unit/api/test_load_balancer.py +++ b/octaviaclient/tests/unit/api/test_load_balancer.py @@ -31,6 +31,7 @@ FAKE_ME = uuidutils.generate_uuid() FAKE_L7PO = uuidutils.generate_uuid() FAKE_L7RU = uuidutils.generate_uuid() FAKE_HM = uuidutils.generate_uuid() +FAKE_PRJ = uuidutils.generate_uuid() LIST_LB_RESP = { @@ -74,6 +75,16 @@ LIST_HM_RESP = { {'id': uuidutils.generate_uuid()}] } +LIST_QT_RESP = { + 'quotas': + [{'health_monitor': -1}, + {'listener': -1}, + {'load_balancer': 5}, + {'member': 10}, + {'pool': 20}, + {'project': uuidutils.generate_uuid()}] +} + SINGLE_LB_RESP = {'loadbalancer': {'id': FAKE_LB, 'name': 'lb1'}} SINGLE_LB_UPDATE = {"loadbalancer": {"admin_state_up": False}} SINGLE_LB_STATS_RESP = {'bytes_in': '0'} @@ -96,6 +107,9 @@ SINGLE_L7RU_UPDATE = {'rule': {'admin_state_up': False}} SINGLE_HM_RESP = {'healthmonitor': {'id': FAKE_ME}} SINGLE_HM_UPDATE = {'healthmonitor': {'admin_state_up': False}} +SINGLE_QT_RESP = {'quota': {'pool': -1}} +SINGLE_QT_UPDATE = {'quota': {'pool': -1}} + class TestLoadBalancerv2(utils.TestCase): @@ -727,3 +741,66 @@ class TestLoadBalancer(TestLoadBalancerv2): self._error_message, self.api.health_monitor_delete, FAKE_HM) + + def test_list_quota_no_options(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL + 'quotas', + json=LIST_QT_RESP, + status_code=200, + ) + ret = self.api.quota_list() + self.assertEqual(LIST_QT_RESP, ret) + + def test_show_quota(self): + self.requests_mock.register_uri( + 'GET', + FAKE_URL + 'quotas/' + FAKE_PRJ, + json=SINGLE_QT_RESP, + status_code=200 + ) + ret = self.api.quota_show(FAKE_PRJ) + self.assertEqual(SINGLE_QT_RESP['quota'], ret) + + def test_set_quota(self): + self.requests_mock.register_uri( + 'PUT', + FAKE_URL + 'quotas/' + FAKE_PRJ, + json=SINGLE_QT_UPDATE, + status_code=200 + ) + ret = self.api.quota_set(FAKE_PRJ, json=SINGLE_QT_UPDATE) + self.assertEqual(SINGLE_QT_UPDATE, ret) + + def test_set_quota_error(self): + self.requests_mock.register_uri( + 'PUT', + FAKE_URL + 'quotas/' + FAKE_PRJ, + text='{"faultstring": "%s"}' % self._error_message, + status_code=400 + ) + self.assertRaisesRegex(lb.OctaviaClientException, + self._error_message, + self.api.quota_set, + FAKE_PRJ, json=SINGLE_QT_UPDATE) + + def test_reset_quota(self): + self.requests_mock.register_uri( + 'DELETE', + FAKE_URL + 'quotas/' + FAKE_PRJ, + status_code=200 + ) + ret = self.api.quota_reset(FAKE_PRJ) + self.assertEqual(200, ret.status_code) + + def test_reset_quota_error(self): + self.requests_mock.register_uri( + 'DELETE', + FAKE_URL + 'quotas/' + FAKE_PRJ, + text='{"faultstring": "%s"}' % self._error_message, + status_code=400 + ) + self.assertRaisesRegex(lb.OctaviaClientException, + self._error_message, + self.api.quota_reset, + FAKE_PRJ) diff --git a/octaviaclient/tests/unit/osc/v2/fakes.py b/octaviaclient/tests/unit/osc/v2/fakes.py index 93e3868..a2ab14d 100644 --- a/octaviaclient/tests/unit/osc/v2/fakes.py +++ b/octaviaclient/tests/unit/osc/v2/fakes.py @@ -264,3 +264,28 @@ class FakeHM(object): loaded=True) return hm + + +class FakeQT(object): + """Fake one or more Quota.""" + + @staticmethod + def create_one_quota(attrs=None): + attrs = attrs or {} + + qt_info = { + "health_monitor": -1, + "listener": None, + "load_balancer": 5, + "member": 50, + "pool": None, + "project_id": uuidutils.generate_uuid(dashed=True) + } + + qt_info.update(attrs) + + qt = fakes.FakeResource( + info=copy.deepcopy(qt_info), + loaded=True) + + return qt diff --git a/octaviaclient/tests/unit/osc/v2/test_quota.py b/octaviaclient/tests/unit/osc/v2/test_quota.py new file mode 100644 index 0000000..503632d --- /dev/null +++ b/octaviaclient/tests/unit/osc/v2/test_quota.py @@ -0,0 +1,227 @@ +# 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 copy +import mock + +from osc_lib import exceptions + +from octaviaclient.osc.v2 import quota +from octaviaclient.tests.unit.osc.v2 import fakes as qt_fakes + +AUTH_TOKEN = "foobar" +AUTH_URL = "http://192.0.2.2" + + +class TestQuota(qt_fakes.TestLoadBalancerv2): + + _qt = qt_fakes.FakeQT.create_one_quota() + + columns = ('project_id', 'load_balancer', 'listener', 'pool', + 'health_monitor', 'member') + + datalist = ( + ( + _qt.project_id, + _qt.load_balancer, + _qt.listener, + _qt.pool, + _qt.health_monitor, + _qt.member + ), + ) + + info = { + 'quotas': + [{ + "project_id": _qt.project_id, + "load_balancer": _qt.load_balancer, + "listener": _qt.listener, + "pool": _qt.pool, + "health_monitor": _qt.health_monitor, + "member": _qt.member + }] + } + qt_info = copy.deepcopy(info) + + def setUp(self): + super(TestQuota, self).setUp() + self.qt_mock = self.app.client_manager.load_balancer.load_balancers + self.qt_mock.reset_mock() + + self.api_mock = mock.Mock() + self.api_mock.quota_list.return_value = self.qt_info + lb_client = self.app.client_manager + lb_client.load_balancer = self.api_mock + + +class TestQuotaList(TestQuota): + + def setUp(self): + super(TestQuotaList, self).setUp() + self.cmd = quota.ListQuota(self.app, None) + + def test_quota_list_no_options(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.api_mock.quota_list.assert_called_with() + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + +class TestQuotaShow(TestQuota): + + def setUp(self): + super(TestQuotaShow, self).setUp() + self.api_mock = mock.Mock() + self.api_mock.quota_list.return_value = self.qt_info + self.api_mock.quota_show.return_value = { + 'quota': self.qt_info['quotas'][0]} + lb_client = self.app.client_manager + lb_client.load_balancer = self.api_mock + + self.cmd = quota.ShowQuota(self.app, None) + + @mock.patch('octaviaclient.osc.v2.utils.get_quota_attrs') + def test_quota_show(self, mock_attrs): + mock_attrs.return_value = self.qt_info['quotas'][0] + arglist = [self._qt.project_id] + verifylist = [ + ('project', self._qt.project_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.quota_show.assert_called_with( + project_id=self._qt.project_id) + + +class TestQuotaDefaultsShow(TestQuota): + + qt_defaults = { + "health_monitor": 1, + "listener": 2, + "load_balancer": 3, + "member": 4, + "pool": 5 + } + + def setUp(self): + super(TestQuotaDefaultsShow, self).setUp() + + self.api_mock = mock.Mock() + self.api_mock.quota_defaults_show.return_value = { + 'quota': self.qt_defaults} + + lb_client = self.app.client_manager + lb_client.load_balancer = self.api_mock + + self.cmd = quota.ShowQuotaDefaults(self.app, None) + + def test_quota_defaults_show(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + rows, data = self.cmd.take_action(parsed_args) + data = dict(zip(rows, data)) + + self.api_mock.quota_defaults_show.assert_called_with() + self.assertEqual(self.qt_defaults, data) + + +class TestQuotaSet(TestQuota): + + def setUp(self): + super(TestQuotaSet, self).setUp() + self.api_mock = mock.Mock() + self.api_mock.quota_set.return_value = { + 'quota': self.qt_info['quotas'][0]} + lb_client = self.app.client_manager + lb_client.load_balancer = self.api_mock + + self.cmd = quota.SetQuota(self.app, None) + + @mock.patch('octaviaclient.osc.v2.utils.get_quota_attrs') + def test_quota_set(self, mock_attrs): + mock_attrs.return_value = { + 'project_id': self._qt.project_id, + 'health_monitor': '-1', + 'listener': '1', + 'load_balancer': '2', + 'member': '3', + 'pool': '4' + } + arglist = [self._qt.project_id, '--healthmonitor', '-1', '--listener', + '1', '--loadbalancer', '2', '--member', '3', '--pool', '4'] + verifylist = [ + ('project', self._qt.project_id), + ('health_monitor', '-1'), + ('listener', '1'), + ('load_balancer', '2'), + ('member', '3'), + ('pool', '4') + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.quota_set.assert_called_with( + self._qt.project_id, json={'quota': {'health_monitor': '-1', + 'listener': '1', + 'load_balancer': '2', + 'member': '3', + 'pool': '4'}}) + + @mock.patch('octaviaclient.osc.v2.utils.get_quota_attrs') + def test_quota_set_no_args(self, mock_attrs): + project_id = ['fake_project_id'] + mock_attrs.return_value = {'project_id': project_id} + + arglist = [project_id] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + self.assertNotCalled(self.api_mock.quota_set) + + +class TestQuotaReset(TestQuota): + + def setUp(self): + super(TestQuotaReset, self).setUp() + self.cmd = quota.ResetQuota(self.app, None) + + @mock.patch('octaviaclient.osc.v2.utils.get_quota_attrs') + def test_quota_reset(self, mock_attrs): + # create new quota, otherwise other quota tests will fail occasionally + # due to a race condition + project_id = 'fake_project_id' + attrs = {'project_id': project_id} + qt_reset = qt_fakes.FakeQT.create_one_quota(attrs) + + mock_attrs.return_value = qt_reset.to_dict() + + arglist = [project_id] + verifylist = [ + ('project', project_id) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + self.api_mock.quota_reset.assert_called_with( + project_id=qt_reset.project_id) diff --git a/releasenotes/notes/add-quota-support-effed2cf2a8c7ad4.yaml b/releasenotes/notes/add-quota-support-effed2cf2a8c7ad4.yaml new file mode 100644 index 0000000..c4702f2 --- /dev/null +++ b/releasenotes/notes/add-quota-support-effed2cf2a8c7ad4.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Octavia quota support for the OpenStack client plugin. + + * List, show, set, reset quotas + * Show quota defaults diff --git a/setup.cfg b/setup.cfg index cc604f3..582c153 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,6 +63,11 @@ openstack.load_balancer.v2 = loadbalancer_healthmonitor_show = octaviaclient.osc.v2.health_monitor:ShowHealthMonitor loadbalancer_healthmonitor_delete = octaviaclient.osc.v2.health_monitor:DeleteHealthMonitor loadbalancer_healthmonitor_set = octaviaclient.osc.v2.health_monitor:SetHealthMonitor + loadbalancer_quota_list = octaviaclient.osc.v2.quota:ListQuota + loadbalancer_quota_show = octaviaclient.osc.v2.quota:ShowQuota + loadbalancer_quota_defaults_show = octaviaclient.osc.v2.quota:ShowQuotaDefaults + loadbalancer_quota_reset = octaviaclient.osc.v2.quota:ResetQuota + loadbalancer_quota_set = octaviaclient.osc.v2.quota:SetQuota [build_sphinx] source-dir = doc/source