diff --git a/distil_ui/api/distil.py b/distil_ui/api/distil.py deleted file mode 100644 index 5c7d254..0000000 --- a/distil_ui/api/distil.py +++ /dev/null @@ -1,316 +0,0 @@ -# Copyright (c) 2014 Catalyst IT Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import collections -import datetime -import logging -import math -import time - -from django.conf import settings -import eventlet - -from openstack_dashboard.api import base - -LOG = logging.getLogger(__name__) -BILLITEM = collections.namedtuple('BillItem', - ['id', 'resource', 'count', 'cost']) - -EMPTY_BREAKDOWN = [BILLITEM(id=1, resource='N/A', count=0, cost=0)] - -RES_NAME_MAPPING = {'Virtual Machine': 'Compute', - 'Volume': 'Block Storage'} - -KNOWN_RESOURCE_TYPE = ['Compute', 'Block Storage', 'Network', 'Router', - 'Image', 'Floating IP', 'Object Storage', 'VPN', - 'Inbound International Traffic', - 'Outbound International Traffic', - 'Inbound National Traffic', - 'Outbound National Traffic'] - -SRV_RES_MAPPING = {'m1.tiny': 'Compute', - 'm1.small': 'Compute', - 'm1.mini': 'Compute', - 'm1.medium': 'Compute', - 'c1.small': 'Compute', - 'm1.large': 'Compute', - 'm1.xlarge': 'Compute', - 'c1.large': 'Compute', - 'c1.xlarge': 'Compute', - 'c1.xxlarge': 'Compute', - 'm1.2xlarge': 'Compute', - 'c1.c1r1': 'Compute', - 'c1.c1r2': 'Compute', - 'c1.c1r4': 'Compute', - 'c1.c2r1': 'Compute', - 'c1.c2r2': 'Compute', - 'c1.c2r4': 'Compute', - 'c1.c2r8': 'Compute', - 'c1.c2r16': 'Compute', - 'c1.c4r2': 'Compute', - 'c1.c4r4': 'Compute', - 'c1.c4r8': 'Compute', - 'c1.c4r16': 'Compute', - 'c1.c4r32': 'Compute', - 'c1.c8r4': 'Compute', - 'c1.c8r8': 'Compute', - 'c1.c8r16': 'Compute', - 'c1.c8r32': 'Compute', - 'b1.standard': 'Block Storage', - 'o1.standard': 'Object Storage', - 'n1.ipv4': 'Floating IP', - 'n1.network': 'Network', - 'n1.router': 'Router', - 'n1.vpn': 'VPN', - 'n1.international-in': 'Inbound International Traffic', - 'n1.international-out': 'Outbound International Traffic', - 'n1.national-in': 'Inbound National Traffic', - 'n1.national-out': 'Outbound National Traffic'} - -TRAFFIC_MAPPING = {'n1.international-in': 'Inbound International Traffic', - 'n1.international-out': 'Outbound International Traffic', - 'n1.national-in': 'Inbound National Traffic', - 'n1.national-out': 'Outbound National Traffic'} - -CACHE = {} - - -def distilclient(request): - try: - try: - from distilclient import client - except Exception: - from distil.client import client - auth_url = base.url_for(request, service_type='identity') - distil_url = base.url_for(request, service_type='rating') - insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) - cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None) - distil = client.Client(distil_url=distil_url, - os_auth_token=request.user.token.id, - os_tenant_id=request.user.tenant_id, - os_auth_url=auth_url, - os_region_name=request.user.services_region, - insecure=insecure, - os_cacert=cacert) - distil.request = request - except Exception as e: - LOG.error(e) - return - return distil - - -def _get_month_cost(distil_client, tenant_id, start_str, end_str, - history_cost, i): - today = datetime.datetime.today() - start = datetime.datetime.strptime(start_str, '%Y-%m-%dT%H:%M:%S') - cache_key = (distil_client.endpoint + '_' + tenant_id + '_' + - start_str + '_' + end_str) - if cache_key in CACHE: - history_cost[i] = CACHE[cache_key] - return - - month_cost = distil_client.get_rated([tenant_id], start_str, - end_str)['usage'] - - resource_cost = collections.OrderedDict() - prices = {} - cost_details = collections.defaultdict(list) - for res in KNOWN_RESOURCE_TYPE: - cost_details[res] = [] - - for res_id, details in month_cost['resources'].items(): - resource_type = details['type'] - for s in details['services']: - if resource_type not in prices: - try: - prices[resource_type] = float(s.get('rate', 0)) - except Exception as e: - LOG.error('Failed to get rate for %s since %s' % (s, e)) - # Only collect service details for current month, we may support - # the details for history in the future. - if ((start.year == today.year and start.month == today.month) or - s['name'] in TRAFFIC_MAPPING): - try: - s_copy = s.copy() - s_copy['volume'] = round(float(s_copy['volume']), 4) - s_copy['resource_id'] = res_id - cd_key = ('Image' if resource_type == 'Image' else - SRV_RES_MAPPING.get(s['name'], resource_type)) - if cd_key in ('Image', 'Block Storage', 'Object Storage'): - s_copy['unit'] = 'gigabyte * hour' - - # NOTE(flwang): Get the related resource info - if resource_type == 'Floating IP': - s_copy['resource'] = details['ip address'] - if resource_type in ('Image', 'Object Storage Container', - 'Network', 'Virtual Machine', - 'Router', 'VPN', 'Volume'): - s_copy['resource'] = details['name'] - - cost_details.get(cd_key).append(s_copy) - except Exception as e: - LOG.error('Failed to save: %s, since %s' % (s, e)) - continue - - res_type = (resource_type if resource_type not in - RES_NAME_MAPPING else RES_NAME_MAPPING[resource_type]) - count, cost = _calculate_count_cost(list(details['services']), - res_type) - - if res_type in resource_cost: - tmp_count_cost = resource_cost[res_type] - tmp_count_cost = [tmp_count_cost[0] + count, - tmp_count_cost[1] + cost] - resource_cost[res_type] = tmp_count_cost - else: - resource_cost[res_type] = [count, cost] - - # NOTE(flwang): Based on current Distil API design, it's making the - # traffic data associate with floating ip and router. So we need to - # get them out and recalculate the cost of floating ip and router. - if ['admin'] in [r.values() for r in distil_client.request.user.roles]: - _calculate_traffic_cost(cost_details, resource_cost) - - breakdown = [] - total_cost = 0 - for resource, count_cost in resource_cost.items(): - rounded_cost = round(count_cost[1], 2) - breakdown.append(BILLITEM(id=len(breakdown) + 1, - resource=resource, - count=count_cost[0], - cost=rounded_cost)) - total_cost += rounded_cost - - if breakdown: - if start.year == today.year and start.month == today.month: - # Only apply/show the discount for current month - end_str = today.strftime('%Y-%m-%dT%H:00:00') - history_cost[i] = _apply_discount((round(total_cost, 2), - breakdown, cost_details), - start_str, - end_str, - prices) - else: - month_cost = (round(total_cost, 2), breakdown, []) - if month_cost: - CACHE[cache_key] = month_cost - history_cost[i] = month_cost - - -def _calculate_count_cost(service_details, resource_type): - count = 0 - cost = 0 - for s in service_details: - if resource_type == 'Image' and s['name'] == 'b1.standard': - count += 1 - cost += float(s['cost']) - if SRV_RES_MAPPING.get(s['name'], '') == resource_type: - count += 1 - cost += float(s['cost']) - return count, cost - - -def _calculate_traffic_cost(cost_details, resource_cost): - for resource_type in TRAFFIC_MAPPING.values(): - if resource_type in cost_details: - (count, cost) = _calculate_count_cost(cost_details[resource_type], - resource_type) - if cost > 0: - resource_cost[resource_type] = (count, cost) - - -def _apply_discount(cost, start_str, end_str, prices): - """Appy discount for the usage costs - - For now we only show the discount info for current month cost, because - the discount for history month has shown on customer's invoice. - """ - total_cost, breakdown, cost_details = cost - start = time.mktime(time.strptime(start_str, '%Y-%m-%dT%H:%M:%S')) - end = time.mktime(time.strptime(end_str, '%Y-%m-%dT%H:%M:%S')) - # Get the integer part of the hours - free_hours = math.floor((end - start) / 3600) - - free_network_cost = round(prices.get('Network', 0.0164) * free_hours, 2) - free_router_cost = round(prices.get('Router', 0.0170) * free_hours, 2) - - for item in breakdown: - if item.resource == 'Network': - free_network_cost = (item.cost if item.cost <= free_network_cost - else free_network_cost) - breakdown[item.id - 1] = item._replace(cost=(item.cost - - free_network_cost)) - total_cost -= free_network_cost - if item.resource == 'Router': - free_router_cost = (item.cost if item.cost <= free_router_cost - else free_router_cost) - breakdown[item.id - 1] = item._replace(cost=(item.cost - - free_router_cost)) - total_cost -= free_router_cost - - return (total_cost, breakdown, cost_details) - - -def _calculate_start_date(today): - last_year = today.year - 1 if today.month < 12 else today.year - month = ((today.month + 1) % 12 if today.month + 1 > 12 - else today.month + 1) - return datetime.datetime(last_year, month, 1) - - -def _calculate_end_date(start): - year = start.year + 1 if start.month + 1 > 12 else start.year - month = (start.month + 1) % 12 or 12 - return datetime.datetime(year, month, 1) - - -def get_cost(request, distil_client=None, enable_eventlet=True): - """Get cost for the last 1atest 12 months include current month - - This function will return the latest 12 months cost and their breakdown - details, which includes current month. - """ - if enable_eventlet: - eventlet.monkey_patch() - thread_pool = eventlet.GreenPool(size=12) - history_cost = [(0, EMPTY_BREAKDOWN, []) for _ in range(12)] - - distil_client = distil_client or distilclient(request) - - if not distil_client: - return history_cost - - today = datetime.date.today() - start = _calculate_start_date(datetime.date.today()) - end = _calculate_end_date(start) - final_end = datetime.datetime(today.year, today.month + 1, 1) - - try: - for i in range(12): - start_str = start.strftime("%Y-%m-%dT00:00:00") - end_str = end.strftime("%Y-%m-%dT00:00:00") - thread_pool.spawn_n(_get_month_cost, - distil_client, request.user.tenant_id, - start_str, end_str, - history_cost, i) - start = end - end = _calculate_end_date(start) - if end > final_end: - break - - thread_pool.waitall() - except Exception as e: - LOG.exception('Failed to get the history cost data', e) - - return history_cost diff --git a/distil_ui/api/distil_v2.py b/distil_ui/api/distil_v2.py new file mode 100644 index 0000000..bb5c65b --- /dev/null +++ b/distil_ui/api/distil_v2.py @@ -0,0 +1,293 @@ +# Copyright (c) 2014 Catalyst IT Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections +import datetime +import logging +import six + +from django.conf import settings + +from openstack_dashboard.api import base + +LOG = logging.getLogger(__name__) + + +COMPUTE_CATEGORY = "Compute" +NETWORK_CATEGORY = "Network" +BLOCKSTORAGE_CATEGORY = "Block Storage" +OBJECTSTORAGE_CATEGORY = "Object Storage" +DISCOUNTS_CATEGORY = "Discounts" + + +def distilclient(request, region_id=None): + try: + from distilclient import client + auth_url = base.url_for(request, service_type='identity') + distil_url = base.url_for(request, service_type='ratingv2', + region=region_id) + insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) + cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None) + version = getattr(settings, 'DISTIL_VERSION', '2') + distil = client.Client(distil_url=distil_url, + input_auth_token=request.user.token.id, + tenant_id=request.user.tenant_id, + auth_url=auth_url, + region_name=request.user.services_region, + insecure=insecure, + os_cacert=cacert, + version=version) + distil.request = request + except Exception as e: + LOG.error(e) + return + return distil + + +def _calculate_start_date(today): + last_year = today.year - 1 if today.month < 12 else today.year + month = ((today.month + 1) % 12 if today.month + 1 > 12 + else today.month + 1) + return datetime.datetime(last_year, month, 1) + + +def _calculate_end_date(start): + year = start.year + 1 if start.month + 1 > 12 else start.year + month = (start.month + 1) % 12 or 12 + return datetime.datetime(year, month, 1) + + +def _wash_details(current_details): + """Apply the discount for current month quotation and merge object storage + + Unfortunately, we have to put it here, though here is not the right place. + Most of the code grab from internal billing script to keep the max + consistency. + :param current_details: The original cost details merged from all regions + :return cost details after applying discount and merging object storage + """ + end = datetime.datetime.utcnow() + start = datetime.datetime.strptime('%s-%s-01T00:00:00' % + (end.year, end.month), + '%Y-%m-%dT00:00:00') + + free_hours = int((end - start).total_seconds() / 3600) + + network_hours = collections.defaultdict(float) + router_hours = collections.defaultdict(float) + swift_usage = collections.defaultdict(list) + washed_details = [] + rate_router = 0 + rate_network = 0 + + for u in current_details["details"]: + # FIXME(flwang): 8 is the magic number here, we need a better way + # to get the region name. + region = u["product"].split(".")[0] + if u['product'].endswith('n1.network'): + network_hours[region] += u['quantity'] + rate_network = u['rate'] + + if u['product'].endswith('n1.router'): + router_hours[region] += u['quantity'] + rate_router = u['rate'] + + if u['product'].endswith('o1.standard'): + swift_usage[u['resource_id']].append(u) + else: + washed_details.append(u) + + free_network_hours_left = free_hours + for region, hours in six.iteritems(network_hours): + free_network_hours = (hours if hours <= free_network_hours_left + else free_network_hours_left) + if not free_network_hours: + break + line_name = 'Free Network Tier in %s' % region + washed_details.append({'product': region + '.n1.network', + 'resource_name': line_name, + 'quantity': free_network_hours, + 'resource_id': '', + 'unit': 'hour', 'rate': -rate_network, + 'cost': round(free_network_hours * + -rate_network, 2)}) + free_network_hours_left -= free_network_hours + + free_router_hours_left = free_hours + for region, hours in six.iteritems(router_hours): + free_router_hours = (hours if hours <= free_router_hours_left + else free_router_hours_left) + if not free_router_hours: + break + line_name = 'Free Router Tier in %s' % region + washed_details.append({'product': region + '.n1.router', + 'resource_name': line_name, + 'quantity': free_router_hours, + 'resource_id': '', + 'unit': 'hour', 'rate': -rate_router, + 'cost': round(free_router_hours * + -rate_router, 2)}) + free_router_hours_left -= free_router_hours + + region_count = 0 + for container, container_usage in swift_usage.items(): + region_count = len(container_usage) + if (len(container_usage) > 0 and + container_usage[0]['product'].endswith('o1.standard')): + # NOTE(flwang): Find the biggest size + container_usage[0]['product'] = "NZ.o1.standard" + container_usage[0]['quantity'] = max([u['quantity'] + for u in container_usage]) + washed_details.append(container_usage[0]) + + current_details["details"] = washed_details + # NOTE(flwang): Currently, the breakdown will accumulate all the object + # storage cost, so we need to deduce the duplicated part. + object_cost = current_details["breakdown"].get(OBJECTSTORAGE_CATEGORY, 0) + dup_object_cost = (region_count - 1) * (object_cost / region_count) + current_details["total_cost"] = (current_details["total_cost"] - + dup_object_cost) + return current_details + + +def _parse_invoice(invoice): + LOG.debug("Start to get invoices.") + parsed = {"total_cost": 0, "breakdown": {}, "details": []} + parsed["total_cost"] += invoice["total_cost"] + breakdown = parsed["breakdown"] + details = parsed["details"] + for category, services in invoice['details'].items(): + if category != DISCOUNTS_CATEGORY: + breakdown[category] = services["total_cost"] + for product in services["breakdown"]: + for order_line in services["breakdown"][product]: + order_line["product"] = product + details.append(order_line) + LOG.debug("Got quotations successfully.") + return parsed + + +def _parse_quotation(quotation, merged_quotations, region=None): + parsed = merged_quotations + parsed["total_cost"] += quotation["total_cost"] + breakdown = parsed["breakdown"] + details = parsed["details"] + for category, services in quotation['details'].items(): + if category in breakdown: + breakdown[category] += services["total_cost"] + else: + breakdown[category] = services["total_cost"] + for product in services["breakdown"]: + for order_line in services["breakdown"][product]: + order_line["product"] = product + details.append(order_line) + + return parsed + + +def _get_quotations(request): + LOG.debug("Start to get quotations from all regions.") + today_date = datetime.date.today().strftime("%Y-%m-%d") + regions = request.user.available_services_regions + + merged_quotations = {"total_cost": 0, "breakdown": {}, "details": [], + "date": today_date, "status": None} + + for region in regions: + region_client = distilclient(request, region_id=region) + resp = region_client.quotations.list(detailed=True) + quotation = resp['quotations'][today_date] + merged_quotations = _parse_quotation(quotation, merged_quotations, + region) + + merged_quotations = _wash_details(merged_quotations) + LOG.debug("Got quotations from all regions successfully.") + return merged_quotations + + +def get_cost(request, distil_client=None): + """Get cost for the 1atest 12 months include current month + + This function will return the latest 12 months cost and the breakdown + details for the each month. + :param request: Horizon request object + :param distil_client: Client object of Distilclient + :return list of cost for last 12 months + """ + # 1. Process invoices + today = datetime.date.today() + start = _calculate_start_date(datetime.date.today()) + # NOTE(flwang): It's OK to get invoice using the 1st day of curent month + # as the "end" date. + end = datetime.datetime(today.year, today.month, 1) + + cost = [{"date": None, "total_cost": 0, "paid": False, "breakdown": {}, + "details": {}}] + + temp_end = end + for i in range(11): + last_day = temp_end - datetime.timedelta(seconds=1) + temp_end = datetime.datetime(last_day.year, last_day.month, 1) + cost.insert(0, {"date": last_day.strftime("%Y-%m-%d"), "total_cost": 0, + "paid": False, "breakdown": {}, "details": {}}) + if temp_end < start: + break + + distil_client = distil_client or distilclient(request) + if not distil_client: + return cost + # FIXME(flwang): Get the last 11 invoices. If "today" is the early of month + # then it's possible that the invoice hasn't been created. And there is no + # way to see it based on current design of Distil API. + invoices = distil_client.invoices.list(start, end, + detailed=True)['invoices'] + + ordered_invoices = collections.OrderedDict(sorted(invoices.items(), + key=lambda t: t[0])) + # NOTE(flwang): The length of invoices dict could be less than 11 based on + # above comments. + for i in range(len(cost)): + month_cost = ordered_invoices.get(cost[i]['date']) + if not month_cost: + continue + cost[i]["total_cost"] = month_cost["total_cost"] + cost[i]["status"] = month_cost.get("status", None) + parsed = _parse_invoice(month_cost) + cost[i]["breakdown"] = parsed["breakdown"] + cost[i]["details"] = parsed["details"] + + # 2. Process quotations from all regions + # NOTE(flwang): The quotations from all regions is always the last one of + # the cost list. + cost[-1] = _get_quotations(request) + + return cost + + +def get_credits(request, distil_client=None): + """Get balance of customer's credit + + For now, it only supports credits like trail, development grant or + education grant. In the future, we will add supports for term discount if + it applys. + :param request: Horizon request object + :param distil_client: Client object of Distilclient + :return dict of credits + """ + distil_client = distil_client or distilclient(request) + + if not distil_client: + return {} + + return distil_client.credits.list() diff --git a/distil_ui/content/billing/base.py b/distil_ui/content/billing/base.py deleted file mode 100644 index 0ff201b..0000000 --- a/distil_ui/content/billing/base.py +++ /dev/null @@ -1,104 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from __future__ import division - -import datetime - -from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ - -from horizon import forms -from horizon import messages - - -class BaseBilling(object): - - def __init__(self, request, project_id=None): - self.project_id = project_id or request.user.tenant_id - self.request = request - self.billing_list = [] - - @property - def today(self): - return timezone.now() - - @staticmethod - def get_start(year, month, day): - start = datetime.datetime(year, month, day, 0, 0, 0) - return timezone.make_aware(start, timezone.utc) - - @staticmethod - def get_end(year, month, day): - end = datetime.datetime(year, month, day, 23, 59, 59) - return timezone.make_aware(end, timezone.utc) - - def get_date_range(self): - if not hasattr(self, "start") or not hasattr(self, "end"): - args_start = (self.today.year, self.today.month, 1) - args_end = (self.today.year, self.today.month, self.today.day) - form = self.get_form() - if form.is_valid(): - start = form.cleaned_data['start'] - end = form.cleaned_data['end'] - args_start = (start.year, - start.month, - start.day) - args_end = (end.year, - end.month, - end.day) - elif form.is_bound: - messages.error(self.request, - _("Invalid date format: " - "Using today as default.")) - self.start = self.get_start(*args_start) - self.end = self.get_end(*args_end) - return self.start, self.end - - def init_form(self): - today = datetime.date.today() - self.start = datetime.date(day=1, month=today.month, year=today.year) - self.end = today - - return self.start, self.end - - def get_form(self): - if not hasattr(self, 'form'): - req = self.request - start = req.GET.get('start', req.session.get('billing_start')) - end = req.GET.get('end', req.session.get('billing_end')) - if start and end: - # bound form - self.form = forms.DateForm({'start': start, 'end': end}) - else: - # non-bound form - init = self.init_form() - start = init[0].isoformat() - end = init[1].isoformat() - self.form = forms.DateForm(initial={'start': start, - 'end': end}) - req.session['billing_start'] = start - req.session['billing_end'] = end - return self.form - - def get_billing_list(self, start, end): - return [] - - def csv_link(self): - form = self.get_form() - data = {} - if hasattr(form, "cleaned_data"): - data = form.cleaned_data - if not ('start' in data and 'end' in data): - data = {"start": self.today.date(), "end": self.today.date()} - return "?start=%s&end=%s&format=csv" % (data['start'], - data['end']) diff --git a/distil_ui/content/billing/templates/billing/index.html b/distil_ui/content/billing/templates/billing/index.html index 8dea152..591aab5 100644 --- a/distil_ui/content/billing/templates/billing/index.html +++ b/distil_ui/content/billing/templates/billing/index.html @@ -18,103 +18,116 @@ {% block main %} -
Disclaimer: This is an estimate for your usage within the current region, not your final invoice. It includes the free router and network discount. All costs are in New Zealand dollars and are exclusive of GST.
+Disclaimer: This is an estimate for your usage cross ALL regions, not your final invoice. It includes the free router and network discount. All costs are in New Zealand dollars and are exclusive of GST.
Product Name | +Resource Name/ID | +Quantity | +Unit | +Rate | +Cost | +
---|---|---|---|---|---|
Product Name | +Resource Name/ID | +Quantity | +Unit | +Rate | +Cost | +
- Usage Details- |
- |||||
---|---|---|---|---|---|
Resource ID |
- Resource Name |
- Volume |
- Unit |
- Rate |
- Cost |
-