From d7fbd0a516596a56ce7728142621c9034e6c82b7 Mon Sep 17 00:00:00 2001 From: Abhishek Kekane Date: Fri, 29 Jun 2018 10:29:04 +0000 Subject: [PATCH] Add support for hide old images Added --hidden argument to list, create and update call. Related to blueprint hidden-images Change-Id: I1f2dcaa545c9da883186b20a96a70c7df994b994 --- glanceclient/tests/unit/v2/test_shell_v2.py | 131 ++++++++++++++++++-- glanceclient/v2/image_schema.py | 5 + glanceclient/v2/shell.py | 50 ++++++-- 3 files changed, 171 insertions(+), 15 deletions(-) diff --git a/glanceclient/tests/unit/v2/test_shell_v2.py b/glanceclient/tests/unit/v2/test_shell_v2.py index 83fa5260..6eeca83f 100644 --- a/glanceclient/tests/unit/v2/test_shell_v2.py +++ b/glanceclient/tests/unit/v2/test_shell_v2.py @@ -263,7 +263,8 @@ class ShellV2Test(testtools.TestCase): 'sort_key': ['name', 'id'], 'sort_dir': ['desc', 'asc'], 'sort': None, - 'verbose': False + 'verbose': False, + 'os_hidden': False } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: @@ -276,7 +277,44 @@ class ShellV2Test(testtools.TestCase): 'member_status': 'Fake', 'visibility': True, 'checksum': 'fake_checksum', - 'tag': 'fake tag' + 'tag': 'fake tag', + 'os_hidden': False + } + mocked_list.assert_called_once_with(page_size=18, + sort_key=['name', 'id'], + sort_dir=['desc', 'asc'], + filters=exp_img_filters) + utils.print_list.assert_called_once_with({}, ['ID', 'Name']) + + def test_do_image_list_with_hidden_true(self): + input = { + 'limit': None, + 'page_size': 18, + 'visibility': True, + 'member_status': 'Fake', + 'owner': 'test', + 'checksum': 'fake_checksum', + 'tag': 'fake tag', + 'properties': [], + 'sort_key': ['name', 'id'], + 'sort_dir': ['desc', 'asc'], + 'sort': None, + 'verbose': False, + 'os_hidden': True + } + args = self._make_args(input) + with mock.patch.object(self.gc.images, 'list') as mocked_list: + mocked_list.return_value = {} + + test_shell.do_image_list(self.gc, args) + + exp_img_filters = { + 'owner': 'test', + 'member_status': 'Fake', + 'visibility': True, + 'checksum': 'fake_checksum', + 'tag': 'fake tag', + 'os_hidden': True } mocked_list.assert_called_once_with(page_size=18, sort_key=['name', 'id'], @@ -297,7 +335,8 @@ class ShellV2Test(testtools.TestCase): 'sort_key': ['name'], 'sort_dir': ['desc'], 'sort': None, - 'verbose': False + 'verbose': False, + 'os_hidden': False } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: @@ -310,7 +349,8 @@ class ShellV2Test(testtools.TestCase): 'member_status': 'Fake', 'visibility': True, 'checksum': 'fake_checksum', - 'tag': 'fake tag' + 'tag': 'fake tag', + 'os_hidden': False } mocked_list.assert_called_once_with(page_size=18, sort_key=['name'], @@ -331,7 +371,8 @@ class ShellV2Test(testtools.TestCase): 'sort': 'name:desc,size:asc', 'sort_key': [], 'sort_dir': [], - 'verbose': False + 'verbose': False, + 'os_hidden': False } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: @@ -344,7 +385,8 @@ class ShellV2Test(testtools.TestCase): 'member_status': 'Fake', 'visibility': True, 'checksum': 'fake_checksum', - 'tag': 'fake tag' + 'tag': 'fake tag', + 'os_hidden': False } mocked_list.assert_called_once_with( page_size=18, @@ -365,7 +407,8 @@ class ShellV2Test(testtools.TestCase): 'sort_key': ['name'], 'sort_dir': ['desc'], 'sort': None, - 'verbose': False + 'verbose': False, + 'os_hidden': False } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: @@ -380,7 +423,8 @@ class ShellV2Test(testtools.TestCase): 'checksum': 'fake_checksum', 'tag': 'fake tag', 'os_distro': 'NixOS', - 'architecture': 'x86_64' + 'architecture': 'x86_64', + 'os_hidden': False } mocked_list.assert_called_once_with(page_size=1, @@ -527,6 +571,35 @@ class ShellV2Test(testtools.TestCase): except Exception: pass + @mock.patch('sys.stdin', autospec=True) + def test_do_image_create_hidden_image(self, mock_stdin): + args = self._make_args({'name': 'IMG-01', 'disk_format': 'vhd', + 'container_format': 'bare', + 'file': None, + 'os_hidden': True}) + with mock.patch.object(self.gc.images, 'create') as mocked_create: + ignore_fields = ['self', 'access', 'file', 'schema'] + expect_image = dict([(field, field) for field in ignore_fields]) + expect_image['id'] = 'pass' + expect_image['name'] = 'IMG-01' + expect_image['disk_format'] = 'vhd' + expect_image['container_format'] = 'bare' + expect_image['os_hidden'] = True + mocked_create.return_value = expect_image + + # Ensure that the test stdin is not considered + # to be supplying image data + mock_stdin.isatty = lambda: True + test_shell.do_image_create(self.gc, args) + + mocked_create.assert_called_once_with(name='IMG-01', + disk_format='vhd', + container_format='bare', + os_hidden=True) + utils.print_dict.assert_called_once_with({ + 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', + 'container_format': 'bare', 'os_hidden': True}) + def test_do_image_create_with_file(self): self.mock_get_data_file.return_value = six.StringIO() try: @@ -1256,6 +1329,48 @@ class ShellV2Test(testtools.TestCase): 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare'}) + def test_do_image_update_hide_image(self): + args = self._make_args({'id': 'pass', 'os_hidden': 'true'}) + with mock.patch.object(self.gc.images, 'update') as mocked_update: + ignore_fields = ['self', 'access', 'file', 'schema'] + expect_image = dict([(field, field) for field in ignore_fields]) + expect_image['id'] = 'pass' + expect_image['name'] = 'IMG-01' + expect_image['disk_format'] = 'vhd' + expect_image['container_format'] = 'bare' + expect_image['os_hidden'] = True + mocked_update.return_value = expect_image + + test_shell.do_image_update(self.gc, args) + + mocked_update.assert_called_once_with('pass', + None, + os_hidden='true') + utils.print_dict.assert_called_once_with({ + 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', + 'container_format': 'bare', 'os_hidden': True}) + + def test_do_image_update_revert_hide_image(self): + args = self._make_args({'id': 'pass', 'os_hidden': 'false'}) + with mock.patch.object(self.gc.images, 'update') as mocked_update: + ignore_fields = ['self', 'access', 'file', 'schema'] + expect_image = dict([(field, field) for field in ignore_fields]) + expect_image['id'] = 'pass' + expect_image['name'] = 'IMG-01' + expect_image['disk_format'] = 'vhd' + expect_image['container_format'] = 'bare' + expect_image['os_hidden'] = False + mocked_update.return_value = expect_image + + test_shell.do_image_update(self.gc, args) + + mocked_update.assert_called_once_with('pass', + None, + os_hidden='false') + utils.print_dict.assert_called_once_with({ + 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', + 'container_format': 'bare', 'os_hidden': False}) + def test_do_image_update_with_user_props(self): args = self._make_args({'id': 'pass', 'name': 'IMG-01', 'property': ['myprop=myval']}) diff --git a/glanceclient/v2/image_schema.py b/glanceclient/v2/image_schema.py index 247faf8e..f3a58e57 100644 --- a/glanceclient/v2/image_schema.py +++ b/glanceclient/v2/image_schema.py @@ -213,6 +213,11 @@ _BASE_SCHEMA = { "type": "boolean", "description": "If true, image will not be deletable." }, + "os_hidden": { + "type": "boolean", + "description": "If true, image will not appear in default " + "image list response." + }, "architecture": { "type": "string", "description": ("Operating system architecture as specified " diff --git a/glanceclient/v2/shell.py b/glanceclient/v2/shell.py index ddaca53c..aaa85bb4 100644 --- a/glanceclient/v2/shell.py +++ b/glanceclient/v2/shell.py @@ -13,8 +13,12 @@ # License for the specific language governing permissions and limitations # under the License. +import json +import os import sys +from oslo_utils import strutils + from glanceclient._i18n import _ from glanceclient.common import progressbar from glanceclient.common import utils @@ -25,8 +29,6 @@ from glanceclient.v2 import images from glanceclient.v2 import namespace_schema from glanceclient.v2 import resource_type_schema from glanceclient.v2 import tasks -import json -import os MEMBER_STATUS_VALUES = image_members.MEMBER_STATUS_VALUES IMAGE_SCHEMA = None @@ -49,8 +51,17 @@ def get_image_schema(): @utils.schema_args(get_image_schema, omit=['created_at', 'updated_at', 'file', 'checksum', 'virtual_size', 'size', 'status', 'schema', 'direct_url', - 'locations', 'self', + 'locations', 'self', 'os_hidden', 'os_hash_value', 'os_hash_algo']) +# NOTE(rosmaita): to make this option more intuitive for end users, we +# do not use the Glance image property name 'os_hidden' here. This means +# we must include 'os_hidden' in the 'omit' list above and handle the +# --hidden argument by hand +@utils.arg('--hidden', type=strutils.bool_from_string, metavar='[True|False]', + default=None, + dest='os_hidden', + help=("If true, image will not appear in default image list " + "response.")) @utils.arg('--property', metavar="", action='append', default=[], help=_('Arbitrary property to associate with image.' ' May be used multiple times.')) @@ -68,6 +79,7 @@ def do_image_create(gc, args): """Create a new image.""" schema = gc.schemas.get("image") _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()] + fields = dict(filter(lambda x: x[1] is not None and (x[0] == 'property' or schema.is_core_property(x[0])), @@ -108,8 +120,14 @@ def do_image_create(gc, args): @utils.schema_args(get_image_schema, omit=['created_at', 'updated_at', 'file', 'checksum', 'virtual_size', 'size', 'status', 'schema', 'direct_url', - 'locations', 'self', + 'locations', 'self', 'os_hidden', 'os_hash_value', 'os_hash_algo']) +# NOTE: --hidden requires special handling; see note at do_image_create +@utils.arg('--hidden', type=strutils.bool_from_string, metavar='[True|False]', + default=None, + dest='os_hidden', + help=("If true, image will not appear in default image list " + "response.")) @utils.arg('--property', metavar="", action='append', default=[], help=_('Arbitrary property to associate with image.' ' May be used multiple times.')) @@ -152,6 +170,7 @@ def do_image_create_via_import(gc, args): """ schema = gc.schemas.get("image") _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()] + fields = dict(filter(lambda x: x[1] is not None and (x[0] == 'property' or schema.is_core_property(x[0])), @@ -258,8 +277,14 @@ def _validate_backend(backend, gc): 'updated_at', 'file', 'checksum', 'virtual_size', 'size', 'status', 'schema', 'direct_url', 'tags', - 'self', 'os_hash_value', - 'os_hash_algo']) + 'self', 'os_hidden', + 'os_hash_value', 'os_hash_algo']) +# NOTE: --hidden requires special handling; see note at do_image_create +@utils.arg('--hidden', type=strutils.bool_from_string, metavar='[True|False]', + default=None, + dest='os_hidden', + help=("If true, image will not appear in default image list " + "response.")) @utils.arg('--property', metavar="", action='append', default=[], help=_('Arbitrary property to associate with image.' ' May be used multiple times.')) @@ -269,6 +294,7 @@ def do_image_update(gc, args): """Update an existing image.""" schema = gc.schemas.get("image") _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()] + fields = dict(filter(lambda x: x[1] is not None and (x[0] in ['property', 'remove_property'] or schema.is_core_property(x[0])), @@ -314,10 +340,20 @@ def do_image_update(gc, args): help=(_("Comma-separated list of sort keys and directions in the " "form of [:]. Valid keys: %s. OPTIONAL." ) % ', '.join(images.SORT_KEY_VALUES))) +@utils.arg('--hidden', + dest='os_hidden', + metavar='[True|False]', + default=None, + type=strutils.bool_from_string, + const=True, + nargs='?', + help="Filters results by hidden status. Default=None.") def do_image_list(gc, args): """List images you can access.""" - filter_keys = ['visibility', 'member_status', 'owner', 'checksum', 'tag'] + filter_keys = ['visibility', 'member_status', 'owner', 'checksum', 'tag', + 'os_hidden'] filter_items = [(key, getattr(args, key)) for key in filter_keys] + if args.properties: filter_properties = [prop.split('=', 1) for prop in args.properties] if any(len(pair) != 2 for pair in filter_properties):