Merge lp:~mbruzek/charms/trusty/fail2ban/trunk into lp:~lazypower/charms/trusty/fail2ban/trunk
- Trusty Tahr (14.04)
- trunk
- Merge into trunk
Proposed by
Matt Bruzek
Status: | Merged |
---|---|
Merged at revision: | 4 |
Proposed branch: | lp:~mbruzek/charms/trusty/fail2ban/trunk |
Merge into: | lp:~lazypower/charms/trusty/fail2ban/trunk |
Diff against target: |
511 lines (+111/-299) 8 files modified
Makefile (+7/-20) README.md (+24/-17) config.yaml (+1/-1) lib/charmhelpers/fetch/__init__.py (+1/-1) metadata.yaml (+2/-2) scripts/charm_helpers_sync.py (+0/-223) tests/00-setup (+2/-2) tests/10-deploy (+74/-33) |
To merge this branch: | bzr merge lp:~mbruzek/charms/trusty/fail2ban/trunk |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Charles Butler | Approve | ||
Review via email: mp+242506@code.launchpad.net |
Commit message
Description of the change
I removed some things from the Makefile, wrote proper tests and updated the README.
To post a comment you must log in.
- 4. By Charles Butler
-
[r=lazypower
]Matt Bruzek 2014-11-21 Updates to Makefile, README.md, config, and metadata, and now with better ...
Matt Bruzek 2014-11-20 Removing redundant sync file and tests.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'Makefile' |
2 | --- Makefile 2014-10-22 04:18:26 +0000 |
3 | +++ Makefile 2014-11-21 16:00:44 +0000 |
4 | @@ -1,37 +1,24 @@ |
5 | PYTHON := /usr/bin/env python |
6 | |
7 | -build: clean test |
8 | - |
9 | clean: |
10 | find . -name \*.pyc -delete |
11 | find . -name '*.bak' -delete |
12 | - rm -f .coverage |
13 | - |
14 | -clean_all: clean |
15 | - rm -rf .venv |
16 | |
17 | lint: |
18 | @flake8 --exclude hooks/charmhelpers --ignore=E125 hooks |
19 | - @charm proof |
20 | - |
21 | -test: .venv |
22 | - @echo Starting unit tests... |
23 | - @PYTHONPATH=./hooks $(PYTHON) .venv/bin/nosetests --nologcapture unit_tests |
24 | - |
25 | -.venv: |
26 | - sudo apt-get install python-virtualenv |
27 | - virtualenv .venv |
28 | - .venv/bin/pip install nose mock pyyaml |
29 | + charm proof |
30 | |
31 | deploy: |
32 | - @echo Deploying local elasticsearch charm |
33 | - @juju deploy --num-units=2 --repository=../.. local:trusty/elasticsearch |
34 | - @ juju deploy --repository=../.. local:trusty/logstash |
35 | + @echo Deploying ubuntu charm |
36 | + juju deploy cs:trusty/ubuntu |
37 | + @echo Deploying local fail2ban charm |
38 | + juju deploy --repository=../.. local:trusty/fail2ban |
39 | + juju add-relation ubuntu:juju-info fail2ban:juju-info |
40 | |
41 | # The following targets are used for charm maintenance only. |
42 | bin/charm_helpers_sync.py: |
43 | @mkdir -p bin |
44 | - @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \ |
45 | + bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \ |
46 | > bin/charm_helpers_sync.py |
47 | |
48 | sync: bin/charm_helpers_sync.py |
49 | |
50 | === modified file 'README.md' |
51 | --- README.md 2014-10-22 04:18:26 +0000 |
52 | +++ README.md 2014-11-21 16:00:44 +0000 |
53 | @@ -1,40 +1,47 @@ |
54 | # Fail2Ban |
55 | |
56 | -Deploys fail2ban monitoring and DDOS prevention service, with exposed |
57 | -configuration for SSH DDOS |
58 | +Deploys fail2ban monitoring and denial-of-service (DoS) prevention service, |
59 | +with exposed configuration to help prevent SSH DoS attacks. |
60 | + |
61 | +The fail2ban service scans log files and bans IPs that have too many password |
62 | +failures. The number of failures, and ban time are configurable. |
63 | |
64 | # Deployment |
65 | |
66 | -Step by step instructions on deploying the charm: |
67 | +The fail2ban charm is a subordinate charm a container to deploy. |
68 | +The fail2ban charm uses the implicit juju-info relationship so it can be |
69 | +related to any container charm. Here are the steps to deploy the charm: |
70 | |
71 | + juju deploy ubuntu |
72 | juju deploy fail2ban |
73 | - juju add-relation fail2ban service |
74 | + juju add-relation fail2ban:juju-info ubuntu:juju-info |
75 | |
76 | -This will install, and configure fail2ban to monitor SSH by default with a 1 |
77 | -hour delay on incorrect password attempts, after 3 failed attempts. |
78 | +These steps will install, and configure fail2ban to monitor SSH by default |
79 | +with a 1 hour delay on incorrect password attempts, after 3 failed attempts. |
80 | |
81 | ## Known Limitations and Issues |
82 | |
83 | -Does not configure any of the other services fail2ban can monitor, such as |
84 | -http, ftp, etc. |
85 | +This charm does not configure any of the other services fail2ban can monitor, |
86 | +such as http, ftp, etc. If you wish to configure these services you can find |
87 | +the configuration file at `/etc/fail2ban/jail.local`. |
88 | |
89 | # Configuration |
90 | |
91 | -- **maxretry**: "number of attempts before ban" |
92 | -- **ignoreip**: "Additional IP's (space separated) to add to the ignore ruleset. |
93 | -Supports IP and CIDR" |
94 | -- **bantime**: "Ban time in seconds (defaults to 1 hour)" |
95 | -- **destemail**: "Address to send mail to on abuse" |
96 | +- **maxretry**: number of attempts before banning the IP adddress. |
97 | +- **ignoreip**: Additional IP's (space separated) to add to the ignore ruleset. |
98 | +Supports IP and CIDR. |
99 | +- **bantime**: Ban time in seconds (defaults to 1 hour). |
100 | +- **destemail**: Email address to send mail to on abuse. |
101 | |
102 | Example configuration |
103 | |
104 | - juju set fail2ban bantime=300 maxretry=5 ignoreip=192.262.3.0/24 |
105 | + juju set fail2ban bantime=3000 maxretry=5 ignoreip=192.262.3.0/24 |
106 | |
107 | ## Maintainer |
108 | |
109 | - [Charles Butler <charles.butler@ubuntu.com>](mailto:charles.butler@ubuntu.com) |
110 | |
111 | -## Upstream Project Name |
112 | +## Fail2ban upstream project |
113 | |
114 | -- [Upstream Website](http://www.fail2ban.org/wiki/index.php/Main_Page) |
115 | -- [Upstream bug tracker](https://github.com/fail2ban/fail2ban/issues) |
116 | +- [Fail2ban Website](http://www.fail2ban.org/wiki/index.php/Main_Page) |
117 | +- [Fail2ban issues](https://github.com/fail2ban/fail2ban/issues) |
118 | |
119 | === modified file 'config.yaml' |
120 | --- config.yaml 2014-10-22 04:18:26 +0000 |
121 | +++ config.yaml 2014-11-21 16:00:44 +0000 |
122 | @@ -2,7 +2,7 @@ |
123 | maxretry: |
124 | type: int |
125 | default: 3 |
126 | - description: "number of attempts before ban" |
127 | + description: "number of attempts before banning the IP address" |
128 | ignoreip: |
129 | type: string |
130 | default: |
131 | |
132 | === modified file 'lib/charmhelpers/fetch/__init__.py' |
133 | --- lib/charmhelpers/fetch/__init__.py 2014-10-22 04:18:26 +0000 |
134 | +++ lib/charmhelpers/fetch/__init__.py 2014-11-21 16:00:44 +0000 |
135 | @@ -256,7 +256,7 @@ |
136 | elif source == 'distro': |
137 | pass |
138 | else: |
139 | - raise SourceConfigError("Unknown source: {!r}".format(source)) |
140 | + log("Unknown source: {!r}".format(source)) |
141 | |
142 | if key: |
143 | if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key: |
144 | |
145 | === modified file 'metadata.yaml' |
146 | --- metadata.yaml 2014-10-22 04:18:26 +0000 |
147 | +++ metadata.yaml 2014-11-21 16:00:44 +0000 |
148 | @@ -1,6 +1,6 @@ |
149 | name: fail2ban |
150 | -summary: ban hosts that cause multiple authentication errors |
151 | -maintainer: charles <charles@desktop> |
152 | +summary: Bans IP addresses that have too many authentication failures. |
153 | +maintainer: Charles Butler <charles.butler@ubuntu.com> |
154 | description: | |
155 | Fail2ban monitors log files (e.g. /var/log/auth.log, |
156 | /var/log/apache/access.log) and temporarily or persistently bans |
157 | |
158 | === removed directory 'scripts' |
159 | === removed file 'scripts/charm_helpers_sync.py' |
160 | --- scripts/charm_helpers_sync.py 2014-10-22 04:18:26 +0000 |
161 | +++ scripts/charm_helpers_sync.py 1970-01-01 00:00:00 +0000 |
162 | @@ -1,223 +0,0 @@ |
163 | -#!/usr/bin/env python |
164 | -# Copyright 2013 Canonical Ltd. |
165 | - |
166 | -# Authors: |
167 | -# Adam Gandelman <adamg@ubuntu.com> |
168 | - |
169 | -import logging |
170 | -import optparse |
171 | -import os |
172 | -import subprocess |
173 | -import shutil |
174 | -import sys |
175 | -import tempfile |
176 | -import yaml |
177 | - |
178 | -from fnmatch import fnmatch |
179 | - |
180 | -CHARM_HELPERS_BRANCH = 'lp:charm-helpers' |
181 | - |
182 | - |
183 | -def parse_config(conf_file): |
184 | - if not os.path.isfile(conf_file): |
185 | - logging.error('Invalid config file: %s.' % conf_file) |
186 | - return False |
187 | - return yaml.load(open(conf_file).read()) |
188 | - |
189 | - |
190 | -def clone_helpers(work_dir, branch): |
191 | - dest = os.path.join(work_dir, 'charm-helpers') |
192 | - logging.info('Checking out %s to %s.' % (branch, dest)) |
193 | - cmd = ['bzr', 'branch', branch, dest] |
194 | - subprocess.check_call(cmd) |
195 | - return dest |
196 | - |
197 | - |
198 | -def _module_path(module): |
199 | - return os.path.join(*module.split('.')) |
200 | - |
201 | - |
202 | -def _src_path(src, module): |
203 | - return os.path.join(src, 'charmhelpers', _module_path(module)) |
204 | - |
205 | - |
206 | -def _dest_path(dest, module): |
207 | - return os.path.join(dest, _module_path(module)) |
208 | - |
209 | - |
210 | -def _is_pyfile(path): |
211 | - return os.path.isfile(path + '.py') |
212 | - |
213 | - |
214 | -def ensure_init(path): |
215 | - ''' |
216 | - ensure directories leading up to path are importable, omitting |
217 | - parent directory, eg path='/hooks/helpers/foo'/: |
218 | - hooks/ |
219 | - hooks/helpers/__init__.py |
220 | - hooks/helpers/foo/__init__.py |
221 | - ''' |
222 | - for d, dirs, files in os.walk(os.path.join(*path.split('/')[:2])): |
223 | - _i = os.path.join(d, '__init__.py') |
224 | - if not os.path.exists(_i): |
225 | - logging.info('Adding missing __init__.py: %s' % _i) |
226 | - open(_i, 'wb').close() |
227 | - |
228 | - |
229 | -def sync_pyfile(src, dest): |
230 | - src = src + '.py' |
231 | - src_dir = os.path.dirname(src) |
232 | - logging.info('Syncing pyfile: %s -> %s.' % (src, dest)) |
233 | - if not os.path.exists(dest): |
234 | - os.makedirs(dest) |
235 | - shutil.copy(src, dest) |
236 | - if os.path.isfile(os.path.join(src_dir, '__init__.py')): |
237 | - shutil.copy(os.path.join(src_dir, '__init__.py'), |
238 | - dest) |
239 | - ensure_init(dest) |
240 | - |
241 | - |
242 | -def get_filter(opts=None): |
243 | - opts = opts or [] |
244 | - if 'inc=*' in opts: |
245 | - # do not filter any files, include everything |
246 | - return None |
247 | - |
248 | - def _filter(dir, ls): |
249 | - incs = [opt.split('=').pop() for opt in opts if 'inc=' in opt] |
250 | - _filter = [] |
251 | - for f in ls: |
252 | - _f = os.path.join(dir, f) |
253 | - |
254 | - if not os.path.isdir(_f) and not _f.endswith('.py') and incs: |
255 | - if True not in [fnmatch(_f, inc) for inc in incs]: |
256 | - logging.debug('Not syncing %s, does not match include ' |
257 | - 'filters (%s)' % (_f, incs)) |
258 | - _filter.append(f) |
259 | - else: |
260 | - logging.debug('Including file, which matches include ' |
261 | - 'filters (%s): %s' % (incs, _f)) |
262 | - elif (os.path.isfile(_f) and not _f.endswith('.py')): |
263 | - logging.debug('Not syncing file: %s' % f) |
264 | - _filter.append(f) |
265 | - elif (os.path.isdir(_f) and not |
266 | - os.path.isfile(os.path.join(_f, '__init__.py'))): |
267 | - logging.debug('Not syncing directory: %s' % f) |
268 | - _filter.append(f) |
269 | - return _filter |
270 | - return _filter |
271 | - |
272 | - |
273 | -def sync_directory(src, dest, opts=None): |
274 | - if os.path.exists(dest): |
275 | - logging.debug('Removing existing directory: %s' % dest) |
276 | - shutil.rmtree(dest) |
277 | - logging.info('Syncing directory: %s -> %s.' % (src, dest)) |
278 | - |
279 | - shutil.copytree(src, dest, ignore=get_filter(opts)) |
280 | - ensure_init(dest) |
281 | - |
282 | - |
283 | -def sync(src, dest, module, opts=None): |
284 | - if os.path.isdir(_src_path(src, module)): |
285 | - sync_directory(_src_path(src, module), _dest_path(dest, module), opts) |
286 | - elif _is_pyfile(_src_path(src, module)): |
287 | - sync_pyfile(_src_path(src, module), |
288 | - os.path.dirname(_dest_path(dest, module))) |
289 | - else: |
290 | - logging.warn('Could not sync: %s. Neither a pyfile or directory, ' |
291 | - 'does it even exist?' % module) |
292 | - |
293 | - |
294 | -def parse_sync_options(options): |
295 | - if not options: |
296 | - return [] |
297 | - return options.split(',') |
298 | - |
299 | - |
300 | -def extract_options(inc, global_options=None): |
301 | - global_options = global_options or [] |
302 | - if global_options and isinstance(global_options, basestring): |
303 | - global_options = [global_options] |
304 | - if '|' not in inc: |
305 | - return (inc, global_options) |
306 | - inc, opts = inc.split('|') |
307 | - return (inc, parse_sync_options(opts) + global_options) |
308 | - |
309 | - |
310 | -def sync_helpers(include, src, dest, options=None): |
311 | - if not os.path.isdir(dest): |
312 | - os.mkdir(dest) |
313 | - |
314 | - global_options = parse_sync_options(options) |
315 | - |
316 | - for inc in include: |
317 | - if isinstance(inc, str): |
318 | - inc, opts = extract_options(inc, global_options) |
319 | - sync(src, dest, inc, opts) |
320 | - elif isinstance(inc, dict): |
321 | - # could also do nested dicts here. |
322 | - for k, v in inc.iteritems(): |
323 | - if isinstance(v, list): |
324 | - for m in v: |
325 | - inc, opts = extract_options(m, global_options) |
326 | - sync(src, dest, '%s.%s' % (k, inc), opts) |
327 | - |
328 | -if __name__ == '__main__': |
329 | - parser = optparse.OptionParser() |
330 | - parser.add_option('-c', '--config', action='store', dest='config', |
331 | - default=None, help='helper config file') |
332 | - parser.add_option('-D', '--debug', action='store_true', dest='debug', |
333 | - default=False, help='debug') |
334 | - parser.add_option('-b', '--branch', action='store', dest='branch', |
335 | - help='charm-helpers bzr branch (overrides config)') |
336 | - parser.add_option('-d', '--destination', action='store', dest='dest_dir', |
337 | - help='sync destination dir (overrides config)') |
338 | - (opts, args) = parser.parse_args() |
339 | - |
340 | - if opts.debug: |
341 | - logging.basicConfig(level=logging.DEBUG) |
342 | - else: |
343 | - logging.basicConfig(level=logging.INFO) |
344 | - |
345 | - if opts.config: |
346 | - logging.info('Loading charm helper config from %s.' % opts.config) |
347 | - config = parse_config(opts.config) |
348 | - if not config: |
349 | - logging.error('Could not parse config from %s.' % opts.config) |
350 | - sys.exit(1) |
351 | - else: |
352 | - config = {} |
353 | - |
354 | - if 'branch' not in config: |
355 | - config['branch'] = CHARM_HELPERS_BRANCH |
356 | - if opts.branch: |
357 | - config['branch'] = opts.branch |
358 | - if opts.dest_dir: |
359 | - config['destination'] = opts.dest_dir |
360 | - |
361 | - if 'destination' not in config: |
362 | - logging.error('No destination dir. specified as option or config.') |
363 | - sys.exit(1) |
364 | - |
365 | - if 'include' not in config: |
366 | - if not args: |
367 | - logging.error('No modules to sync specified as option or config.') |
368 | - sys.exit(1) |
369 | - config['include'] = [] |
370 | - [config['include'].append(a) for a in args] |
371 | - |
372 | - sync_options = None |
373 | - if 'options' in config: |
374 | - sync_options = config['options'] |
375 | - tmpd = tempfile.mkdtemp() |
376 | - try: |
377 | - checkout = clone_helpers(tmpd, config['branch']) |
378 | - sync_helpers(config['include'], checkout, config['destination'], |
379 | - options=sync_options) |
380 | - except Exception, e: |
381 | - logging.error("Could not sync: %s" % e) |
382 | - raise e |
383 | - finally: |
384 | - logging.debug('Cleaning up %s' % tmpd) |
385 | - shutil.rmtree(tmpd) |
386 | |
387 | === modified file 'tests/00-setup' |
388 | --- tests/00-setup 2014-10-22 04:18:26 +0000 |
389 | +++ tests/00-setup 2014-11-21 16:00:44 +0000 |
390 | @@ -1,5 +1,5 @@ |
391 | #!/bin/bash |
392 | |
393 | sudo add-apt-repository ppa:juju/stable -y |
394 | -sudo apt-get update |
395 | -sudo apt-get install amulet python-requests -y |
396 | +sudo apt-get update -qq |
397 | +sudo apt-get install amulet python3-requests -y |
398 | |
399 | === modified file 'tests/10-deploy' |
400 | --- tests/10-deploy 2014-10-22 04:18:26 +0000 |
401 | +++ tests/10-deploy 2014-11-21 16:00:44 +0000 |
402 | @@ -1,35 +1,76 @@ |
403 | -#!/usr/bin/python3 |
404 | +#!/usr/bin/env python3 |
405 | + |
406 | +# The amulet code to test the fail2ban charm. |
407 | |
408 | import amulet |
409 | -import requests |
410 | - |
411 | -d = amulet.Deployment() |
412 | - |
413 | -d.add('fail2ban') |
414 | -d.expose('fail2ban') |
415 | - |
416 | -try: |
417 | - d.setup(timeout=900) |
418 | - d.sentry.wait() |
419 | -except amulet.helpers.TimeoutError: |
420 | - amulet.raise_status(amulet.SKIP, msg="Environment wasn't stood up in time") |
421 | -except: |
422 | - raise |
423 | - |
424 | -unit = d.sentry.unit['fail2ban/0'] |
425 | - |
426 | -# test we can access over http |
427 | -page = requests.get('http://{}'.format(unit.info['public-address'])) |
428 | -page.raise_for_status() |
429 | - |
430 | - |
431 | -# Now you can use d.sentry.unit[UNIT] to address each of the units and perform |
432 | -# more in-depth steps. There are three test statuses: amulet.PASS, amulet.FAIL, |
433 | -# and amulet.SKIP - these can be triggered with amulet.raise_status(). Each |
434 | -# d.sentry.unit[] has the following methods: |
435 | -# - .info - An array of the information of that unit from Juju |
436 | -# - .file(PATH) - Get the details of a file on that unit |
437 | -# - .file_contents(PATH) - Get plain text output of PATH file from that unit |
438 | -# - .directory(PATH) - Get details of directory |
439 | -# - .directory_contents(PATH) - List files and folders in PATH on that unit |
440 | -# - .relation(relation, service:rel) - Get relation data from return service |
441 | +import unittest |
442 | + |
443 | + |
444 | +# The number of seconds to wait for Juju to set up the environment. |
445 | +seconds = 900 |
446 | + |
447 | +# Configure fail2ban |
448 | +configuration = { |
449 | + 'maxretry': '6', |
450 | + 'bantime': '3660', |
451 | + 'destemail': 'amulet@juju.com' |
452 | +} |
453 | + |
454 | + |
455 | +class TestDeployment(unittest.TestCase): |
456 | + ''' A subclass of TestCase to perform a deployment and run the tests. ''' |
457 | + @classmethod |
458 | + def setUpClass(self): |
459 | + ''' The setup class gets run one time before tests. ''' |
460 | + self.deployment = amulet.Deployment(series='trusty') |
461 | + self.deployment.add('ubuntu') |
462 | + self.deployment.add('fail2ban') |
463 | + self.deployment.configure('fail2ban', configuration) |
464 | + # Relate the two charms. |
465 | + self.deployment.relate('ubuntu:juju-info', 'fail2ban:juju-info') |
466 | + |
467 | + try: |
468 | + self.deployment.setup(timeout=seconds) |
469 | + self.deployment.sentry.wait() |
470 | + except amulet.helpers.TimeoutError: |
471 | + message = 'The environment did not set up in %d seconds!' % seconds |
472 | + amulet.raise_status(amulet.SKIP, msg=message) |
473 | + except: |
474 | + raise |
475 | + # Get a reference to the Ubuntu unit. |
476 | + self.unit = self.deployment.sentry.unit['ubuntu/0'] |
477 | + |
478 | + def test_status(self): |
479 | + ''' Test the status of the fail2ban process. ''' |
480 | + # Verify the fail2ban service is installed and runing on ubuntu. |
481 | + command = 'sudo service fail2ban status' |
482 | + print(command) |
483 | + # Run the command to see if fail2ban is running. |
484 | + output, code = self.unit.run(command) |
485 | + print(output) |
486 | + if code != 0: |
487 | + message = 'The fail2ban process is not running!' |
488 | + amulet.raise_status(amulet.FAIL, msg=message) |
489 | + |
490 | + def test_config(self): |
491 | + ''' Test the configuration was set on the local configuration file. ''' |
492 | + config_string = self.unit.file_contents('/etc/fail2ban/jail.local') |
493 | + print(config_string) |
494 | + if configuration['destemail'] not in config_string: |
495 | + message = 'The destemail value was not in the configuration file!' |
496 | + amulet.raise_status(amulet.FAIL, msg=message) |
497 | + |
498 | + def test_iptables(self): |
499 | + ''' Test to set if 'fail2ban' is in the iptables firewall rules. ''' |
500 | + command = 'sudo iptables -S' |
501 | + print(command) |
502 | + # Run the command to see if fail2ban is in the iptables rules. |
503 | + output, code = self.unit.run(command) |
504 | + print(output) |
505 | + if 'fail2ban' not in output: |
506 | + message = 'There were no firewall rules including fail2ban!' |
507 | + amulet.raise_status(amulet.FAIL, msg=message) |
508 | + |
509 | + |
510 | +if __name__ == '__main__': |
511 | + unittest.main() |
Thanks for the cleanup Matt! These additions look good to me.