diff --git a/bilean/api/openstack/v1/__init__.py b/bilean/api/openstack/v1/__init__.py index e027918..508bfe5 100644 --- a/bilean/api/openstack/v1/__init__.py +++ b/bilean/api/openstack/v1/__init__.py @@ -13,6 +13,7 @@ import routes +from bilean.api.openstack.v1 import consumptions from bilean.api.openstack.v1 import events from bilean.api.openstack.v1 import policies from bilean.api.openstack.v1 import resources @@ -172,4 +173,21 @@ class API(wsgi.Router): action="index", conditions={'method': 'GET'}) + # Consumptions + cons_resource = consumptions.create_resource(conf) + cons_path = "/consumptions" + with mapper.submapper(controller=cons_resource, + path_prefix=cons_path) as cons_mapper: + + # Consumption collection + cons_mapper.connect("consumption_index", + "", + action="index", + conditions={'method': 'GET'}) + + cons_mapper.connect("consumption_statistics", + "/statistics", + action="statistics", + conditions={'method': 'GET'}) + super(API, self).__init__(mapper) diff --git a/bilean/api/openstack/v1/consumptions.py b/bilean/api/openstack/v1/consumptions.py new file mode 100644 index 0000000..6efab17 --- /dev/null +++ b/bilean/api/openstack/v1/consumptions.py @@ -0,0 +1,88 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from bilean.api.openstack.v1 import util +from bilean.common import consts +from bilean.common import serializers +from bilean.common import utils +from bilean.common import wsgi +from bilean.rpc import client as rpc_client + + +class ConsumptionController(object): + """WSGI controller for Consumptions in Bilean v1 API.""" + # Define request scope (must match what is in policy.json) + REQUEST_SCOPE = 'consumptions' + + def __init__(self, options): + self.options = options + self.rpc_client = rpc_client.EngineClient() + + @util.policy_enforce + def index(self, req): + """Lists all consumptions.""" + filter_whitelist = { + 'resource_type': 'mixed', + } + param_whitelist = { + 'user_id': 'single', + 'start_time': 'single', + 'end_time': 'single', + 'limit': 'single', + 'marker': 'single', + 'sort_dir': 'single', + 'sort_keys': 'multi', + } + params = util.get_allowed_params(req.params, param_whitelist) + filters = util.get_allowed_params(req.params, filter_whitelist) + + key = consts.PARAM_LIMIT + if key in params: + params[key] = utils.parse_int_param(key, params[key]) + + if not filters: + filters = None + consumptions = self.rpc_client.consumption_list(req.context, + filters=filters, + **params) + + return {'consumptions': consumptions} + + @util.policy_enforce + def statistics(self, req): + '''Consumptions statistics.''' + filter_whitelist = { + 'resource_type': 'mixed', + } + param_whitelist = { + 'user_id': 'single', + 'start_time': 'single', + 'end_time': 'single', + } + params = util.get_allowed_params(req.params, param_whitelist) + filters = util.get_allowed_params(req.params, filter_whitelist) + + if not filters: + filters = None + statistics = self.rpc_client.consumption_statistics(req.context, + filters=filters, + **params) + + return {'statistics': statistics} + + +def create_resource(ops): + """Consumption resource factory method.""" + deserializer = wsgi.JSONRequestDeserializer() + serializer = serializers.JSONResponseSerializer() + return wsgi.Resource(ConsumptionController(ops), deserializer, serializer) diff --git a/bilean/db/api.py b/bilean/db/api.py index 8799e72..27863d0 100644 --- a/bilean/db/api.py +++ b/bilean/db/api.py @@ -312,9 +312,12 @@ def consumption_get(context, consumption_id, project_safe=True): project_safe=project_safe) -def consumption_get_all(context, limit=None, marker=None, sort_keys=None, - sort_dir=None, filters=None, project_safe=True): - return IMPL.consumption_get_all(context, limit=limit, +def consumption_get_all(context, user_id=None, limit=None, marker=None, + sort_keys=None, sort_dir=None, filters=None, + project_safe=True): + return IMPL.consumption_get_all(context, + user_id=user_id, + limit=limit, marker=marker, sort_keys=sort_keys, sort_dir=sort_dir, diff --git a/bilean/db/sqlalchemy/api.py b/bilean/db/sqlalchemy/api.py index 2f153c3..17c1863 100644 --- a/bilean/db/sqlalchemy/api.py +++ b/bilean/db/sqlalchemy/api.py @@ -881,14 +881,17 @@ def consumption_get(context, consumption_id, project_safe=True): return consumption -def consumption_get_all(context, limit=None, marker=None, sort_keys=None, - sort_dir=None, filters=None, project_safe=True): +def consumption_get_all(context, user_id=None, limit=None, marker=None, + sort_keys=None, sort_dir=None, filters=None, + project_safe=True): query = model_query(context, models.Consumption) if context.is_admin: project_safe = False if project_safe: query = query.filter_by(user_id=context.project) + elif user_id: + query = query.filter_by(user_id=user_id) if filters is None: filters = {} diff --git a/bilean/engine/consumption.py b/bilean/engine/consumption.py index 8aa79c7..bc2d3fe 100644 --- a/bilean/engine/consumption.py +++ b/bilean/engine/consumption.py @@ -67,11 +67,14 @@ class Consumption(object): return cls.from_db_record(record) @classmethod - def load_all(cls, context, limit=None, marker=None, sort_keys=None, - sort_dir=None, filters=None, project_safe=True): + def load_all(cls, context, user_id=None, limit=None, marker=None, + sort_keys=None, sort_dir=None, filters=None, + project_safe=True): '''Retrieve all consumptions from database.''' - records = db_api.consumption_get_all(context, limit=limit, + records = db_api.consumption_get_all(context, + user_id=user_id, + limit=limit, marker=marker, filters=filters, sort_keys=sort_keys, diff --git a/bilean/engine/service.py b/bilean/engine/service.py index 95fcc2e..8da41c7 100644 --- a/bilean/engine/service.py +++ b/bilean/engine/service.py @@ -34,6 +34,7 @@ from bilean.common import schema from bilean.common import utils from bilean.db import api as db_api from bilean.engine.actions import base as action_mod +from bilean.engine import consumption as cons_mod from bilean.engine import dispatcher from bilean.engine import environment from bilean.engine import event as event_mod @@ -679,3 +680,71 @@ class EngineService(service.Service): self.TG.start_action(self.engine_id, action_id=action_id) LOG.info(_LI('User settle_account action queued: %s'), action_id) + + @request_context + def consumption_list(self, cnxt, user_id=None, limit=None, + marker=None, sort_keys=None, sort_dir=None, + filters=None, project_safe=True): + if limit is not None: + limit = utils.parse_int_param('limit', limit) + + consumptions = cons_mod.Consumption.load_all(cnxt, + user_id=user_id, + limit=limit, + marker=marker, + sort_keys=sort_keys, + sort_dir=sort_dir, + filters=filters, + project_safe=project_safe) + return [c.to_dict() for c in consumptions] + + @request_context + def consumption_statistics(self, cnxt, user_id=None, filters=None, + start_time=None, end_time=None, + project_safe=True): + result = {} + if start_time is None: + start_time = 0 + else: + start_time = utils.format_time_to_seconds(start_time) + start_time = utils.make_decimal(start_time) + + now_time = utils.format_time_to_seconds(timeutils.utcnow()) + now_time = utils.make_decimal(now_time) + if end_time is None: + end_time = now_time + else: + end_time = utils.format_time_to_seconds(end_time) + end_time = utils.make_decimal(end_time) + + consumptions = cons_mod.Consumption.load_all(cnxt, user_id=user_id, + project_safe=project_safe) + for cons in consumptions: + if cons.start_time > end_time or cons.end_time < start_time: + continue + et = min(cons.end_time, end_time) + st = max(cons.start_time, start_time) + seconds = et - st + cost = cons.rate * seconds + if cons.resource_type not in result: + result[cons.resource_type] = cost + else: + result[cons.resource_type] += cost + + resources = plugin_base.Resource.load_all(cnxt, user_id=user_id, + project_safe=project_safe) + for res in resources: + if res.last_bill > end_time or now_time < start_time: + continue + et = min(now_time, end_time) + st = max(res.last_bill, start_time) + seconds = et - st + cost = res.rate * seconds + if res.resource_type not in result: + result[res.resource_type] = cost + else: + result[res.resource_type] += cost + + for key in six.iterkeys(result): + result[key] = utils.dec2str(result[key]) + return result diff --git a/bilean/rpc/client.py b/bilean/rpc/client.py index f2b1b6a..a0ea46c 100644 --- a/bilean/rpc/client.py +++ b/bilean/rpc/client.py @@ -210,3 +210,25 @@ class EngineClient(object): return self.call(ctxt, self.make_msg('settle_account', user_id=user_id, task=task)) + + # consumptions + def consumption_list(self, ctxt, user_id=None, limit=None, marker=None, + sort_keys=None, sort_dir=None, filters=None, + project_safe=True): + return self.call(ctxt, self.make_msg('consumption_list', + user_id=user_id, + limit=limit, marker=marker, + sort_keys=sort_keys, + sort_dir=sort_dir, + filters=filters, + project_safe=project_safe)) + + def consumption_statistics(self, ctxt, user_id=None, filters=None, + start_time=None, end_time=None, + project_safe=True): + return self.call(ctxt, self.make_msg('consumption_statistics', + user_id=user_id, + filters=filters, + start_time=start_time, + end_time=end_time, + project_safe=project_safe))