Merge lp:~sseman/juju-chaos-monkey/chaos-monkey into lp:juju-chaos-monkey

Proposed by Seman
Status: Needs review
Proposed branch: lp:~sseman/juju-chaos-monkey/chaos-monkey
Merge into: lp:juju-chaos-monkey
Diff against target: 751 lines (+683/-0)
13 files modified
.bzrignore (+1/-0)
Makefile (+6/-0)
chaos/kill.py (+68/-0)
chaos/net.py (+119/-0)
chaos_monkey.py (+58/-0)
chaos_monkey_base.py (+27/-0)
runner.py (+40/-0)
tests/test_chaos_monkey.py (+85/-0)
tests/test_kill.py (+41/-0)
tests/test_net.py (+99/-0)
tests/test_runner.py (+81/-0)
tests/test_utility.py (+22/-0)
utility.py (+36/-0)
To merge this branch: bzr merge lp:~sseman/juju-chaos-monkey/chaos-monkey
Reviewer Review Type Date Requested Status
John George (community) Approve
Review via email: mp+257212@code.launchpad.net

Description of the change

Initial version of Chaos Monkey. It supports blocking a network and killing jujud process.

To post a comment you must log in.
Revision history for this message
John George (jog) wrote :

I added a few initial comments and will save this now but need to continue later.

Revision history for this message
Adam Collard (adam-collard) :
Revision history for this message
John George (jog) wrote :
Download full text (21.2 KiB)

On Wed, Apr 22, 2015 at 8:21 PM, Seman <email address hidden> wrote:

