changed plugin management to simple rq jobs instead of using pkg_resources.

This commit is contained in:
Stefan Midjich 2016-04-15 19:22:16 +02:00
parent fe5e85f96c
commit 1bf19c791b
13 changed files with 156 additions and 77 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
*.log
.*.swp
.*.swo
*.pyc

View file

@ -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()
))

View file

@ -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

32
plugins/logging.py Normal file
View file

@ -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')
))

View file

@ -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

View file

@ -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/<path:path>')
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__':

View file

@ -1,2 +1,3 @@
rq
bottle
--index-url https://pypi.python.org/simple/
-e

View file

@ -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'
]
)

View file

@ -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;
}

1
static/images/radio.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><svg width='38px' height='38px' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="uil-blank"><rect x="0" y="0" width="100" height="100" fill="none" class="bk"></rect><g transform="scale(0.55)"><circle cx="30" cy="150" r="30" fill="#eb8614"><animate attributeName="opacity" from="0" to="1" dur="1s" begin="0" repeatCount="indefinite" keyTimes="0;0.5;1" values="0;1;1"></animate></circle><path d="M90,150h30c0-49.7-40.3-90-90-90v30C63.1,90,90,116.9,90,150z" fill="#fb9610"><animate attributeName="opacity" from="0" to="1" dur="1s" begin="0.1" repeatCount="indefinite" keyTimes="0;0.5;1" values="0;1;1"></animate></path><path d="M150,150h30C180,67.2,112.8,0,30,0v30C96.3,30,150,83.7,150,150z" fill="#fb9610"><animate attributeName="opacity" from="0" to="1" dur="1s" begin="0.2" repeatCount="indefinite" keyTimes="0;0.5;1" values="0;1;1"></animate></path></g></svg>

After

Width:  |  Height:  |  Size: 944 B

View file

@ -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('<img src="/static/images/radio.svg" alt="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)
});
}
});

View file

@ -1,5 +0,0 @@
$(document).ready(function () {
$('#resetForm').submit(function (event) {
event.preventDefault();
});
});

View file

@ -13,34 +13,41 @@
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/captiveportal.css">
</head>
<body>
<div class="container">
<div class="row">
<div class="six columns" style="margin-top: 15%">
<div class="six columns" style="margin-top: 10%">
<h4>End User Agreement</h4>
<p>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.</p>
<p>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).</p>
</div>
</div>
<form id="approveForm">
<form id="approveForm" method="post">
<div class="row">
<div class="three columns">
<label for="approveSend"></label>
<input class="button-primary" value="Approve" type="submit">
<div class="four columns">
<label>
<input type="checkbox" id="approveCheckbox" required> I approve this user agreement
</label>
</div>
<div id="approveButtonDiv" class="one column u-pull-left">
<label>
<input id="approveButton" class="button-primary" value="Approve" type="submit">
</label>
</div>
</div>
</form>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-1.12.2.min.js"></script>
<script src="/static/js/portal.js"></script>
<script src="/static/js/captiveportal.js"></script>
</body>
</html>