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 4d739ecc04
commit b765ac3814
7 changed files with 235 additions and 9 deletions

View File

@ -18,6 +18,48 @@ from fuelclient.commands import base
class PluginsMixIn(object):
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):
"""Show list of all available plugins."""
@ -34,16 +76,42 @@ class PluginsSync(PluginsMixIn, base.BaseCommand):
def get_parser(self, prog_name):
parser = super(PluginsSync, self).get_parser(prog_name)
parser.add_argument(
'ids',
type=int,
nargs='*',
metavar='plugin-id',
help='Synchronise only plugins with specified ids')
self.add_plugin_ids_argument(parser)
return parser
def take_action(self, parsed_args):
ids = parsed_args.ids if len(parsed_args.ids) > 0 else None
self.client.sync(ids=ids)
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
: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)
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 PluginV2
from fuelclient.tests.unit.v1 import base
from fuelclient import utils
@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)
del_mock.assert_called_once_with('plugins/123')
@patch.object(utils, 'file_exists', return_value=True)
@patch.object(Plugins, 'register')
@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)
register_mock.return_value = {'id': 1}
self.plugin.install(self.path)
@ -230,6 +233,7 @@ class TestPluginsObject(base.UnitTestCase):
plugin_obj.install.assert_called_once_with(self.path, force=False)
register_mock.assert_called_once_with(
'retrieved_name', 'retrieved_version', force=False)
file_exists_mock.assert_called_once_with(self.path)
@patch.object(Plugins, 'unregister')
@patch.object(Plugins, 'make_obj_by_name')

View File

@ -15,16 +15,19 @@
# under the License.
import mock
import tempfile
from fuelclient.tests.unit.v2.cli import test_engine
from fuelclient.tests.utils import fake_plugin
class TestPluginsCommand(test_engine.BaseCLITest):
"""Tests for fuel2 node * commands."""
"""Tests for fuel2 plugins * commands."""
def setUp(self):
super(TestPluginsCommand, self).setUp()
self.name = 'fuel_plugin'
self.version = '1.0.0'
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_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
# License for the specific language governing permissions and limitations
# under the License.
import json
from mock import mock
import fuelclient
from fuelclient.cli import error
from fuelclient.tests.unit.v2.lib import test_api
from fuelclient.tests import utils
@ -63,3 +67,104 @@ class TestPluginsFacade(test_api.BaseLibTest):
self.client.sync(ids=ids)
self.assertTrue(matcher.called)
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)
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):
return PluginsClient(connection)

View File

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