
Some specific Catalyst Cloud stuff are here and there, they will be cleaned up later after we migrated to Distil v2. Change-Id: I81df0765cfc876faaa58a43469d87825d30c167e
317 lines
13 KiB
Python
317 lines
13 KiB
Python
# 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
|