Add two plugins related commands into fuel2

* fuel2 plugins install <path>
 * fuel2 plugins remove <name> <version>

DocImpact
Change-Id: Id166eacad0ffe8b9f6fd98519aaa15246a9c1956
Closes-Bug: #1668253
This commit is contained in:
Fedor Zhadaev 2017-03-07 12:50:07 +04:00
parent 3d5cb6da0f
commit 85035fbd18
7 changed files with 235 additions and 9 deletions

View File

@ -18,6 +18,48 @@ from fuelclient.commands import base
class PluginsMixIn(object): class PluginsMixIn(object):
entity_name = 'plugins' entity_name = 'plugins'
@staticmethod
def add_plugin_file_argument(parser):
parser.add_argument(
'file',
type=str,
help='Path to plugin file to install'
)
@staticmethod
def add_plugin_name_argument(parser):
parser.add_argument(
'name',
type=str,
help='Name of plugin to remove'
)
@staticmethod
def add_plugin_version_argument(parser):
parser.add_argument(
'version',
type=str,
help='Version of plugin to remove'
)
@staticmethod
def add_plugin_ids_argument(parser):
parser.add_argument(
'ids',
type=int,
nargs='*',
metavar='plugin-id',
help='Synchronise only plugins with specified ids'
)
@staticmethod
def add_plugin_install_force_argument(parser):
parser.add_argument(
'-f', '--force',
action='store_true',
help='Used for reinstall plugin with the same version'
)
class PluginsList(PluginsMixIn, base.BaseListCommand): class PluginsList(PluginsMixIn, base.BaseListCommand):
"""Show list of all available plugins.""" """Show list of all available plugins."""
@ -34,16 +76,42 @@ class PluginsSync(PluginsMixIn, base.BaseCommand):
def get_parser(self, prog_name): def get_parser(self, prog_name):
parser = super(PluginsSync, self).get_parser(prog_name) parser = super(PluginsSync, self).get_parser(prog_name)
parser.add_argument( self.add_plugin_ids_argument(parser)
'ids',
type=int,
nargs='*',
metavar='plugin-id',
help='Synchronise only plugins with specified ids')
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
ids = parsed_args.ids if len(parsed_args.ids) > 0 else None ids = parsed_args.ids if len(parsed_args.ids) > 0 else None
self.client.sync(ids=ids) self.client.sync(ids=ids)
self.app.stdout.write("Plugins were successfully synchronized.\n") self.app.stdout.write("Plugins were successfully synchronized.\n")
class PluginInstall(PluginsMixIn, base.BaseCommand):
"""Install plugin archive and register in API service."""
def get_parser(self, prog_name):
parser = super(PluginInstall, self).get_parser(prog_name)
self.add_plugin_file_argument(parser)
self.add_plugin_install_force_argument(parser)
return parser
def take_action(self, parsed_args):
self.client.install(parsed_args.file, force=parsed_args.force)
self.app.stdout.write(
"Plugin {0} was successfully installed.\n".format(parsed_args.file)
)
class PluginRemove(PluginsMixIn, base.BaseCommand):
"""Remove the plugin package, and update data in API service."""
def get_parser(self, prog_name):
parser = super(PluginRemove, self).get_parser(prog_name)
self.add_plugin_name_argument(parser)
self.add_plugin_version_argument(parser)
return parser
def take_action(self, parsed_args):
self.client.remove(parsed_args.name, parsed_args.version)
self.app.stdout.write(
"Plugin {0} was successfully removed.\n".format(parsed_args.name)
)

View File

@ -336,6 +336,10 @@ class Plugins(base.BaseObject):
:return: Plugins information :return: Plugins information
:rtype: dict :rtype: dict
""" """
if not utils.file_exists(plugin_path):
raise error.BadDataException(
"No such plugin file: {0}".format(plugin_path)
)
plugin = cls.make_obj_by_file(plugin_path) plugin = cls.make_obj_by_file(plugin_path)
name = plugin.name_from_file(plugin_path) name = plugin.name_from_file(plugin_path)

View File

@ -25,6 +25,7 @@ from fuelclient.objects.plugins import Plugins
from fuelclient.objects.plugins import PluginV1 from fuelclient.objects.plugins import PluginV1
from fuelclient.objects.plugins import PluginV2 from fuelclient.objects.plugins import PluginV2
from fuelclient.tests.unit.v1 import base from fuelclient.tests.unit.v1 import base
from fuelclient import utils
@patch('fuelclient.objects.plugins.raise_error_if_not_master') @patch('fuelclient.objects.plugins.raise_error_if_not_master')
@ -220,9 +221,11 @@ class TestPluginsObject(base.UnitTestCase):
get_mock.assert_called_once_with(self.name, self.version) get_mock.assert_called_once_with(self.name, self.version)
del_mock.assert_called_once_with('plugins/123') del_mock.assert_called_once_with('plugins/123')
@patch.object(utils, 'file_exists', return_value=True)
@patch.object(Plugins, 'register') @patch.object(Plugins, 'register')
@patch.object(Plugins, 'make_obj_by_file') @patch.object(Plugins, 'make_obj_by_file')
def test_install(self, make_obj_by_file_mock, register_mock): def test_install(self, make_obj_by_file_mock, register_mock,
file_exists_mock):
plugin_obj = self.mock_make_obj_by_file(make_obj_by_file_mock) plugin_obj = self.mock_make_obj_by_file(make_obj_by_file_mock)
register_mock.return_value = {'id': 1} register_mock.return_value = {'id': 1}
self.plugin.install(self.path) self.plugin.install(self.path)
@ -230,6 +233,7 @@ class TestPluginsObject(base.UnitTestCase):
plugin_obj.install.assert_called_once_with(self.path, force=False) plugin_obj.install.assert_called_once_with(self.path, force=False)
register_mock.assert_called_once_with( register_mock.assert_called_once_with(
'retrieved_name', 'retrieved_version', force=False) 'retrieved_name', 'retrieved_version', force=False)
file_exists_mock.assert_called_once_with(self.path)
@patch.object(Plugins, 'unregister') @patch.object(Plugins, 'unregister')
@patch.object(Plugins, 'make_obj_by_name') @patch.object(Plugins, 'make_obj_by_name')

View File

@ -15,16 +15,19 @@
# under the License. # under the License.
import mock import mock
import tempfile
from fuelclient.tests.unit.v2.cli import test_engine from fuelclient.tests.unit.v2.cli import test_engine
from fuelclient.tests.utils import fake_plugin from fuelclient.tests.utils import fake_plugin
class TestPluginsCommand(test_engine.BaseCLITest): class TestPluginsCommand(test_engine.BaseCLITest):
"""Tests for fuel2 node * commands.""" """Tests for fuel2 plugins * commands."""
def setUp(self): def setUp(self):
super(TestPluginsCommand, self).setUp() super(TestPluginsCommand, self).setUp()
self.name = 'fuel_plugin'
self.version = '1.0.0'
get_fake_plugins = fake_plugin.get_fake_plugins get_fake_plugins = fake_plugin.get_fake_plugins
@ -57,3 +60,25 @@ class TestPluginsCommand(test_engine.BaseCLITest):
self.m_get_client.assert_called_once_with('plugins', mock.ANY) self.m_get_client.assert_called_once_with('plugins', mock.ANY)
self.m_client.sync.assert_called_once_with(ids=ids) self.m_client.sync.assert_called_once_with(ids=ids)
def exec_install(self, ext='rpm', force=False):
path = tempfile.mkstemp(suffix='.{}'.format(ext))[1]
args = 'plugins install {0} {1}'.format(path,
'--force' if force else '')
self.exec_command(args)
self.m_get_client.assert_called_once_with('plugins', mock.ANY)
self.m_client.install.assert_called_once_with(path, force=force)
def test_plugin_install(self):
self.exec_install()
def test_plugin_install_with_force(self):
self.exec_install(force=True)
def test_plugin_remove(self):
args = 'plugins remove {0} {1}'.format(self.name, self.version)
self.exec_command(args)
self.m_get_client.assert_called_once_with('plugins', mock.ANY)
self.m_client.remove.assert_called_once_with(self.name, self.version)

View File

