distil/api/web.py
2014-03-03 11:30:54 +13:00

267 lines
8.1 KiB
Python

import flask
from flask import Flask, Blueprint
from artifice import interface, database
from artifice.models import UsageEntry, SalesOrder, Tenant, billing
import sqlalchemy
from sqlalchemy import create_engine, func
from sqlalchemy.orm import scoped_session, create_session
from sqlalchemy.pool import NullPool
from decimal import Decimal
from datetime import datetime
import importlib
import collections
import pytz
import json
from .helpers import returns_json, json_must
engine = None
# Session.configure(bind=create_engine(conn_string))
Session = None
# db = Session()
app = Blueprint("main", __name__)
config = None
invoicer = None
DEFAULT_TIMEZONE = "Pacific/Auckland"
current_region = "Wellington" # FIXME
def get_app(conf):
actual_app = Flask(__name__)
actual_app.register_blueprint(app, url_prefix="/")
global engine
engine = create_engine(conf["main"]["database_uri"], poolclass=NullPool)
global config
config = conf
global Session
Session = scoped_session(lambda: create_session(bind=engine))
global invoicer
module, kls = config["main"]["export_provider"].split(":")
invoicer = getattr(importlib.import_module(module), kls)
if config["main"].get("timezone"):
global DEFAULT_TIMEZONE
DEFAULT_TIMEZONE = config["main"]["timezone"]
return actual_app
# Some useful constants
iso_time = "%Y-%m-%dT%H:%M:%S"
iso_date = "%Y-%m-%d"
dawn_of_time = "2012-01-01"
class validators(object):
@classmethod
def iterable(cls, val):
return isinstance(val, collections.Iterable)
class DecimalEncoder(json.JSONEncoder):
"""Simple encoder which handles Decimal objects, rendering them to strings.
*REQUIRES* use of a decimal-aware decoder.
"""
def default(self, obj):
if isinstance(obj, Decimal):
return str(obj)
return json.JSONEncoder.default(self, obj)
def fetch_endpoint(region):
return config.get("keystone_endpoint")
# return "http://0.0.0.0:35357/v2.0" # t\/his ought to be in config. #FIXME
def keystone(func):
"""Will eventually provide a keystone wrapper for validating a query.
Currently does not.
"""
return func # disabled for now
# admin_token = config.get("admin_token")
# def _perform_keystone(*args, **kwargs):
# headers = flask.request.headers
# if not 'user_id' in headers:
# flask.abort(401) # authentication required
#
# endpoint = fetch_endpoint( current_region )
# keystone = keystoneclient.v2_0.client.Client(token=admin_token,
# endpoint=endpoint)
# return _perform_keystone
@app.route("collect_usage", methods=["POST"])
@keystone
def run_usage_collection():
"""
Adds usage for a given tenant T and resource R.
Expects to receive a Resource ID, a time range, and a volume.
The volume will be parsed from JSON as a Decimal object.
"""
session = Session()
artifice = interface.Artifice(config)
d = database.Database(session)
tenants = artifice.tenants
resp = {"tenants": [],
"errors": 0}
for tenant in tenants:
d.insert_tenant(tenant.conn['id'], tenant.conn['name'],
tenant.conn['description'])
session.begin(subtransactions=True)
start = session.query(func.max(UsageEntry.end).label('end')).\
filter(UsageEntry.tenant_id == tenant.conn['id']).first().end
if not start:
start = datetime.strptime(dawn_of_time, iso_date)
end = datetime.now(pytz.timezone(DEFAULT_TIMEZONE)).\
replace(minute=0, second=0, microsecond=0)
usage = tenant.usage(start, end)
# enter all resources into the db
d.enter(usage.values(), start, end)
try:
session.commit()
resp["tenants"].append(
{"id": tenant.conn['id'],
"updated": True,
"start": start.strftime(iso_time),
"end": end.strftime(iso_time)
}
)
except sqlalchemy.exc.IntegrityError:
# this is fine.
resp["tenants"].append(
{"id": tenant.conn['id'],
"updated": False,
"error": "Integrity error",
"start": start.strftime(iso_time),
"end": end.strftime(iso_time)
}
)
resp["errors"] += 1
session.close()
return json.dumps(resp)
def generate_sales_order(tenant, session):
db = database.Database(session)
session.begin()
# Get the last sales order for this tenant, to establish
# the proper ranging
start = session.query(func.max(SalesOrder.end).label('end')).\
filter(SalesOrder.tenant == tenant).first().end
if not start:
start = datetime.strptime(dawn_of_time, iso_date)
# Today, the beginning of.
end = datetime.now().\
replace(hour=0, minute=0, second=0, microsecond=0)
# Invoicer is pulled from the configfile and set up above.
usage = db.usage(start, end, tenant.id)
order = SalesOrder(tenant_id=tenant.id, start=start, end=end)
session.add(order)
try:
# Commit the record before we generate the bill, to mark this as a
# billed region of data. Avoids race conditions by marking a tenant
# BEFORE we start to generate the data for it.
session.commit()
# Transform the query result into a billable dict.
# This is non-portable and very much tied to the CSV exporter
# and will probably result in the CSV exporter being changed.
billable = billing.build_billable(usage, session)
session.close()
exporter = invoicer(start, end, config["export_config"])
exporter.bill(billable)
exporter.close()
return {"id": tenant.id,
"generated": True,
"start": str(start),
"end": str(end)}
except sqlalchemy.exc.IntegrityError:
session.rollback()
session.close()
return {"id": tenant.id,
"generated": False,
"start": str(start),
"end": str(end)}
@app.route("sales_order", methods=["POST"])
@keystone
@json_must()
@returns_json
def run_sales_order_generation():
session = Session()
# TODO: ensure cases work as follows:
# if no body or content type: generate orders for all
# if no body and json content type: throw 400? Or should this order all?
# if body, and json, and parsed, but no tenants, throw 400? Or order all.
# If request has body, content type must be json.
# else: throw 400
# if request has body and content type json, body must parse to json
# else: throw 400
# if 'tenants' is not None, and not a list, throw a 400 response.
# if the list is empty, throw 400 and invalid parameter 'list is empty'.
# Or allow return 200 and resp['tenants'] will just be empty?
# if list isn't empty, but query produces no results, throw 400?
# Or allow return 200 and resp['tenants'] will just be empty?
# any missing cases? Any cases not worth covering?
# should these checks be here, or in decorators.
# if in decorators can we make these as parameters for the decorators,
# to keep them fairly generic, or are these cases too specific?
tenants = flask.request.json.get("tenants", None)
tenant_query = session.query(Tenant)
if isinstance(tenants, list):
tenant_query = tenant_query.filter(Tenant.id.in_(tenants))
if tenant_query.count() == 0:
# if an explicit list of tenants is passed, and none of them
# exist in the db, then we consider that an error.
return 400, {"errors": ["No tenants found from given list."]}
elif tenants is not None:
return 400, {"missing parameters": {"tenants": "A list of tenant ids."}}
# Handled like this for a later move to Celery distributed workers
resp = {"tenants": []}
for tenant in tenant_query:
resp['tenants'].append(generate_sales_order(tenant, session))
return 200, resp
if __name__ == '__main__':
pass