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 .*.swp
.*.swo .*.swo
*.pyc *.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] # Lists all the plugins, or jobs, and whether they are enabled or not. Each
LogDispatch = logging_plugin:LoggingPlugin # 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] [portal]
plugins_dir=./plugins
listen_host=localhost listen_host=localhost
listen_port=9080 listen_port=9080
debug=True debug=True
redis_host=127.0.0.1
redis_port=6379
[logging] [logging]
log_format = %(asctime)s %(name)s[%(process)s] %(levelname)s: %(message)s log_format = %(asctime)s %(name)s[%(process)s] %(levelname)s: %(message)s
log_debug = False 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 configparser import RawConfigParser
from logging import Formatter, getLogger, DEBUG, WARN, INFO from logging import Formatter, getLogger, DEBUG, WARN, INFO
from logging.handlers import SysLogHandler, RotatingFileHandler 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 route, run, default_app
from bottle import request, template, static_file from bottle import request, response, template, static_file
config = RawConfigParser() config = RawConfigParser()
config.readfp(open('./portal.cfg')) config.readfp(open('./portal.cfg'))
config.read(['/etc/captiveportal/portal.cfg', './portal_local.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 # Setup logging
formatter = Formatter(config.get('logging', 'log_format')) logFormatter = Formatter(config.get('logging', 'log_format'))
l = getLogger('captiveportal') l = getLogger('captiveportal')
if config.get('logging', 'log_handler') == 'syslog': if config.get('logging', 'log_handler') == 'syslog':
syslog_address = config.get('logging', 'syslog_address') syslog_address = config.get('logging', 'syslog_address')
if syslog_address.startswith('/'): if syslog_address.startswith('/'):
h = SysLogHandler( logHandler = SysLogHandler(
address=syslog_address, address=syslog_address,
facility=SysLogHandler.LOG_LOCAL0 facility=SysLogHandler.LOG_LOCAL0
) )
else: else:
h = SysLogHandler( logHandler = SysLogHandler(
address=( address=(
config.get('logging', 'syslog_address'), config.get('logging', 'syslog_address'),
config.getint('logging', 'syslog_port') config.getint('logging', 'syslog_port')
@ -30,72 +42,80 @@ if config.get('logging', 'log_handler') == 'syslog':
facility=SysLogHandler.LOG_LOCAL0 facility=SysLogHandler.LOG_LOCAL0
) )
else: else:
h = RotatingFileHandler( logHandler = RotatingFileHandler(
config.get('logging', 'log_file'), config.get('logging', 'log_file'),
maxBytes=config.getint('logging', 'log_max_bytes'), maxBytes=config.getint('logging', 'log_max_bytes'),
backupCount=config.getint('logging', 'log_max_copies') backupCount=config.getint('logging', 'log_max_copies')
) )
h.setFormatter(formatter) logHandler.setFormatter(logFormatter)
l.addHandler(h) l.addHandler(logHandler)
if config.get('logging', 'log_debug'): if config.get('logging', 'log_debug'):
l.setLevel(DEBUG) l.setLevel(DEBUG)
else: else:
l.setLevel(WARN) l.setLevel(WARN)
# Redis Queue
R = Redis(
host=config.get('portal', 'redis_host'),
port=config.getint('portal', 'redis_port')
)
@route('/') @route('/')
def portalindex(): def portalindex():
return template('portalindex') return template('portalindex')
@route('/static/<path:path>') @route('/static/<path:path>')
def server_static(path): def server_static(path):
return static_file(path, root='./static') return static_file(path, root='./static')
@route('/approve', method='POST') @route('/approve', method='POST')
def approve_client(): def approve_client():
_dispatch_plugins(request) response.content_type = 'application/json'
jobs = dispatch_plugins()
# TODO: return job ID # TODO: return job ID
# Maybe use the client IP as job ID to enable easier lookups of the job # Maybe use the client IP as job ID to enable easier lookups of the job
# status. # status.
return return json.dumps(jobs)
def _dispatch_plugins(request):
for entrypoint in pkg_resources.iter_entry_points('portal.plugins'): # Add plugins to job queue
l.debug('Loading entry point {point}'.format( def dispatch_plugins():
point=entrypoint.name Q = Queue(connection=R)
jobs = []
for plugin in plugin_config.sections():
l.debug('Loading plugin {plugin}'.format(
plugin=plugin
)) ))
plugin_class = entrypoint.load() arg = {}
plugin_name = entrypoint.name
plugin_log = getLogger('portal_'+plugin_name) # Import some values from WSGI environ
plugin_log.addHandler(h) arg['environ'] = {}
plugin_log.setLevel(DEBUG) 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: try:
inst = plugin_class( plugin_job = Q.enqueue(
request=request, plugin_module.run,
config=config, arg
log=plugin_log
) )
except Exception as e: except Exception as e:
l.error('{plugin}: {exception}'.format( l.error('{plugin}: {error}'.format(
plugin=plugin_name, error=str(e),
exception=str(e) plugin=plugin
)) ))
continue continue
# Run plugin.run() method jobs.append(plugin_job)
try:
inst.run()
except Exception as e:
l.error('{plugin}: {exception}'.format(
plugin=plugin_name,
exception=str(e)
))
continue
if __name__ == '__main__': if __name__ == '__main__':

View file

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

View file

@ -1,15 +1,13 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
try:
plugins = open('/etc/captiveportal/plugins.cfg')
except:
plugins = open('./plugins.cfg')
setup( setup(
name="CaptivePortal", name="CaptivePortal",
version="0.1", version="0.1",
description="Captive Portal webpage", description="Captive Portal webpage",
author="Stefan Midjich", author="Stefan Midjich",
packages=find_packages(), 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/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css"> <link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/captiveportal.css">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="six columns" style="margin-top: 15%"> <div class="six columns" style="margin-top: 10%">
<h4>End User Agreement</h4> <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>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> <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>
<form id="approveForm">
<div class="row">
<div class="three columns">
<label for="approveSend"></label>
<input class="button-primary" value="Approve" type="submit">
</div>
</div>
</form>
</div> </div>
</div> </div>
<form id="approveForm" method="post">
<div class="row">
<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>
<script src="https://code.jquery.com/jquery-1.12.2.min.js"></script> <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> </body>
</html> </html>