diff --git a/fuelclient/__init__.py b/fuelclient/__init__.py index b73ac00..5797464 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 9af59c2..392d19c 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 0000000..9a59afa --- /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 96780d9..69f99cd 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 0000000..696253d --- /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 3c14c21..33ea9af 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 0000000..891b4d8 --- /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 0000000..335a14f --- /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 8ee5b88..9c5f5cd 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 0000000..eaa57f0 --- /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 6b49bff..b5df34f 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 0000000..e7e427e --- /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 10fcf28..29e4929 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