From 7937f06a64eb7d6795989b24b50a4fc4cc3c1aa5 Mon Sep 17 00:00:00 2001 From: Vijendar Komalla Date: Fri, 27 Jan 2017 09:08:55 -0600 Subject: [PATCH] Add magnum client support for resource quotas Change-Id: I5fd050a4deedb84c7287f75cf8c606e8c0d4db9a Closes-Bug: #1659125 --- magnumclient/tests/v1/test_quotas.py | 152 +++++++++++++++++++++ magnumclient/tests/v1/test_quotas_shell.py | 117 ++++++++++++++++ magnumclient/v1/client.py | 2 + magnumclient/v1/quotas.py | 78 +++++++++++ magnumclient/v1/quotas_shell.py | 143 +++++++++++++++++++ magnumclient/v1/shell.py | 2 + 6 files changed, 494 insertions(+) create mode 100644 magnumclient/tests/v1/test_quotas.py create mode 100644 magnumclient/tests/v1/test_quotas_shell.py create mode 100644 magnumclient/v1/quotas.py create mode 100644 magnumclient/v1/quotas_shell.py diff --git a/magnumclient/tests/v1/test_quotas.py b/magnumclient/tests/v1/test_quotas.py new file mode 100644 index 00000000..96091a2c --- /dev/null +++ b/magnumclient/tests/v1/test_quotas.py @@ -0,0 +1,152 @@ +# 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 testtools +from testtools import matchers + +from magnumclient.tests import utils +from magnumclient.v1 import quotas + + +QUOTA1 = { + 'id': 123, + 'resource': "Cluster", + 'hard_limit': 5, + 'project_id': 'abc' +} + +QUOTA2 = { + 'id': 124, + 'resource': "Cluster", + 'hard_limit': 10, + 'project_id': 'bcd' +} + +CREATE_QUOTA = copy.deepcopy(QUOTA1) +del CREATE_QUOTA['id'] + +UPDATED_QUOTA = copy.deepcopy(QUOTA2) +NEW_HARD_LIMIT = 20 +UPDATED_QUOTA['hard_limit'] = NEW_HARD_LIMIT + +fake_responses = { + '/v1/quotas?all_tenants=True': + { + 'GET': ( + {}, + {'quotas': [QUOTA1, QUOTA2]}, + ), + }, + '/v1/quotas': + { + 'GET': ( + {}, + {'quotas': [QUOTA1]}, + ), + 'POST': ( + {}, + QUOTA1, + ), + }, + '/v1/quotas/%(id)s/%(res)s' % {'id': QUOTA2['project_id'], + 'res': QUOTA2['resource']}: + { + 'GET': ( + {}, + QUOTA2, + ), + 'PATCH': ( + {}, + UPDATED_QUOTA, + ), + 'DELETE': ( + {}, + None, + ), + }, +} + + +class QuotasManagerTest(testtools.TestCase): + + def setUp(self): + super(QuotasManagerTest, self).setUp() + self.api = utils.FakeAPI(fake_responses) + self.mgr = quotas.QuotasManager(self.api) + + def test_list_quotas(self): + quotas = self.mgr.list() + expect = [ + ('GET', '/v1/quotas', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(quotas, matchers.HasLength(1)) + + def test_list_quotas_all(self): + quotas = self.mgr.list(all_tenants=True) + expect = [ + ('GET', '/v1/quotas?all_tenants=True', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(quotas, matchers.HasLength(2)) + + def test_show_project_resource_quota(self): + expect = [ + ('GET', + '/v1/quotas/%(id)s/%(res)s' % {'id': QUOTA2['project_id'], + 'res': QUOTA2['resource']}, + {}, + None), + ] + quotas = self.mgr.get(QUOTA2['project_id'], QUOTA2['resource']) + self.assertEqual(expect, self.api.calls) + expected_quotas = QUOTA2 + self.assertEqual(expected_quotas, quotas._info) + + def test_quota_create(self): + quota = self.mgr.create(**CREATE_QUOTA) + expect = [ + ('POST', '/v1/quotas', {}, CREATE_QUOTA), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(QUOTA1, quota._info) + + def test_quota_update(self): + patch = { + 'resource': "Cluster", + 'hard_limit': NEW_HARD_LIMIT, + 'project_id': 'bcd' + } + quota = self.mgr.update(id=QUOTA2['project_id'], + resource=QUOTA2['resource'], + patch=patch) + expect = [ + ('PATCH', '/v1/quotas/%(id)s/%(res)s' % { + 'id': QUOTA2['project_id'], + 'res': QUOTA2['resource']}, {}, patch), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(NEW_HARD_LIMIT, quota.hard_limit) + + def test_quota_delete(self): + quota = self.mgr.delete(QUOTA2['project_id'], QUOTA2['resource']) + expect = [ + ('DELETE', + '/v1/quotas/%(id)s/%(res)s' % {'id': QUOTA2['project_id'], + 'res': QUOTA2['resource']}, + {}, + None), + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(quota) diff --git a/magnumclient/tests/v1/test_quotas_shell.py b/magnumclient/tests/v1/test_quotas_shell.py new file mode 100644 index 00000000..0f4f2691 --- /dev/null +++ b/magnumclient/tests/v1/test_quotas_shell.py @@ -0,0 +1,117 @@ +# 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 + +from magnumclient.tests.v1 import shell_test_base + + +class ShellTest(shell_test_base.TestCommandLineArgument): + + def _get_expected_args_list(self, marker=None, limit=None, sort_dir=None, + sort_key=None, all_tenants=False): + expected_args = {} + expected_args['marker'] = marker + expected_args['limit'] = limit + expected_args['sort_dir'] = sort_dir + expected_args['sort_key'] = sort_key + expected_args['all_tenants'] = False + return expected_args + + def _get_expected_args_create(self, project_id, resource, hard_limit): + expected_args = {} + expected_args['project_id'] = project_id + expected_args['resource'] = resource + expected_args['hard_limit'] = hard_limit + return expected_args + + @mock.patch('magnumclient.v1.quotas.QuotasManager.list') + def test_quotas_list_success(self, mock_list): + self._test_arg_success('quotas-list') + expected_args = self._get_expected_args_list() + mock_list.assert_called_once_with(**expected_args) + + @mock.patch('magnumclient.v1.quotas.QuotasManager.list') + def test_quotas_list_failure(self, mock_list): + self._test_arg_failure('quotas-list --wrong', + self._unrecognized_arg_error) + mock_list.assert_not_called() + + @mock.patch('magnumclient.v1.quotas.QuotasManager.create') + def test_quotas_create_success(self, mock_create): + self._test_arg_success('quotas-create --project-id abc ' + '--resource Cluster ' + '--hard-limit 15') + expected_args = self._get_expected_args_create('abc', 'Cluster', 15) + mock_create.assert_called_with(**expected_args) + + @mock.patch('magnumclient.v1.quotas.QuotasManager.create') + def test_quotas_create_failure_only_project_id(self, mock_create): + self._test_arg_failure('quotas-create --project-id abc', + self._mandatory_arg_error) + mock_create.assert_not_called() + + @mock.patch('magnumclient.v1.quotas.QuotasManager.create') + def test_quotas_create_failure_only_resource(self, mock_create): + self._test_arg_failure('quotas-create --resource Cluster', + self._mandatory_arg_error) + mock_create.assert_not_called() + + @mock.patch('magnumclient.v1.quotas.QuotasManager.create') + def test_quotas_create_failure_only_hard_limit(self, mock_create): + self._test_arg_failure('quotas-create --hard-limit 10', + self._mandatory_arg_error) + mock_create.assert_not_called() + + @mock.patch('magnumclient.v1.quotas.QuotasManager.create') + def test_quotas_create_failure_no_arg(self, mock_create): + self._test_arg_failure('quotas-create', + self._mandatory_arg_error) + mock_create.assert_not_called() + + @mock.patch('magnumclient.v1.quotas.QuotasManager.delete') + def test_quotas_delete_success(self, mock_delete): + self._test_arg_success( + 'quotas-delete --project-id xxx --resource Cluster') + mock_delete.assert_called_once_with('xxx', 'Cluster') + + @mock.patch('magnumclient.v1.quotas.QuotasManager.delete') + def test_quotas_delete_failure_no_project_id(self, mock_delete): + self._test_arg_failure('quotas-delete --resource Cluster', + self._mandatory_arg_error) + mock_delete.assert_not_called() + + @mock.patch('magnumclient.v1.quotas.QuotasManager.delete') + def test_quotas_delete_failure_no_resource(self, mock_delete): + self._test_arg_failure('quotas-delete --project-id xxx', + self._mandatory_arg_error) + mock_delete.assert_not_called() + + @mock.patch('magnumclient.v1.quotas.QuotasManager.get') + def test_quotas_show_success(self, mock_show): + self._test_arg_success('quotas-show --project-id abc ' + '--resource Cluster') + mock_show.assert_called_once_with('abc', 'Cluster') + + @mock.patch('magnumclient.v1.quotas.QuotasManager.get') + def test_quotas_show_failure_no_arg(self, mock_show): + self._test_arg_failure('quotas-show', + self._mandatory_arg_error) + mock_show.assert_not_called() + + @mock.patch('magnumclient.v1.quotas.QuotasManager.update') + def test_quotas_update_success(self, mock_update): + self._test_arg_success('quotas-update --project-id abc ' + '--resource Cluster ' + '--hard-limit 20') + patch = {'project_id': 'abc', 'resource': 'Cluster', 'hard_limit': 20} + mock_update.assert_called_once_with('abc', 'Cluster', patch) diff --git a/magnumclient/v1/client.py b/magnumclient/v1/client.py index 15e9f5a5..4bb4f152 100644 --- a/magnumclient/v1/client.py +++ b/magnumclient/v1/client.py @@ -25,6 +25,7 @@ from magnumclient.v1 import certificates from magnumclient.v1 import cluster_templates from magnumclient.v1 import clusters from magnumclient.v1 import mservices +from magnumclient.v1 import quotas from magnumclient.v1 import stats profiler = importutils.try_import("osprofiler.profiler") @@ -213,3 +214,4 @@ class Client(object): # initialization of osprofiler on the server side. profiler.init(profile) self.stats = stats.StatsManager(self.http_client) + self.quotas = quotas.QuotasManager(self.http_client) diff --git a/magnumclient/v1/quotas.py b/magnumclient/v1/quotas.py new file mode 100644 index 00000000..833669b3 --- /dev/null +++ b/magnumclient/v1/quotas.py @@ -0,0 +1,78 @@ +# 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 magnumclient.common import utils +from magnumclient import exceptions +from magnumclient.v1 import basemodels + + +CREATION_ATTRIBUTES = ['project_id', 'resource', 'hard_limit'] + + +class Quotas(basemodels.BaseModel): + model_name = "Quotas" + + +class QuotasManager(basemodels.BaseModelManager): + api_name = "quotas" + resource_class = Quotas + + @staticmethod + def _path(id=None, resource=None): + if not id: + return '/v1/quotas' + + return '/v1/quotas/%(id)s/%(res)s' % {'id': id, 'res': resource} + + def list(self, limit=None, marker=None, sort_key=None, + sort_dir=None, all_tenants=False): + + if limit is not None: + limit = int(limit) + + filters = utils.common_filters(marker, limit, sort_key, sort_dir) + + if all_tenants: + filters.append('all_tenants=True') + + path = self._path() + if filters: + path += '?' + '&'.join(filters) + + if limit is None: + return self._list(path, self.api_name) + else: + return self._list_pagination(path, self.api_name, + limit=limit) + + def get(self, id, resource): + try: + return self._list(self._path(id, resource))[0] + except IndexError: + return None + + def create(self, **kwargs): + new = {} + for (key, value) in kwargs.items(): + if key in CREATION_ATTRIBUTES: + new[key] = value + else: + raise exceptions.InvalidAttribute( + "Key must be in %s" % ",".join(CREATION_ATTRIBUTES)) + return self._create(self._path(), new) + + def delete(self, id, resource): + return self._delete(self._path(id, resource)) + + def update(self, id, resource, patch): + url = self._path(id, resource) + return self._update(url, patch) diff --git a/magnumclient/v1/quotas_shell.py b/magnumclient/v1/quotas_shell.py new file mode 100644 index 00000000..ff98c009 --- /dev/null +++ b/magnumclient/v1/quotas_shell.py @@ -0,0 +1,143 @@ +# 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 magnumclient.common import cliutils as utils +from magnumclient.common import utils as magnum_utils +from magnumclient.i18n import _ + + +def _show_quota(quota): + utils.print_dict(quota._info) + + +@utils.arg('--marker', + metavar='', + default=None, + help=_('The last quota UUID of the previous page; ' + 'displays list of quotas after "marker".')) +@utils.arg('--limit', + metavar='', + type=int, + help=_('Maximum number of quotas to return.')) +@utils.arg('--sort-key', + metavar='', + help=_('Column to sort results by.')) +@utils.arg('--sort-dir', + metavar='', + choices=['desc', 'asc'], + help=_('Direction to sort. "asc" or "desc".')) +@utils.arg('--all-tenants', + action='store_true', + default=False, + help=_('Flag to indicate list all tenant quotas.')) +def do_quotas_list(cs, args): + """Print a list of available quotas.""" + quotas = cs.quotas.list(marker=args.marker, + limit=args.limit, + sort_key=args.sort_key, + sort_dir=args.sort_dir, + all_tenants=args.all_tenants) + columns = ['project_id', 'resource', 'hard_limit'] + utils.print_list(quotas, columns, + {'versions': magnum_utils.print_list_field('versions')}, + sortby_index=None) + + +@utils.arg('--project-id', + required=True, + metavar='', + help=_('Project Id.')) +@utils.arg('--resource', + required=True, + metavar='', + help=_('Resource name.')) +@utils.arg('--hard-limit', + metavar='', + type=int, + default=1, + help=_('Max resource limit.')) +def do_quotas_create(cs, args): + """Create a quota.""" + + opts = dict() + opts['project_id'] = args.project_id + opts['resource'] = args.resource + opts['hard_limit'] = args.hard_limit + + try: + quota = cs.quotas.create(**opts) + _show_quota(quota) + except Exception as e: + print("Create quota for project_id %(id)s resource %(res)s failed: " + "%(e)s" % {'id': args.project_id, + 'res': args.resource, + 'e': e.details}) + + +@utils.arg('--project-id', + required=True, + metavar='', + help=_('Project ID.')) +@utils.arg('--resource', + required=True, + metavar='', + help=_('Resource name')) +def do_quotas_delete(cs, args): + """Delete specified resource quota.""" + try: + cs.quotas.delete(args.project_id, args.resource) + print("Request to delete quota for project id %(id)s and resource " + "%(res)s has been accepted." % { + 'id': args.project_id, 'res': args.resource}) + except Exception as e: + print("Quota delete failed for project id %(id)s and resource " + "%(res)s :%(e)s" % {'id': args.project_id, + 'res': args.resource, + 'e': e.details}) + + +@utils.arg('--project-id', + required=True, + metavar='', + help=_('Project ID.')) +@utils.arg('--resource', + required=True, + metavar='', + help=_('Resource name')) +def do_quotas_show(cs, args): + """Show details about the given project resource quota.""" + quota = cs.quotas.get(args.project_id, args.resource) + _show_quota(quota) + + +@utils.arg('--project-id', + required=True, + metavar='', + help=_('Project Id.')) +@utils.arg('--resource', + required=True, + metavar='', + help=_('Resource name.')) +@utils.arg('--hard-limit', + metavar='', + type=int, + default=1, + help=_('Max resource limit.')) +def do_quotas_update(cs, args): + """Update information about the given project resource quota.""" + patch = dict() + patch['project_id'] = args.project_id + patch['resource'] = args.resource + patch['hard_limit'] = args.hard_limit + + quota = cs.quotas.update(args.project_id, args.resource, patch) + _show_quota(quota) diff --git a/magnumclient/v1/shell.py b/magnumclient/v1/shell.py index a095d407..3f9125d5 100644 --- a/magnumclient/v1/shell.py +++ b/magnumclient/v1/shell.py @@ -19,6 +19,7 @@ from magnumclient.v1 import certificates_shell from magnumclient.v1 import cluster_templates_shell from magnumclient.v1 import clusters_shell from magnumclient.v1 import mservices_shell +from magnumclient.v1 import quotas_shell from magnumclient.v1 import stats_shell COMMAND_MODULES = [ @@ -29,4 +30,5 @@ COMMAND_MODULES = [ cluster_templates_shell, mservices_shell, stats_shell, + quotas_shell, ]