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
This commit is contained in:
Alexander Gordeev 2016-08-31 20:59:21 +03:00
parent d29dcfcc50
commit ea08c45295
13 changed files with 430 additions and 3 deletions

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)]

View File

@ -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',

View File

@ -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)

View File

@ -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