Merge lp:~sseman/juju-chaos-monkey/chaos-monkey into lp:juju-chaos-monkey
- chaos-monkey
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
John George (community) | Approve | ||
Review via email: mp+257212@code.launchpad.net |
Commit message
Description of the change
Initial version of Chaos Monkey. It supports blocking a network and killing jujud process.
Adam Collard (adam-collard) : | # |
John George (jog) wrote : | # |
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:/
>
> 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(ChaosMonke
> + """
> + 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_
> + if not pids:
> + return None
> + return pids.split(' ')
> +
> + def kill_jujud(self):
> + log("Kill.
> + pids = self.get_
>
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_
> +
> + def kill_mongodb(self):
> + log("Kill.
> + pids = self.get_
> + 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_
> +
> + def get_chaos(self):
> + chaos = list()
> + chaos.append(
> + Chaos(
> + enable=
> + disable=None,
> + group=self.group,
> + command_
> + chaos.append(
> + Chaos(
> + enable=
> + disable=None,
> + group=self.gr...
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:/
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:/
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_
Main:
action='append' with integers results in parse_args raising an error.
Please make sure your do some basic testing before proposing.
"action=
You should not need to pass 'sys.argv[1:]' to parser.parse_args()
https:/
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_
Typo: autospect --> autospec
Needs additional test coverage to cover the logic:
1. assertRaises for pause_timeout > run_timeout
2. Test pause_timeout assignement
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
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_
> + cm = ChaosMonkey.
> + for _ in xrange(total_run):
> + cm.run_
> + if not is_juju_ok():
> + report_juju_error()
> + break
> + cm.shutdown()
John George (jog) wrote : | # |
More comments and questions in-line.
Seman (sseman) wrote : | # |
Thanks for you review and suggestions. Please see inline comments and code updates.
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.
Preview Diff
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 |
I added a few initial comments and will save this now but need to continue later.