From ea08c45295b788d9f51fd4d969a4235f221ba3aa Mon Sep 17 00:00:00 2001 From: Alexander Gordeev Date: Wed, 31 Aug 2016 20:59:21 +0300 Subject: [PATCH] Add fuel2 commands to operate on extensions Releases new fuel2 commands to operate on extensions: fuel2 env extension enable fuel2 env extension disable fuel2 env extension show fuel2 extension list DocImpact Also, this change introduces the option for BaseListCommand for specifying the default fields to sort by. Change-Id: Idee06633689efece18838766de5c4afcd24190d7 Implements: blueprint extensions-management Partial-Bug: #1614526 --- fuelclient/__init__.py | 1 + fuelclient/commands/base.py | 11 ++- fuelclient/commands/extension.py | 98 +++++++++++++++++++ fuelclient/objects/__init__.py | 1 + fuelclient/objects/extension.py | 47 +++++++++ fuelclient/tests/functional/v2/test_client.py | 31 ++++++ .../tests/unit/v2/cli/test_extension.py | 79 +++++++++++++++ .../tests/unit/v2/lib/test_extension.py | 83 ++++++++++++++++ fuelclient/tests/utils/__init__.py | 3 + fuelclient/tests/utils/fake_extension.py | 36 +++++++ fuelclient/v1/__init__.py | 2 + fuelclient/v1/extension.py | 37 +++++++ setup.cfg | 4 + 13 files changed, 430 insertions(+), 3 deletions(-) create mode 100644 fuelclient/commands/extension.py create mode 100644 fuelclient/objects/extension.py create mode 100644 fuelclient/tests/unit/v2/cli/test_extension.py create mode 100644 fuelclient/tests/unit/v2/lib/test_extension.py create mode 100644 fuelclient/tests/utils/fake_extension.py create mode 100644 fuelclient/v1/extension.py diff --git a/fuelclient/__init__.py b/fuelclient/__init__.py index b73ac005..57974642 100644 --- a/fuelclient/__init__.py +++ b/fuelclient/__init__.py @@ -64,6 +64,7 @@ def get_client(resource, version='v1', connection=None): 'deployment_history': v1.deployment_history, 'deployment-info': v1.deployment_info, 'environment': v1.environment, + 'extension': v1.extension, 'fuel-version': v1.fuelversion, 'graph': v1.graph, 'network-configuration': v1.network_configuration, diff --git a/fuelclient/commands/base.py b/fuelclient/commands/base.py index 8f4351c8..fe4c23ec 100644 --- a/fuelclient/commands/base.py +++ b/fuelclient/commands/base.py @@ -78,6 +78,10 @@ class BaseListCommand(lister.Lister, BaseCommand): filters = {} + @property + def default_sorting_by(self): + return ['id'] + @abc.abstractproperty def columns(self): """Names of columns in the resulting table.""" @@ -100,10 +104,11 @@ class BaseListCommand(lister.Lister, BaseCommand): nargs='+', choices=self.columns, metavar='SORT_COLUMN', - default=['id'], + default=self.default_sorting_by, help='Space separated list of keys for sorting ' - 'the data. Defaults to id. Wrong values ' - 'are ignored.') + 'the data. Defaults to {}. Wrong values ' + 'are ignored.'.format( + ', '.join(self.default_sorting_by))) return parser diff --git a/fuelclient/commands/extension.py b/fuelclient/commands/extension.py new file mode 100644 index 00000000..9a59afad --- /dev/null +++ b/fuelclient/commands/extension.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 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 show + +from fuelclient.commands import base + + +class ExtensionMixIn(object): + entity_name = 'extension' + + +class ExtensionList(ExtensionMixIn, base.BaseListCommand): + """Show list of all available extensions.""" + + columns = ("name", + "version", + "description", + "provides") + default_sorting_by = ["name"] + + +class EnvExtensionShow(ExtensionMixIn, base.BaseShowCommand): + """Show list of enabled extensions for environment with given id.""" + + columns = ("extensions", ) + + def get_parser(self, prog_name): + # Avoid adding id argument by BaseShowCommand + # Because it adds 'id' with wrong help message for this class + parser = show.ShowOne.get_parser(self, prog_name) + + parser.add_argument('id', type=int, help='Id of the environment.') + + return parser + + +class EnvExtensionEnable(ExtensionMixIn, base.BaseCommand): + """Enable specified extensions for environment with given id.""" + + def get_parser(self, prog_name): + parser = super(EnvExtensionEnable, self).get_parser(prog_name) + + parser.add_argument('id', type=int, help='Id of the environment.') + parser.add_argument('-E', + '--extensions', + required=True, + nargs='+', + help='Names of extensions to enable.') + + return parser + + def take_action(self, parsed_args): + self.client.enable_extensions(parsed_args.id, parsed_args.extensions) + + msg = ('The following extensions: {e} have been enabled for ' + 'the environment with id {id}.\n'.format( + e=', '.join(parsed_args.extensions), id=parsed_args.id)) + + self.app.stdout.write(msg) + + +class EnvExtensionDisable(ExtensionMixIn, base.BaseCommand): + """Disable specified extensions for environment with given id.""" + + def get_parser(self, prog_name): + parser = super(EnvExtensionDisable, self).get_parser(prog_name) + + parser.add_argument('id', type=int, help='Id of the environment.') + parser.add_argument('-E', + '--extensions', + required=True, + nargs='+', + help='Names of extensions to disable.') + + return parser + + def take_action(self, parsed_args): + self.client.disable_extensions(parsed_args.id, parsed_args.extensions) + + msg = ('The following extensions: {e} have been disabled for ' + 'the environment with id {id}.\n'.format( + e=', '.join(parsed_args.extensions), id=parsed_args.id)) + + self.app.stdout.write(msg) diff --git a/fuelclient/objects/__init__.py b/fuelclient/objects/__init__.py index 96780d9e..69f99cde 100644 --- a/fuelclient/objects/__init__.py +++ b/fuelclient/objects/__init__.py @@ -18,6 +18,7 @@ functionality from nailgun objects. from fuelclient.objects.base import BaseObject from fuelclient.objects.environment import Environment +from fuelclient.objects.extension import Extension from fuelclient.objects.node import Node from fuelclient.objects.node import NodeCollection from fuelclient.objects.openstack_config import OpenstackConfig diff --git a/fuelclient/objects/extension.py b/fuelclient/objects/extension.py new file mode 100644 index 00000000..696253db --- /dev/null +++ b/fuelclient/objects/extension.py @@ -0,0 +1,47 @@ +# Copyright 2016 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 fuelclient.objects.base import BaseObject + + +class Extension(BaseObject): + + class_api_path = "extensions/" + instance_api_path = "clusters/{0}/extensions/" + + @property + def extensions_url(self): + return self.instance_api_path.format(self.id) + + def get_env_extensions(self): + """Get list of extensions through request to the Nailgun API + + """ + return self.connection.get_request(self.extensions_url) + + def enable_env_extensions(self, extensions): + """Enable extensions through request to the Nailgun API + + :param extensions: list of extenstion to be enabled + """ + return self.connection.put_request(self.extensions_url, extensions) + + def disable_env_extensions(self, extensions): + """Disable extensions through request to the Nailgun API + + :param extensions: list of extenstion to be disabled + """ + url = '{0}?extension_names={1}'.format(self.extensions_url, + ','.join(extensions)) + return self.connection.delete_request(url) diff --git a/fuelclient/tests/functional/v2/test_client.py b/fuelclient/tests/functional/v2/test_client.py index 3c14c216..33ea9afe 100644 --- a/fuelclient/tests/functional/v2/test_client.py +++ b/fuelclient/tests/functional/v2/test_client.py @@ -49,3 +49,34 @@ class TestDeployChanges(base.CLIv2TestCase): self.check_for_stdout_by_regexp(self.cmd_redeploy_changes, self.pattern_success) + + +class TestExtensionManagement(base.CLIv2TestCase): + + cmd_create_env = "env create -r {0} cluster-test-extensions-mgmt" + cmd_disable_exts = "env extension disable 1 --extensions volume_manager" + cmd_enable_exts = "env extension enable 1 --extensions volume_manager" + + pattern_enable_success = (r"^The following extensions: volume_manager " + r"have been enabled for the environment with " + r"id 1.\n$") + pattern_disable_success = (r"^The following extensions: volume_manager " + r"have been disabled for the environment with " + r"id 1.\n$") + + def setUp(self): + super(TestExtensionManagement, self).setUp() + self.load_data_to_nailgun_server() + release_id = self.get_first_deployable_release_id() + self.cmd_create_env = self.cmd_create_env.format(release_id) + self.run_cli_commands(( + self.cmd_create_env, + )) + + def test_disable_extensions(self): + self.check_for_stdout_by_regexp(self.cmd_disable_exts, + self.pattern_disable_success) + + def test_enable_extensions(self): + self.check_for_stdout_by_regexp(self.cmd_enable_exts, + self.pattern_enable_success) diff --git a/fuelclient/tests/unit/v2/cli/test_extension.py b/fuelclient/tests/unit/v2/cli/test_extension.py new file mode 100644 index 00000000..891b4d8f --- /dev/null +++ b/fuelclient/tests/unit/v2/cli/test_extension.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 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 fuelclient.tests.unit.v2.cli import test_engine +from fuelclient.tests import utils + + +class TestExtensionCommand(test_engine.BaseCLITest): + """Tests for fuel2 extension * commands.""" + + def test_extensions_list(self): + self.m_client.get_all.return_value = utils.get_fake_extensions(2) + args = 'extension list' + self.exec_command(args) + self.m_client.get_all.assert_called_once_with() + self.m_get_client.assert_called_once_with('extension', mock.ANY) + + def test_env_extensions_show(self): + self.m_client.get_extensions.return_value = \ + utils.get_fake_env_extensions() + env_id = 45 + args = 'env extension show {id}'.format(id=env_id) + self.exec_command(args) + self.m_client.get_by_id.assert_called_once_with(env_id) + self.m_get_client.assert_called_once_with('extension', mock.ANY) + + @mock.patch('sys.stderr') + def test_env_extension_show_fail(self, mocked_stderr): + args = 'env extension show' + self.assertRaises(SystemExit, self.exec_command, args) + self.assertIn('id', + mocked_stderr.write.call_args_list[0][0][0]) + + @mock.patch('sys.stderr') + def test_env_extension_enable_fail(self, mocked_stderr): + args = 'env extension enable 1' + self.assertRaises(SystemExit, self.exec_command, args) + self.assertIn('-E/--extensions', + mocked_stderr.write.call_args_list[-1][0][0]) + + @mock.patch('sys.stderr') + def test_env_extension_disable_fail(self, mocked_stderr): + args = 'env extension disable 1' + self.assertRaises(SystemExit, self.exec_command, args) + self.assertIn('-E/--extensions', + mocked_stderr.write.call_args_list[-1][0][0]) + + def test_env_extensions_enable(self): + exts = utils.get_fake_env_extensions() + env_id = 45 + args = 'env extension enable {id} --extensions {exts}'.format( + id=env_id, exts=' '.join(exts)) + self.exec_command(args) + self.m_client.enable_extensions.assert_called_once_with(env_id, exts) + self.m_get_client.assert_called_once_with('extension', mock.ANY) + + def test_env_extensions_disable(self): + exts = utils.get_fake_env_extensions() + env_id = 45 + args = 'env extension disable {id} --extensions {exts}'.format( + id=env_id, exts=' '.join(exts)) + self.exec_command(args) + self.m_client.disable_extensions.assert_called_once_with(env_id, exts) + self.m_get_client.assert_called_once_with('extension', mock.ANY) diff --git a/fuelclient/tests/unit/v2/lib/test_extension.py b/fuelclient/tests/unit/v2/lib/test_extension.py new file mode 100644 index 00000000..335a14f2 --- /dev/null +++ b/fuelclient/tests/unit/v2/lib/test_extension.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 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 fuelclient +from fuelclient.tests.unit.v2.lib import test_api +from fuelclient.tests import utils + + +class TestExtensionFacade(test_api.BaseLibTest): + + def setUp(self): + super(TestExtensionFacade, self).setUp() + + self.version = 'v1' + self.res_uri = '/api/{version}/extensions/'.format( + version=self.version) + self.res_env_uri = '/api/{version}/clusters/'.format( + version=self.version) + self.fake_ext = ['fake_ext1'] + self.fake_extensions = utils.get_fake_extensions(10) + self.fake_env_extensions = utils.get_fake_env_extensions() + + self.client = fuelclient.get_client('extension', self.version) + + def test_extension_list(self): + matcher = self.m_request.get(self.res_uri, json=self.fake_extensions) + self.client.get_all() + + self.assertTrue(matcher.called) + + def test_env_extension_list(self): + env_id = 42 + expected_uri = self.get_object_uri(self.res_env_uri, env_id, + '/extensions/') + matcher = self.m_request.get(expected_uri, + json=self.fake_env_extensions) + extensions = self.client.get_by_id(env_id) + + self.assertTrue(matcher.called) + for ext in self.fake_env_extensions: + self.assertIn(ext, extensions['extensions']) + + def test_env_extension_enable(self): + env_id = 42 + fake_ext = ['enabled_fake_ext4'] + expected_uri = self.get_object_uri(self.res_env_uri, env_id, + '/extensions/') + put_matcher = self.m_request.put(expected_uri, + json=self.fake_env_extensions) + + self.client.enable_extensions(env_id, fake_ext) + + self.assertTrue(put_matcher.called) + self.assertIn(fake_ext[0], put_matcher.last_request.json()) + + def test_env_extension_disable(self): + env_id = 42 + expected_uri = self.get_object_uri( + self.res_env_uri, + env_id, + '/extensions/?extension_names={0}'.format( + ','.join(self.fake_env_extensions))) + + delete_matcher = self.m_request.delete(expected_uri, + complete_qs=True, + json=self.fake_env_extensions) + + self.client.disable_extensions(env_id, self.fake_env_extensions) + + self.assertTrue(delete_matcher.called) diff --git a/fuelclient/tests/utils/__init__.py b/fuelclient/tests/utils/__init__.py index 8ee5b888..9c5f5cda 100644 --- a/fuelclient/tests/utils/__init__.py +++ b/fuelclient/tests/utils/__init__.py @@ -32,6 +32,9 @@ from fuelclient.tests.utils.fake_net_conf import get_fake_network_config from fuelclient.tests.utils.fake_network_group import get_fake_network_group from fuelclient.tests.utils.fake_node import get_fake_node from fuelclient.tests.utils.fake_env import get_fake_env +from fuelclient.tests.utils.fake_extension import get_fake_env_extensions +from fuelclient.tests.utils.fake_extension import get_fake_extension +from fuelclient.tests.utils.fake_extension import get_fake_extensions from fuelclient.tests.utils.fake_fuel_version import get_fake_fuel_version from fuelclient.tests.utils.fake_task import get_fake_task from fuelclient.tests.utils.fake_node_group import get_fake_node_group diff --git a/fuelclient/tests/utils/fake_extension.py b/fuelclient/tests/utils/fake_extension.py new file mode 100644 index 00000000..eaa57f08 --- /dev/null +++ b/fuelclient/tests/utils/fake_extension.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2016 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. + + +def get_fake_env_extensions(names=None): + """Create a list of fake extensions for particular env""" + + return names or ['fake_ext1', 'fake_ext2', 'fake_ext3'] + + +def get_fake_extension(name=None, version=None, provides=None, + description=None): + return {'name': name or 'fake_name', + 'version': version or 'fake_version', + 'provides': provides or ['fake_method_call'], + 'description': description or 'fake_description', + } + + +def get_fake_extensions(extension_count, **kwargs): + """Create a random fake list of extensions.""" + return [get_fake_extension(**kwargs) + for _ in range(extension_count)] diff --git a/fuelclient/v1/__init__.py b/fuelclient/v1/__init__.py index 6b49bff6..b5df34f5 100644 --- a/fuelclient/v1/__init__.py +++ b/fuelclient/v1/__init__.py @@ -16,6 +16,7 @@ from fuelclient.v1 import cluster_settings from fuelclient.v1 import deployment_history from fuelclient.v1 import deployment_info from fuelclient.v1 import environment +from fuelclient.v1 import extension from fuelclient.v1 import fuelversion from fuelclient.v1 import graph from fuelclient.v1 import network_configuration @@ -35,6 +36,7 @@ __all__ = ('cluster_settings', 'deployment_history', 'deployment_info', 'environment', + 'extension', 'fuelversion', 'graph', 'network_configuration', diff --git a/fuelclient/v1/extension.py b/fuelclient/v1/extension.py new file mode 100644 index 00000000..e7e427ed --- /dev/null +++ b/fuelclient/v1/extension.py @@ -0,0 +1,37 @@ +# Copyright 2016 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 fuelclient import objects +from fuelclient.v1 import base_v1 + + +class ExtensionClient(base_v1.BaseV1Client): + + _entity_wrapper = objects.Extension + + def get_by_id(self, environment_id): + ext_obj = self._entity_wrapper(environment_id) + return {'extensions': ', '.join(ext_obj.get_env_extensions())} + + def enable_extensions(self, environment_id, extensions): + ext_obj = self._entity_wrapper(environment_id) + return ext_obj.enable_env_extensions(extensions) + + def disable_extensions(self, environment_id, extensions): + ext_obj = self._entity_wrapper(environment_id) + return ext_obj.disable_env_extensions(extensions) + + +def get_client(connection): + return ExtensionClient(connection) diff --git a/setup.cfg b/setup.cfg index 10fcf288..29e4929b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,6 +37,9 @@ fuelclient = env_deployment-facts_download=fuelclient.commands.environment:EnvDeploymentFactsDownload env_deployment-facts_get-default=fuelclient.commands.environment:EnvDeploymentFactsGetDefault env_deployment-facts_upload=fuelclient.commands.environment:EnvDeploymentFactsUpload + env_extension_disable=fuelclient.commands.extension:EnvExtensionDisable + env_extension_enable=fuelclient.commands.extension:EnvExtensionEnable + env_extension_show=fuelclient.commands.extension:EnvExtensionShow env_list=fuelclient.commands.environment:EnvList env_network_download=fuelclient.commands.environment:EnvNetworkDownload env_network_upload=fuelclient.commands.environment:EnvNetworkUpload @@ -56,6 +59,7 @@ fuelclient = env_spawn-vms=fuelclient.commands.environment:EnvSpawnVms env_stop-deployment=fuelclient.commands.environment:EnvStopDeploy env_update=fuelclient.commands.environment:EnvUpdate + extension_list=fuelclient.commands.extension:ExtensionList fuel-version=fuelclient.commands.fuelversion:FuelVersion graph_download=fuelclient.commands.graph:GraphDownload graph_execute=fuelclient.commands.graph:GraphExecute