diff --git a/rally/plugins/openstack/hook/__init__.py b/rally/plugins/openstack/hook/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/rally/plugins/openstack/hook/fault_injection.py b/rally/plugins/openstack/hook/fault_injection.py new file mode 100644 index 0000000000..f0d6e9fd80 --- /dev/null +++ b/rally/plugins/openstack/hook/fault_injection.py @@ -0,0 +1,79 @@ +# Copyright 2016: Mirantis Inc. +# All Rights Reserved. +# +# 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_faults + +from rally import api +from rally.common import logging +from rally import consts +from rally.task import hook + +LOG = logging.getLogger(__name__) + + +@hook.configure(name="fault_injection") +class FaultInjectionHook(hook.Hook): + """Performs fault injection using os-faults library. + + Configuration: + action - string that represents an action (more info in [1]) + verify - whether to verify connection to cloud nodes or not + + This plugin discovers extra config of ExistingCloud + and looks for "cloud_config" field. If cloud_config is present then + it will be used to connect to the cloud by os-faults. + + Another option is to provide os-faults config file through + OS_FAULTS_CONFIG env variable. Format of the config can + be found in [1]. + + [1] http://os-faults.readthedocs.io/en/latest/usage.html + """ + + CONFIG_SCHEMA = { + "type": "object", + "$schema": consts.JSON_SCHEMA, + "properties": { + "action": {"type": "string"}, + "verify": {"type": "boolean"}, + }, + "required": [ + "action", + ], + "additionalProperties": False, + } + + def get_cloud_config(self): + deployment = api.Deployment.get(self.task["deployment_uuid"]) + deployment_config = deployment["config"] + if deployment_config["type"] != "ExistingCloud": + return None + + extra_config = deployment_config.get("extra", {}) + return extra_config.get("cloud_config") + + def run(self): + # get cloud configuration + cloud_config = self.get_cloud_config() + + # connect to the cloud + injector = os_faults.connect(cloud_config) + + # verify that all nodes are available + if self.config.get("verify"): + injector.verify() + + LOG.debug("Injecting fault: %s", self.config["action"]) + os_faults.human_api(injector, self.config["action"]) diff --git a/requirements.txt b/requirements.txt index 1bd37f0af9..8e7477b29d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,6 +28,7 @@ six>=1.9.0,<=1.10.0 # MIT boto>=2.32.1,<=2.42.0 # MIT gnocchiclient>=2.2.0,<=2.6.0 # Apache Software License keystoneauth1>=2.10.0,<=2.13.0 # Apache Software License +os-faults>=0.1.5,<0.2.0 # Apache License, Version 2.0 python-ceilometerclient>=2.5.0,<=2.6.1 # Apache Software License python-cinderclient>=1.6.0,!=1.7.0,!=1.7.1,<=1.9.0 # Apache Software License python-cueclient>=1.0.0 # Apache License, Version 2.0 diff --git a/tests/unit/plugins/openstack/hook/__init__.py b/tests/unit/plugins/openstack/hook/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/plugins/openstack/hook/test_fault_injection.py b/tests/unit/plugins/openstack/hook/test_fault_injection.py new file mode 100644 index 0000000000..26cbd5a3e2 --- /dev/null +++ b/tests/unit/plugins/openstack/hook/test_fault_injection.py @@ -0,0 +1,139 @@ +# Copyright 2016: Mirantis Inc. +# All Rights Reserved. +# +# 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 ddt +import jsonschema +import mock +from os_faults.api import error + +from rally import consts +from rally.plugins.openstack.hook import fault_injection +from tests.unit import fakes +from tests.unit import test + + +def create_config(**kwargs): + return { + "name": "fault_injection", + "args": kwargs, + "trigger": { + "name": "event", + "args": { + "unit": "iteration", + "at": [10] + } + } + } + + +@ddt.ddt +class FaultInjectionHookTestCase(test.TestCase): + + def setUp(self): + super(FaultInjectionHookTestCase, self).setUp() + self.task = {"deployment_uuid": "foo_uuid"} + + @ddt.data((create_config(action="foo"), True), + (create_config(action="foo", verify=True), True), + (create_config(action=10), False), + (create_config(action="foo", verify=10), False), + (create_config(), False)) + @ddt.unpack + def test_config_schema(self, config, valid): + if valid: + fault_injection.FaultInjectionHook.validate(config) + else: + self.assertRaises(jsonschema.ValidationError, + fault_injection.FaultInjectionHook.validate, + config) + + @mock.patch("rally.cli.commands.deployment.api.Deployment.get") + @mock.patch("os_faults.human_api") + @mock.patch("os_faults.connect") + @mock.patch("rally.common.utils.Timer", side_effect=fakes.FakeTimer) + def test_run(self, mock_timer, mock_connect, mock_human_api, + mock_deployment_get): + injector_inst = mock_connect.return_value + hook = fault_injection.FaultInjectionHook( + self.task, {"action": "foo", "verify": True}, + {"iteration": 1}) + + hook.run_sync() + + self.assertEqual( + {"finished_at": fakes.FakeTimer().finish_timestamp(), + "started_at": fakes.FakeTimer().timestamp(), + "status": consts.HookStatus.SUCCESS, + "triggered_by": {"iteration": 1}}, + hook.result()) + + mock_connect.assert_called_once_with(None) + injector_inst.verify.assert_called_once_with() + mock_human_api.assert_called_once_with(injector_inst, "foo") + + @mock.patch("rally.cli.commands.deployment.api.Deployment.get") + @mock.patch("os_faults.human_api") + @mock.patch("os_faults.connect") + @mock.patch("rally.common.utils.Timer", side_effect=fakes.FakeTimer) + def test_run_extra_config(self, mock_timer, mock_connect, mock_human_api, + mock_deployment_get): + mock_deployment_get.return_value = { + "config": {"type": "ExistingCloud", + "extra": {"cloud_config": {"conf": "foo_config"}}}} + injector_inst = mock_connect.return_value + hook = fault_injection.FaultInjectionHook( + self.task, {"action": "foo"}, {"iteration": 1}) + + hook.run_sync() + + self.assertEqual( + {"finished_at": fakes.FakeTimer().finish_timestamp(), + "started_at": fakes.FakeTimer().timestamp(), + "status": consts.HookStatus.SUCCESS, + "triggered_by": {"iteration": 1}}, + hook.result()) + + mock_connect.assert_called_once_with({"conf": "foo_config"}) + mock_human_api.assert_called_once_with(injector_inst, "foo") + + @mock.patch("rally.cli.commands.deployment.api.Deployment.get") + @mock.patch("os_faults.human_api") + @mock.patch("os_faults.connect") + @mock.patch("rally.common.utils.Timer", side_effect=fakes.FakeTimer) + def test_run_error(self, mock_timer, mock_connect, mock_human_api, + mock_deployment_get): + injector_inst = mock_connect.return_value + mock_human_api.side_effect = error.OSFException("foo error") + hook = fault_injection.FaultInjectionHook( + self.task, {"action": "foo", "verify": True}, + {"iteration": 1}) + + hook.run_sync() + + self.assertEqual( + {"finished_at": fakes.FakeTimer().finish_timestamp(), + "started_at": fakes.FakeTimer().timestamp(), + "status": consts.HookStatus.FAILED, + "error": { + "details": mock.ANY, + "etype": "OSFException", + "msg": "foo error"}, + "triggered_by": {"iteration": 1}}, + hook.result()) + + mock_connect.assert_called_once_with(None) + injector_inst.verify.assert_called_once_with() + mock_human_api.assert_called_once_with(injector_inst, "foo")