Fault injection hook plugin

This patch adds os-faults to requirements.txt and allows to
use this library as hook plugin.

Change-Id: Ifa8f33fbcff3e41f6ae019c6823f0a5ec328d780
This commit is contained in:
Anton Studenov 2016-11-03 14:54:36 +03:00
parent 23060ebc60
commit 46ca5c4d77
5 changed files with 219 additions and 0 deletions

View File

View File

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

View File

@ -28,6 +28,7 @@ six>=1.9.0,<=1.10.0 # MIT
boto>=2.32.1,<=2.42.0 # MIT boto>=2.32.1,<=2.42.0 # MIT
gnocchiclient>=2.2.0,<=2.6.0 # Apache Software License gnocchiclient>=2.2.0,<=2.6.0 # Apache Software License
keystoneauth1>=2.10.0,<=2.13.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-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-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 python-cueclient>=1.0.0 # Apache License, Version 2.0

View File

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