@ -13,8 +13,12 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
from mock import mock
import fuelclient import fuelclient
from fuelclient.cli import error
from fuelclient.tests.unit.v2.lib import test_api from fuelclient.tests.unit.v2.lib import test_api
from fuelclient.tests import utils from fuelclient.tests import utils
@ -63,3 +67,104 @@ class TestPluginsFacade(test_api.BaseLibTest):
self.client.sync(ids=ids) self.client.sync(ids=ids)
self.assertTrue(matcher.called) self.assertTrue(matcher.called)
self.assertEqual(ids, matcher.last_request.json()['ids']) self.assertEqual(ids, matcher.last_request.json()['ids'])
class TestPluginInstallFacade(TestPluginsFacade):
def setUp(self):
super(TestPluginInstallFacade, self).setUp()
self.m_exec = mock.patch.object(fuelclient.utils, 'exec_cmd')
self.m_is_master = mock.patch('fuelclient.objects.plugins.IS_MASTER',
True)
self.m_meta = mock.patch.object(fuelclient.utils,
'glob_and_parse_yaml',
return_value=self.fake_plugins)
self.m_file_exists = mock.patch.object(fuelclient.utils,
'file_exists',
return_value=True)
def exec_install(self, force=False):
path = '/path/to/plugin.rpm'
post_matcher = self.m_request.post(self.res_uri, json={})
get_matcher = self.m_request.get(self.res_uri, json={})
put_matcher = None
fake_plugin = self.fake_plugins[0]
if force:
put_uri = '/api/{version}/plugins/{id}'.format(
version=self.version,
id=fake_plugin['id'])
put_matcher = self.m_request.put(put_uri, json={})
post_matcher = self.m_request.post(self.res_uri, json={
'message': json.dumps({'id': fake_plugin['id']})
}, status_code=409)
m_name = mock.patch.object(fuelclient.objects.plugins.PluginV2,
'name_from_file',
return_value=fake_plugin['name'])
m_version = mock.patch.object(fuelclient.objects.plugins.PluginV2,
'version_from_file',
return_value=fake_plugin['version'])
with m_name, m_version, self.m_is_master, self.m_meta, self.m_exec,\
self.m_file_exists:
self.client.install(path, force)
self.assertTrue(get_matcher.called)
self.assertTrue(post_matcher.called)
self.assertEqual(fake_plugin,
json.loads(post_matcher.last_request.body))
if force:
self.assertTrue(put_matcher.called)
self.assertEqual(fake_plugin,
json.loads(put_matcher.last_request.body))
def test_install_plugin(self):
self.exec_install()
def test_install_plugin_force(self):
self.exec_install(True)
def test_install_plugin_fail_not_master(self):
self.m_is_master = mock.patch('fuelclient.objects.plugins.IS_MASTER',
False)
self.assertRaises(error.WrongEnvironmentError, self.exec_install)
def test_install_plugin_fail_file_not_exists(self):
self.m_file_exists = mock.patch.object(fuelclient.utils,
'file_exists',
return_value=False)
self.assertRaises(error.BadDataException, self.exec_install)
class TestPluginRemoveFacade(TestPluginsFacade):
def setUp(self):
super(TestPluginRemoveFacade, self).setUp()
self.m_exec = mock.patch.object(fuelclient.utils, 'exec_cmd')
self.m_is_master = mock.patch('fuelclient.objects.plugins.IS_MASTER',
True)
def exec_remove(self):
fake_plugin = self.fake_plugins[0]
expected_uri = '/api/{version}/plugins/{id}'.format(
version=self.version, id=fake_plugin['id'])
del_matcher = self.m_request.delete(expected_uri, json={})
get_matcher = self.m_request.get(self.res_uri, json=self.fake_plugins)
with self.m_is_master, self.m_exec:
self.client.remove(fake_plugin['name'], fake_plugin['version'])
self.assertTrue(get_matcher.called)
self.assertTrue(del_matcher.called)
self.assertIsNone(del_matcher.last_request.body)
def test_remove_plugin(self):
self.exec_remove()
def test_remove_plugin_fail_not_master(self):
self.m_is_master = mock.patch('fuelclient.objects.plugins.IS_MASTER',
False)
self.assertRaises(error.WrongEnvironmentError, self.exec_remove)

View File

@ -53,6 +53,24 @@ class PluginsClient(base_v1.BaseV1Client):
self._entity_wrapper.sync(plugin_ids=ids) self._entity_wrapper.sync(plugin_ids=ids)
def install(self, plugin_path, force=False):
"""Install plugin archive and register in API service.
:param plugin_path: Path to plugin file
:type plugin_path: str
:param force: Update existent plugin even if it is not updatable
:type force: bool
"""
return self._entity_wrapper.install(plugin_path, force=force)
def remove(self, plugin_name, plugin_version):
"""Remove the plugin package, and update data in API service.
:param str plugin_name: Name of plugin to remove
:param str plugin_version: Version of plugin to remove
"""
return self._entity_wrapper.remove(plugin_name, plugin_version)
def get_client(connection): def get_client(connection):
return PluginsClient(connection) return PluginsClient(connection)

View File

@ -103,7 +103,9 @@ fuelclient =
openstack-config_execute=fuelclient.commands.openstack_config:OpenstackConfigExecute openstack-config_execute=fuelclient.commands.openstack_config:OpenstackConfigExecute
openstack-config_list=fuelclient.commands.openstack_config:OpenstackConfigList openstack-config_list=fuelclient.commands.openstack_config:OpenstackConfigList
openstack-config_upload=fuelclient.commands.openstack_config:OpenstackConfigUpload openstack-config_upload=fuelclient.commands.openstack_config:OpenstackConfigUpload
plugins_install=fuelclient.commands.plugins:PluginInstall
plugins_list=fuelclient.commands.plugins:PluginsList plugins_list=fuelclient.commands.plugins:PluginsList
plugins_remove=fuelclient.commands.plugins:PluginRemove
plugins_sync=fuelclient.commands.plugins:PluginsSync plugins_sync=fuelclient.commands.plugins:PluginsSync
release_component_list=fuelclient.commands.release:ReleaseComponentList release_component_list=fuelclient.commands.release:ReleaseComponentList
release_list=fuelclient.commands.release:ReleaseList release_list=fuelclient.commands.release:ReleaseList