From 1bf19c791bb347bd336a55c3ca622abe46b80ee2 Mon Sep 17 00:00:00 2001 From: Stefan Midjich Date: Fri, 15 Apr 2016 19:22:16 +0200 Subject: [PATCH] changed plugin management to simple rq jobs instead of using pkg_resources. --- .gitignore | 1 + logging_plugin/__init__.py | 14 ------ plugins.cfg | 6 ++- plugins/logging.py | 32 +++++++++++++ portal.cfg | 4 ++ portal.py | 92 ++++++++++++++++++++++-------------- requirements.txt | 5 +- setup.py | 10 ++-- static/css/captiveportal.css | 9 ++++ static/images/radio.svg | 1 + static/js/captiveportal.js | 23 +++++++++ static/js/ise.js | 5 -- views/portalindex.tpl | 31 +++++++----- 13 files changed, 156 insertions(+), 77 deletions(-) delete mode 100644 logging_plugin/__init__.py create mode 100644 plugins/logging.py create mode 100644 static/css/captiveportal.css create mode 100644 static/images/radio.svg create mode 100644 static/js/captiveportal.js delete mode 100644 static/js/ise.js diff --git a/.gitignore b/.gitignore index 4a5ea75..586ae4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.log .*.swp .*.swo *.pyc diff --git a/logging_plugin/__init__.py b/logging_plugin/__init__.py deleted file mode 100644 index d96b610..0000000 --- a/logging_plugin/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Also an example that simply logs data to the logging handler provided. - -class LoggingPlugin(object): - plugin_name = 'LoggingPlugin' - - def __init__(self, **kw): - self.l = kw.['logging'] - self.config = kw.['config'] - self.request = kw['request'] - - def run(self): - self.l.info('Request params: {params}'.format( - params=self.request.params.keys() - )) diff --git a/plugins.cfg b/plugins.cfg index 00e618a..96bd426 100644 --- a/plugins.cfg +++ b/plugins.cfg @@ -1,2 +1,4 @@ -[portal.plugins] -LogDispatch = logging_plugin:LoggingPlugin +# Lists all the plugins, or jobs, and whether they are enabled or not. Each +# section name must correspond to a plugin name in the plugins dir. +[logging] +enabled = True diff --git a/plugins/logging.py b/plugins/logging.py new file mode 100644 index 0000000..e84318c --- /dev/null +++ b/plugins/logging.py @@ -0,0 +1,32 @@ +# Demonstration plugin, only logs a message. + +# Sets up logging by importing from the bottle app in the parent dir. +from logging import getLogger, DEBUG, WARN, INFO +from portal import logHandler, logFormatter + +def run(arg): + # The WSGI environ dict should always be there, sans any special objects + # like io streams. + environ = arg['environ'] + + l = getLogger('plugin_logging') + l.addHandler(logHandler) + l.setLevel(DEBUG) + + log_url = '{proto}://{server}:{port}{request}'.format( + proto=environ.get('wsgi.url_scheme'), + server=environ.get('SERVER_NAME'), + port=environ.get('SERVER_PORT'), + request=environ.get('PATH_INFO') + ) + + log_client = '{client_ip}'.format( + client_ip=environ.get('REMOTE_ADDR') + ) + + # Log a msg + l.info('{log_client} - {method} - {log_url}'.format( + log_client=log_client, + log_url=log_url, + method=environ.get('REQUEST_METHOD') + )) diff --git a/portal.cfg b/portal.cfg index 805f6a8..85f02f1 100644 --- a/portal.cfg +++ b/portal.cfg @@ -1,8 +1,12 @@ [portal] +plugins_dir=./plugins listen_host=localhost listen_port=9080 debug=True +redis_host=127.0.0.1 +redis_port=6379 + [logging] log_format = %(asctime)s %(name)s[%(process)s] %(levelname)s: %(message)s log_debug = False diff --git a/portal.py b/portal.py index c5ace46..ed2ca29 100644 --- a/portal.py +++ b/portal.py @@ -1,28 +1,40 @@ +# Captiveportal web application using Bottle.py + +import json +from pprint import pprint +from importlib import import_module from configparser import RawConfigParser from logging import Formatter, getLogger, DEBUG, WARN, INFO from logging.handlers import SysLogHandler, RotatingFileHandler -import pkg_resources +from redis import Redis +from rq import Queue from bottle import route, run, default_app -from bottle import request, template, static_file +from bottle import request, response, template, static_file config = RawConfigParser() config.readfp(open('./portal.cfg')) config.read(['/etc/captiveportal/portal.cfg', './portal_local.cfg']) +# Plugins configuration is separate so plugins can be disabled by having +# their section removed/commented from the config file. +plugin_config = RawConfigParser() +plugin_config.readfp(open('./plugins.cfg')) +plugin_config.read(['/etc/captiveportal/plugins.cfg']) + # Setup logging -formatter = Formatter(config.get('logging', 'log_format')) +logFormatter = Formatter(config.get('logging', 'log_format')) l = getLogger('captiveportal') if config.get('logging', 'log_handler') == 'syslog': syslog_address = config.get('logging', 'syslog_address') if syslog_address.startswith('/'): - h = SysLogHandler( + logHandler = SysLogHandler( address=syslog_address, facility=SysLogHandler.LOG_LOCAL0 ) else: - h = SysLogHandler( + logHandler = SysLogHandler( address=( config.get('logging', 'syslog_address'), config.getint('logging', 'syslog_port') @@ -30,72 +42,80 @@ if config.get('logging', 'log_handler') == 'syslog': facility=SysLogHandler.LOG_LOCAL0 ) else: - h = RotatingFileHandler( + logHandler = RotatingFileHandler( config.get('logging', 'log_file'), maxBytes=config.getint('logging', 'log_max_bytes'), backupCount=config.getint('logging', 'log_max_copies') ) -h.setFormatter(formatter) -l.addHandler(h) +logHandler.setFormatter(logFormatter) +l.addHandler(logHandler) if config.get('logging', 'log_debug'): l.setLevel(DEBUG) else: l.setLevel(WARN) +# Redis Queue +R = Redis( + host=config.get('portal', 'redis_host'), + port=config.getint('portal', 'redis_port') +) + + @route('/') def portalindex(): return template('portalindex') + @route('/static/') def server_static(path): return static_file(path, root='./static') + @route('/approve', method='POST') def approve_client(): - _dispatch_plugins(request) + response.content_type = 'application/json' + jobs = dispatch_plugins() # TODO: return job ID # Maybe use the client IP as job ID to enable easier lookups of the job # status. - return + return json.dumps(jobs) -def _dispatch_plugins(request): - for entrypoint in pkg_resources.iter_entry_points('portal.plugins'): - l.debug('Loading entry point {point}'.format( - point=entrypoint.name + +# Add plugins to job queue +def dispatch_plugins(): + Q = Queue(connection=R) + jobs = [] + + for plugin in plugin_config.sections(): + l.debug('Loading plugin {plugin}'.format( + plugin=plugin )) - plugin_class = entrypoint.load() - plugin_name = entrypoint.name + arg = {} - plugin_log = getLogger('portal_'+plugin_name) - plugin_log.addHandler(h) - plugin_log.setLevel(DEBUG) + # Import some values from WSGI environ + arg['environ'] = {} + for key in request.environ: + value = request.environ.get(key) + if isinstance(value, (int, str, float, dict, set, tuple)): + arg['environ'][key] = value - # Instantiate the plugin class + plugin_module = import_module('plugins.'+plugin) try: - inst = plugin_class( - request=request, - config=config, - log=plugin_log + plugin_job = Q.enqueue( + plugin_module.run, + arg ) except Exception as e: - l.error('{plugin}: {exception}'.format( - plugin=plugin_name, - exception=str(e) + l.error('{plugin}: {error}'.format( + error=str(e), + plugin=plugin )) continue - # Run plugin.run() method - try: - inst.run() - except Exception as e: - l.error('{plugin}: {exception}'.format( - plugin=plugin_name, - exception=str(e) - )) - continue + jobs.append(plugin_job) if __name__ == '__main__': diff --git a/requirements.txt b/requirements.txt index 76a2821..263f4d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ -rq -bottle +--index-url https://pypi.python.org/simple/ + +-e diff --git a/setup.py b/setup.py index dce25f2..63f9e4f 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,13 @@ from setuptools import setup, find_packages -try: - plugins = open('/etc/captiveportal/plugins.cfg') -except: - plugins = open('./plugins.cfg') - setup( name="CaptivePortal", version="0.1", description="Captive Portal webpage", author="Stefan Midjich", packages=find_packages(), - entry_points=plugins.read() + install_requires=[ + 'rq', + 'bottle' + ] ) diff --git a/static/css/captiveportal.css b/static/css/captiveportal.css new file mode 100644 index 0000000..32a9039 --- /dev/null +++ b/static/css/captiveportal.css @@ -0,0 +1,9 @@ +.button-loading { + background: #80c8ff url('/static/images/loader.gif') no-repeat 80% 80%; + cursor: default; +} + +#approveButtonDiv img { + height: 100px; + width: 100px; +} diff --git a/static/images/radio.svg b/static/images/radio.svg new file mode 100644 index 0000000..b456864 --- /dev/null +++ b/static/images/radio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/js/captiveportal.js b/static/js/captiveportal.js new file mode 100644 index 0000000..b6e9eea --- /dev/null +++ b/static/js/captiveportal.js @@ -0,0 +1,23 @@ +$('#approveForm').submit(function (event) { + var api_url = '/approve'; + event.preventDefault(); + + // Had some issues trying to set a background image on the button. + if ($('#approveCheckbox').is(':checked')) { + $('#approveButton').prop('disabled', true); + $('#approveButton').val(''); + $('#approveButton').addClass('button-loading'); + + $('#approveButtonDiv').replaceWith('Loading, please wait...'); + + var ajaxReq = $.post(api_url); + + ajaxReq.done(function(data) { + console.log(data); + }); + + ajaxReq.fail(function(XMLHttpRequest, textStatus, errorThrown) { + console.log('Request Error: '+ XMLHttpRequest.responseText + ', status:' + XMLHttpRequest.status + ', status text: ' + XMLHttpRequest.statusText) + }); + } +}); diff --git a/static/js/ise.js b/static/js/ise.js deleted file mode 100644 index 93ab720..0000000 --- a/static/js/ise.js +++ /dev/null @@ -1,5 +0,0 @@ -$(document).ready(function () { - $('#resetForm').submit(function (event) { - event.preventDefault(); - }); -}); diff --git a/views/portalindex.tpl b/views/portalindex.tpl index be0413c..47fabcb 100644 --- a/views/portalindex.tpl +++ b/views/portalindex.tpl @@ -13,34 +13,41 @@ +
-
+

End User Agreement

Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).

- -
-
-
- - -
-
-
-
+ +
+
+
+ +
+
+ +
+
+
+
- +