Add cli commands to interact with plugins
- List all plugins installed fuel plugins fuel plugins --list - Install plugin fuel plugins --install /tmp/sample.pb 1. Extracts metadata from plugin tar 2. If plugin successfully registered in nailgun 3. Untar all the files in /var/www/nailgun/plugins - If you want to update plugin when it is already installed add --force flag fuel plugins --install /tmp/sample --force The procedure is the same as for simple install, but if plugin already registered - it will try to update (send PUT request) - Refactored tests to allow writing unit tests, with mock and all that goodness related to blueprint cinder-neutron-plugins-in-fuel Change-Id: I642a300abcb9c50ef5039a343064f2ac78e39cd8
This commit is contained in:
parent
243cea0b60
commit
316b8854af
|
@ -31,6 +31,7 @@ from fuelclient.cli.actions.settings import SettingsAction
|
|||
from fuelclient.cli.actions.snapshot import SnapshotAction
|
||||
from fuelclient.cli.actions.task import TaskAction
|
||||
from fuelclient.cli.actions.user import UserAction
|
||||
from fuelclient.cli.actions.plugins import PluginAction
|
||||
|
||||
actions_tuple = (
|
||||
ReleaseAction,
|
||||
|
@ -47,7 +48,8 @@ actions_tuple = (
|
|||
TaskAction,
|
||||
SnapshotAction,
|
||||
HealthCheckAction,
|
||||
UserAction
|
||||
UserAction,
|
||||
PluginAction
|
||||
)
|
||||
|
||||
actions = dict(
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
# Copyright 2014 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.cli.actions.base import Action
|
||||
import fuelclient.cli.arguments as Args
|
||||
from fuelclient.cli.formatting import format_table
|
||||
from fuelclient.objects.plugins import Plugins
|
||||
|
||||
|
||||
class PluginAction(Action):
|
||||
"""List and modify currently available releases
|
||||
"""
|
||||
action_name = "plugins"
|
||||
|
||||
acceptable_keys = (
|
||||
"id",
|
||||
"name",
|
||||
"version",
|
||||
"package_version",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super(PluginAction, self).__init__()
|
||||
self.args = [
|
||||
Args.get_list_arg("List all available plugins."),
|
||||
Args.get_plugin_install_arg("Install action"),
|
||||
Args.get_force_arg("Update action"),
|
||||
]
|
||||
self.flag_func_map = (
|
||||
("install", self.install),
|
||||
(None, self.list),
|
||||
)
|
||||
|
||||
def list(self, params):
|
||||
"""Print all available plugins:
|
||||
fuel plugins
|
||||
fuel plugins --list
|
||||
"""
|
||||
plugins = Plugins.get_all_data()
|
||||
self.serializer.print_to_output(
|
||||
plugins,
|
||||
format_table(
|
||||
plugins,
|
||||
acceptable_keys=self.acceptable_keys
|
||||
)
|
||||
)
|
||||
|
||||
def install(self, params):
|
||||
"""Enable plugin for environment
|
||||
fuel plugins --install /tmp/plugin_sample.fb
|
||||
"""
|
||||
results = Plugins.install_plugin(params.install, params.force)
|
||||
self.serializer.print_to_output(
|
||||
results,
|
||||
"Plugin {0} was successfully installed.".format(
|
||||
params.install))
|
|
@ -367,3 +367,11 @@ def get_task_arg(help_msg):
|
|||
"help": help_msg
|
||||
}
|
||||
return get_arg("task", **default_kwargs)
|
||||
|
||||
|
||||
def get_plugin_install_arg(help_msg):
|
||||
return get_str_arg(
|
||||
"install",
|
||||
flags=("--install",),
|
||||
help=help_msg
|
||||
)
|
||||
|
|
|
@ -35,6 +35,14 @@ class FuelClientException(Exception):
|
|||
self.message = args[0]
|
||||
|
||||
|
||||
class BadDataException(FuelClientException):
|
||||
"""Should be raised when user provides corrupted data."""
|
||||
|
||||
|
||||
class WrongEnvironmentError(FuelClientException):
|
||||
"""Raised when particular action is not supported on environment."""
|
||||
|
||||
|
||||
class ServerDataException(FuelClientException):
|
||||
"""ServerDataException - must be raised when
|
||||
data returned from server cannot be processed by Fuel-Client methods.
|
||||
|
|
|
@ -29,8 +29,8 @@ class Parser:
|
|||
and based on available actions, serializers and additional flags
|
||||
populates it.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.args = sys.argv
|
||||
def __init__(self, argv):
|
||||
self.args = argv
|
||||
self.parser = argparse.ArgumentParser(
|
||||
usage="""
|
||||
Configuration for client you can find in
|
||||
|
@ -172,6 +172,6 @@ class Parser:
|
|||
|
||||
|
||||
@exceptions_decorator
|
||||
def main():
|
||||
parser = Parser()
|
||||
def main(args=sys.argv):
|
||||
parser = Parser(args)
|
||||
parser.parse()
|
||||
|
|
|
@ -160,10 +160,7 @@ class Client(object):
|
|||
|
||||
return resp.json()
|
||||
|
||||
@exceptions_decorator
|
||||
def post_request(self, api, data, ostf=False):
|
||||
"""Make POST request to specific API with some data
|
||||
"""
|
||||
def post_request_raw(self, api, data, ostf=False):
|
||||
url = (self.ostf_root if ostf else self.api_root) + api
|
||||
data_json = json.dumps(data)
|
||||
self.print_debug(
|
||||
|
@ -173,7 +170,13 @@ class Client(object):
|
|||
|
||||
headers = {'content-type': 'application/json',
|
||||
'x-auth-token': self.auth_token}
|
||||
resp = requests.post(url, data=data_json, headers=headers)
|
||||
return requests.post(url, data=data_json, headers=headers)
|
||||
|
||||
@exceptions_decorator
|
||||
def post_request(self, api, data, ostf=False):
|
||||
"""Make POST request to specific API with some data
|
||||
"""
|
||||
resp = self.post_request_raw(api, data, ostf=ostf)
|
||||
resp.raise_for_status()
|
||||
|
||||
return resp.json()
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
# Copyright 2014 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 os
|
||||
import tarfile
|
||||
|
||||
import yaml
|
||||
|
||||
from fuelclient.cli import error
|
||||
from fuelclient.objects import base
|
||||
|
||||
|
||||
EXTRACT_PATH = "/var/www/nailgun/plugins/"
|
||||
VERSIONS_PATH = '/etc/fuel/version.yaml'
|
||||
|
||||
|
||||
class Plugins(base.BaseObject):
|
||||
|
||||
class_api_path = "plugins/"
|
||||
class_instance_path = "plugins/{id}"
|
||||
|
||||
metadata_config = 'metadata.yaml'
|
||||
|
||||
@classmethod
|
||||
def validate_environment(cls):
|
||||
return os.path.exists(VERSIONS_PATH)
|
||||
|
||||
@classmethod
|
||||
def get_metadata(cls, plugin_tar):
|
||||
for member_name in plugin_tar.getnames():
|
||||
if cls.metadata_config in member_name:
|
||||
return yaml.load(plugin_tar.extractfile(member_name).read())
|
||||
raise error.BadDataException("Tarfile {0} doesn't have {1}".format(
|
||||
plugin_tar.name, cls.metadata_config))
|
||||
|
||||
@classmethod
|
||||
def add_plugin(cls, plugin_meta, plugin_tar):
|
||||
return plugin_tar.extractall(EXTRACT_PATH)
|
||||
|
||||
@classmethod
|
||||
def install_plugin(cls, plugin_path, force=False):
|
||||
if not cls.validate_environment():
|
||||
raise error.WrongEnvironmentError(
|
||||
'Plugin can be installed only from master node.')
|
||||
plugin_tar = tarfile.open(plugin_path, 'r')
|
||||
try:
|
||||
metadata = cls.get_metadata(plugin_tar)
|
||||
resp = cls.connection.post_request_raw(
|
||||
cls.class_api_path, metadata)
|
||||
if resp.status_code == 409 and force:
|
||||
url = cls.class_instance_path.format(id=resp.json()['id'])
|
||||
resp = cls.connection.put_request(
|
||||
url, metadata)
|
||||
else:
|
||||
resp.raise_for_status()
|
||||
cls.add_plugin(metadata, plugin_tar)
|
||||
finally:
|
||||
plugin_tar.close()
|
||||
return resp
|
|
@ -25,6 +25,9 @@ import subprocess
|
|||
import sys
|
||||
import tempfile
|
||||
|
||||
from fuelclient.cli.parser import main
|
||||
|
||||
|
||||
logging.basicConfig(stream=sys.stderr)
|
||||
log = logging.getLogger("CliTest.ExecutionLog")
|
||||
log.setLevel(logging.DEBUG)
|
||||
|
@ -45,7 +48,14 @@ class CliExectutionResult:
|
|||
return self.return_code == 0
|
||||
|
||||
|
||||
class BaseTestCase(TestCase):
|
||||
class UnitTestCase(TestCase):
|
||||
"""Base test class which does not require nailgun server to run."""
|
||||
|
||||
def execute(self, command):
|
||||
return main(command)
|
||||
|
||||
|
||||
class BaseTestCase(UnitTestCase):
|
||||
root_path = os.path.abspath(
|
||||
os.path.join(
|
||||
os.curdir,
|
||||
|
@ -68,6 +78,18 @@ class BaseTestCase(TestCase):
|
|||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_directory)
|
||||
|
||||
@staticmethod
|
||||
def run_command(*args):
|
||||
handle = subprocess.Popen(
|
||||
[" ".join(args)],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
shell=True
|
||||
)
|
||||
log.debug("Running " + " ".join(args))
|
||||
out, err = handle.communicate()
|
||||
log.debug("Finished command with {0} - {1}".format(out, err))
|
||||
|
||||
def upload_command(self, cmd):
|
||||
return "{0} --upload --dir {1}".format(cmd, self.temp_directory)
|
||||
|
||||
|
@ -88,18 +110,6 @@ class BaseTestCase(TestCase):
|
|||
)
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
def run_command(*args):
|
||||
handle = subprocess.Popen(
|
||||
[" ".join(args)],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
shell=True
|
||||
)
|
||||
log.debug("Running " + " ".join(args))
|
||||
out, err = handle.communicate()
|
||||
log.debug("Finished command with {0} - {1}".format(out, err))
|
||||
|
||||
def run_cli_command(self, command_line, check_errors=False):
|
||||
modified_env = os.environ.copy()
|
||||
command_args = [" ".join((self.fuel_path, command_line))]
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
import os
|
||||
|
||||
from tests.base import BaseTestCase
|
||||
from fuelclient.tests.base import BaseTestCase
|
||||
|
||||
|
||||
class TestHandlers(BaseTestCase):
|
|
@ -0,0 +1,62 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014 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 mock import Mock
|
||||
from mock import patch
|
||||
|
||||
from fuelclient.tests import base
|
||||
|
||||
DATA = """
|
||||
name: sample
|
||||
version: 0.1.0
|
||||
"""
|
||||
|
||||
|
||||
@patch('fuelclient.client.requests')
|
||||
class TestPluginsActions(base.UnitTestCase):
|
||||
|
||||
def test_001_plugins_action(self, mrequests):
|
||||
self.execute(['fuel', 'plugins'])
|
||||
plugins_call = mrequests.get.call_args_list[-1]
|
||||
url = plugins_call[0][0]
|
||||
self.assertIn('api/v1/plugins', url)
|
||||
|
||||
@patch('fuelclient.objects.plugins.tarfile')
|
||||
@patch('fuelclient.objects.plugins.os')
|
||||
def test_install_plugin(self, mos, mtar, mrequests):
|
||||
mos.path.exists.return_value = True
|
||||
mtar.open().getnames.return_value = ['metadata.yaml']
|
||||
mtar.open().extractfile().read.return_value = DATA
|
||||
response_mock = Mock(status_code=201)
|
||||
mrequests.post.return_value = response_mock
|
||||
self.execute(
|
||||
['fuel', 'plugins', '--install', '/tmp/sample.fp'])
|
||||
self.assertEqual(mrequests.post.call_count, 1)
|
||||
self.assertEqual(mrequests.put.call_count, 0)
|
||||
|
||||
@patch('fuelclient.objects.plugins.tarfile')
|
||||
@patch('fuelclient.objects.plugins.os')
|
||||
def test_install_plugin_with_force(self, mos, mtar, mrequests):
|
||||
mos.path.exists.return_value = True
|
||||
mtar.open().getnames.return_value = ['metadata.yaml']
|
||||
mtar.open().extractfile().read.return_value = DATA
|
||||
response_mock = Mock(status_code=409)
|
||||
response_mock.json.return_value = {'id': '12'}
|
||||
mrequests.post.return_value = response_mock
|
||||
self.execute(
|
||||
['fuel', 'plugins', '--install', '/tmp/sample.fp', '--force'])
|
||||
self.assertEqual(mrequests.post.call_count, 1)
|
||||
self.assertEqual(mrequests.put.call_count, 1)
|
|
@ -339,7 +339,7 @@ function run_webui_tests {
|
|||
# We are going to pass nailgun url to test runner.
|
||||
function run_cli_tests {
|
||||
local SERVER_PORT=$FUELCLIENT_SERVER_PORT
|
||||
local TESTS=$ROOT/fuelclient/tests
|
||||
local TESTS=$ROOT/fuelclient/fuelclient/tests
|
||||
local artifacts=$ARTIFACTS/cli
|
||||
local config=$artifacts/test.yaml
|
||||
prepare_artifacts $artifacts $config
|
||||
|
|
Loading…
Reference in New Issue