From f1dca7fa86e4b84f157ae5349bee82d2471eb7a9 Mon Sep 17 00:00:00 2001 From: Evstifeev Roman Date: Tue, 27 Sep 2016 23:50:47 +0300 Subject: [PATCH] Don't crash when receiving unknown configuration variables Instead of crashing if the monkey deserializes an unknown configuration variable, send an error message to the current monkey server and keep on working. Add utnittests. fixes #26 --- chaos_monkey/__init__.py | 1 - chaos_monkey/config.py | 9 ++++++- chaos_monkey/control.py | 19 ++++++++++++- chaos_monkey/requirements.txt | 2 ++ chaos_monkey/test/__init__.py | 0 chaos_monkey/test/config__test.py | 45 +++++++++++++++++++++++++++++++ monkey_island/cc/main.py | 2 ++ 7 files changed, 75 insertions(+), 3 deletions(-) delete mode 100644 chaos_monkey/__init__.py create mode 100644 chaos_monkey/test/__init__.py create mode 100644 chaos_monkey/test/config__test.py diff --git a/chaos_monkey/__init__.py b/chaos_monkey/__init__.py deleted file mode 100644 index 05a457b0c..000000000 --- a/chaos_monkey/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'itamar' diff --git a/chaos_monkey/config.py b/chaos_monkey/config.py index addb9fd54..b26c911b0 100644 --- a/chaos_monkey/config.py +++ b/chaos_monkey/config.py @@ -45,6 +45,11 @@ def _cast_by_example(value, example): class Configuration(object): def from_dict(self, data): + """ + Get a dict of config variables, set known variables as attributes on self. + Return dict of unknown variables encountered. + """ + unknown_variables = {} for key, value in data.items(): if key.startswith('_'): continue @@ -55,9 +60,11 @@ class Configuration(object): try: default_value = getattr(Configuration, key) except AttributeError: - raise + unknown_variables[key] = value + continue setattr(self, key, _cast_by_example(value, default_value)) + return unknown_variables def as_dict(self): result = {} diff --git a/chaos_monkey/control.py b/chaos_monkey/control.py index f8f3e3ee0..cdeb47a76 100644 --- a/chaos_monkey/control.py +++ b/chaos_monkey/control.py @@ -124,7 +124,7 @@ class ControlClient(object): return try: - WormConfiguration.from_dict(reply.json().get('config')) + unknown_variables = WormConfiguration.from_dict(reply.json().get('config')) LOG.info("New configuration was loaded from server: %r" % (WormConfiguration.as_dict(),)) except Exception, exc: # we don't continue with default conf here because it might be dangerous @@ -132,6 +132,23 @@ class ControlClient(object): WormConfiguration.current_server, reply._content, exc) raise Exception("Couldn't load from from server's configuration, aborting. %s" % exc) + if unknown_variables: + ControlClient.send_config_error() + + @staticmethod + def send_config_error(): + if not WormConfiguration.current_server: + return + try: + requests.patch("https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), + data=json.dumps({'config_error': True}), + headers={'content-type': 'application/json'}, + verify=False, + proxies=ControlClient.proxies) + except Exception, exc: + LOG.warn("Error connecting to control server %s: %s", WormConfiguration.current_server, exc) + return {} + @staticmethod def check_for_stop(): ControlClient.load_control_config() diff --git a/chaos_monkey/requirements.txt b/chaos_monkey/requirements.txt index 8c77333f9..34f24c78f 100644 --- a/chaos_monkey/requirements.txt +++ b/chaos_monkey/requirements.txt @@ -12,3 +12,5 @@ psutil PyInstaller ecdsa netifaces +mock +nose diff --git a/chaos_monkey/test/__init__.py b/chaos_monkey/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/chaos_monkey/test/config__test.py b/chaos_monkey/test/config__test.py new file mode 100644 index 000000000..fccde2f0d --- /dev/null +++ b/chaos_monkey/test/config__test.py @@ -0,0 +1,45 @@ +# -*- coding: UTF-8 -*- +# NOTE: Launch all tests with `nosetests` command from chaos_monkey dir. + +import json +import unittest + +from mock import Mock, patch + +import control + +from config import GUID + + +class ReportConfigErrorTestCase(unittest.TestCase): + """ + When unknown config variable received form the island server, skip it and report config + error back to the server. + """ + + config_response = Mock(json=Mock(return_value={'config': {'blah': 'blah'}})) + + def teardown(self): + patch.stopall() + + def test_config(self): + patch('control.requests.patch', Mock()).start() + patch('control.WormConfiguration', Mock(current_server='127.0.0.1:123')).start() + + # GIVEN the server with uknown config variable + patch('control.requests.get', Mock(return_value=self.config_response)).start() + + # WHEN monkey tries to load config from server + control.ControlClient.load_control_config() + + # THEN she reports config error back to the server + control.requests.patch.assert_called_once_with( + "https://127.0.0.1:123/api/monkey/%s" % GUID, + data=json.dumps({'config_error': True}), + headers={'content-type': 'application/json'}, + verify=False, + proxies=control.ControlClient.proxies) + + +if __name__ == '__main__': + unittest.main() diff --git a/monkey_island/cc/main.py b/monkey_island/cc/main.py index ccf3fe995..b05c42e70 100644 --- a/monkey_island/cc/main.py +++ b/monkey_island/cc/main.py @@ -86,6 +86,8 @@ class Monkey(restful.Resource): update['$set']['config'] = monkey_json['config'] if 'tunnel' in monkey_json: update['$set']['tunnel'] = monkey_json['tunnel'] + if 'config_error' in monkey_json: + update['$set']['config_error'] = monkey_json['config_error'] return mongo.db.monkey.update({"guid": guid}, update, upsert=False)