diff --git a/admin-costs-plugin/garr_costs/content/costs/tables.py b/admin-costs-plugin/garr_costs/content/costs/tables.py index c182e4c30e5389f94d816e9092566999b61d5160..80ab5708d4e7186692115c58c7e633d063d9f107 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 0d51798b840cdf9280943abbb3b6750d35edba2d..84f833d03589c56ba3c9ade687b28b48b774b58d 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 6e7ea6b3478322a756a7eaf149916c36c1b006ec..f9079eb1eb571117ecaf0ad80c6bfaa6deb4c0df 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 42762333a4e8d1fd2f9147c1198856d2a770ec99..80ab5708d4e7186692115c58c7e633d063d9f107 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 ef039b6763b53c71d2ba465c762aa6d8589508c7..84f833d03589c56ba3c9ade687b28b48b774b58d 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 605e61c7b8f48e7dc6fd4c8ca6aa96c2c2a5b6c8..f9079eb1eb571117ecaf0ad80c6bfaa6deb4c0df 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 aa3ec0e4c36dea930dd1ba8dd2517573e78f9256..80ab5708d4e7186692115c58c7e633d063d9f107 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 ef039b6763b53c71d2ba465c762aa6d8589508c7..84f833d03589c56ba3c9ade687b28b48b774b58d 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 6c1c5fc96853a9c87cb43a649c1a7370e4ef7f2e..f9079eb1eb571117ecaf0ad80c6bfaa6deb4c0df 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 7bc775eb0b94ff096d98133caa64b62108e0a931..80ab5708d4e7186692115c58c7e633d063d9f107 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 0d51798b840cdf9280943abbb3b6750d35edba2d..84f833d03589c56ba3c9ade687b28b48b774b58d 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 6e7ea6b3478322a756a7eaf149916c36c1b006ec..f9079eb1eb571117ecaf0ad80c6bfaa6deb4c0df 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