> Seman has proposed merging lp:~sseman/juju-chaos-monkey/chaos-moneky into
> lp:juju-chaos-monkey.
>
> Requested reviews:
> Juju QA Engineering (juju-qa)
>
> For more details, see:
>
> https://code.launchpad.net/~sseman/juju-chaos-monkey/chaos-moneky/+merge/257212
>
> Initial version of Chaos Monkey. It supports blocking a network and
> killing jujud process.
> --
> Your team Juju QA Engineering is requested to review the proposed merge of
> lp:~sseman/juju-chaos-monkey/chaos-moneky into lp:juju-chaos-monkey.
>
> === added file '.bzrignore'
> --- .bzrignore 1970-01-01 00:00:00 +0000
> +++ .bzrignore 2015-04-23 03:18:54 +0000
> @@ -0,0 +1,1 @@
> +/.idea
>
> === added file 'Makefile'
> --- Makefile 1970-01-01 00:00:00 +0000
> +++ Makefile 2015-04-23 03:18:54 +0000
> @@ -0,0 +1,6 @@
> +test:
> + python -m unittest discover -vv tests -p '*.py'
> +lint:
> + flake8 *.py tests/*.py
> +clean:
> + find . -name '*.pyc' -delete
>
> === added file '__init__.py'
> === added directory 'chaos'
> === added file 'chaos/__init__.py'
> === added file 'chaos/kill.py'
> --- chaos/kill.py 1970-01-01 00:00:00 +0000
> +++ chaos/kill.py 2015-04-23 03:18:54 +0000
> @@ -0,0 +1,62 @@
> +from chaos_monkey_base import (
> + Chaos,
> + ChaosMonkeyBase,
> +)
> +from utility import (
> + log,
> + NotFound,
> + run_shell_command,
> +)
> +
> +__metaclass__ = type
> +
> +
> +class Kill(ChaosMonkeyBase):
> + """
> + Kills a process
> + """
> + def __init__(self):
> + self.group = 'kill'
> + super(Kill, self).__init__()
> +
> + @classmethod
> + def factory(cls):
> + return cls()
> +
> + def get_pids(self, process):
> + pids = run_shell_command('pidof ' + process)
> + if not pids:
> + return None
> + return pids.split(' ')
> +
> + def kill_jujud(self):
> + log("Kill.kill_jujud")
> + pids = self.get_pids('jujud')
>
Do you really want to raise an exception here? Won't that terminate the
chaos monkey. If so I think you might want to just log that there were no
processes to kill, so the monkey can move on to it's not bit of chaos.

> + if not pids:
> + raise NotFound('Process id not found')
>
See my comment about explicit signal selection below.

> + run_shell_command('kill ' + pids[0])
> +
> + def kill_mongodb(self):
> + log("Kill.kill_mongod")
> + pids = self.get_pids('mongod')
> + if pids:
>
Should you be explicit about the signal you are sending with kill? By
default SIGTERM is sent but you may really want SIGKILL to ensure the
process is really terminated.

> + run_shell_command('kill ' + pids[0])
> +
> + def get_chaos(self):
> + chaos = list()
> + chaos.append(
> + Chaos(
> + enable=self.kill_jujud,
> + disable=None,
> + group=self.group,
> + command_str='jujud'))
> + chaos.append(
> + Chaos(
> + enable=self.kill_jujud,
> + disable=None,
> + group=self.gr...

Revision history for this message
John George (jog) wrote :

These are my comments related to runner.py. I'm posting these now so you can respond but will continue reviewing the rest of the merge proposal.

Please update all docstring formatting to follow:
https://www.python.org/dev/peps/pep-0257/

runner.py random()
    Please follow import formatting described in pep8 (required for merge)

    argparse and sys should be grouped together.

    chaos_monkey and utility should be grouped together.

    https://www.python.org/dev/peps/pep-0008/
    Imports should be grouped in the following order:

         1. standard library imports
         2. related third party imports
         3. local application/library specific imports

         You should put a blank line between each group of imports.

    For the merge is_juju_ok and report_juju_error don't need to be included.
    They are stubs that currently have not use. I'm not convinced this code
    is responsible for checking if juju is OK. how about just putting a TODO
    comment in rather than the stub code? (not required for merge but I think
    it would keep the code cleaner).

    I don't think the random() function is unit tested well enough, please see my
    comments under tests/test_runner.py (better unit testing required for merge)

    Main:
    action='append' with integers results in parse_args raising an error.
    Please make sure your do some basic testing before proposing.
    "action='append'" should be "type=int" (required for merge; fails to run otherwise)

    You should not need to pass 'sys.argv[1:]' to parser.parse_args()
    https://docs.python.org/2/library/argparse.html
    In a script, parse_args() will typically be called with no arguments,
    and the ArgumentParser will automatically determine the command-line
    arguments from sys.argv.

tests/test_runner.py TestRunner()
    Typo: autospect --> autospec

    Needs additional test coverage to cover the logic:
        1. assertRaises for pause_timeout > run_timeout
        2. Test pause_timeout assignement
            pause_timeout <= 0 test
                pause_timeout = 1 or pause_timeout
        3. Test run_random_chaos is called expected number of times
           (First see comments about if a pre-determind call count is really
            the right approach for total_timeout.)
        4. Test report_juju_error is call when is_juju_ok() is False.
        5. Test cm.shutdown is called

Revision history for this message
John George (jog) wrote :

Here is what I was referring to, in my last comment, when I said see my comments about a pre-determind call count. I'd made that comment in email but apparently Launchpad did not include it in the above comment grrrr

> + pause_timeout = 1 if pause_timeout <= 0 else pause_timeout
>
Total_run is a count used in the for loop to call run_random_chaos a
predetermined number of times?
If the random chaos functions take varying time to run, especially as
additional functions are added, it might make sense to check if the
run_timeout has been reached before making the next run_random_chaos call.
(this is not required for merge, just a suggestion)

> + total_run = int(run_timeout/pause_timeout)
> + cm = ChaosMonkey.factory()
> + for _ in xrange(total_run):
> + cm.run_random_chaos(pause_timeout)
> + if not is_juju_ok():
> + report_juju_error()
> + break
> + cm.shutdown()

Revision history for this message
John George (jog) wrote :

More comments and questions in-line.

Revision history for this message
John George (jog) :
review: Needs Fixing
3. By Seman

Updated the make file to include all subdirectories.

4. By Seman

Updated after a code review

Revision history for this message
Seman (sseman) wrote :

Thanks for you review and suggestions. Please see inline comments and code updates.

Revision history for this message
John George (jog) wrote :

I left a couple comments in-line but I think you can go ahead and land this branch.
I'm still a bit skeptical about your unit tests for run_random and how they can predict which chaos functions will be selected. I suspect these will need to change as additional chaos classes are added.

review: Approve

Unmerged revisions

4. By Seman

Updated after a code review

3. By Seman

Updated the make file to include all subdirectories.

2. By Seman

Initial commit

1. By Seman

CMonkey test

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.bzrignore'
2--- .bzrignore 1970-01-01 00:00:00 +0000
3+++ .bzrignore 2015-04-29 03:47:51 +0000
4@@ -0,0 +1,1 @@
5+/.idea
6
7=== added file 'Makefile'
8--- Makefile 1970-01-01 00:00:00 +0000
9+++ Makefile 2015-04-29 03:47:51 +0000
10@@ -0,0 +1,6 @@
11+test:
12+ python -m unittest discover -vv tests -p '*.py'
13+lint:
14+ flake8 .
15+clean:
16+ find . -name '*.pyc' -delete
17
18=== added file '__init__.py'
19=== added directory 'chaos'
20=== added file 'chaos/__init__.py'
21=== added file 'chaos/kill.py'
22--- chaos/kill.py 1970-01-01 00:00:00 +0000
23+++ chaos/kill.py 2015-04-29 03:47:51 +0000
24@@ -0,0 +1,68 @@
25+from chaos_monkey_base import (
26+ Chaos,
27+ ChaosMonkeyBase,
28+)
29+from utility import (
30+ log,
31+ NotFound,
32+ run_shell_command,
33+)
34+
35+__metaclass__ = type
36+
37+
38+class Kill(ChaosMonkeyBase):
39+ """Kills a process"""
40+
41+ def __init__(self):
42+ self.group = 'kill'
43+ super(Kill, self).__init__()
44+
45+ @classmethod
46+ def factory(cls):
47+ return cls()
48+
49+ def get_pids(self, process):
50+ pids = run_shell_command('pidof ' + process)
51+ if not pids:
52+ return None
53+ return pids.split(' ')
54+
55+ def kill_jujud(self, quiet_mode=True):
56+ log("Kill.kill_jujud")
57+ pids = self.get_pids('jujud')
58+ if not pids:
59+ log("Jujud process ID not found")
60+ if not quiet_mode:
61+ raise NotFound('Process id not found')
62+ return
63+ run_shell_command('kill -s SIGKILL ' + pids[0])
64+
65+ def kill_mongodb(self, quiet_mode=True):
66+ log("Kill.kill_mongod")
67+ pids = self.get_pids('mongod')
68+ if not pids:
69+ log("MongoDB process ID not found")
70+ if not quiet_mode:
71+ raise NotFound('Process id not found')
72+ return
73+ run_shell_command('kill -s SIGKILL ' + pids[0])
74+
75+ def get_chaos(self):
76+ chaos = list()
77+ chaos.append(
78+ Chaos(
79+ enable=self.kill_jujud,
80+ disable=None,
81+ group=self.group,
82+ command_str='jujud'))
83+ chaos.append(
84+ Chaos(
85+ enable=self.kill_jujud,
86+ disable=None,
87+ group=self.group,
88+ command_str='mongod'))
89+ return chaos
90+
91+ def shutdown(self):
92+ pass
93
94=== added file 'chaos/net.py'
95--- chaos/net.py 1970-01-01 00:00:00 +0000
96+++ chaos/net.py 2015-04-29 03:47:51 +0000
97@@ -0,0 +1,119 @@
98+from chaos_monkey_base import (
99+ Chaos,
100+ ChaosMonkeyBase,
101+)
102+from utility import (
103+ log,
104+ run_shell_command,
105+)
106+
107+__metaclass__ = type
108+
109+
110+class Net(ChaosMonkeyBase):
111+ """Creates networking chaos."""
112+
113+ def __init__(self):
114+ self.group = 'net'
115+ super(Net, self).__init__()
116+
117+ @classmethod
118+ def factory(cls):
119+ return cls()
120+
121+ def reset(self):
122+ log("Net.reset ")
123+ cmd = 'ufw reset'
124+ run_shell_command(cmd)
125+
126+ def default_deny(self):
127+ log("Net.default_deny")
128+ cmd = "ufw default deny"
129+ run_shell_command(cmd)
130+
131+ def default_allow(self):
132+ log("Net.default_allow")
133+ cmd = "ufw default allow"
134+ run_shell_command(cmd)
135+
136+ def allow_ssh(self):
137+ log("Net.allow_ssh")
138+ cmd = 'ufw allow ssh'
139+ run_shell_command(cmd)
140+
141+ def deny_ssh(self):
142+ log("Net.deny_ssh")
143+ cmd = 'ufw deny ssh'
144+ run_shell_command(cmd)
145+
146+ def deny_all_incoming_and_outgoing_except_ssh(self):
147+ log("Net.deny_all_incoming_and_outgoing_except_ssh")
148+ self.deny_all_incoming_except_ssh()
149+ self.deny_all_outgoing_except_ssh()
150+
151+ def allow_all_incoming_and_outgoing(self):
152+ log("Net.allow_all_incoming_and_outgoing")
153+ self.allow_all_incoming()
154+ self.allow_all_outgoing()
155+
156+ def deny_all_incoming_except_ssh(self):
157+ log("Net.deny_all_incoming_except_ssh")
158+ self.allow_ssh()
159+ self.default_deny()
160+
161+ def allow_all_incoming(self):
162+ log("Net.allow_all_incoming")
163+ self.default_allow()
164+
165+ def deny_all_outgoing_except_ssh(self):
166+ log("Net.deny_all_outgoing_except_ssh")
167+ self.allow_ssh()
168+ cmd = 'ufw deny out to any'
169+ run_shell_command(cmd)
170+
171+ def allow_all_outgoing(self):
172+ log("Net.allow_all_outgoing")
173+ cmd = 'ufw delete deny out to any'
174+ run_shell_command(cmd)
175+
176+ def deny_port(self, port=8080):
177+ log("Net.deny_port port=%s" % port)
178+ cmd = 'ufw deny ' + str(port)
179+ run_shell_command(cmd)
180+
181+ def get_chaos(self):
182+ chaos = list()
183+ chaos.append(
184+ Chaos(
185+ enable=self.deny_all_incoming_and_outgoing_except_ssh,
186+ disable=self.allow_all_incoming_and_outgoing,
187+ group=self.group,
188+ command_str='deny-all'))
189+ chaos.append(
190+ Chaos(
191+ enable=self.deny_all_incoming_except_ssh,
192+ disable=self.allow_all_incoming,
193+ group=self.group,
194+ command_str='deny-incoming'))
195+ chaos.append(
196+ Chaos(
197+ enable=self.deny_all_outgoing_except_ssh,
198+ disable=self.allow_all_outgoing,
199+ group=self.group,
200+ command_str='deny-outgoing'))
201+ chaos.append(
202+ Chaos(
203+ enable=self.allow_ssh,
204+ disable=None,
205+ group=self.group,
206+ command_str='allow-ssh'))
207+ chaos.append(
208+ Chaos(
209+ enable=self.deny_ssh,
210+ disable=self.allow_ssh,
211+ group=self.group,
212+ command_str='deny-ssh'))
213+ return chaos
214+
215+ def shutdown(self):
216+ self.reset()
217
218=== added file 'chaos_monkey.py'
219--- chaos_monkey.py 1970-01-01 00:00:00 +0000
220+++ chaos_monkey.py 2015-04-29 03:47:51 +0000
221@@ -0,0 +1,58 @@
222+import random
223+from time import sleep
224+
225+from chaos import (
226+ kill,
227+ net
228+)
229+from utility import NotFound
230+
231+__metaclass__ = type
232+
233+
234+class ChaosMonkey:
235+ """Runs chaos monkey commands."""
236+
237+ def __init__(self, chaos, factory_obj):
238+ self.chaos = chaos
239+ self.factory_obj = factory_obj
240+
241+ @classmethod
242+ def factory(cls):
243+ all_chaos, factory_obj = ChaosMonkey.get_all_chaos()
244+ return cls(all_chaos, factory_obj)
245+
246+ @staticmethod
247+ def get_all_chaos():
248+ all_chaos = []
249+ all_factory_obj = []
250+ factories = [net.Net.factory, kill.Kill.factory]
251+ for factory in factories:
252+ factory_obj = factory()
253+ all_factory_obj.append(factory_obj)
254+ all_chaos.extend(factory_obj.get_chaos())
255+ return all_chaos, all_factory_obj
256+
257+ def run_random_chaos(self, timeout=2):
258+ random_chaos = random.choice(self.chaos)
259+ self._run_command(random_chaos, timeout=timeout)
260+
261+ def run_chaos(self, group, command_str, timeout=2):
262+ command_found = False
263+ for chaos in self.chaos:
264+ if chaos.group == group and chaos.command_str == command_str:
265+ command_found = True
266+ self._run_command(chaos, timeout=timeout)
267+ if not command_found:
268+ raise NotFound("Command not found: group: %s command_str:%s" % (
269+ group, command_str))
270+
271+ def _run_command(self, chaos, timeout=2):
272+ chaos.enable()
273+ sleep(timeout)
274+ if chaos.disable:
275+ chaos.disable()
276+
277+ def shutdown(self):
278+ for obj in self.factory_obj:
279+ obj.shutdown()
280
281=== added file 'chaos_monkey_base.py'
282--- chaos_monkey_base.py 1970-01-01 00:00:00 +0000
283+++ chaos_monkey_base.py 2015-04-29 03:47:51 +0000
284@@ -0,0 +1,27 @@
285+import abc
286+
287+__metaclass__ = type
288+
289+
290+class ChaosMonkeyBase:
291+ __metaclass__ = abc.ABCMeta
292+
293+ def __init__(self):
294+ pass
295+
296+ @abc.abstractmethod
297+ def get_chaos(self):
298+ raise NotImplemented
299+
300+ @abc.abstractmethod
301+ def shutdown(self):
302+ raise NotImplemented
303+
304+
305+class Chaos:
306+
307+ def __init__(self, enable, disable, group, command_str):
308+ self.enable = enable
309+ self.disable = disable
310+ self.group = group
311+ self.command_str = command_str
312
313=== added file 'runner.py'
314--- runner.py 1970-01-01 00:00:00 +0000
315+++ runner.py 2015-04-29 03:47:51 +0000
316@@ -0,0 +1,40 @@
317+from argparse import ArgumentParser
318+from time import time
319+
320+from chaos_monkey import ChaosMonkey
321+from utility import BadRequest
322+
323+
324+def random(run_timeout, enablement_timeout):
325+ """
326+ Runs a random chaos
327+ :param run_timeout: Total time to run the chaos
328+ :param enablement_timeout: Timeout between enabling and disabling a chaos.
329+ Example: disable all the network, wait for timeout and enable it back
330+ :rtype none
331+ """
332+ if enablement_timeout > run_timeout:
333+ raise BadRequest(
334+ "Total run timeout can't be less than enablement timeout")
335+ if run_timeout <= 0:
336+ raise BadRequest("Invalid value for run timeout")
337+ if enablement_timeout < 0:
338+ raise BadRequest("Invalid value for enablement timeout")
339+ cm = ChaosMonkey.factory()
340+ expire_time = time() + run_timeout
341+ while time() < expire_time:
342+ cm.run_random_chaos(enablement_timeout)
343+ cm.shutdown()
344+
345+
346+if __name__ == '__main__':
347+ parser = ArgumentParser()
348+ parser.add_argument(
349+ '-pt', '--enablement-timeout', default=10, type=int,
350+ help="Enablement timeout in seconds")
351+ parser.add_argument(
352+ '-tt', '--total-timeout', default=60, type=int,
353+ help="Total timeout in seconds")
354+ args = parser.parse_args()
355+ random(run_timeout=args.total_timeout,
356+ enablement_timeout=args.enablement_timeout)
357
358=== added directory 'tests'
359=== added file 'tests/test_chaos_monkey.py'
360--- tests/test_chaos_monkey.py 1970-01-01 00:00:00 +0000
361+++ tests/test_chaos_monkey.py 2015-04-29 03:47:51 +0000
362@@ -0,0 +1,85 @@
363+from unittest import TestCase
364+
365+from mock import patch, call
366+
367+from chaos_monkey import ChaosMonkey, NotFound
368+from chaos_monkey_base import Chaos
369+from chaos.net import Net
370+
371+
372+class TestChaosMonkey(TestCase):
373+
374+ def test_factory(self):
375+ cm = ChaosMonkey.factory()
376+ self.assertIs(type(cm), ChaosMonkey)
377+
378+ def test_get_all_chaos(self):
379+ cm = ChaosMonkey.factory()
380+ all_chaos, all_factory_obj = cm.get_all_chaos()
381+ self.assertItemsEqual(
382+ self._get_all_command_str(all_chaos), self._command_strings())
383+
384+ def test_run_random_chaos(self):
385+ cm = ChaosMonkey.factory()
386+ with patch('utility.check_output', autospec=True) as mock:
387+ cm.run_random_chaos(timeout=0)
388+ self.assertEqual(mock.called, True)
389+
390+ def test_run_random_chaos_passes_timeout(self):
391+ cm = ChaosMonkey.factory()
392+ with patch('chaos_monkey.ChaosMonkey._run_command',
393+ autospec=True) as mock:
394+ cm.run_random_chaos(timeout=1)
395+ self.assertEqual(1, mock.call_args_list[0][1]['timeout'])
396+
397+ def test_run_chaos(self):
398+ cm = ChaosMonkey.factory()
399+ with patch('utility.check_output', autospec=True) as mock:
400+ cm.run_chaos('net', 'allow-ssh', timeout=0)
401+ mock.assert_called_once_with(['ufw', 'allow', 'ssh'])
402+
403+ def test_run_chaos_passes_timeout(self):
404+ cm = ChaosMonkey.factory()
405+ with patch('chaos_monkey.ChaosMonkey._run_command',
406+ autospec=True) as mock:
407+ cm.run_chaos('net', 'allow-ssh', timeout=0)
408+ self.assertEqual(0, mock.call_args_list[0][1]['timeout'])
409+
410+ def test_run_chaos_raises_for_command_str(self):
411+ cm = ChaosMonkey.factory()
412+ with patch('utility.check_output', autospec=True):
413+ with self.assertRaisesRegexp(
414+ NotFound,
415+ "Command not found: group: net command_str:foo"):
416+ cm.run_chaos('net', 'foo', timeout=0)
417+
418+ def test_run_chaos_raises_for_group(self):
419+ cm = ChaosMonkey.factory()
420+ with patch('utility.check_output', autospec=True):
421+ with self.assertRaisesRegexp(
422+ NotFound,
423+ "Command not found: group: bar command_str:allow-ssh"):
424+ cm.run_chaos('bar', 'allow-ssh', timeout=0)
425+
426+ def test_run_command(self):
427+ cm = ChaosMonkey.factory()
428+ net = Net()
429+ chaos = Chaos(enable=net.deny_ssh, disable=net.allow_ssh,
430+ group='net', command_str='deny-ssh')
431+ with patch('utility.check_output', autospec=True) as mock:
432+ cm._run_command(chaos, timeout=0)
433+ self.assertEqual(mock.mock_calls, [
434+ call(['ufw', 'deny', 'ssh']), call(['ufw', 'allow', 'ssh'])])
435+
436+ def test_shutdown(self):
437+ cm = ChaosMonkey.factory()
438+ with patch('utility.check_output', autospec=True) as mock:
439+ cm.shutdown()
440+ mock.assert_any_call(['ufw', 'reset'])
441+
442+ def _get_all_command_str(self, chaos):
443+ return [c.command_str for c in chaos]
444+
445+ def _command_strings(self):
446+ return ['deny-all', 'deny-incoming', 'deny-outgoing', 'allow-ssh',
447+ 'deny-ssh', 'jujud', 'mongod']
448
449=== added file 'tests/test_kill.py'
450--- tests/test_kill.py 1970-01-01 00:00:00 +0000
451+++ tests/test_kill.py 2015-04-29 03:47:51 +0000
452@@ -0,0 +1,41 @@
453+from unittest import TestCase
454+
455+from mock import patch, call
456+
457+from chaos.kill import Kill
458+
459+
460+class TestKill(TestCase):
461+
462+ def test_get_pids(self):
463+ kill = Kill()
464+ with patch('utility.check_output', autospec=True,
465+ return_value='1234 2345') as mock:
466+ pids = kill.get_pids('jujud')
467+ self.assertEqual(pids, ['1234', '2345'])
468+ mock.assert_called_once_with(['pidof', 'jujud'])
469+
470+ def test_kill_jujud(self):
471+ kill = Kill()
472+ with patch('utility.check_output', autospec=True,
473+ return_value='1234 2345') as mock:
474+ kill.kill_jujud()
475+ self.assertEqual(mock.mock_calls, [
476+ call(['pidof', 'jujud']),
477+ call(['kill', '-s', 'SIGKILL', '1234'])
478+ ])
479+
480+ def test_kill_mongodb(self):
481+ kill = Kill()
482+ with patch('utility.check_output', autospec=True,
483+ return_value='1234 2345') as mock:
484+ kill.kill_mongodb()
485+ self.assertEqual(mock.mock_calls, [
486+ call(['pidof', 'mongod']),
487+ call(['kill', '-s', 'SIGKILL', '1234'])
488+ ])
489+
490+ def test_get_chaos(self):
491+ kill = Kill()
492+ chaos = kill.get_chaos()
493+ self.assertEqual(len(chaos), 2)
494
495=== added file 'tests/test_net.py'
496--- tests/test_net.py 1970-01-01 00:00:00 +0000
497+++ tests/test_net.py 2015-04-29 03:47:51 +0000
498@@ -0,0 +1,99 @@
499+from unittest import TestCase
500+
501+from mock import patch, call
502+
503+from chaos.net import Net
504+
505+
506+class TestNet(TestCase):
507+
508+ def _run_test(self, method_call, assert_args):
509+ net = Net()
510+ with patch('utility.check_output', autospec=True) as mock:
511+ getattr(net, method_call)()
512+ mock.assert_called_once_with(assert_args)
513+
514+ def _run_mock_calls(self, method_call, call_list):
515+ net = Net()
516+ with patch('utility.check_output', autospec=True) as mock:
517+ getattr(net, method_call)()
518+ self.assertEqual(mock.mock_calls, call_list)
519+
520+ def test_reset(self):
521+ self._run_test('reset', ['ufw', 'reset'])
522+
523+ def test_default_deny(self):
524+ self._run_test('default_deny', ['ufw', 'default', 'deny'])
525+
526+ def test_default_allow(self):
527+ self._run_test('default_allow', ['ufw', 'default', 'allow'])
528+
529+ def test_allow_ssh(self):
530+ self._run_test('allow_ssh', ['ufw', 'allow', 'ssh'])
531+
532+ def test_deny_ssh(self):
533+ self._run_test('deny_ssh', ['ufw', 'deny', 'ssh'])
534+
535+ def test_deny_all_incoming_and_outgoing_except_ssh(self):
536+ self._run_mock_calls(
537+ 'deny_all_incoming_and_outgoing_except_ssh',
538+ [self._allow_ssh_call(), self._default_deny_call(),
539+ self._allow_ssh_call(),
540+ call(['ufw', 'deny', 'out', 'to', 'any'])])
541+
542+ def test_allow_all_incoming_and_outgoing(self):
543+ self._run_mock_calls(
544+ 'allow_all_incoming_and_outgoing',
545+ [self._default_allow_call(),
546+ call(['ufw', 'delete', 'deny', 'out', 'to', 'any'])])
547+
548+ def test_deny_all_incoming_except_ssh(self):
549+ self._run_mock_calls(
550+ 'deny_all_incoming_except_ssh',
551+ [self._allow_ssh_call(), self._default_deny_call()])
552+
553+ def test_allow_all_incoming(self):
554+ self._run_test('allow_all_incoming', ['ufw', 'default', 'allow'])
555+
556+ def test_deny_all_outgoing_except_ssh(self):
557+ self._run_mock_calls(
558+ 'deny_all_outgoing_except_ssh',
559+ [self._allow_ssh_call(),
560+ call(['ufw', 'deny', 'out', 'to', 'any'])])
561+
562+ def test_allow_all_outgoing(self):
563+ self._run_test(
564+ 'allow_all_outgoing',
565+ ['ufw', 'delete', 'deny', 'out', 'to', 'any'])
566+
567+ def test_deny_port(self):
568+ self._run_test(
569+ 'deny_port', ['ufw', 'deny', '8080'])
570+
571+ def test_get_chaos(self):
572+ net = Net()
573+ chaos = net.get_chaos()
574+ self.assertEqual(len(chaos), 5)
575+ self.assertItemsEqual(
576+ self._get_all_command_str(chaos), self._command_strings())
577+ for c in chaos:
578+ self.assertEqual('net', c.group)
579+
580+ def _get_all_command_str(self, chaos):
581+ return [c.command_str for c in chaos]
582+
583+ def _command_strings(self):
584+ return ['deny-all', 'deny-incoming', 'deny-outgoing', 'allow-ssh',
585+ 'deny-ssh']
586+
587+ def test_shutdown(self):
588+ self._run_test('reset', ['ufw', 'reset'])
589+
590+ def _allow_ssh_call(self):
591+ return call(['ufw', 'allow', 'ssh'])
592+
593+ def _default_deny_call(self):
594+ return call(['ufw', 'default', 'deny'])
595+
596+ def _default_allow_call(self):
597+ return call(['ufw', 'default', 'allow'])
598
599=== added file 'tests/test_runner.py'
600--- tests/test_runner.py 1970-01-01 00:00:00 +0000
601+++ tests/test_runner.py 2015-04-29 03:47:51 +0000
602@@ -0,0 +1,81 @@
603+from time import time
604+from unittest import TestCase
605+
606+from mock import patch
607+
608+from utility import BadRequest
609+from runner import random
610+
611+
612+class TestRunner(TestCase):
613+
614+ def test_random(self):
615+ with patch('utility.check_output', autospec=True) as mock:
616+ random(run_timeout=1, enablement_timeout=1)
617+ self.assertEqual(mock.called, True)
618+
619+ def test_random_enablement_zero(self):
620+ with patch('utility.check_output', autospec=True) as mock:
621+ random(run_timeout=1, enablement_timeout=0)
622+ self.assertEqual(mock.called, True)
623+
624+ def test_random_raises_exception(self):
625+ with patch('utility.check_output', autospec=True):
626+ with self.assertRaisesRegexp(
627+ BadRequest,
628+ "Total run timeout can't be less than enablement timeout"):
629+ random(run_timeout=1, enablement_timeout=2)
630+
631+ def test_random_run_timeout_raises_exception_for_zero(self):
632+ with patch('utility.check_output', autospec=True):
633+ with self.assertRaisesRegexp(
634+ BadRequest, "Invalid value for run timeout"):
635+ random(run_timeout=0, enablement_timeout=-1)
636+
637+ def test_random_run_timeout_raises_exception_for_less_than_zero(self):
638+ with patch('utility.check_output', autospec=True):
639+ with self.assertRaisesRegexp(
640+ BadRequest, "Invalid value for run timeout"):
641+ random(run_timeout=-1, enablement_timeout=-2)
642+
643+ def test_random_run_enablement_raises_exception_for_less_than_zero(self):
644+ with patch('utility.check_output', autospec=True):
645+ with self.assertRaisesRegexp(
646+ BadRequest, "Invalid value for enablement timeout"):
647+ random(run_timeout=2, enablement_timeout=-1)
648+
649+ def test_random_verify_timeout(self):
650+ run_timeout = 6
651+ with patch('utility.check_output', autospec=True) as mock:
652+ current_time = time()
653+ random(run_timeout=run_timeout, enablement_timeout=2)
654+ end_time = time()
655+ self.assertEqual(run_timeout, int(end_time-current_time))
656+ self.assertEqual(mock.called, True)
657+
658+ def test_random_assert_chaos_monkey_methods_called(self):
659+ with patch('runner.ChaosMonkey', autospec=True) as cm_mock:
660+ random(run_timeout=1, enablement_timeout=1)
661+ cm_mock.factory.return_value.run_random_chaos.assert_called_with(1)
662+ cm_mock.factory.return_value.shutdown.assert_called_with()
663+
664+ def test_random_assert_chaos_methods_called(self):
665+ net_ctx = patch('chaos.net.Net', autospec=True)
666+ kill_ctx = patch('chaos.kill.Kill', autospec=True)
667+ with patch('utility.check_output', autospec=True):
668+ with patch('chaos_monkey.ChaosMonkey.run_random_chaos',
669+ autospec=True):
670+ with net_ctx as net_mock:
671+ with kill_ctx as kill_mock:
672+ random(run_timeout=1, enablement_timeout=1)
673+ net_mock.factory.return_value.shutdown.assert_called_with()
674+ kill_mock.factory.return_value.shutdown.assert_called_with()
675+ net_mock.factory.return_value.get_chaos.assert_called_with()
676+ kill_mock.factory.return_value.get_chaos.assert_called_with()
677+
678+ def test_random_passes_timeout(self):
679+ with patch('utility.check_output', autospec=True):
680+ with patch('chaos_monkey.ChaosMonkey._run_command',
681+ autospec=True) as mock:
682+ random(run_timeout=3, enablement_timeout=2)
683+ self.assertEqual(mock.call_args_list[0][1]['timeout'], 2)
684
685=== added file 'tests/test_utility.py'
686--- tests/test_utility.py 1970-01-01 00:00:00 +0000
687+++ tests/test_utility.py 2015-04-29 03:47:51 +0000
688@@ -0,0 +1,22 @@
689+from subprocess import CalledProcessError
690+from unittest import TestCase
691+
692+from mock import patch
693+
694+from utility import run_shell_command
695+
696+
697+class TestUtility(TestCase):
698+
699+ def test_run_shell_command(self):
700+ with patch('utility.check_output', autospec=True) as mock:
701+ run_shell_command('foo')
702+ mock.assert_called_once_with(['foo'])
703+
704+ def test_run_shell_command_error(self):
705+ with self.assertRaisesRegexp(CalledProcessError, ""):
706+ run_shell_command('ls -W', quiet_mode=False)
707+
708+ def test_run_shell_command_output(self):
709+ output = run_shell_command('echo "hello"')
710+ self.assertEqual(output, '"hello"\n')
711
712=== added file 'utility.py'
713--- utility.py 1970-01-01 00:00:00 +0000
714+++ utility.py 2015-04-29 03:47:51 +0000
715@@ -0,0 +1,36 @@
716+from __future__ import print_function
717+
718+from subprocess import (
719+ CalledProcessError,
720+ check_output,
721+)
722+
723+
724+def run_shell_command(cmd, quiet_mode=True):
725+ shell_cmd = cmd.split(' ') if type(cmd) is str else cmd
726+ output = None
727+ try:
728+ output = check_output(shell_cmd)
729+ except CalledProcessError:
730+ log("Command generated error: %s " % cmd)
731+ if not quiet_mode:
732+ raise
733+ return output
734+
735+
736+def log(log_str):
737+ print(log_str)
738+
739+
740+class NotFound(Exception):
741+ """
742+ Requested resource not found
743+ """
744+ error_code = 404
745+
746+
747+class BadRequest(Exception):
748+ """
749+ Incorrectly formatted request
750+ """
751+ error_code = 400

Subscribers

People subscribed via source and target branches