diff options
-rw-r--r-- | glanceclient/tests/unit/v2/test_info.py | 37 | ||||
-rw-r--r-- | glanceclient/tests/unit/v2/test_shell_v2.py | 62 | ||||
-rw-r--r-- | glanceclient/v2/client.py | 3 | ||||
-rw-r--r-- | glanceclient/v2/images.py | 7 | ||||
-rw-r--r-- | glanceclient/v2/info.py | 23 | ||||
-rw-r--r-- | glanceclient/v2/shell.py | 17 | ||||
-rw-r--r-- | setup.cfg | 1 | ||||
-rw-r--r-- | tox.ini | 14 |
8 files changed, 157 insertions, 7 deletions
diff --git a/glanceclient/tests/unit/v2/test_info.py b/glanceclient/tests/unit/v2/test_info.py new file mode 100644 index 0000000..645c15c --- /dev/null +++ b/glanceclient/tests/unit/v2/test_info.py @@ -0,0 +1,37 @@ +# Copyright 2022 Red Hat, Inc. +# +# 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 testtools +from unittest import mock + +from glanceclient.v2 import info + + +class TestController(testtools.TestCase): + def setUp(self): + super(TestController, self).setUp() + self.fake_client = mock.MagicMock() + self.info_controller = info.Controller(self.fake_client, None) + + def test_get_usage(self): + fake_usage = { + 'usage': { + 'quota1': {'limit': 10, 'usage': 0}, + 'quota2': {'limit': 20, 'usage': 5}, + } + } + self.fake_client.get.return_value = (mock.MagicMock(), fake_usage) + usage = self.info_controller.get_usage() + self.assertEqual(fake_usage['usage'], usage) + self.fake_client.get.assert_called_once_with('/v2/info/usage') diff --git a/glanceclient/tests/unit/v2/test_shell_v2.py b/glanceclient/tests/unit/v2/test_shell_v2.py index 057cacd..e91045d 100644 --- a/glanceclient/tests/unit/v2/test_shell_v2.py +++ b/glanceclient/tests/unit/v2/test_shell_v2.py @@ -150,8 +150,44 @@ class ShellV2Test(testtools.TestCase): ] } + stores_info_detail_response = { + "stores": [ + { + "default": "true", + "id": "ceph1", + "type": "rbd", + "description": "RBD backend for glance.", + "properties": { + "pool": "pool1", + "chunk_size": "4" + } + }, + { + "id": "file2", + "type": "file", + "description": "Filesystem backend for glance.", + "properties": {} + }, + { + "id": "file1", + "type": "file", + "description": "Filesystem backend for gkance.", + "properties": {} + }, + { + "id": "ceph2", + "type": "rbd", + "description": "RBD backend for glance.", + "properties": { + "pool": "pool2", + "chunk_size": "4" + } + } + ] + } + def test_do_stores_info(self): - args = [] + args = self._make_args({'detail': False}) with mock.patch.object(self.gc.images, 'get_stores_info') as mocked_list: mocked_list.return_value = self.stores_info_response @@ -166,7 +202,7 @@ class ShellV2Test(testtools.TestCase): def test_neg_stores_info( self, mock_stdin, mock_utils_exit): expected_msg = ('Multi Backend support is not enabled') - args = [] + args = self._make_args({'detail': False}) mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_stores_info') as mocked_info: @@ -178,6 +214,18 @@ class ShellV2Test(testtools.TestCase): pass mock_utils_exit.assert_called_once_with(expected_msg) + def test_do_stores_info_with_detail(self): + args = self._make_args({'detail': True}) + with mock.patch.object(self.gc.images, + 'get_stores_info_detail') as mocked_list: + mocked_list.return_value = self.stores_info_detail_response + + test_shell.do_stores_info(self.gc, args) + + mocked_list.assert_called_once_with() + utils.print_dict.assert_called_once_with( + self.stores_info_detail_response) + @mock.patch('sys.stderr') def test_image_create_missing_disk_format(self, __): e = self.assertRaises(exc.CommandError, self._run_command, @@ -614,6 +662,16 @@ class ShellV2Test(testtools.TestCase): mock_exit.assert_called_once_with( 'Server does not support image tasks API (v2.12)') + def test_usage(self): + with mock.patch.object(self.gc.info, 'get_usage') as mock_usage: + mock_usage.return_value = {'quota1': {'limit': 10, 'usage': 0}, + 'quota2': {'limit': 20, 'usage': 5}} + test_shell.do_usage(self.gc, []) + utils.print_dict_list.assert_called_once_with( + [{'quota': 'quota1', 'limit': 10, 'usage': 0}, + {'quota': 'quota2', 'limit': 20, 'usage': 5}], + ['Quota', 'Limit', 'Usage']) + @mock.patch('sys.stdin', autospec=True) def test_do_image_create_no_user_props(self, mock_stdin): args = self._make_args({'name': 'IMG-01', 'disk_format': 'vhd', diff --git a/glanceclient/v2/client.py b/glanceclient/v2/client.py index 279be63..8b96bc7 100644 --- a/glanceclient/v2/client.py +++ b/glanceclient/v2/client.py @@ -19,6 +19,7 @@ from glanceclient.common import utils from glanceclient.v2 import image_members from glanceclient.v2 import image_tags from glanceclient.v2 import images +from glanceclient.v2 import info from glanceclient.v2 import metadefs from glanceclient.v2 import schemas from glanceclient.v2 import tasks @@ -48,6 +49,8 @@ class Client(object): self.image_members = image_members.Controller(self.http_client, self.schemas) + self.info = info.Controller(self.http_client, self.schemas) + self.tasks = tasks.Controller(self.http_client, self.schemas) self.metadefs_resource_type = ( diff --git a/glanceclient/v2/images.py b/glanceclient/v2/images.py index b412c42..eeb5ee1 100644 --- a/glanceclient/v2/images.py +++ b/glanceclient/v2/images.py @@ -323,6 +323,13 @@ class Controller(object): return body, resp @utils.add_req_id_to_object() + def get_stores_info_detail(self): + """Get available stores info from discovery endpoint.""" + url = '/v2/info/stores/detail' + resp, body = self.http_client.get(url) + return body, resp + + @utils.add_req_id_to_object() def delete_from_store(self, store_id, image_id): """Delete image data from specific store.""" url = ('/v2/stores/%(store)s/%(image)s' % {'store': store_id, diff --git a/glanceclient/v2/info.py b/glanceclient/v2/info.py new file mode 100644 index 0000000..1c40567 --- /dev/null +++ b/glanceclient/v2/info.py @@ -0,0 +1,23 @@ +# Copyright 2022 Red Hat, Inc. +# +# 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. + + +class Controller: + def __init__(self, http_client, schema_client): + self.http_client = http_client + self.schema_client = schema_client + + def get_usage(self, **kwargs): + resp, body = self.http_client.get('/v2/info/usage') + return body['usage'] diff --git a/glanceclient/v2/shell.py b/glanceclient/v2/shell.py index 407fa44..05fc464 100644 --- a/glanceclient/v2/shell.py +++ b/glanceclient/v2/shell.py @@ -486,6 +486,15 @@ def do_image_tasks(gc, args): utils.exit('Server does not support image tasks API (v2.12)') +def do_usage(gc, args): + """Get quota usage information.""" + columns = ['Quota', 'Limit', 'Usage'] + usage = gc.info.get_usage() + utils.print_dict_list( + [dict(v, quota=k) for k, v in usage.items()], + columns) + + @utils.arg('--image-id', metavar='<IMAGE_ID>', required=True, help=_('Image to display members of.')) def do_member_list(gc, args): @@ -574,11 +583,15 @@ def do_import_info(gc, args): else: utils.print_dict(import_info) - +@utils.arg('--detail', default=False, action='store_true', + help='Shows details of stores. Admin only.') def do_stores_info(gc, args): """Print available backends from Glance.""" try: - stores_info = gc.images.get_stores_info() + if args.detail: + stores_info = gc.images.get_stores_info_detail() + else: + stores_info = gc.images.get_stores_info() except exc.HTTPNotFound: utils.exit('Multi Backend support is not enabled') else: @@ -23,6 +23,7 @@ classifier = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 [files] packages = @@ -1,5 +1,5 @@ [tox] -envlist = py38,pep8 +envlist = py39,pep8 minversion = 2.0 skipsdist = True @@ -8,8 +8,12 @@ usedevelop = True setenv = OS_STDOUT_NOCAPTURE=False OS_STDERR_NOCAPTURE=False +# Nowadays, TOX_CONSTRAINTS_FILE should be used, but some older scripts might +# still be using UPPER_CONSTRAINTS_FILE, so we check both variables and use the +# first one that is defined. If none of them is defined, we fallback to the +# default value. deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = stestr run --slowest {posargs} @@ -55,8 +59,12 @@ commands = [testenv:releasenotes] basepython = python3 +# Nowadays, TOX_CONSTRAINTS_FILE should be used, but some older scripts might +# still be using UPPER_CONSTRAINTS_FILE, so we check both variables and use the +# first one that is defined. If none of them is defined, we fallback to the +# default value. deps = - -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -c{env:TOX_CONSTRAINTS_FILE:{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html |