259 lines
6.9 KiB
Python
259 lines
6.9 KiB
Python
from flask import Flask
|
|
app = Flask(__name__)
|
|
|
|
from artifice.models import Session, usage
|
|
from sqlalchemy import type
|
|
from decimal import Decimal
|
|
from datetime import datetime
|
|
|
|
conn_string = ('postgresql://%(username)s:%(password)s@' +
|
|
'%(host)s:%(port)s/%(database)s') % conn_dict
|
|
|
|
Session.configure(bind=create_engine(conn_string))
|
|
|
|
db = Session()
|
|
|
|
config = load_config()
|
|
|
|
|
|
# Some useful constants
|
|
|
|
iso_time = "%Y-%m-%dT%H:%M:%S"
|
|
iso_date = "%Y-%m-%d"
|
|
|
|
dawn_of_time = "2012-01-01"
|
|
|
|
|
|
current_region = "None" # FIXME
|
|
|
|
class DecimalEncoder(json.JSONEncoder):
|
|
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):
|
|
|
|
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
|
|
|
|
# TODO: fill me in
|
|
def must(*args):
|
|
return lambda(func): func
|
|
|
|
@app.get("/usage")
|
|
@app.get("/usage/{resource_id}") # also allow for querying by resource ID.
|
|
@keystone
|
|
@must("resource_id", "tenant")
|
|
def retrieve_usage(resource_id=None):
|
|
"""Retrieves usage for a given tenant ID.
|
|
Tenant ID will be passed in via the query string.
|
|
Expects a keystone auth string in the headers
|
|
and will attempt to perform keystone auth
|
|
"""
|
|
tenant = flask.request.params.get("tenant", None)
|
|
if not tenant:
|
|
flask.abort(403, json.dumps({"error":"tenant ID required"})) # Bad request
|
|
|
|
# expect a start and an end timepoint
|
|
|
|
start = flask.request.params.get("start", None)
|
|
end = flask.request.params.get("end", None)
|
|
|
|
if not end:
|
|
end = datetime.now().strftime(iso_date)
|
|
|
|
if not start:
|
|
# Hmm. I think this is okay.
|
|
# We just make a date in the dawn of time.
|
|
start = dawn_of_time
|
|
|
|
start = datetime.strptime(start, iso_date)
|
|
end = datetime.strptime(end, iso_date)
|
|
usages = session.query(usage.Usage)\
|
|
.filter(Usage.tenant_id == tenant)\
|
|
.filter(Usage.time.contained_by(start, end))
|
|
|
|
if resource_id:
|
|
usages.filter(usage.Usage.resource_id == resource_id)
|
|
|
|
resource = None
|
|
usages = defaultdict(Decimal)
|
|
for usage in usages:
|
|
if usage.resource_id != resource:
|
|
resource = usage.resource_id
|
|
usages[resource] += Decimal(usage.volume)
|
|
|
|
# 200 okay
|
|
return ( 200, json.dumps({
|
|
"status":"ok",
|
|
"tenant": tenant,
|
|
"total": usages
|
|
}, cls=DecimalEncoder) ) # Specifically encode decimals appropriate, without float logic
|
|
|
|
|
|
@app.post("/usage")
|
|
@keystone
|
|
@must("amount", "start", "end", "tenant")
|
|
def add_usage():
|
|
"""
|
|
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.
|
|
"""
|
|
|
|
body = json.loads(request.body, parse_float=Decimal)
|
|
db.begin()
|
|
for resource in body["resources"]:
|
|
start = datetime.strptime(resource.get("start"), date_iso)
|
|
end = datetime.strptime(resource.get("end"), date_iso)
|
|
id_ = resource["id"]
|
|
u = usage.Usage(
|
|
resource=id_,
|
|
tenant=request.params["tenant"],
|
|
value=resource["amount"],
|
|
start=start,
|
|
end=end)
|
|
db.add(u)
|
|
try:
|
|
db.commit()
|
|
except Exception as e:
|
|
# Explodytime
|
|
status(500)
|
|
return(json.dumps(
|
|
{"status": "error",
|
|
"error" : "database transaction error"
|
|
}))
|
|
|
|
status(201)
|
|
return json.dumps({
|
|
"status": "ok",
|
|
"saved": len(body["resources"])
|
|
})
|
|
|
|
@app.get("/bills/{id}")
|
|
@keystone
|
|
@must("tenant", "start", "end")
|
|
def get_bill(id_):
|
|
"""
|
|
Returns either a single bill or a set of the most recent
|
|
bills for a given Tenant.
|
|
"""
|
|
|
|
# TODO: Put these into an input validator instead
|
|
try:
|
|
start = datetime.strptime(request.params["start"], date_iso)
|
|
except:
|
|
abort(
|
|
403,
|
|
json.dumps(
|
|
{"status":"error",
|
|
"error": "start date is not ISO-compliant"})
|
|
)
|
|
try:
|
|
end = datetime.strptime(request.params["end"], date_iso)
|
|
except:
|
|
abort(
|
|
403,
|
|
json.dumps(
|
|
{"status":"error",
|
|
"error": "end date is not ISO-compliant"})
|
|
)
|
|
|
|
try:
|
|
bill = BillInterface(session).get(id_)
|
|
except:
|
|
abort(404)
|
|
|
|
if not bill:
|
|
abort(404)
|
|
|
|
resp = {"status": "ok",
|
|
"bill": [],
|
|
"total": str(bill.total),
|
|
"tenant": bill.tenant_id
|
|
}
|
|
|
|
for resource in billed:
|
|
resp["bill"].append({
|
|
'resource_id': bill.resource_id,
|
|
'volume': str( bill.volume ),
|
|
'rate': str( bill.rate ),
|
|
# 'metadata': # TODO: This will be filled in with extra data
|
|
})
|
|
|
|
return (200, json.dumps(resp))
|
|
|
|
@app.post("/usage/current")
|
|
@keystone
|
|
@must("tenant_id")
|
|
def get_current_usage():
|
|
"""
|
|
Is intended to return a running total of the current billing periods'
|
|
dataset. Performs a Rate transformer on each transformed datapoint and
|
|
returns the result.
|
|
|
|
TODO: Implement
|
|
"""
|
|
pass
|
|
|
|
@app.post("/bill")
|
|
@keystone
|
|
def make_a_bill():
|
|
"""Generates a bill for a given user.
|
|
Expects a JSON dict, with a tenant id and a time range.
|
|
Authentication is expected to be present.
|
|
This *will* interact with the ERP plugin and perform a bill-generation
|
|
cycle.
|
|
"""
|
|
|
|
body = json.loads(request.body)
|
|
tenant = body.get("tenant", None)
|
|
if not tenant:
|
|
return abort(403) # bad request
|
|
|
|
start = body.get("start", None)
|
|
end = body.get("end", None)
|
|
if not start or not end:
|
|
return abort(403) # All three *must* be defined
|
|
|
|
|
|
bill = BillInterface(session)
|
|
thebill = bill.generate(body["tenant_id"], start, end)
|
|
# assumes the bill is saved
|
|
|
|
if not thebill.is_saved:
|
|
# raise an error
|
|
abort(500)
|
|
|
|
resp = {"status":"created",
|
|
"id": thebill.id,
|
|
"contents": [],
|
|
"total": None
|
|
}
|
|
for resource in thebill.resources:
|
|
total += Decimal(billed.total)
|
|
resp["contents"].append({
|
|
'resource_id': bill.resource_id,
|
|
'volume': str( bill.volume ),
|
|
'rate': str( bill.rate ),
|
|
# 'metadata': # TODO: This will be filled in with extra data
|
|
})
|
|
|
|
resp["total"] = thebill.total
|
|
return (201, json.dumps(resp))
|