Merge lp:~mbruzek/charms/trusty/fail2ban/trunk into lp:~lazypower/charms/trusty/fail2ban/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
Reviewer Review Type Date Requested Status
Charles Butler Approve
Review via email: mp+242506@code.launchpad.net

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.
Revision history for this message
Charles Butler (lazypower) wrote :

Thanks for the cleanup Matt! These additions look good to me.

review: Approve
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()

Subscribers

People subscribed via source and target branches

to all changes: