From 4b371536cbdaf10dca966b57e39ca268bdb80c8f Mon Sep 17 00:00:00 2001 From: Nichita Herciu <nherciu@cloudbasesolutions.com> Date: Tue, 2 Apr 2019 15:26:48 +0300 Subject: [PATCH] Added support for resource price changes. When calculating the costs for a given resource, all the rows in the price table for that resource are going to be taken into consideration. The most recent price is going to be displayed in the costs page. --- .../garr_costs/content/costs/tables.py | 16 +- .../garr_costs/content/costs/utils.py | 176 +++++++++- .../garr_costs/content/costs/views.py | 24 +- .../files/plugins/admin-costs/tables.py | 23 +- .../files/plugins/admin-costs/utils.py | 184 ++++++++-- .../files/plugins/admin-costs/views.py | 316 ++++++++++-------- .../files/plugins/project-costs/tables.py | 5 +- .../files/plugins/project-costs/utils.py | 184 ++++++++-- .../files/plugins/project-costs/views.py | 54 ++- .../garr_costs/content/costs/tables.py | 2 +- .../garr_costs/content/costs/utils.py | 176 +++++++++- .../garr_costs/content/costs/views.py | 24 +- 12 files changed, 931 insertions(+), 253 deletions(-) diff --git a/admin-costs-plugin/garr_costs/content/costs/tables.py b/admin-costs-plugin/garr_costs/content/costs/tables.py index c182e4c..80ab570 100644 --- a/admin-costs-plugin/garr_costs/content/costs/tables.py +++ b/admin-costs-plugin/garr_costs/content/costs/tables.py @@ -1,3 +1,4 @@ +import logging from django.contrib.humanize.templatetags import humanize from django.utils import text from django.utils.translation import ugettext_lazy as _ @@ -12,15 +13,20 @@ from django import forms as django_forms from horizon import tables from horizon import forms +LOG = logging.getLogger(__name__) def get_link(resource): - if resource['resource_type'] == 'instance': + resource_type = resource.get('resource_type', None) + if resource_type == 'instance': link = 'horizon:project:instances:detail' - elif resource['resource_type'] == 'volume': + elif resource_type == 'volume': link = 'horizon:project:volumes:detail' else: return '' - - return urlresolvers.reverse(link, args=(resource['id'],)) + try: + url = urlresolvers.reverse(link, args=(resource['id'],)) + except Exception: + url = '' + return url class CostsTable(tables.DataTable): resource = tables.WrappingColumn('name', verbose_name=_('Resource Name'), @@ -33,7 +39,7 @@ class CostsTable(tables.DataTable): flavor = tables.Column('flavor', verbose_name=_("Resource flavor")) value = tables.Column('value', verbose_name=_('Usage')) unit = tables.Column('unit', verbose_name=_("Unit")) - price = tables.Column('price', verbose_name=_("Price")) + price = tables.Column('price', verbose_name=_("Latest price")) cost = tables.Column('cost', verbose_name=_("Total Cost")) def get_object_id(self, resource): diff --git a/admin-costs-plugin/garr_costs/content/costs/utils.py b/admin-costs-plugin/garr_costs/content/costs/utils.py index 0d51798..84f833d 100644 --- a/admin-costs-plugin/garr_costs/content/costs/utils.py +++ b/admin-costs-plugin/garr_costs/content/costs/utils.py @@ -26,20 +26,14 @@ def get_measure_value(measures, metric, resource): result = convert_nanoseconds(result) return result -def get_volume_usage(request, resource, price, usage_start, usage_end, gnocchi_client): - unit = 'GB*hour' - try: - api.cinder.volume_get(request, resource['id']) - except Exception: - raise Exception('Volume %s not found' % resource['id']) - +def compute_volume_cost(resource, price, start_date, end_date, gnocchi_client): measures = [measure[2] for measure in gnocchi_client.metric.get_measures( - 'volume.size', start=usage_start, stop=usage_end, + 'volume.size', start=start_date, stop=end_date, aggregation='max', resource_id=resource['id'] )] if not measures: - return 0, 0, unit + return 0, 0 usage = sum(measures)/len(measures) cost = '{0:.2f}'.format( @@ -47,16 +41,170 @@ def get_volume_usage(request, resource, price, usage_start, usage_end, gnocchi_c ) usage = '{0:.2f}'.format(usage) - return cost, usage, unit + return cost, usage -def get_instance_usage(resource, price, usage_start, usage_end, gnocchi_client): - unit = 'hour' - cpu_util_measures = gnocchi_client.metric.get_measures(metric=resource['metrics']['cpu_util'],start=usage_start,stop=usage_end) +def get_volume_usage(request, resource, price_list, usage_start, usage_end, gnocchi_client): + unit = 'GB*hour' + try: + api.cinder.volume_get(request, resource['id']) + except Exception: + raise Exception('Volume %s not found' % resource['id']) + + cost = '{0:.2f}'.format(0) + usage = '{0:.2f}'.format(0) + + if len(price_list)==1: + cost, usage = compute_volume_cost(resource, price_list[0][1], usage_start, usage_end, gnocchi_client) + return cost, usage, unit + + for i in range(len(price_list)-1): + if price_list[i+1][0] < usage_start: + #price[i+1] is older than usage_start + if i+1 == len(price_list): + #price[i+1] is the most recent in the DB though + interval_start = usage_start + interval_end = usage_end + interval_price = price_list[i+1][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + else: + continue + + if price_list[i][0] > usage_end: + #price[i] is newer than usage_end + break + + if price_list[i][0] <= usage_start and price_list[i+1][0] > usage_end: + #the resource was only used between the dates of price[i] and price[i+1] + interval_start=usage_start + interval_end=usage_end + interval_price=price_list[i][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + + if price_list[i][0] < usage_start and price_list[i+1][0] > usage_start and price_list[i+1][0] < usage_end: + #price[i] is older than usage_start and price[i+1] is newer than usage_start but older than usage_end + interval_start = usage_start + interval_end = price_list[i+1][0] + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + continue + + if price_list[i][0] >= usage_start and price_list[i+1][0] < usage_end: + #price[i] and price[i+1] existed between usage_start and usage_end + interval_start = price_list[i][0] + interval_end = price_list[i+1][0] + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + continue + + if price_list[i][0] < usage_end and price_list[i+1][0] > usage_end and price_list[i][0] > usage_start: + #price[i] is older than usage_end while price[i+1] is newer and they're all newer than usage_start + interval_start = price_list[i][0] + interval_end = usage_end + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + + return cost, usage, unit + +def compute_instance_cost(resource, price, start_date, end_date, gnocchi_client): + cpu_util_measures = gnocchi_client.metric.get_measures(metric=resource['metrics']['cpu_util'],start=start_date,stop=end_date) hours_interval = (len(cpu_util_measures)*5)/60 cost = '{0:.2f}'.format( round(hours_interval * price, 2) ) - return cost, hours_interval, unit + return cost, hours_interval + +def get_instance_usage(resource, price_list, usage_start, usage_end, gnocchi_client): + unit = 'hour' + usage = 0 + cost = '{0:.2f}'.format(0) + + if len(price_list)==1: + cost, usage = compute_instance_cost(resource, price_list[0][1], usage_start, usage_end, gnocchi_client) + return cost, usage, unit + + for i in range(len(price_list)-1): + if price_list[i+1][0] < usage_start: + #price[i+1] is older than usage_start + if i+1 == len(price_list): + #price[i+1] is the most recent in the DB though + interval_start = usage_start + interval_end = usage_end + interval_price = price_list[i+1][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + else: + continue + + if price_list[i][0] > usage_end: + #price[i] is newer than usage_end + break + + if price_list[i][0] <= usage_start and price_list[i+1][0] > usage_end: + #the resource was only used between the dates of price[i] and price[i+1] + interval_start=usage_start + interval_end=usage_end + interval_price=price_list[i][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + + if price_list[i][0] < usage_start and price_list[i+1][0] > usage_start and price_list[i+1][0] < usage_end: + #price[i] is older than usage_start and price[i+1] is newer than usage_start but older than usage_end + interval_start = usage_start + interval_end = price_list[i+1][0] + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + continue + + if price_list[i][0] >= usage_start and price_list[i+1][0] < usage_end: + #price[i] and price[i+1] existed between usage_start and usage_end + interval_start = price_list[i][0] + interval_end = price_list[i+1][0] + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + continue + + if price_list[i][0] < usage_end and price_list[i+1][0] > usage_end and price_list[i][0] > usage_start: + #price[i] is older than usage_end while price[i+1] is newer and they're all newer than usage_start + interval_start = price_list[i][0] + interval_end = usage_end + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + + return cost, usage, unit diff --git a/admin-costs-plugin/garr_costs/content/costs/views.py b/admin-costs-plugin/garr_costs/content/costs/views.py index 6e7ea6b..f9079eb 100644 --- a/admin-costs-plugin/garr_costs/content/costs/views.py +++ b/admin-costs-plugin/garr_costs/content/costs/views.py @@ -114,7 +114,7 @@ class IndexView(tables.DataTableView): resource_usage_end = usage_end try: - price, cost_unit = self.get_price(resource, self.request, session) + price_list, cost_unit = self.get_price(resource, self.request, session) except Exception as e: LOG.error("Unable to fetch price for resource %s of type %s" % (resource_name, resource['type'])) continue @@ -124,10 +124,10 @@ class IndexView(tables.DataTableView): try: if resource['type'] == 'instance': - cost, usage, unit = get_instance_usage(resource, price, resource_usage_start, resource_usage_end, client) + cost, usage, unit = get_instance_usage(resource, price_list, resource_usage_start, resource_usage_end, client) resource_flavor = resource['flavor_name'] elif resource['type'] == 'volume': - cost, usage, unit = get_volume_usage(self.request, resource, price, resource_usage_start, resource_usage_end, client) + cost, usage, unit = get_volume_usage(self.request, resource, price_list, resource_usage_start, resource_usage_end, client) resource_flavor = api.cinder.volume_type_get(self.request, resource['volume_type']).name else: continue @@ -139,12 +139,14 @@ class IndexView(tables.DataTableView): cost = '0' usage = '0' + latest_price = price_list[-1][1] + result.append( { 'name': resource_name, 'unit': unit, 'value': usage, 'id': resource['id'], - 'price': '{}/{}'.format(price, cost_unit), + 'price': '{}/{}'.format(latest_price, cost_unit), 'resource_type': resource['type'], 'cost': cost, 'flavor': resource_flavor @@ -158,10 +160,16 @@ class IndexView(tables.DataTableView): flavor_name = resource.get('flavor_name', None) if not flavor_name: flavor_name = api.nova.flavor_get(request, resource['flavor_id']).name flavor = db_session.query(Flavor).filter(Flavor.name == flavor_name).first() - price = db_session.query(Price).filter(Price.resource == flavor.id, Price.type == 'flavor').first() + row_list = db_session.query(Price).filter(Price.resource == flavor.id, Price.type == 'flavor').all() elif resource['type'] == 'volume': volume_type = resource['volume_type'] or '' volume = db_session.query(Storage).filter(Storage.volume == volume_type).first() - price = db_session.query(Price).filter(Price.resource == volume.id, Price.type == 'storage').first() - - return price.price, price.unit + row_list = db_session.query(Price).filter(Price.resource == volume.id, Price.type == 'storage').all() + + price_list = [] + for row in row_list: + date = row.since + price = row.price + price_list.append((date,price)) + + return sorted(price_list, key=lambda x: x[0]), row_list[0].unit diff --git a/charms/garr-dashboard/files/plugins/admin-costs/tables.py b/charms/garr-dashboard/files/plugins/admin-costs/tables.py index 4276233..80ab570 100644 --- a/charms/garr-dashboard/files/plugins/admin-costs/tables.py +++ b/charms/garr-dashboard/files/plugins/admin-costs/tables.py @@ -1,3 +1,4 @@ +import logging from django.contrib.humanize.templatetags import humanize from django.utils import text from django.utils.translation import ugettext_lazy as _ @@ -12,32 +13,38 @@ from django import forms as django_forms from horizon import tables from horizon import forms +LOG = logging.getLogger(__name__) def get_link(resource): - if resource['resource_type'] == 'instance': + resource_type = resource.get('resource_type', None) + if resource_type == 'instance': link = 'horizon:project:instances:detail' - elif resource['resource_type'] == 'volume': + elif resource_type == 'volume': link = 'horizon:project:volumes:detail' else: return '' + try: + url = urlresolvers.reverse(link, args=(resource['id'],)) + except Exception: + url = '' + return url - return urlresolvers.reverse(link, args=(resource['id'],)) - -class CostsTable(tables.DataTable): +class CostsTable(tables.DataTable): resource = tables.WrappingColumn('name', verbose_name=_('Resource Name'), link=get_link) id = tables.WrappingColumn('id', verbose_name=_('ID'), attrs={'data-type': 'uuid'}, hidden=True) resource_type = tables.WrappingColumn('resource_type', verbose_name=_('Resource Type')) - + + flavor = tables.Column('flavor', verbose_name=_("Resource flavor")) value = tables.Column('value', verbose_name=_('Usage')) unit = tables.Column('unit', verbose_name=_("Unit")) - price = tables.Column('price', verbose_name=_("Price")) + price = tables.Column('price', verbose_name=_("Latest price")) cost = tables.Column('cost', verbose_name=_("Total Cost")) def get_object_id(self, resource): return resource['id'] - + class Meta(object): name = 'costs_table' verbose_name = _("Costs") diff --git a/charms/garr-dashboard/files/plugins/admin-costs/utils.py b/charms/garr-dashboard/files/plugins/admin-costs/utils.py index ef039b6..84f833d 100644 --- a/charms/garr-dashboard/files/plugins/admin-costs/utils.py +++ b/charms/garr-dashboard/files/plugins/admin-costs/utils.py @@ -26,20 +26,14 @@ def get_measure_value(measures, metric, resource): result = convert_nanoseconds(result) return result -def get_volume_usage(request, resource, price, usage_start, usage_end, gnocchi_client): - unit = 'GB' - try: - api.cinder.volume_get(request, resource['id']) - except Exception: - raise Exception('Volume %s not found' % resource['id']) - +def compute_volume_cost(resource, price, start_date, end_date, gnocchi_client): measures = [measure[2] for measure in gnocchi_client.metric.get_measures( - 'volume.size', start=usage_start, stop=usage_end, + 'volume.size', start=start_date, stop=end_date, aggregation='max', resource_id=resource['id'] )] if not measures: - return 0, 0, unit + return 0, 0 usage = sum(measures)/len(measures) cost = '{0:.2f}'.format( @@ -47,22 +41,170 @@ def get_volume_usage(request, resource, price, usage_start, usage_end, gnocchi_c ) usage = '{0:.2f}'.format(usage) + return cost, usage + + +def get_volume_usage(request, resource, price_list, usage_start, usage_end, gnocchi_client): + unit = 'GB*hour' + try: + api.cinder.volume_get(request, resource['id']) + except Exception: + raise Exception('Volume %s not found' % resource['id']) + + cost = '{0:.2f}'.format(0) + usage = '{0:.2f}'.format(0) + + if len(price_list)==1: + cost, usage = compute_volume_cost(resource, price_list[0][1], usage_start, usage_end, gnocchi_client) + return cost, usage, unit + + for i in range(len(price_list)-1): + if price_list[i+1][0] < usage_start: + #price[i+1] is older than usage_start + if i+1 == len(price_list): + #price[i+1] is the most recent in the DB though + interval_start = usage_start + interval_end = usage_end + interval_price = price_list[i+1][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + else: + continue + + if price_list[i][0] > usage_end: + #price[i] is newer than usage_end + break + + if price_list[i][0] <= usage_start and price_list[i+1][0] > usage_end: + #the resource was only used between the dates of price[i] and price[i+1] + interval_start=usage_start + interval_end=usage_end + interval_price=price_list[i][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + + if price_list[i][0] < usage_start and price_list[i+1][0] > usage_start and price_list[i+1][0] < usage_end: + #price[i] is older than usage_start and price[i+1] is newer than usage_start but older than usage_end + interval_start = usage_start + interval_end = price_list[i+1][0] + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + continue + + if price_list[i][0] >= usage_start and price_list[i+1][0] < usage_end: + #price[i] and price[i+1] existed between usage_start and usage_end + interval_start = price_list[i][0] + interval_end = price_list[i+1][0] + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + continue + + if price_list[i][0] < usage_end and price_list[i+1][0] > usage_end and price_list[i][0] > usage_start: + #price[i] is older than usage_end while price[i+1] is newer and they're all newer than usage_start + interval_start = price_list[i][0] + interval_end = usage_end + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + return cost, usage, unit -def get_instance_usage(resource, price, usage_start, usage_end): - unit = 'hour' - start_time = parse_datetime(resource['started_at']).replace(tzinfo=None) - start_query = datetime.strptime(usage_start, "%Y-%m-%d") - diff = start_query - start_time - - hours_interval = get_hours(usage_start, usage_end) - if diff.days < 0: - end_query = datetime.strptime(usage_end, "%Y-%m-%d") - diff = end_query - start_time - hours_interval = diff.days * 24 +def compute_instance_cost(resource, price, start_date, end_date, gnocchi_client): + cpu_util_measures = gnocchi_client.metric.get_measures(metric=resource['metrics']['cpu_util'],start=start_date,stop=end_date) + hours_interval = (len(cpu_util_measures)*5)/60 cost = '{0:.2f}'.format( round(hours_interval * price, 2) ) - return cost, hours_interval, unit + return cost, hours_interval + +def get_instance_usage(resource, price_list, usage_start, usage_end, gnocchi_client): + unit = 'hour' + usage = 0 + cost = '{0:.2f}'.format(0) + + if len(price_list)==1: + cost, usage = compute_instance_cost(resource, price_list[0][1], usage_start, usage_end, gnocchi_client) + return cost, usage, unit + + for i in range(len(price_list)-1): + if price_list[i+1][0] < usage_start: + #price[i+1] is older than usage_start + if i+1 == len(price_list): + #price[i+1] is the most recent in the DB though + interval_start = usage_start + interval_end = usage_end + interval_price = price_list[i+1][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + else: + continue + + if price_list[i][0] > usage_end: + #price[i] is newer than usage_end + break + + if price_list[i][0] <= usage_start and price_list[i+1][0] > usage_end: + #the resource was only used between the dates of price[i] and price[i+1] + interval_start=usage_start + interval_end=usage_end + interval_price=price_list[i][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + + if price_list[i][0] < usage_start and price_list[i+1][0] > usage_start and price_list[i+1][0] < usage_end: + #price[i] is older than usage_start and price[i+1] is newer than usage_start but older than usage_end + interval_start = usage_start + interval_end = price_list[i+1][0] + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + continue + + if price_list[i][0] >= usage_start and price_list[i+1][0] < usage_end: + #price[i] and price[i+1] existed between usage_start and usage_end + interval_start = price_list[i][0] + interval_end = price_list[i+1][0] + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + continue + + if price_list[i][0] < usage_end and price_list[i+1][0] > usage_end and price_list[i][0] > usage_start: + #price[i] is older than usage_end while price[i+1] is newer and they're all newer than usage_start + interval_start = price_list[i][0] + interval_end = usage_end + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + + return cost, usage, unit diff --git a/charms/garr-dashboard/files/plugins/admin-costs/views.py b/charms/garr-dashboard/files/plugins/admin-costs/views.py index 605e61c..f9079eb 100644 --- a/charms/garr-dashboard/files/plugins/admin-costs/views.py +++ b/charms/garr-dashboard/files/plugins/admin-costs/views.py @@ -1,141 +1,175 @@ -import logging - -from oslo_utils import units - -from django.utils.translation import ugettext_lazy as _ -from django.utils import timezone -from horizon import exceptions -from horizon import messages -from horizon import tables -from horizon import fogitrms - -from openstack_dashboard import api -from openstack_dashboard import policy -from sqlalchemy.orm import Session - -from openstack_dashboard.dashboards.project.costs import tables as costs_tables - -from gnocchi import gnocchi_client -from utils import convert_nanoseconds, get_instance_usage, get_volume_usage -from orm import engine, Flavor, Price, Storage - -LOG = logging.getLogger(__name__) - -GARR_DB_KEY = 'GARR_DATABASE' - - -class IndexView(tables.DataTableView): - table_class = costs_tables.CostsTable - page_title = _("Project Costs") - template_name = 'project/costs/index.html' - - def get_context_data(self, **kwargs): - context = super(IndexView, self).get_context_data(**kwargs) - context['form'] = self.get_form(self.request) - context['overall_cost'] = sum(float(row['cost']) for row in context['table'].data) - return context - - @staticmethod - def get_default_interval(): - start_time = timezone.now() - timezone.timedelta(days=1) - end_time = timezone.now() - start = start_time.strftime('%Y-%m-%d') - end = end_time.strftime('%Y-%m-%d') - - return start, end - - def get_form(self, request): - if not hasattr(self, 'form'): - req = request - start = req.GET.get('start', None) - end = req.GET.get('end', None) - if not start and not end: - start, end = self.get_default_interval() - self.form = forms.DateForm(initial={'start': start, 'end': end}) - return self.form - - @staticmethod - def get_interval(request): - usage_start = request.GET.get('start', None) - usage_end = request.GET.get('end', None) - if not usage_start and not usage_end: - usage_start, usage_end = IndexView.get_default_interval() - - return usage_start, usage_end - - def get_data(self): - try: - session = Session(engine) - except Exception as e: - LOG.error('Unable to connect to the database') - LOG.error(e) - return [] - project_id = self.request.GET.get('project', self.request.user.project_id) - query = { - "=": { - "project_id": project_id - } - } - - resource_types = ['instance', 'volume'] - client = gnocchi_client(self.request) - project_resources = [] - for resource_type in resource_types: - try: - resources = client.resource.search(resource_type=resource_type, query=query, details=True) - except Exception: - LOG.error('Error while calling gnocchi api') - messages.error(self.request, _("Error while calling Gnocchi API")) - continue - - project_resources.extend(resources) - - usage_start, usage_end = self.get_interval(self.request) - result = [] - for resource in project_resources: - resource_name = resource.get('name', None) or resource.get('display_name', None) or resource['id'] - try: - price, cost_unit = self.get_price(resource, self.request, session) - except Exception as e: - LOG.error("Unable to fetch price for resource %s of type %s" % (resource_name, resource['type'])) - continue - - if type(cost_unit) is set: - cost_unit = cost_unit.pop() - - try: - if resource['type'] == 'instance': - cost, usage, unit = get_instance_usage(resource, price, usage_start, usage_end) - elif resource['type'] == 'volume': - cost, usage, unit = get_volume_usage(self.request, resource, price, usage_start, usage_end, client) - else: - continue - except Exception as ex: - LOG.error('Unable to get usage for resource %s' % resource) - continue - - result.append( { - 'name': resource_name, - 'unit': unit, - 'value': usage, - 'id': resource['id'], - 'price': '{}/{}'.format(price, cost_unit), - 'resource_type': resource['type'], - 'cost': cost - }) - - return sorted(result, key=lambda resource: resource['name']) - - - def get_price(self, resource, request, db_session): - if resource['type'] == 'instance': - flavor_name = resource.get('flavor_name', None) - if not flavor_name: flavor_name = api.nova.flavor_get(request, resource['flavor_id']).name - flavor = db_session.query(Flavor).filter(Flavor.name == flavor_name).first() - price = db_session.query(Price).filter(Price.resource == flavor.id, Price.type == 'flavor').first() - elif resource['type'] == 'volume': - volume_type = resource['volume_type'] or '' - volume = db_session.query(Storage).filter(Storage.volume == volume_type).first() - price = db_session.query(Price).filter(Price.resource == volume.id, Price.type == 'storage').first() - - return price.price, price.unit +import logging + +from oslo_utils import units + +from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone +from horizon import exceptions +from horizon import messages +from horizon import tables +from horizon import forms + +from openstack_dashboard import api +from openstack_dashboard import policy +from sqlalchemy.orm import Session + +from openstack_dashboard.dashboards.project.costs import tables as costs_tables + +from gnocchi import gnocchi_client +from utils import convert_nanoseconds, get_instance_usage, get_volume_usage +from orm import engine, Flavor, Price, Storage + +LOG = logging.getLogger(__name__) + +GARR_DB_KEY = 'GARR_DATABASE' + + +class IndexView(tables.DataTableView): + table_class = costs_tables.CostsTable + page_title = _("Project Costs") + template_name = 'project/costs/index.html' + + def get_context_data(self, **kwargs): + context = super(IndexView, self).get_context_data(**kwargs) + context['form'] = self.get_form(self.request) + context['overall_cost'] = sum(float(row['cost']) for row in context['table'].data) + return context + + @staticmethod + def get_default_interval(): + start_time = timezone.now() - timezone.timedelta(days=1) + end_time = timezone.now() + start = start_time.strftime('%Y-%m-%d') + end = end_time.strftime('%Y-%m-%d') + + return start, end + + def get_form(self, request): + if not hasattr(self, 'form'): + req = request + start = req.GET.get('start', None) + end = req.GET.get('end', None) + if not start and not end: + start, end = self.get_default_interval() + self.form = forms.DateForm(initial={'start': start, 'end': end}) + return self.form + + @staticmethod + def get_interval(request): + usage_start = request.GET.get('start', None) + usage_end = request.GET.get('end', None) + if (not usage_start) or (not usage_end) or not (usage_end > usage_start): + usage_start, usage_end = IndexView.get_default_interval() + + return usage_start, usage_end + + def get_data(self): + try: + session = Session(engine) + except Exception as e: + LOG.error('Unable to connect to the database') + LOG.error(e) + return [] + + query = { + "=": { + "project_id": self.request.user.project_id + } + } + + resource_types = ['instance', 'volume'] + client = gnocchi_client(self.request) + project_resources = [] + for resource_type in resource_types: + try: + resources = client.resource.search(resource_type=resource_type, query=query, details=True) + except Exception: + LOG.error('Error while calling gnocchi api') + messages.error(self.request, _("Error while calling Gnocchi API")) + continue + + project_resources.extend(resources) + + usage_start, usage_end = self.get_interval(self.request) + result = [] + for resource in project_resources: + resource_not_in_timeframe = False + + resource_name = resource.get('name', None) or resource.get('display_name', None) or resource['id'] + + resource_usage_start = resource['started_at'][:10] + if resource['ended_at']: + resource_usage_end = resource['ended_at'][:10] + else: + resource_usage_end = usage_end + + # make sure that the resource existed during this interval + if (resource_usage_end < usage_start) or (resource_usage_start > usage_end): + resource_not_in_timeframe = True + + # truncate the interval defined by the resource_usage_* variables + if not (resource_usage_start > usage_start): + resource_usage_start = usage_start + if not (resource_usage_end < usage_end): + resource_usage_end = usage_end + + try: + price_list, cost_unit = self.get_price(resource, self.request, session) + except Exception as e: + LOG.error("Unable to fetch price for resource %s of type %s" % (resource_name, resource['type'])) + continue + + if type(cost_unit) is set: + cost_unit = cost_unit.pop() + + try: + if resource['type'] == 'instance': + cost, usage, unit = get_instance_usage(resource, price_list, resource_usage_start, resource_usage_end, client) + resource_flavor = resource['flavor_name'] + elif resource['type'] == 'volume': + cost, usage, unit = get_volume_usage(self.request, resource, price_list, resource_usage_start, resource_usage_end, client) + resource_flavor = api.cinder.volume_type_get(self.request, resource['volume_type']).name + else: + continue + except Exception as ex: + LOG.error('Unable to get usage for resource %s' % resource) + continue + + if resource_not_in_timeframe: + cost = '0' + usage = '0' + + latest_price = price_list[-1][1] + + result.append( { + 'name': resource_name, + 'unit': unit, + 'value': usage, + 'id': resource['id'], + 'price': '{}/{}'.format(latest_price, cost_unit), + 'resource_type': resource['type'], + 'cost': cost, + 'flavor': resource_flavor + }) + + return sorted(result, key=lambda resource: resource['name']) + + + def get_price(self, resource, request, db_session): + if resource['type'] == 'instance': + flavor_name = resource.get('flavor_name', None) + if not flavor_name: flavor_name = api.nova.flavor_get(request, resource['flavor_id']).name + flavor = db_session.query(Flavor).filter(Flavor.name == flavor_name).first() + row_list = db_session.query(Price).filter(Price.resource == flavor.id, Price.type == 'flavor').all() + elif resource['type'] == 'volume': + volume_type = resource['volume_type'] or '' + volume = db_session.query(Storage).filter(Storage.volume == volume_type).first() + row_list = db_session.query(Price).filter(Price.resource == volume.id, Price.type == 'storage').all() + + price_list = [] + for row in row_list: + date = row.since + price = row.price + price_list.append((date,price)) + + return sorted(price_list, key=lambda x: x[0]), row_list[0].unit diff --git a/charms/garr-dashboard/files/plugins/project-costs/tables.py b/charms/garr-dashboard/files/plugins/project-costs/tables.py index aa3ec0e..80ab570 100644 --- a/charms/garr-dashboard/files/plugins/project-costs/tables.py +++ b/charms/garr-dashboard/files/plugins/project-costs/tables.py @@ -36,9 +36,10 @@ class CostsTable(tables.DataTable): hidden=True) resource_type = tables.WrappingColumn('resource_type', verbose_name=_('Resource Type')) + flavor = tables.Column('flavor', verbose_name=_("Resource flavor")) value = tables.Column('value', verbose_name=_('Usage')) unit = tables.Column('unit', verbose_name=_("Unit")) - price = tables.Column('price', verbose_name=_("Price")) + price = tables.Column('price', verbose_name=_("Latest price")) cost = tables.Column('cost', verbose_name=_("Total Cost")) def get_object_id(self, resource): @@ -48,4 +49,4 @@ class CostsTable(tables.DataTable): name = 'costs_table' verbose_name = _("Costs") table_actions = () - multi_select = False \ No newline at end of file + multi_select = False diff --git a/charms/garr-dashboard/files/plugins/project-costs/utils.py b/charms/garr-dashboard/files/plugins/project-costs/utils.py index ef039b6..84f833d 100644 --- a/charms/garr-dashboard/files/plugins/project-costs/utils.py +++ b/charms/garr-dashboard/files/plugins/project-costs/utils.py @@ -26,20 +26,14 @@ def get_measure_value(measures, metric, resource): result = convert_nanoseconds(result) return result -def get_volume_usage(request, resource, price, usage_start, usage_end, gnocchi_client): - unit = 'GB' - try: - api.cinder.volume_get(request, resource['id']) - except Exception: - raise Exception('Volume %s not found' % resource['id']) - +def compute_volume_cost(resource, price, start_date, end_date, gnocchi_client): measures = [measure[2] for measure in gnocchi_client.metric.get_measures( - 'volume.size', start=usage_start, stop=usage_end, + 'volume.size', start=start_date, stop=end_date, aggregation='max', resource_id=resource['id'] )] if not measures: - return 0, 0, unit + return 0, 0 usage = sum(measures)/len(measures) cost = '{0:.2f}'.format( @@ -47,22 +41,170 @@ def get_volume_usage(request, resource, price, usage_start, usage_end, gnocchi_c ) usage = '{0:.2f}'.format(usage) + return cost, usage + + +def get_volume_usage(request, resource, price_list, usage_start, usage_end, gnocchi_client): + unit = 'GB*hour' + try: + api.cinder.volume_get(request, resource['id']) + except Exception: + raise Exception('Volume %s not found' % resource['id']) + + cost = '{0:.2f}'.format(0) + usage = '{0:.2f}'.format(0) + + if len(price_list)==1: + cost, usage = compute_volume_cost(resource, price_list[0][1], usage_start, usage_end, gnocchi_client) + return cost, usage, unit + + for i in range(len(price_list)-1): + if price_list[i+1][0] < usage_start: + #price[i+1] is older than usage_start + if i+1 == len(price_list): + #price[i+1] is the most recent in the DB though + interval_start = usage_start + interval_end = usage_end + interval_price = price_list[i+1][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + else: + continue + + if price_list[i][0] > usage_end: + #price[i] is newer than usage_end + break + + if price_list[i][0] <= usage_start and price_list[i+1][0] > usage_end: + #the resource was only used between the dates of price[i] and price[i+1] + interval_start=usage_start + interval_end=usage_end + interval_price=price_list[i][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + + if price_list[i][0] < usage_start and price_list[i+1][0] > usage_start and price_list[i+1][0] < usage_end: + #price[i] is older than usage_start and price[i+1] is newer than usage_start but older than usage_end + interval_start = usage_start + interval_end = price_list[i+1][0] + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + continue + + if price_list[i][0] >= usage_start and price_list[i+1][0] < usage_end: + #price[i] and price[i+1] existed between usage_start and usage_end + interval_start = price_list[i][0] + interval_end = price_list[i+1][0] + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + continue + + if price_list[i][0] < usage_end and price_list[i+1][0] > usage_end and price_list[i][0] > usage_start: + #price[i] is older than usage_end while price[i+1] is newer and they're all newer than usage_start + interval_start = price_list[i][0] + interval_end = usage_end + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + return cost, usage, unit -def get_instance_usage(resource, price, usage_start, usage_end): - unit = 'hour' - start_time = parse_datetime(resource['started_at']).replace(tzinfo=None) - start_query = datetime.strptime(usage_start, "%Y-%m-%d") - diff = start_query - start_time - - hours_interval = get_hours(usage_start, usage_end) - if diff.days < 0: - end_query = datetime.strptime(usage_end, "%Y-%m-%d") - diff = end_query - start_time - hours_interval = diff.days * 24 +def compute_instance_cost(resource, price, start_date, end_date, gnocchi_client): + cpu_util_measures = gnocchi_client.metric.get_measures(metric=resource['metrics']['cpu_util'],start=start_date,stop=end_date) + hours_interval = (len(cpu_util_measures)*5)/60 cost = '{0:.2f}'.format( round(hours_interval * price, 2) ) - return cost, hours_interval, unit + return cost, hours_interval + +def get_instance_usage(resource, price_list, usage_start, usage_end, gnocchi_client): + unit = 'hour' + usage = 0 + cost = '{0:.2f}'.format(0) + + if len(price_list)==1: + cost, usage = compute_instance_cost(resource, price_list[0][1], usage_start, usage_end, gnocchi_client) + return cost, usage, unit + + for i in range(len(price_list)-1): + if price_list[i+1][0] < usage_start: + #price[i+1] is older than usage_start + if i+1 == len(price_list): + #price[i+1] is the most recent in the DB though + interval_start = usage_start + interval_end = usage_end + interval_price = price_list[i+1][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + else: + continue + + if price_list[i][0] > usage_end: + #price[i] is newer than usage_end + break + + if price_list[i][0] <= usage_start and price_list[i+1][0] > usage_end: + #the resource was only used between the dates of price[i] and price[i+1] + interval_start=usage_start + interval_end=usage_end + interval_price=price_list[i][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + + if price_list[i][0] < usage_start and price_list[i+1][0] > usage_start and price_list[i+1][0] < usage_end: + #price[i] is older than usage_start and price[i+1] is newer than usage_start but older than usage_end + interval_start = usage_start + interval_end = price_list[i+1][0] + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + continue + + if price_list[i][0] >= usage_start and price_list[i+1][0] < usage_end: + #price[i] and price[i+1] existed between usage_start and usage_end + interval_start = price_list[i][0] + interval_end = price_list[i+1][0] + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + continue + + if price_list[i][0] < usage_end and price_list[i+1][0] > usage_end and price_list[i][0] > usage_start: + #price[i] is older than usage_end while price[i+1] is newer and they're all newer than usage_start + interval_start = price_list[i][0] + interval_end = usage_end + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + + return cost, usage, unit diff --git a/charms/garr-dashboard/files/plugins/project-costs/views.py b/charms/garr-dashboard/files/plugins/project-costs/views.py index 6c1c5fc..f9079eb 100644 --- a/charms/garr-dashboard/files/plugins/project-costs/views.py +++ b/charms/garr-dashboard/files/plugins/project-costs/views.py @@ -58,7 +58,7 @@ class IndexView(tables.DataTableView): def get_interval(request): usage_start = request.GET.get('start', None) usage_end = request.GET.get('end', None) - if not usage_start and not usage_end: + if (not usage_start) or (not usage_end) or not (usage_end > usage_start): usage_start, usage_end = IndexView.get_default_interval() return usage_start, usage_end @@ -93,9 +93,28 @@ class IndexView(tables.DataTableView): usage_start, usage_end = self.get_interval(self.request) result = [] for resource in project_resources: + resource_not_in_timeframe = False + resource_name = resource.get('name', None) or resource.get('display_name', None) or resource['id'] + + resource_usage_start = resource['started_at'][:10] + if resource['ended_at']: + resource_usage_end = resource['ended_at'][:10] + else: + resource_usage_end = usage_end + + # make sure that the resource existed during this interval + if (resource_usage_end < usage_start) or (resource_usage_start > usage_end): + resource_not_in_timeframe = True + + # truncate the interval defined by the resource_usage_* variables + if not (resource_usage_start > usage_start): + resource_usage_start = usage_start + if not (resource_usage_end < usage_end): + resource_usage_end = usage_end + try: - price, cost_unit = self.get_price(resource, self.request, session) + price_list, cost_unit = self.get_price(resource, self.request, session) except Exception as e: LOG.error("Unable to fetch price for resource %s of type %s" % (resource_name, resource['type'])) continue @@ -105,23 +124,32 @@ class IndexView(tables.DataTableView): try: if resource['type'] == 'instance': - cost, usage, unit = get_instance_usage(resource, price, usage_start, usage_end) + cost, usage, unit = get_instance_usage(resource, price_list, resource_usage_start, resource_usage_end, client) + resource_flavor = resource['flavor_name'] elif resource['type'] == 'volume': - cost, usage, unit = get_volume_usage(self.request, resource, price, usage_start, usage_end, client) + cost, usage, unit = get_volume_usage(self.request, resource, price_list, resource_usage_start, resource_usage_end, client) + resource_flavor = api.cinder.volume_type_get(self.request, resource['volume_type']).name else: continue except Exception as ex: LOG.error('Unable to get usage for resource %s' % resource) continue + if resource_not_in_timeframe: + cost = '0' + usage = '0' + + latest_price = price_list[-1][1] + result.append( { 'name': resource_name, 'unit': unit, 'value': usage, 'id': resource['id'], - 'price': '{}/{}'.format(price, cost_unit), + 'price': '{}/{}'.format(latest_price, cost_unit), 'resource_type': resource['type'], - 'cost': cost + 'cost': cost, + 'flavor': resource_flavor }) return sorted(result, key=lambda resource: resource['name']) @@ -132,10 +160,16 @@ class IndexView(tables.DataTableView): flavor_name = resource.get('flavor_name', None) if not flavor_name: flavor_name = api.nova.flavor_get(request, resource['flavor_id']).name flavor = db_session.query(Flavor).filter(Flavor.name == flavor_name).first() - price = db_session.query(Price).filter(Price.resource == flavor.id, Price.type == 'flavor').first() + row_list = db_session.query(Price).filter(Price.resource == flavor.id, Price.type == 'flavor').all() elif resource['type'] == 'volume': volume_type = resource['volume_type'] or '' volume = db_session.query(Storage).filter(Storage.volume == volume_type).first() - price = db_session.query(Price).filter(Price.resource == volume.id, Price.type == 'storage').first() - - return price.price, price.unit + row_list = db_session.query(Price).filter(Price.resource == volume.id, Price.type == 'storage').all() + + price_list = [] + for row in row_list: + date = row.since + price = row.price + price_list.append((date,price)) + + return sorted(price_list, key=lambda x: x[0]), row_list[0].unit diff --git a/costs-plugin/garr_costs/content/costs/tables.py b/costs-plugin/garr_costs/content/costs/tables.py index 7bc775e..80ab570 100644 --- a/costs-plugin/garr_costs/content/costs/tables.py +++ b/costs-plugin/garr_costs/content/costs/tables.py @@ -39,7 +39,7 @@ class CostsTable(tables.DataTable): flavor = tables.Column('flavor', verbose_name=_("Resource flavor")) value = tables.Column('value', verbose_name=_('Usage')) unit = tables.Column('unit', verbose_name=_("Unit")) - price = tables.Column('price', verbose_name=_("Price")) + price = tables.Column('price', verbose_name=_("Latest price")) cost = tables.Column('cost', verbose_name=_("Total Cost")) def get_object_id(self, resource): diff --git a/costs-plugin/garr_costs/content/costs/utils.py b/costs-plugin/garr_costs/content/costs/utils.py index 0d51798..84f833d 100644 --- a/costs-plugin/garr_costs/content/costs/utils.py +++ b/costs-plugin/garr_costs/content/costs/utils.py @@ -26,20 +26,14 @@ def get_measure_value(measures, metric, resource): result = convert_nanoseconds(result) return result -def get_volume_usage(request, resource, price, usage_start, usage_end, gnocchi_client): - unit = 'GB*hour' - try: - api.cinder.volume_get(request, resource['id']) - except Exception: - raise Exception('Volume %s not found' % resource['id']) - +def compute_volume_cost(resource, price, start_date, end_date, gnocchi_client): measures = [measure[2] for measure in gnocchi_client.metric.get_measures( - 'volume.size', start=usage_start, stop=usage_end, + 'volume.size', start=start_date, stop=end_date, aggregation='max', resource_id=resource['id'] )] if not measures: - return 0, 0, unit + return 0, 0 usage = sum(measures)/len(measures) cost = '{0:.2f}'.format( @@ -47,16 +41,170 @@ def get_volume_usage(request, resource, price, usage_start, usage_end, gnocchi_c ) usage = '{0:.2f}'.format(usage) - return cost, usage, unit + return cost, usage -def get_instance_usage(resource, price, usage_start, usage_end, gnocchi_client): - unit = 'hour' - cpu_util_measures = gnocchi_client.metric.get_measures(metric=resource['metrics']['cpu_util'],start=usage_start,stop=usage_end) +def get_volume_usage(request, resource, price_list, usage_start, usage_end, gnocchi_client): + unit = 'GB*hour' + try: + api.cinder.volume_get(request, resource['id']) + except Exception: + raise Exception('Volume %s not found' % resource['id']) + + cost = '{0:.2f}'.format(0) + usage = '{0:.2f}'.format(0) + + if len(price_list)==1: + cost, usage = compute_volume_cost(resource, price_list[0][1], usage_start, usage_end, gnocchi_client) + return cost, usage, unit + + for i in range(len(price_list)-1): + if price_list[i+1][0] < usage_start: + #price[i+1] is older than usage_start + if i+1 == len(price_list): + #price[i+1] is the most recent in the DB though + interval_start = usage_start + interval_end = usage_end + interval_price = price_list[i+1][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + else: + continue + + if price_list[i][0] > usage_end: + #price[i] is newer than usage_end + break + + if price_list[i][0] <= usage_start and price_list[i+1][0] > usage_end: + #the resource was only used between the dates of price[i] and price[i+1] + interval_start=usage_start + interval_end=usage_end + interval_price=price_list[i][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + + if price_list[i][0] < usage_start and price_list[i+1][0] > usage_start and price_list[i+1][0] < usage_end: + #price[i] is older than usage_start and price[i+1] is newer than usage_start but older than usage_end + interval_start = usage_start + interval_end = price_list[i+1][0] + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + continue + + if price_list[i][0] >= usage_start and price_list[i+1][0] < usage_end: + #price[i] and price[i+1] existed between usage_start and usage_end + interval_start = price_list[i][0] + interval_end = price_list[i+1][0] + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + continue + + if price_list[i][0] < usage_end and price_list[i+1][0] > usage_end and price_list[i][0] > usage_start: + #price[i] is older than usage_end while price[i+1] is newer and they're all newer than usage_start + interval_start = price_list[i][0] + interval_end = usage_end + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_volume_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + + return cost, usage, unit + +def compute_instance_cost(resource, price, start_date, end_date, gnocchi_client): + cpu_util_measures = gnocchi_client.metric.get_measures(metric=resource['metrics']['cpu_util'],start=start_date,stop=end_date) hours_interval = (len(cpu_util_measures)*5)/60 cost = '{0:.2f}'.format( round(hours_interval * price, 2) ) - return cost, hours_interval, unit + return cost, hours_interval + +def get_instance_usage(resource, price_list, usage_start, usage_end, gnocchi_client): + unit = 'hour' + usage = 0 + cost = '{0:.2f}'.format(0) + + if len(price_list)==1: + cost, usage = compute_instance_cost(resource, price_list[0][1], usage_start, usage_end, gnocchi_client) + return cost, usage, unit + + for i in range(len(price_list)-1): + if price_list[i+1][0] < usage_start: + #price[i+1] is older than usage_start + if i+1 == len(price_list): + #price[i+1] is the most recent in the DB though + interval_start = usage_start + interval_end = usage_end + interval_price = price_list[i+1][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + else: + continue + + if price_list[i][0] > usage_end: + #price[i] is newer than usage_end + break + + if price_list[i][0] <= usage_start and price_list[i+1][0] > usage_end: + #the resource was only used between the dates of price[i] and price[i+1] + interval_start=usage_start + interval_end=usage_end + interval_price=price_list[i][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + + if price_list[i][0] < usage_start and price_list[i+1][0] > usage_start and price_list[i+1][0] < usage_end: + #price[i] is older than usage_start and price[i+1] is newer than usage_start but older than usage_end + interval_start = usage_start + interval_end = price_list[i+1][0] + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + continue + + if price_list[i][0] >= usage_start and price_list[i+1][0] < usage_end: + #price[i] and price[i+1] existed between usage_start and usage_end + interval_start = price_list[i][0] + interval_end = price_list[i+1][0] + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + continue + + if price_list[i][0] < usage_end and price_list[i+1][0] > usage_end and price_list[i][0] > usage_start: + #price[i] is older than usage_end while price[i+1] is newer and they're all newer than usage_start + interval_start = price_list[i][0] + interval_end = usage_end + interval_price = price_list[i][1] + + interval_cost, interval_usage = compute_instance_cost(resource, interval_price, interval_start, interval_end, gnocchi_client) + cost = cost + interval_cost + usage = usage + interval_usage + break + + return cost, usage, unit diff --git a/costs-plugin/garr_costs/content/costs/views.py b/costs-plugin/garr_costs/content/costs/views.py index 6e7ea6b..f9079eb 100644 --- a/costs-plugin/garr_costs/content/costs/views.py +++ b/costs-plugin/garr_costs/content/costs/views.py @@ -114,7 +114,7 @@ class IndexView(tables.DataTableView): resource_usage_end = usage_end try: - price, cost_unit = self.get_price(resource, self.request, session) + price_list, cost_unit = self.get_price(resource, self.request, session) except Exception as e: LOG.error("Unable to fetch price for resource %s of type %s" % (resource_name, resource['type'])) continue @@ -124,10 +124,10 @@ class IndexView(tables.DataTableView): try: if resource['type'] == 'instance': - cost, usage, unit = get_instance_usage(resource, price, resource_usage_start, resource_usage_end, client) + cost, usage, unit = get_instance_usage(resource, price_list, resource_usage_start, resource_usage_end, client) resource_flavor = resource['flavor_name'] elif resource['type'] == 'volume': - cost, usage, unit = get_volume_usage(self.request, resource, price, resource_usage_start, resource_usage_end, client) + cost, usage, unit = get_volume_usage(self.request, resource, price_list, resource_usage_start, resource_usage_end, client) resource_flavor = api.cinder.volume_type_get(self.request, resource['volume_type']).name else: continue @@ -139,12 +139,14 @@ class IndexView(tables.DataTableView): cost = '0' usage = '0' + latest_price = price_list[-1][1] + result.append( { 'name': resource_name, 'unit': unit, 'value': usage, 'id': resource['id'], - 'price': '{}/{}'.format(price, cost_unit), + 'price': '{}/{}'.format(latest_price, cost_unit), 'resource_type': resource['type'], 'cost': cost, 'flavor': resource_flavor @@ -158,10 +160,16 @@ class IndexView(tables.DataTableView): flavor_name = resource.get('flavor_name', None) if not flavor_name: flavor_name = api.nova.flavor_get(request, resource['flavor_id']).name flavor = db_session.query(Flavor).filter(Flavor.name == flavor_name).first() - price = db_session.query(Price).filter(Price.resource == flavor.id, Price.type == 'flavor').first() + row_list = db_session.query(Price).filter(Price.resource == flavor.id, Price.type == 'flavor').all() elif resource['type'] == 'volume': volume_type = resource['volume_type'] or '' volume = db_session.query(Storage).filter(Storage.volume == volume_type).first() - price = db_session.query(Price).filter(Price.resource == volume.id, Price.type == 'storage').first() - - return price.price, price.unit + row_list = db_session.query(Price).filter(Price.resource == volume.id, Price.type == 'storage').all() + + price_list = [] + for row in row_list: + date = row.since + price = row.price + price_list.append((date,price)) + + return sorted(price_list, key=lambda x: x[0]), row_list[0].unit -- GitLab