captive.whump.shanti-portal/portal.py

240 lines
6.4 KiB
Python
Raw Normal View History

2017-03-03 00:04:12 +00:00
# Captiveportal
# This is the web API the portal website speaks to and requests new clients
# to be added by running jobs in rq.
import json
2016-12-14 15:37:32 +00:00
from pprint import pprint as pp
2016-04-15 20:12:17 +00:00
from uuid import UUID
from importlib import import_module
2016-12-14 23:17:18 +00:00
from logging import Formatter, getLogger, DEBUG, INFO, WARN
from logging.handlers import SysLogHandler, RotatingFileHandler
2016-04-16 14:50:35 +00:00
2016-04-16 16:48:39 +00:00
# Until pyvenv-3.4 is fixed on centos 7 support python 2.
2016-04-16 14:50:35 +00:00
try:
from configparser import RawConfigParser
except ImportError:
from ConfigParser import RawConfigParser
from redis import Redis
from rq import Queue
2016-12-17 22:16:19 +00:00
from bottle import Bottle, default_app, debug
from bottle import request, response, template, static_file
2016-04-05 14:40:37 +00:00
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_defaults = {
'enabled': 'False',
'mandatory': 'True',
'debug': 'False'
}
plugin_config = RawConfigParser(defaults=plugin_defaults)
plugin_config.readfp(open('./plugins.cfg'))
plugin_config.read(['/etc/captiveportal/plugins.cfg', './plugins_local.cfg'])
2016-04-05 14:40:37 +00:00
# Setup logging
logFormatter = Formatter(config.get('logging', 'log_format'))
2016-04-05 14:40:37 +00:00
l = getLogger('captiveportal')
if config.get('logging', 'log_handler') == 'syslog':
syslog_address = config.get('logging', 'syslog_address')
if syslog_address.startswith('/'):
logHandler = SysLogHandler(
2016-04-05 14:40:37 +00:00
address=syslog_address,
facility=SysLogHandler.LOG_LOCAL0
)
else:
logHandler = SysLogHandler(
2016-04-05 14:40:37 +00:00
address=(
config.get('logging', 'syslog_address'),
config.getint('logging', 'syslog_port')
),
facility=SysLogHandler.LOG_LOCAL0
)
else:
logHandler = RotatingFileHandler(
2016-04-05 14:40:37 +00:00
config.get('logging', 'log_file'),
maxBytes=config.getint('logging', 'log_max_bytes'),
backupCount=config.getint('logging', 'log_max_copies')
)
logHandler.setFormatter(logFormatter)
l.addHandler(logHandler)
2016-04-05 14:40:37 +00:00
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')
)
2016-04-15 20:12:17 +00:00
# Custom UUID route filter for bottle.py
def uuid_filter(config):
2016-04-16 14:50:35 +00:00
# Should catch UUIDv4 type strings
2016-04-15 20:12:17 +00:00
regexp = r'[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}'
2016-04-05 14:40:37 +00:00
2016-04-15 20:12:17 +00:00
def to_python(match):
return UUID(match, version=4)
2016-04-15 20:12:17 +00:00
def to_url(uuid):
return str(uuid)
2016-04-05 14:40:37 +00:00
2016-04-15 20:12:17 +00:00
return regexp, to_python, to_url
2016-04-05 14:40:37 +00:00
# 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
2016-04-05 14:40:37 +00:00
))
arg = {}
2016-04-05 14:40:37 +00:00
# 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
2016-04-05 14:40:37 +00:00
2016-04-16 17:09:22 +00:00
# Import all the plugin configuration values as OrderedDict
config_sections = plugin_config._sections
arg['config'] = config_sections[plugin]
2016-04-17 09:21:37 +00:00
# Is plugin enabled?
if not plugin_config.getboolean(plugin, 'enabled'):
2016-04-17 09:23:50 +00:00
l.debug('{plugin}: Not enabled, skipping'.format(
plugin=plugin
))
2016-04-17 09:21:37 +00:00
continue
2016-04-16 17:09:22 +00:00
2016-04-16 16:48:39 +00:00
# Import the plugin
try:
plugin_module = import_module('plugins.'+plugin)
except Exception as e:
l.warn('{plugin}: failed import: {error}'.format(
plugin=plugin,
error=str(e)
))
continue
2016-12-14 15:37:32 +00:00
# Let plugin run for 30 more seconds than the defined plugin_timeout
# because that value is also used by the JS code to poll the job
# status so we don't want rq to kill the job before JS has timed out.
plugin_timeout = config.getint('portal', 'plugin_timeout')+30
2016-12-22 14:40:25 +00:00
# Queue plugin.run()
2016-04-05 14:40:37 +00:00
try:
plugin_job = Q.enqueue(
plugin_module.run,
2016-04-16 14:50:35 +00:00
arg,
2016-12-14 15:37:32 +00:00
timeout=plugin_timeout
2016-04-05 14:40:37 +00:00
)
except Exception as e:
2016-04-16 16:48:39 +00:00
l.warn('{plugin}: {error}'.format(
error=str(e),
plugin=plugin
2016-04-05 14:40:37 +00:00
))
continue
plugin_meta = {}
plugin_meta['mandatory'] = plugin_config.getboolean(
plugin,
'mandatory'
)
plugin_job.meta = plugin_meta
plugin_job.save()
jobs[plugin] = {
'id': plugin_job.id
}
2016-04-15 20:12:17 +00:00
return jobs
2016-04-16 14:50:35 +00:00
# Define app so we can add a custom filter to app.router
2016-04-15 20:12:17 +00:00
app = Bottle()
app.router.add_filter('uuid', uuid_filter)
@app.route('/')
def portalindex():
2016-04-16 14:50:35 +00:00
return template(
config.get('portal', 'index_page'),
2016-12-14 15:37:32 +00:00
plugin_timeout=config.getint('portal', 'plugin_timeout')
2016-04-16 14:50:35 +00:00
)
2016-04-15 20:12:17 +00:00
@app.route('/static/<path:path>')
def server_static(path):
2016-04-16 14:50:35 +00:00
return static_file(path, root=config.get('portal', 'static_dir'))
2016-04-15 20:12:17 +00:00
2016-04-18 20:57:38 +00:00
@app.route('/jobs')
def list_jobs():
Q = Queue(connection=R)
jobs = Q.get_job_ids()
response.content_tye = 'application/json'
return json.dumps(jobs)
2016-04-15 20:12:17 +00:00
@app.route('/job/<job_id:uuid>')
def job_status(job_id):
Q = Queue(connection=R)
job = Q.fetch_job(str(job_id))
2016-04-16 14:50:35 +00:00
response.content_type = 'application/json'
if job is None:
response.status = 404
2016-04-15 20:12:17 +00:00
return json.dumps({'error': 'Job not found'})
# Get data on the job to return to the client
job_data = {
'id': job.id,
'is_failed': job.is_failed,
'is_finished': job.is_finished,
'is_queued': job.is_queued,
2016-04-16 14:50:35 +00:00
'is_started': job.is_started,
'result': job.result,
'meta': job.meta
2016-04-15 20:12:17 +00:00
}
2016-12-14 15:37:32 +00:00
2016-04-15 20:12:17 +00:00
return json.dumps(job_data)
@app.route('/approve', method='POST')
def approve_client():
response.content_type = 'application/json'
try:
jobs = dispatch_plugins()
except Exception as e:
response.status = 500
jobs = {
'result': {
'error': str(e)
}
}
2016-04-15 20:12:17 +00:00
return json.dumps(jobs)
2016-04-05 14:40:37 +00:00
if __name__ == '__main__':
2016-04-15 20:12:17 +00:00
app.run(
2016-04-05 14:40:37 +00:00
host=config.get('portal', 'listen_host'),
port=config.getint('portal', 'listen_port')
)
2016-12-17 22:16:19 +00:00
debug(config.getboolean('portal', 'debug'))
2016-04-05 14:40:37 +00:00
else:
2016-04-15 20:12:17 +00:00
application = app