distil-ui/distil_ui/api/distil_v2.py

296 lines
12 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 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 = (0 if region_count == 0 else
(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()