From a56b5936e2994e35daca6f85d83f637553ffd4ad Mon Sep 17 00:00:00 2001 From: Andrey Pavlov Date: Mon, 28 Sep 2015 17:57:30 +0300 Subject: [PATCH] Adding Images support to CLI Adding Images commands to Sahara OpenstackClient plugin: $ dataprocessing image list $ dataprocessing image show $ dataprocessing image register $ dataprocessing image unregister $ dataprocessing image tags set $ dataprocessing image tags add $ dataprocessing image tags remove Partially implements: blueprint cli-as-openstackclient-plugin Change-Id: Ib84fbb76b6cbc3a321efeb54c88bc58c76fb063e --- saharaclient/osc/v1/images.py | 306 +++++++++++++++ saharaclient/osc/v1/utils.py | 4 + saharaclient/tests/unit/osc/v1/test_images.py | 349 ++++++++++++++++++ setup.cfg | 8 + 4 files changed, 667 insertions(+) create mode 100644 saharaclient/osc/v1/images.py create mode 100644 saharaclient/tests/unit/osc/v1/test_images.py diff --git a/saharaclient/osc/v1/images.py b/saharaclient/osc/v1/images.py new file mode 100644 index 00000000..11e164d0 --- /dev/null +++ b/saharaclient/osc/v1/images.py @@ -0,0 +1,306 @@ +# Copyright (c) 2015 Mirantis 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. + +from cliff import command +from cliff import lister +from cliff import show +from openstackclient.common import utils as osc_utils +from oslo_log import log as logging + +from saharaclient.osc.v1 import utils + + +class ListImages(lister.Lister): + """Lists registered images""" + + log = logging.getLogger(__name__ + ".ListImages") + + def get_parser(self, prog_name): + parser = super(ListImages, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output', + ) + parser.add_argument( + '--name', + metavar="", + help="Regular expression to match image name" + ) + parser.add_argument( + '--tags', + metavar="", + nargs="+", + help="List images with specific tag(s)" + ) + parser.add_argument( + '--username', + metavar="", + help="List images with specific username" + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + client = self.app.client_manager.data_processing + search_opts = {'tags': parsed_args.tags} if parsed_args.tags else {} + + data = client.images.list(search_opts=search_opts) + + if parsed_args.name: + data = utils.get_by_name_substring(data, parsed_args.name) + + if parsed_args.username: + data = [i for i in data if parsed_args.username in i.username] + + if parsed_args.long: + columns = ('name', 'id', 'username', 'tags', 'status', + 'description') + column_headers = [c.capitalize() for c in columns] + + else: + columns = ('name', 'id', 'username', 'tags') + column_headers = [c.capitalize() for c in columns] + + return ( + column_headers, + (osc_utils.get_item_properties( + s, + columns, + formatters={ + 'tags': osc_utils.format_list + }, + ) for s in data) + ) + + +class ShowImage(show.ShowOne): + """Display image details""" + + log = logging.getLogger(__name__ + ".ShowImage") + + def get_parser(self, prog_name): + parser = super(ShowImage, self).get_parser(prog_name) + parser.add_argument( + "image", + metavar="", + help="Name or id of the image to display", + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + client = self.app.client_manager.data_processing + + data = utils.get_resource( + client.images, parsed_args.image).to_dict() + data['tags'] = osc_utils.format_list(data['tags']) + + fields = ['name', 'id', 'username', 'tags', 'status', 'description'] + data = utils.prepare_data(data, fields) + + return self.dict2columns(data) + + +class RegisterImage(show.ShowOne): + """Register an image""" + + log = logging.getLogger(__name__ + ".RegisterImage") + + def get_parser(self, prog_name): + parser = super(RegisterImage, self).get_parser(prog_name) + parser.add_argument( + "image", + metavar="", + help="Id of the image to register", + ) + parser.add_argument( + "--username", + metavar="", + help="Username of privileged user in the image [REQUIRED]", + required=True + ) + parser.add_argument( + "--description", + metavar="", + help="Description of the image", + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + client = self.app.client_manager.data_processing + + description = parsed_args.description or '' + data = client.images.update_image( + parsed_args.image, user_name=parsed_args.username, + desc=description).to_dict() + + data['tags'] = osc_utils.format_list(data['tags']) + + fields = ['name', 'id', 'username', 'tags', 'status', 'description'] + data = utils.prepare_data(data, fields) + + return self.dict2columns(data) + + +class UnregisterImage(command.Command): + """Unregister image(s)""" + + log = logging.getLogger(__name__ + ".RegisterImage") + + def get_parser(self, prog_name): + parser = super(UnregisterImage, self).get_parser(prog_name) + parser.add_argument( + "image", + metavar="", + nargs="+", + help="Name(s) or id(s) of the image(s) to unregister", + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + client = self.app.client_manager.data_processing + for image in parsed_args.image: + image_id = utils.get_resource(client.images, image).id + client.images.unregister_image(image_id) + + +class SetImageTags(show.ShowOne): + """Set image tags (Replace current image tags with provided ones)""" + + log = logging.getLogger(__name__ + ".AddImageTags") + + def get_parser(self, prog_name): + parser = super(SetImageTags, self).get_parser(prog_name) + parser.add_argument( + "image", + metavar="", + help="Name or id of the image", + ) + parser.add_argument( + '--tags', + metavar="", + nargs="+", + required=True, + help="Tag(s) to set [REQUIRED]" + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + client = self.app.client_manager.data_processing + + image_id = utils.get_resource(client.images, parsed_args.image).id + data = client.images.update_tags(image_id, parsed_args.tags).to_dict() + + data['tags'] = osc_utils.format_list(data['tags']) + + fields = ['name', 'id', 'username', 'tags', 'status', 'description'] + data = utils.prepare_data(data, fields) + + return self.dict2columns(data) + + +class AddImageTags(show.ShowOne): + """Add image tags""" + + log = logging.getLogger(__name__ + ".AddImageTags") + + def get_parser(self, prog_name): + parser = super(AddImageTags, self).get_parser(prog_name) + parser.add_argument( + "image", + metavar="", + help="Name or id of the image", + ) + parser.add_argument( + '--tags', + metavar="", + nargs="+", + required=True, + help="Tag(s) to add [REQUIRED]" + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + client = self.app.client_manager.data_processing + + image = utils.get_resource(client.images, parsed_args.image) + parsed_args.tags.extend(image.tags) + data = client.images.update_tags( + image.id, list(set(parsed_args.tags))).to_dict() + + data['tags'] = osc_utils.format_list(data['tags']) + + fields = ['name', 'id', 'username', 'tags', 'status', 'description'] + data = utils.prepare_data(data, fields) + + return self.dict2columns(data) + + +class RemoveImageTags(show.ShowOne): + """Remove image tags""" + + log = logging.getLogger(__name__ + ".RemoveImageTags") + + def get_parser(self, prog_name): + parser = super(RemoveImageTags, self).get_parser(prog_name) + parser.add_argument( + "image", + metavar="", + help="Name or id of the image", + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + '--tags', + metavar="", + nargs="+", + help="Tag(s) to remove" + ), + group.add_argument( + '--all', + action='store_true', + default=False, + help='Remove all tags from image', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + client = self.app.client_manager.data_processing + + image = utils.get_resource(client.images, parsed_args.image) + + if parsed_args.all: + data = client.images.update_tags(image.id, []).to_dict() + else: + parsed_args.tags = parsed_args.tags or [] + new_tags = list(set(image.tags) - set(parsed_args.tags)) + data = client.images.update_tags(image.id, new_tags).to_dict() + + data['tags'] = osc_utils.format_list(data['tags']) + + fields = ['name', 'id', 'username', 'tags', 'status', 'description'] + data = utils.prepare_data(data, fields) + + return self.dict2columns(data) diff --git a/saharaclient/osc/v1/utils.py b/saharaclient/osc/v1/utils.py index 499d0ea0..d21408c1 100644 --- a/saharaclient/osc/v1/utils.py +++ b/saharaclient/osc/v1/utils.py @@ -40,3 +40,7 @@ def prepare_data(data, fields): def prepare_column_headers(columns): return [c.replace('_', ' ').capitalize() for c in columns] + + +def get_by_name_substring(data, name): + return [obj for obj in data if name in obj.name] diff --git a/saharaclient/tests/unit/osc/v1/test_images.py b/saharaclient/tests/unit/osc/v1/test_images.py new file mode 100644 index 00000000..8812d2cd --- /dev/null +++ b/saharaclient/tests/unit/osc/v1/test_images.py @@ -0,0 +1,349 @@ +# Copyright (c) 2015 Mirantis 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. + +from openstackclient.tests import utils as osc_utils + +from saharaclient.api import images as api_images +from saharaclient.osc.v1 import images as osc_images +from saharaclient.tests.unit.osc.v1 import fakes + + +IMAGE_INFO = {'id': 'id', 'name': 'image', 'username': 'ubuntu', + 'status': "Active", 'tags': ['fake', '0.1'], + 'description': 'Image for tests'} + + +class TestImages(fakes.TestDataProcessing): + def setUp(self): + super(TestImages, self).setUp() + self.image_mock = ( + self.app.client_manager.data_processing.images) + self.image_mock.reset_mock() + + +class TestListImages(TestImages): + def setUp(self): + super(TestListImages, self).setUp() + self.image_mock.list.return_value = [api_images.Image( + None, IMAGE_INFO)] + + # Command to test + self.cmd = osc_images.ListImages(self.app, None) + + def test_images_list_no_options(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # Check that columns are correct + expected_columns = ['Name', 'Id', 'Username', 'Tags'] + self.assertEqual(expected_columns, columns) + + # Check that data is correct + expected_data = [('image', 'id', 'ubuntu', '0.1, fake')] + self.assertEqual(expected_data, list(data)) + + def test_images_list_long(self): + arglist = ['--long'] + verifylist = [('long', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # Check that columns are correct + expected_columns = ['Name', 'Id', 'Username', 'Tags', 'Status', + 'Description'] + self.assertEqual(expected_columns, columns) + + # Check that data is correct + expected_data = [('image', 'id', 'ubuntu', '0.1, fake', 'Active', + 'Image for tests')] + self.assertEqual(expected_data, list(data)) + + def test_images_list_successful_selection(self): + arglist = ['--name', 'image', '--tags', 'fake', '--username', 'ubuntu'] + verifylist = [('name', 'image'), ('tags', ['fake']), + ('username', 'ubuntu')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # Check that correct arguments were passed + self.image_mock.list.assert_called_once_with( + search_opts={'tags': ['fake']}) + + # Check that columns are correct + expected_columns = ['Name', 'Id', 'Username', 'Tags'] + self.assertEqual(expected_columns, columns) + + # Check that data is correct + expected_data = [('image', 'id', 'ubuntu', '0.1, fake')] + self.assertEqual(expected_data, list(data)) + + def test_images_list_with_name_nothing_selected(self): + arglist = ['--name', 'img'] + verifylist = [('name', 'img')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # Check that columns are correct + expected_columns = ['Name', 'Id', 'Username', 'Tags'] + self.assertEqual(expected_columns, columns) + + # Check that data is correct + expected_data = [] + self.assertEqual(expected_data, list(data)) + + def test_images_list_with_username_nothing_selected(self): + arglist = ['--username', 'fedora'] + verifylist = [('username', 'fedora')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # Check that columns are correct + expected_columns = ['Name', 'Id', 'Username', 'Tags'] + self.assertEqual(expected_columns, columns) + + # Check that data is correct + expected_data = [] + self.assertEqual(expected_data, list(data)) + + +class TestShowImage(TestImages): + def setUp(self): + super(TestShowImage, self).setUp() + self.image_mock.find_unique.return_value = api_images.Image( + None, IMAGE_INFO) + + # Command to test + self.cmd = osc_images.ShowImage(self.app, None) + + def test_image_show_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(osc_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_image_show(self): + arglist = ['image'] + verifylist = [('image', 'image')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # Check that correct arguments were passed + self.image_mock.find_unique.assert_called_once_with(name='image') + + # Check that columns are correct + expected_columns = ('Description', 'Id', 'Name', 'Status', 'Tags', + 'Username') + self.assertEqual(expected_columns, columns) + + # Check that data is correct + expected_data = ['Image for tests', 'id', 'image', 'Active', + '0.1, fake', 'ubuntu'] + self.assertEqual(expected_data, list(data)) + + +class TestRegisterImage(TestImages): + def setUp(self): + super(TestRegisterImage, self).setUp() + self.image_mock.update_image.return_value = api_images.Image( + None, IMAGE_INFO) + + # Command to test + self.cmd = osc_images.RegisterImage(self.app, None) + + def test_image_register_without_username(self): + arglist = ['id'] + verifylist = [('image', 'id')] + + self.assertRaises(osc_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_image_register(self): + arglist = ['id', '--username', 'ubuntu'] + verifylist = [('image', 'id'), ('username', 'ubuntu')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # Check that correct arguments were passed + self.image_mock.update_image.assert_called_once_with( + 'id', desc='', user_name='ubuntu') + + # Check that columns are correct + expected_columns = ('Description', 'Id', 'Name', 'Status', 'Tags', + 'Username') + self.assertEqual(expected_columns, columns) + + # Check that data is correct + expected_data = ['Image for tests', 'id', 'image', 'Active', + '0.1, fake', 'ubuntu'] + self.assertEqual(expected_data, list(data)) + + +class TestUnregisterImage(TestImages): + def setUp(self): + super(TestUnregisterImage, self).setUp() + self.image_mock.find_unique.return_value = api_images.Image( + None, IMAGE_INFO) + + # Command to test + self.cmd = osc_images.UnregisterImage(self.app, None) + + def test_image_unregister_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(osc_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_image_unregister(self): + arglist = ['image'] + verifylist = [('image', ['image'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # Check that correct arguments were passed + self.image_mock.find_unique.assert_called_once_with(name='image') + self.image_mock.unregister_image.assert_called_once_with('id') + + +class TestSetImageTags(TestImages): + def setUp(self): + super(TestSetImageTags, self).setUp() + image_info = IMAGE_INFO.copy() + image_info['tags'] = [] + self.image_mock.find_unique.return_value = api_images.Image( + None, image_info) + self.image_mock.update_tags.return_value = api_images.Image( + None, image_info) + + # Command to test + self.cmd = osc_images.SetImageTags(self.app, None) + + def test_image_tags_set_without_tags(self): + arglist = ['id'] + verifylist = [('image', 'id')] + + self.assertRaises(osc_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_image_tags_set(self): + arglist = ['image', '--tags', 'fake', '0.1'] + verifylist = [('image', 'image'), ('tags', ['fake', '0.1'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # Check that correct arguments were passed + self.image_mock.find_unique.assert_called_with(name='image') + self.image_mock.update_tags.assert_called_once_with( + 'id', ['fake', '0.1']) + + +class TestAddImageTags(TestImages): + def setUp(self): + super(TestAddImageTags, self).setUp() + image_info = IMAGE_INFO.copy() + image_info['tags'] = [] + self.image_mock.update_tags.return_value = api_images.Image( + None, image_info) + self.image_mock.find_unique.return_value = api_images.Image( + None, image_info) + + # Command to test + self.cmd = osc_images.AddImageTags(self.app, None) + + def test_image_tags_add_without_tags(self): + arglist = ['id'] + verifylist = [('image', 'id')] + + self.assertRaises(osc_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_image_tags_add(self): + arglist = ['image', '--tags', 'fake'] + verifylist = [('image', 'image'), ('tags', ['fake'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # Check that correct arguments were passed + self.image_mock.find_unique.assert_called_with(name='image') + self.image_mock.update_tags.assert_called_once_with( + 'id', ['fake']) + + +class TestRemoveImageTags(TestImages): + def setUp(self): + super(TestRemoveImageTags, self).setUp() + self.image_mock.update_tags.return_value = api_images.Image( + None, IMAGE_INFO) + self.image_mock.find_unique.return_value = api_images.Image( + None, IMAGE_INFO) + + # Command to test + self.cmd = osc_images.RemoveImageTags(self.app, None) + + def test_image_tags_remove_both_options(self): + arglist = ['id', '--all', '--tags', 'fake'] + verifylist = [('image', 'id'), ('all', True), ('tags', ['fake'])] + + self.assertRaises(osc_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_image_tags_remove(self): + arglist = ['image', '--tags', 'fake'] + verifylist = [('image', 'image'), ('tags', ['fake'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # Check that correct arguments were passed + self.image_mock.find_unique.assert_called_with(name='image') + self.image_mock.update_tags.assert_called_once_with( + 'id', ['0.1']) + + def test_image_tags_remove_all(self): + arglist = ['image', '--all'] + verifylist = [('image', 'image'), ('all', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # Check that correct arguments were passed + self.image_mock.find_unique.assert_called_with(name='image') + self.image_mock.update_tags.assert_called_once_with( + 'id', []) diff --git a/setup.cfg b/setup.cfg index e49c8b8a..eef3a5e0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,14 @@ openstack.data_processing.v1 = dataprocessing_data_source_delete = saharaclient.osc.v1.data_sources:DeleteDataSource dataprocessing_data_source_update = saharaclient.osc.v1.data_sources:UpdateDataSource + dataprocessing_image_list = saharaclient.osc.v1.images:ListImages + dataprocessing_image_show = saharaclient.osc.v1.images:ShowImage + dataprocessing_image_register = saharaclient.osc.v1.images:RegisterImage + dataprocessing_image_unregister = saharaclient.osc.v1.images:UnregisterImage + dataprocessing_image_tags_add = saharaclient.osc.v1.images:AddImageTags + dataprocessing_image_tags_remove = saharaclient.osc.v1.images:RemoveImageTags + dataprocessing_image_tags_set = saharaclient.osc.v1.images:SetImageTags + [build_sphinx] all_files = 1 build-dir = doc/build