From f0a3abeecf2a27e1de118969c00249580686d02b Mon Sep 17 00:00:00 2001 From: Andrey Pavlov Date: Fri, 24 Jul 2015 16:36:05 +0300 Subject: [PATCH] Add initial commit for integration with Openstackclient This patch adds support of Sahara CLI to Openstackclient by setting entry points and implementing interface functions. Also it adds Plugins functionality and unit tests to cover it. Co-Authored-By: Sergey Reshetnyak Partially implements: blueprint cli-as-openstackclient-plugin Change-Id: If5c33f8446d64385a71e02a0ae7bf23d7b40f862 --- requirements.txt | 2 + saharaclient/osc/__init__.py | 0 saharaclient/osc/plugin.py | 57 +++++++ saharaclient/osc/v1/__init__.py | 0 saharaclient/osc/v1/plugins.py | 135 +++++++++++++++ saharaclient/osc/v1/utils.py | 43 +++++ saharaclient/tests/unit/osc/__init__.py | 0 saharaclient/tests/unit/osc/test_plugin.py | 38 +++++ saharaclient/tests/unit/osc/v1/__init__.py | 0 saharaclient/tests/unit/osc/v1/fakes.py | 26 +++ .../tests/unit/osc/v1/test_plugins.py | 160 ++++++++++++++++++ saharaclient/tests/unit/osc/v1/test_utils.py | 76 +++++++++ setup.cfg | 10 +- 13 files changed, 546 insertions(+), 1 deletion(-) create mode 100644 saharaclient/osc/__init__.py create mode 100644 saharaclient/osc/plugin.py create mode 100644 saharaclient/osc/v1/__init__.py create mode 100644 saharaclient/osc/v1/plugins.py create mode 100644 saharaclient/osc/v1/utils.py create mode 100644 saharaclient/tests/unit/osc/__init__.py create mode 100644 saharaclient/tests/unit/osc/test_plugin.py create mode 100644 saharaclient/tests/unit/osc/v1/__init__.py create mode 100644 saharaclient/tests/unit/osc/v1/fakes.py create mode 100644 saharaclient/tests/unit/osc/v1/test_plugins.py create mode 100644 saharaclient/tests/unit/osc/v1/test_utils.py diff --git a/requirements.txt b/requirements.txt index 227cf33d..744dd492 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,10 +5,12 @@ pbr<2.0,>=1.3 Babel>=1.3 +cliff>=1.14.0 # Apache-2.0 netaddr>=0.7.12 oslo.i18n>=1.5.0 # Apache-2.0 oslo.utils>=1.9.0 # Apache-2.0 python-keystoneclient>=1.6.0 +python-openstackclient>=1.5.0 requests>=2.5.2 six>=1.9.0 PrettyTable<0.8,>=0.7 diff --git a/saharaclient/osc/__init__.py b/saharaclient/osc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/saharaclient/osc/plugin.py b/saharaclient/osc/plugin.py new file mode 100644 index 00000000..9329703b --- /dev/null +++ b/saharaclient/osc/plugin.py @@ -0,0 +1,57 @@ +# 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.common import utils +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + +DEFAULT_DATA_PROCESSING_API_VERSION = "1.1" +API_VERSION_OPTION = "os_data_processing_api_version" +API_NAME = "data_processing" +API_VERSIONS = { + "1.1": "saharaclient.api.client.Client" +} + + +def make_client(instance): + data_processing_client = utils.get_client_class( + API_NAME, + instance._api_version[API_NAME], + API_VERSIONS) + LOG.debug('Instantiating data-processing client: %s', + data_processing_client) + + client = data_processing_client( + session=instance.session, + region_name=instance._region_name, + cacert=instance._cacert, + insecure=instance._insecure + ) + return client + + +def build_option_parser(parser): + """Hook to add global options.""" + parser.add_argument( + "--os-data-processing-api-version", + metavar="", + default=utils.env( + 'OS_DATA_PROCESSING_API_VERSION', + default=DEFAULT_DATA_PROCESSING_API_VERSION), + help=("Data processing API version, default=" + + DEFAULT_DATA_PROCESSING_API_VERSION + + ' (Env: OS_DATA_PROCESSING_API_VERSION)')) + return parser diff --git a/saharaclient/osc/v1/__init__.py b/saharaclient/osc/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/saharaclient/osc/v1/plugins.py b/saharaclient/osc/v1/plugins.py new file mode 100644 index 00000000..3a992372 --- /dev/null +++ b/saharaclient/osc/v1/plugins.py @@ -0,0 +1,135 @@ +# 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 os import path + +from cliff import command +from cliff import lister +from cliff import show +from openstackclient.common import utils +from oslo_log import log as logging +from oslo_serialization import jsonutils + + +class ListPlugins(lister.Lister): + """Lists plugins""" + + log = logging.getLogger(__name__ + ".ListPlugins") + + def get_parser(self, prog_name): + parser = super(ListPlugins, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output', + ) + + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + client = self.app.client_manager.data_processing + data = client.plugins.list() + + if parsed_args.long: + columns = ('name', 'title', 'versions', 'description') + column_headers = [c.capitalize() for c in columns] + + else: + columns = ('name', 'versions') + column_headers = [c.capitalize() for c in columns] + + return ( + column_headers, + (utils.get_item_properties( + s, + columns, + formatters={ + 'versions': utils.format_list + }, + ) for s in data) + ) + + +class ShowPlugin(show.ShowOne): + """Display plugin details""" + + log = logging.getLogger(__name__ + ".ShowPlugin") + + def get_parser(self, prog_name): + parser = super(ShowPlugin, self).get_parser(prog_name) + parser.add_argument( + "plugin", + metavar="", + help="Name of the plugin 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 = client.plugins.get(parsed_args.plugin).to_dict() + data['versions'] = utils.format_list(data['versions']) + + return self.dict2columns(data) + + +class GetPluginConfigs(command.Command): + """Get plugin configs""" + + log = logging.getLogger(__name__ + ".GetPluginConfigs") + + def get_parser(self, prog_name): + parser = super(GetPluginConfigs, self).get_parser(prog_name) + parser.add_argument( + "plugin", + metavar="", + help="Name of the plugin to provide config information about", + ) + parser.add_argument( + "version", + metavar="", + help="Version of the plugin to provide config information about", + ) + parser.add_argument( + '--file', + metavar="", + help='Destination file (defaults to plugin name)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + client = self.app.client_manager.data_processing + + if not parsed_args.file: + parsed_args.file = parsed_args.plugin + + data = client.plugins.get_version_details( + parsed_args.plugin, parsed_args.version).to_dict() + + if path.exists(parsed_args.file): + self.log.error('File "%s" already exists. Chose another one with ' + '--file argument.' % parsed_args.file) + else: + with open(parsed_args.file, 'w') as f: + jsonutils.dump(data, f, indent=4) + self.log.info( + '"%(plugin)s" plugin configs was saved in "%(file)s"' + 'file' % {'plugin': parsed_args.plugin, + 'file': parsed_args.file}) diff --git a/saharaclient/osc/v1/utils.py b/saharaclient/osc/v1/utils.py new file mode 100644 index 00000000..1108a748 --- /dev/null +++ b/saharaclient/osc/v1/utils.py @@ -0,0 +1,43 @@ +# 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.common import exceptions +from openstackclient.common import utils + + +def get_resource(manager, name_or_id): + resource = utils.find_resource(manager, name_or_id) + if isinstance(resource, list): + if not resource: + msg = "No %s with a name or ID of '%s' exists." % \ + (manager.resource_class.__name__.lower(), name_or_id) + raise exceptions.CommandError(msg) + if len(resource) > 1: + msg = "More than one %s exists with the name '%s'." % \ + (manager.resource_class.__name__.lower(), name_or_id) + raise exceptions.CommandError(msg) + return resource[0] + + else: + return resource + + +def prepare_data(data, fields): + new_data = {} + for f in fields: + if f in data: + new_data[f.replace('_', ' ').capitalize()] = data[f] + + return new_data diff --git a/saharaclient/tests/unit/osc/__init__.py b/saharaclient/tests/unit/osc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/saharaclient/tests/unit/osc/test_plugin.py b/saharaclient/tests/unit/osc/test_plugin.py new file mode 100644 index 00000000..7d057f47 --- /dev/null +++ b/saharaclient/tests/unit/osc/test_plugin.py @@ -0,0 +1,38 @@ +# 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. + +import mock + +from saharaclient.osc import plugin +from saharaclient.tests.unit import base + + +class TestDataProcessingPlugin(base.BaseTestCase): + + @mock.patch("saharaclient.api.client.Client") + def test_make_client(self, p_client): + + instance = mock.Mock() + instance._api_version = {"data_processing": '1.1'} + instance.session = 'session' + instance._region_name = 'region_name' + instance._cacert = 'cacert' + instance._insecure = 'insecure' + + plugin.make_client(instance) + p_client.assert_called_with(session='session', + region_name='region_name', + cacert='cacert', + insecure='insecure') diff --git a/saharaclient/tests/unit/osc/v1/__init__.py b/saharaclient/tests/unit/osc/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/saharaclient/tests/unit/osc/v1/fakes.py b/saharaclient/tests/unit/osc/v1/fakes.py new file mode 100644 index 00000000..d8062a3e --- /dev/null +++ b/saharaclient/tests/unit/osc/v1/fakes.py @@ -0,0 +1,26 @@ +# 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. + + +import mock +from openstackclient.tests import utils + + +class TestDataProcessing(utils.TestCommand): + + def setUp(self): + super(TestDataProcessing, self).setUp() + + self.app.client_manager.data_processing = mock.Mock() diff --git a/saharaclient/tests/unit/osc/v1/test_plugins.py b/saharaclient/tests/unit/osc/v1/test_plugins.py new file mode 100644 index 00000000..1f222ebd --- /dev/null +++ b/saharaclient/tests/unit/osc/v1/test_plugins.py @@ -0,0 +1,160 @@ +# 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. + +import mock + +from saharaclient.api import plugins as api_plugins +from saharaclient.osc.v1 import plugins as osc_plugins +from saharaclient.tests.unit.osc.v1 import fakes + + +PLUGIN_INFO = {'name': 'fake', + 'title': 'Fake Plugin', + 'versions': ['0.1', '0.2'], + 'description': 'Plugin for tests'} + + +class TestPlugins(fakes.TestDataProcessing): + def setUp(self): + super(TestPlugins, self).setUp() + self.plugins_mock = self.app.client_manager.data_processing.plugins + self.plugins_mock.reset_mock() + + +class TestListPlugins(TestPlugins): + def setUp(self): + super(TestListPlugins, self).setUp() + self.plugins_mock.list.return_value = [api_plugins.Plugin( + None, PLUGIN_INFO)] + + # Command to test + self.cmd = osc_plugins.ListPlugins(self.app, None) + + def test_plugins_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', 'Versions'] + self.assertEqual(expected_columns, columns) + + # Check that data is correct + expected_data = [('fake', '0.1, 0.2')] + self.assertEqual(expected_data, list(data)) + + def test_plugins_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', 'Title', 'Versions', 'Description'] + self.assertEqual(expected_columns, columns) + + # Check that data is correct + expected_data = [('fake', 'Fake Plugin', '0.1, 0.2', + 'Plugin for tests')] + self.assertEqual(expected_data, list(data)) + + +class TestShowPlugin(TestPlugins): + def setUp(self): + super(TestShowPlugin, self).setUp() + self.plugins_mock.get.return_value = api_plugins.Plugin( + None, PLUGIN_INFO) + + # Command to test + self.cmd = osc_plugins.ShowPlugin(self.app, None) + + def test_plugin_show(self): + arglist = ['fake'] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + # Check that correct arguments was passed + self.plugins_mock.get.assert_called_once_with('fake') + + # Check that columns are correct + expected_columns = ('description', 'name', 'title', 'versions') + self.assertEqual(expected_columns, columns) + + # Check that data is correct + expected_data = ('Plugin for tests', 'fake', 'Fake Plugin', '0.1, 0.2') + self.assertEqual(expected_data, data) + + +class TestGetPluginConfigs(TestPlugins): + def setUp(self): + super(TestGetPluginConfigs, self).setUp() + self.plugins_mock.get_version_details.return_value = ( + api_plugins.Plugin(None, PLUGIN_INFO)) + + # Command to test + self.cmd = osc_plugins.GetPluginConfigs(self.app, None) + + @mock.patch('oslo_serialization.jsonutils.dump') + def test_get_plugin_configs_default_file(self, p_dump): + m_open = mock.mock_open() + with mock.patch('six.moves.builtins.open', m_open, create=True): + arglist = ['fake', '0.1'] + verifylist = [('plugin', 'fake'), ('version', '0.1')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # Check that correct arguments was passed + self.plugins_mock.get_version_details.assert_called_once_with( + 'fake', '0.1') + + args_to_dump = p_dump.call_args[0] + # Check that the right data will be saved + + self.assertEqual(PLUGIN_INFO, args_to_dump[0]) + # Check that data will be saved to the right file + self.assertEqual('fake', m_open.call_args[0][0]) + + @mock.patch('oslo_serialization.jsonutils.dump') + def test_get_plugin_configs_specified_file(self, p_dump): + m_open = mock.mock_open() + with mock.patch('six.moves.builtins.open', m_open): + arglist = ['fake', '0.1', '--file', 'testfile'] + verifylist = [('plugin', 'fake'), ('version', '0.1'), + ('file', 'testfile')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # Check that correct arguments was passed + self.plugins_mock.get_version_details.assert_called_once_with( + 'fake', '0.1') + + args_to_dump = p_dump.call_args[0] + # Check that the right data will be saved + + self.assertEqual(PLUGIN_INFO, args_to_dump[0]) + # Check that data will be saved to the right file + self.assertEqual('testfile', m_open.call_args[0][0]) diff --git a/saharaclient/tests/unit/osc/v1/test_utils.py b/saharaclient/tests/unit/osc/v1/test_utils.py new file mode 100644 index 00000000..f7d3cf0e --- /dev/null +++ b/saharaclient/tests/unit/osc/v1/test_utils.py @@ -0,0 +1,76 @@ +# 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.common import exceptions + +from saharaclient.osc.v1 import utils +from saharaclient.tests.unit import base + + +class TestUtils(base.BaseTestCase): + def test_prepare_data(self): + data = {'id': '123', 'name_of_res': 'name', 'description': 'descr'} + + fields = ['id', 'name_of_res', 'description'] + expected_data = {'Description': 'descr', 'Id': '123', + 'Name of res': 'name'} + self.assertEqual(expected_data, utils.prepare_data(data, fields)) + + fields = ['id', 'name_of_res'] + expected_data = {'Id': '123', 'Name of res': 'name'} + self.assertEqual(expected_data, utils.prepare_data(data, fields)) + + fields = ['name_of_res'] + expected_data = {'Name of res': 'name'} + self.assertEqual(expected_data, utils.prepare_data(data, fields)) + + def test_get_resource_id(self): + class TestResource(object): + def __init__(self, id): + self.id = id + + class TestManager(object): + + resource_class = TestResource + + def get(self, id): + if id == 'id': + return TestResource('from_id') + else: + raise + + def find(self, name): + if name == 'name': + return [TestResource('from_name')] + if name == 'null': + return [] + if name == 'mult': + return [TestResource('1'), TestResource('2')] + + # check case when resource id is passed + self.assertEqual('from_id', utils.get_resource( + TestManager(), 'id').id) + + # check case when resource name is passed + self.assertEqual('from_name', utils.get_resource( + TestManager(), 'name').id) + + # check that error is raised when resource doesn't exists + self.assertRaises(exceptions.CommandError, utils.get_resource, + TestManager(), 'null') + + # check that error is raised when multiple resources choice + self.assertRaises(exceptions.CommandError, utils.get_resource, + TestManager(), 'mult') diff --git a/setup.cfg b/setup.cfg index caf595d3..972154a5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,13 +25,21 @@ classifier = setup-hooks = pbr.hooks.setup_hook [files] -packages = +packages = saharaclient [entry_points] console_scripts = sahara = saharaclient.shell:main +openstack.cli.extension = + data_processing = saharaclient.osc.plugin + +openstack.data_processing.v1 = + dataprocessing_plugin_list = saharaclient.osc.v1.plugins:ListPlugins + dataprocessing_plugin_show = saharaclient.osc.v1.plugins:ShowPlugin + dataprocessing_plugin_configs_get = saharaclient.osc.v1.plugins:GetPluginConfigs + [build_sphinx] all_files = 1 build-dir = doc/build