"""
Main entry point for flask application
"""
from __future__ import absolute_import, unicode_literals
import json
import logging
import os
from flask import Flask, jsonify, request, render_template, url_for, g
# pylint: disable=no-name-in-module, import-error
from flask.ext.assets import Environment
from flask.ext.htpasswd import HtPasswdAuth
from werkzeug.contrib.fixers import ProxyFix
from six import string_types
from archelond.data import MemoryData, ElasticData, ORDER_TYPES
from archelond.log import configure_logging
from archelond.util import jsonify_code
log = logging.getLogger('archelond') # pylint: disable=invalid-name
V1_ROOT = '/api/v1/'
[docs]def run_server():
"""
If started from command line, rebuild object in
debug mode and run directly
"""
host = os.environ.get('ARCHELOND_HOST', 'localhost')
port = int(os.environ.get('ARCHELOND_PORT', '8580'))
app.debug = True
log.critical(
'Running in debug mode. Do not run this way in production'
)
app.config['LOG_LEVEL'] = 'DEBUG'
app.config['ASSETS_DEBUG'] = True
configure_logging(app)
app.run(host=host, port=port)
[docs]def wsgi_app():
"""
Start flask application runtime
"""
# Disabling check since both types are implementations of same base class
# pylint: disable=redefined-variable-type
# Setup the app
new_app = Flask('archelond')
# Get configuration from default or via environment variable
if os.environ.get('ARCHELOND_CONF'):
new_app.config.from_envvar('ARCHELOND_CONF')
else:
new_app.config.from_object('archelond.config')
# Setup database
if new_app.config['DATABASE_TYPE'] == 'MemoryData':
new_app.data = MemoryData(new_app.config)
elif new_app.config['DATABASE_TYPE'] == 'ElasticData':
new_app.data = ElasticData(new_app.config)
else:
raise Exception('No valid database type is set')
# Set up logging
configure_logging(new_app)
return new_app
# Setup flask application
app = wsgi_app() # pylint: disable=invalid-name
assets = Environment(app) # pylint: disable=invalid-name
htpasswd = HtPasswdAuth(app) # pylint: disable=invalid-name
# Add proxy fixer
app.wsgi_app = ProxyFix(app.wsgi_app)
@app.route('/')
[docs]def index():
"""
Simple index view for documentation and navigation.
"""
return render_template('index.html', user=g.user), 200
@app.route('{}token'.format(V1_ROOT), methods=['GET'])
[docs]def token():
"""
Return the user token for API auth that is based off the
flask secret and user password
"""
return jsonify({'token': htpasswd.generate_token(g.user)})
@app.route('{}history'.format(V1_ROOT), methods=['GET', 'POST'])
[docs]def history():
"""
POST=Add entry
GET=Get entries with query
"""
# We have a lot of logic here since we are doing query string
# handling, so let pylint know that is ok.
# pylint: disable=too-many-return-statements,too-many-branches
if request.method == 'GET':
query = request.args.get('q')
order = request.args.get('o')
page = int(request.args.get('p', 0))
order_type = None
if order:
if order not in ORDER_TYPES:
return jsonify_code(
{'error': 'Order specified is not an option'},
422
)
else:
order_type = order
if query:
results = app.data.filter(
query, order_type, g.user, request.remote_addr, page=page
)
else:
results = app.data.all(
order_type, g.user, request.remote_addr, page=page
)
return jsonify({'commands': results})
if request.method == 'POST':
# Accept json or form type
from_form = True
if request.json:
data = request.json
from_form = False
else:
data = request.form
command = data.get('command')
commands = data.get('commands')
if not (command or commands):
return jsonify_code({'error': 'Missing command(s) parameter'}, 422)
# Allow bulk posting to speedup imports with commands parameter
if commands:
if from_form:
commands = json.loads(commands)
results_list = []
if not isinstance(commands, list):
return jsonify_code({'error': 'Commands must be list'}, 422)
for command in commands:
cmd_id = app.data.add(command, g.user, request.remote_addr)
results_list.append(
{
'response': '',
'status_code': 201,
'headers': {
'location': url_for('history_item', cmd_id=cmd_id)
},
}
)
return jsonify({'responses': results_list})
if not isinstance(command, string_types):
return jsonify_code({'error': 'Command must be a string'}, 422)
cmd_id = app.data.add(command, g.user, request.remote_addr)
return '', 201, {'location': url_for('history_item', cmd_id=cmd_id)}
else: # pragma: no cover
log.critical('Unsupported method used')
raise Exception('Unsupported http method used')
@app.route('{}history/<cmd_id>'.format(V1_ROOT),
methods=['GET', 'PUT', 'DELETE'])
[docs]def history_item(cmd_id):
"""Actions for individual command history items.
Updates, gets, or deletes a command from the active data store.
PUT: Takes a payload in either form or JSON request, and runs the
add routine by passing the dictinoary minus ``command``,
``username``, and ``host`` as kwargs to the data stores ``add``
routine.
"""
# We have to handle several methods, which requires branches and
# extra returns. Until/when we switch to pluggable views, let
# pylint know that is ok for this view.
# pylint: disable=too-many-return-statements,too-many-branches
if request.method == 'GET':
log.debug('Retrieving %s for %s', cmd_id, g.user)
try:
cmd = app.data.get(cmd_id, g.user, request.remote_addr)
except KeyError:
return jsonify_code({'error': 'No such history item'}, 404)
return jsonify(cmd)
if request.method == 'PUT':
# This will only update kwargs since we
# have a deduplicated data structure by command.
log.debug('Updating %s for %s', cmd_id, g.user)
try:
cmd = app.data.get(cmd_id, g.user, request.remote_addr)
except KeyError:
return jsonify_code({'error': 'No such history item'}, 404)
from_form = True
if request.json:
data = request.json
from_form = False
else:
data = request.form
if not data:
return jsonify_code(
{'error': 'Data is required, received empty PUT'},
422
)
put_command = data.get('payload')
if not put_command:
return jsonify_code(
{'error': 'Request must contain ``payload`` parameter'},
422
)
if from_form:
put_command = json.loads(put_command)
# Make sure we don't let them overwrite server side params
try:
del put_command['command']
del put_command['username']
del put_command['host']
except KeyError:
pass
app.data.add(
cmd['command'], g.user, request.remote_addr, **put_command
)
return '', 204
if request.method == 'DELETE':
log.debug('Deleting %s for %s', cmd_id, g.user)
try:
app.data.delete(cmd_id, g.user, request.remote_addr)
except KeyError:
return jsonify_code({'error': 'No such history item'}, 404)
return jsonify(message='success')
else: # pragma: no cover
log.critical('Unsupported method used')
raise Exception('Unsupported http method used')