Merge "Support JSON file as an ERP driver"
This commit is contained in:
commit
aafa2c0038
@ -115,6 +115,12 @@ ODOO_OPTS = [
|
|||||||
'c1.c1r2-windows or c1.c2r4-sql-server-standard-windows')
|
'c1.c1r2-windows or c1.c2r4-sql-server-standard-windows')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
JSONFILE_OPTS = [
|
||||||
|
cfg.StrOpt('products_file_path',
|
||||||
|
default='/etc/distil/products.json',
|
||||||
|
help='Json file to contain the products and prices.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
CLI_OPTS = [
|
CLI_OPTS = [
|
||||||
cfg.StrOpt(
|
cfg.StrOpt(
|
||||||
@ -128,10 +134,12 @@ CLI_OPTS = [
|
|||||||
AUTH_GROUP = 'keystone_authtoken'
|
AUTH_GROUP = 'keystone_authtoken'
|
||||||
ODOO_GROUP = 'odoo'
|
ODOO_GROUP = 'odoo'
|
||||||
COLLECTOR_GROUP = 'collector'
|
COLLECTOR_GROUP = 'collector'
|
||||||
|
JSONFILE_GROUP = 'jsonfile'
|
||||||
|
|
||||||
|
|
||||||
CONF.register_opts(DEFAULT_OPTIONS)
|
CONF.register_opts(DEFAULT_OPTIONS)
|
||||||
CONF.register_opts(ODOO_OPTS, group=ODOO_GROUP)
|
CONF.register_opts(ODOO_OPTS, group=ODOO_GROUP)
|
||||||
|
CONF.register_opts(JSONFILE_OPTS, group=JSONFILE_GROUP)
|
||||||
CONF.register_opts(COLLECTOR_OPTS, group=COLLECTOR_GROUP)
|
CONF.register_opts(COLLECTOR_OPTS, group=COLLECTOR_GROUP)
|
||||||
CONF.register_cli_opts(CLI_OPTS)
|
CONF.register_cli_opts(CLI_OPTS)
|
||||||
|
|
||||||
|
323
distil/erp/drivers/jsonfile.py
Normal file
323
distil/erp/drivers/jsonfile.py
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
# Copyright 2019 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 copy
|
||||||
|
from datetime import datetime
|
||||||
|
from datetime import timedelta
|
||||||
|
import decimal
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from distil.common import cache
|
||||||
|
from distil.common import constants
|
||||||
|
from distil import exceptions
|
||||||
|
from distil.common import general
|
||||||
|
from distil.db import api as db_api
|
||||||
|
from distil.erp import driver
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class JsonFileDriver(driver.BaseDriver):
|
||||||
|
"""Json driver
|
||||||
|
"""
|
||||||
|
conf = None
|
||||||
|
|
||||||
|
def __init__(self, conf):
|
||||||
|
self.conf = conf
|
||||||
|
|
||||||
|
def _load_products(self):
|
||||||
|
try:
|
||||||
|
with open(self.conf['jsonfile']['products_file_path']) as fh:
|
||||||
|
products = json.load(fh)
|
||||||
|
return products
|
||||||
|
except Exception as e:
|
||||||
|
LOG.critical('Failed to load rates json file: `%s`' % e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def is_healthy(self):
|
||||||
|
"""Check if the ERP back end is healthy or not
|
||||||
|
|
||||||
|
:returns True if the ERP is healthy, otherwise False
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
p = self._load_products()
|
||||||
|
return p is not None
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_products(self, regions=[]):
|
||||||
|
"""List products based on given regions
|
||||||
|
|
||||||
|
:param regions: List of regions to get projects
|
||||||
|
:returns Dict of products based on the given regions
|
||||||
|
"""
|
||||||
|
# TODO(flwang): Apply regions
|
||||||
|
products = self._load_products()
|
||||||
|
if regions:
|
||||||
|
region_products = {}
|
||||||
|
for r in regions:
|
||||||
|
if r in products:
|
||||||
|
region_products[r] = products[r]
|
||||||
|
return region_products
|
||||||
|
return products
|
||||||
|
|
||||||
|
def create_product(self, product):
|
||||||
|
"""Create product in ERP backend.
|
||||||
|
|
||||||
|
:param product: info used to create product
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_credits(self, project_id, expiry_date):
|
||||||
|
"""Get project credits
|
||||||
|
|
||||||
|
:param project_id: Project ID
|
||||||
|
:param expiry_date: Any credit which expires after this date can be
|
||||||
|
listed
|
||||||
|
:returns list of credits current project can get
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def create_credit(self, project, credit):
|
||||||
|
"""Create credit for a given project
|
||||||
|
|
||||||
|
:param project: project
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _get_invoice_time_ranges(self, start, end):
|
||||||
|
previous_months = []
|
||||||
|
|
||||||
|
indicator = end
|
||||||
|
while start <= indicator - timedelta(seconds=1):
|
||||||
|
end_inv = indicator.replace(
|
||||||
|
day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
|
||||||
|
last_month = indicator.month - 1
|
||||||
|
if last_month:
|
||||||
|
indicator = indicator.replace(
|
||||||
|
month=last_month, day=1, hour=0, minute=0, second=0,
|
||||||
|
microsecond=0)
|
||||||
|
else:
|
||||||
|
last_year = indicator.year - 1
|
||||||
|
indicator = indicator.replace(
|
||||||
|
year=last_year, month=12, day=1, hour=0, minute=0,
|
||||||
|
second=0, microsecond=0)
|
||||||
|
|
||||||
|
bill_date = end_inv - timedelta(seconds=1)
|
||||||
|
|
||||||
|
previous_months.append((
|
||||||
|
indicator,
|
||||||
|
end_inv,
|
||||||
|
bill_date.strftime('%Y-%m-%d')
|
||||||
|
))
|
||||||
|
|
||||||
|
return previous_months
|
||||||
|
|
||||||
|
def get_invoices(self, start, end, project_id, detailed=False):
|
||||||
|
"""Get history invoices from ERP service given a time range.
|
||||||
|
|
||||||
|
This will only get the history for the current given region.
|
||||||
|
|
||||||
|
:param start: Start time, a datetime object.
|
||||||
|
:param end: End time, a datetime object.
|
||||||
|
:param project_id: project ID.
|
||||||
|
:param detailed: If get detailed information or not.
|
||||||
|
:return: The history invoices information for each month.
|
||||||
|
"""
|
||||||
|
region_name = CONF.keystone_authtoken.region_name
|
||||||
|
|
||||||
|
previous_months = self._get_invoice_time_ranges(start, end)
|
||||||
|
|
||||||
|
invoices = {}
|
||||||
|
for start_inv, end_inv, bill_date_str in previous_months:
|
||||||
|
|
||||||
|
usage = db_api.usage_get(project_id, start_inv, end_inv)
|
||||||
|
all_resource_ids = set(
|
||||||
|
[entry.get('resource_id') for entry in usage])
|
||||||
|
res_list = db_api.resource_get_by_ids(project_id, all_resource_ids)
|
||||||
|
quotation = self.get_quotations(
|
||||||
|
region_name,
|
||||||
|
project_id,
|
||||||
|
measurements=usage,
|
||||||
|
resources=res_list,
|
||||||
|
detailed=detailed
|
||||||
|
)
|
||||||
|
quotation['status'] = 'paid'
|
||||||
|
invoices[bill_date_str] = quotation
|
||||||
|
return invoices
|
||||||
|
|
||||||
|
@cache.memoize
|
||||||
|
def _get_service_mapping(self, products):
|
||||||
|
"""Gets mapping from service name to service type.
|
||||||
|
|
||||||
|
:param products: Product dict in a region returned from odoo.
|
||||||
|
"""
|
||||||
|
srv_mapping = {}
|
||||||
|
|
||||||
|
for category, p_list in products.items():
|
||||||
|
for p in p_list:
|
||||||
|
srv_mapping[p['name']] = category.title()
|
||||||
|
|
||||||
|
return srv_mapping
|
||||||
|
|
||||||
|
@cache.memoize
|
||||||
|
def _get_service_price(self, service_name, service_type, products):
|
||||||
|
"""Get service price information from price definitions."""
|
||||||
|
price = {'service_name': service_name}
|
||||||
|
|
||||||
|
if service_type in products:
|
||||||
|
for s in products[service_type]:
|
||||||
|
if s['name'] == service_name:
|
||||||
|
price.update({'rate': s['rate'], 'unit': s['unit']})
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
found = False
|
||||||
|
for category, services in products.items():
|
||||||
|
for s in services:
|
||||||
|
if s['name'] == service_name:
|
||||||
|
price.update({'rate': s['rate'], 'unit': s['unit']})
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
raise exceptions.NotFoundException(
|
||||||
|
'Price not found, service name: %s, service type: %s' %
|
||||||
|
(service_name, service_type)
|
||||||
|
)
|
||||||
|
|
||||||
|
return price
|
||||||
|
|
||||||
|
def _get_entry_info(self, entry, resources_info, service_mapping):
|
||||||
|
service_name = entry.get('service')
|
||||||
|
volume = entry.get('volume')
|
||||||
|
unit = entry.get('unit')
|
||||||
|
res_id = entry.get('resource_id')
|
||||||
|
resource = resources_info.get(res_id, {})
|
||||||
|
# resource_type is the type defined in meter_mappings.yml.
|
||||||
|
resource_type = resource.get('type')
|
||||||
|
service_type = service_mapping.get(service_name, resource_type)
|
||||||
|
|
||||||
|
return (service_name, service_type, volume, unit, resource,
|
||||||
|
resource_type)
|
||||||
|
|
||||||
|
def get_quotations(self, region, project_id, measurements=[], resources=[],
|
||||||
|
detailed=False):
|
||||||
|
"""Get current month quotation.
|
||||||
|
|
||||||
|
Return value is in the following format:
|
||||||
|
{
|
||||||
|
'<current_date>': {
|
||||||
|
'total_cost': 100,
|
||||||
|
'details': {
|
||||||
|
'Compute': {
|
||||||
|
'total_cost': xxx,
|
||||||
|
'breakdown': {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:param region: Region name.
|
||||||
|
:param project_id: Project ID.
|
||||||
|
:param measurements: Current month usage collection.
|
||||||
|
:param resources: List of resources.
|
||||||
|
:param detailed: If get detailed information or not.
|
||||||
|
:return: Current month quotation.
|
||||||
|
"""
|
||||||
|
total_cost = 0
|
||||||
|
price_mapping = {}
|
||||||
|
cost_details = {}
|
||||||
|
|
||||||
|
resources_info = {}
|
||||||
|
for row in resources:
|
||||||
|
info = json.loads(row.info)
|
||||||
|
info.update({'id': row.id})
|
||||||
|
resources_info[row.id] = info
|
||||||
|
|
||||||
|
# NOTE(flwang): For most of the cases of Distil API, the request comes
|
||||||
|
# from billing panel. Billing panel sends 1 API call for /invoices and
|
||||||
|
# several API calls for /quotations against different regions. So it's
|
||||||
|
# not efficient to specify the region for get_products method because
|
||||||
|
# it won't help cache the products based on the parameters.
|
||||||
|
products = self.get_products()[region]
|
||||||
|
service_mapping = self._get_service_mapping(products)
|
||||||
|
|
||||||
|
for entry in measurements:
|
||||||
|
(service_name, service_type, volume, unit, resource,
|
||||||
|
resource_type) = self._get_entry_info(entry, resources_info,
|
||||||
|
service_mapping)
|
||||||
|
res_id = resource['id']
|
||||||
|
|
||||||
|
if service_type not in cost_details:
|
||||||
|
cost_details[service_type] = {
|
||||||
|
'total_cost': 0,
|
||||||
|
'breakdown': collections.defaultdict(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
if service_name not in price_mapping:
|
||||||
|
price_spec = self._get_service_price(
|
||||||
|
service_name, service_type, products
|
||||||
|
)
|
||||||
|
price_mapping[service_name] = price_spec
|
||||||
|
|
||||||
|
price_spec = price_mapping[service_name]
|
||||||
|
|
||||||
|
# Convert volume according to unit in price definition.
|
||||||
|
volume = float(
|
||||||
|
general.convert_to(volume, unit, price_spec['unit'])
|
||||||
|
)
|
||||||
|
cost = (round(volume * price_spec['rate'], constants.PRICE_DIGITS)
|
||||||
|
if price_spec['rate'] else 0)
|
||||||
|
|
||||||
|
total_cost += cost
|
||||||
|
|
||||||
|
if detailed:
|
||||||
|
erp_service_name = "%s.%s" % (region, service_name)
|
||||||
|
|
||||||
|
cost_details[service_type]['total_cost'] = round(
|
||||||
|
(cost_details[service_type]['total_cost'] + cost),
|
||||||
|
constants.PRICE_DIGITS
|
||||||
|
)
|
||||||
|
cost_details[service_type]['breakdown'][
|
||||||
|
erp_service_name
|
||||||
|
].append(
|
||||||
|
{
|
||||||
|
"resource_name": resource.get('name', ''),
|
||||||
|
"resource_id": res_id,
|
||||||
|
"cost": cost,
|
||||||
|
"quantity": round(volume, 3),
|
||||||
|
"rate": round(price_spec['rate'],
|
||||||
|
constants.RATE_DIGITS),
|
||||||
|
"unit": price_spec['unit'],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'total_cost': round(float(total_cost), constants.PRICE_DIGITS)
|
||||||
|
}
|
||||||
|
|
||||||
|
if detailed:
|
||||||
|
result.update({'details': cost_details})
|
||||||
|
|
||||||
|
return result
|
@ -52,7 +52,6 @@ def get_quotations(project_id, detailed=False):
|
|||||||
usage = db_api.usage_get(project_id, start, end)
|
usage = db_api.usage_get(project_id, start, end)
|
||||||
all_resource_ids = set([entry.get('resource_id') for entry in usage])
|
all_resource_ids = set([entry.get('resource_id') for entry in usage])
|
||||||
res_list = db_api.resource_get_by_ids(project_id, all_resource_ids)
|
res_list = db_api.resource_get_by_ids(project_id, all_resource_ids)
|
||||||
|
|
||||||
erp_driver = erp_utils.load_erp_driver(CONF)
|
erp_driver = erp_utils.load_erp_driver(CONF)
|
||||||
quotations = erp_driver.get_quotations(
|
quotations = erp_driver.get_quotations(
|
||||||
region_name,
|
region_name,
|
||||||
|
316
distil/tests/unit/erp/drivers/test_jsonfile.py
Normal file
316
distil/tests/unit/erp/drivers/test_jsonfile.py
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
# Copyright (c) 2019 Catalyst Cloud 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.
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from distil.erp.drivers import jsonfile
|
||||||
|
from distil.tests.unit import base
|
||||||
|
|
||||||
|
REGION = namedtuple('Region', ['id'])
|
||||||
|
|
||||||
|
PRODUCTS = [
|
||||||
|
{
|
||||||
|
'id': 1,
|
||||||
|
'categ_id': [1, 'All products (.NET) / nz-1 / Compute'],
|
||||||
|
'name_template': 'nz-1.c1.c1r1',
|
||||||
|
'lst_price': 0.00015,
|
||||||
|
'default_code': 'hour',
|
||||||
|
'description': '1 CPU, 1GB RAM'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 2,
|
||||||
|
'categ_id': [2, 'All products (.NET) / nz-1 / Network'],
|
||||||
|
'name_template': 'nz-1.n1.router',
|
||||||
|
'lst_price': 0.00025,
|
||||||
|
'default_code': 'hour',
|
||||||
|
'description': 'Router'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 3,
|
||||||
|
'categ_id': [1, 'All products (.NET) / nz-1 / Block Storage'],
|
||||||
|
'name_template': 'nz-1.b1.volume',
|
||||||
|
'lst_price': 0.00035,
|
||||||
|
'default_code': 'hour',
|
||||||
|
'description': 'Block storage'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TestJsonFileDriver(base.DistilTestCase):
|
||||||
|
config_file = 'distil.conf'
|
||||||
|
|
||||||
|
def test_get_products(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@mock.patch('distil.erp.drivers.jsonfile.db_api')
|
||||||
|
@mock.patch('distil.erp.drivers.jsonfile.JsonFileDriver.get_quotations')
|
||||||
|
def test_get_invoices_without_details(self, mock_get_quotations,
|
||||||
|
mock_db_api):
|
||||||
|
start = datetime(2017, 4, 1)
|
||||||
|
end = datetime(2018, 3, 1)
|
||||||
|
fake_project = '123'
|
||||||
|
|
||||||
|
|
||||||
|
mock_get_quotations.return_value = {'total_cost': 20}
|
||||||
|
mock_db_api.usage_get.return_value = []
|
||||||
|
mock_db_api.resource_get_by_ids.return_value = []
|
||||||
|
|
||||||
|
jsondriver = jsonfile.JsonFileDriver(self.conf)
|
||||||
|
|
||||||
|
invoices = jsondriver.get_invoices(start, end, fake_project)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
'2017-04-30': {'status': 'paid', 'total_cost': 20},
|
||||||
|
'2017-05-31': {'status': 'paid', 'total_cost': 20},
|
||||||
|
'2017-06-30': {'status': 'paid', 'total_cost': 20},
|
||||||
|
'2017-07-31': {'status': 'paid', 'total_cost': 20},
|
||||||
|
'2017-08-31': {'status': 'paid', 'total_cost': 20},
|
||||||
|
'2017-09-30': {'status': 'paid', 'total_cost': 20},
|
||||||
|
'2017-10-31': {'status': 'paid', 'total_cost': 20},
|
||||||
|
'2017-11-30': {'status': 'paid', 'total_cost': 20},
|
||||||
|
'2017-12-31': {'status': 'paid', 'total_cost': 20},
|
||||||
|
'2018-01-31': {'status': 'paid', 'total_cost': 20},
|
||||||
|
'2018-02-28': {'status': 'paid', 'total_cost': 20}
|
||||||
|
},
|
||||||
|
invoices
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('distil.erp.drivers.jsonfile.db_api')
|
||||||
|
@mock.patch('distil.erp.drivers.jsonfile.JsonFileDriver.get_products')
|
||||||
|
@mock.patch('distil.erp.drivers.jsonfile.CONF')
|
||||||
|
def test_get_invoices_with_details(self, mock_conf, mock_get_products,
|
||||||
|
mock_db_api):
|
||||||
|
start = datetime(2018, 2, 1)
|
||||||
|
end = datetime(2018, 3, 1)
|
||||||
|
fake_project = '123'
|
||||||
|
|
||||||
|
mock_conf.keystone_authtoken.region_name = "nz-1"
|
||||||
|
|
||||||
|
mock_get_products.return_value = {
|
||||||
|
'nz-1': {
|
||||||
|
'Compute': [
|
||||||
|
{
|
||||||
|
'name': 'c1.c1r1', 'description': 'c1.c1r1',
|
||||||
|
'rate': 0.01, 'unit': 'hour'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# mock_get_quotations.return_value = {'total_cost': 20}
|
||||||
|
mock_db_api.usage_get.return_value = [
|
||||||
|
{'service': 'c1.c1r1', 'volume': 500, 'unit': 'hour',
|
||||||
|
'resource_id': '47aa'}
|
||||||
|
]
|
||||||
|
mock_db_api.resource_get_by_ids.return_value = [
|
||||||
|
mock.Mock(
|
||||||
|
id='47aa',
|
||||||
|
info='{"name": "test-1", "type": "Virtual Machine"}')
|
||||||
|
]
|
||||||
|
|
||||||
|
jsondriver = jsonfile.JsonFileDriver(self.conf)
|
||||||
|
|
||||||
|
invoices = jsondriver.get_invoices(
|
||||||
|
start, end, fake_project, detailed=True)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
'2018-02-28': {
|
||||||
|
'details': {
|
||||||
|
'Compute': {
|
||||||
|
'breakdown': {
|
||||||
|
'nz-1.c1.c1r1': [
|
||||||
|
{
|
||||||
|
'rate': 0.01,
|
||||||
|
'resource_id': '47aa',
|
||||||
|
'cost': 5.0,
|
||||||
|
'unit': 'hour',
|
||||||
|
'quantity': 500.0,
|
||||||
|
'resource_name': 'test-1'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'total_cost': 5.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'total_cost': 5.0,
|
||||||
|
'status': 'paid',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
invoices
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('distil.erp.drivers.jsonfile.JsonFileDriver.get_products')
|
||||||
|
def test_get_quotations_without_details(self, mock_get_products):
|
||||||
|
mock_get_products.return_value = {
|
||||||
|
'nz-1': {
|
||||||
|
'Compute': [
|
||||||
|
{
|
||||||
|
'name': 'c1.c2r16', 'description': 'c1.c2r16',
|
||||||
|
'rate': 0.01, 'unit': 'hour'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'Block Storage': [
|
||||||
|
{
|
||||||
|
'name': 'b1.standard', 'description': 'b1.standard',
|
||||||
|
'rate': 0.02, 'unit': 'gigabyte'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Resource(object):
|
||||||
|
def __init__(self, id, info):
|
||||||
|
self.id = id
|
||||||
|
self.info = info
|
||||||
|
|
||||||
|
resources = [
|
||||||
|
Resource(1, '{"name": "", "type": "Volume"}'),
|
||||||
|
Resource(2, '{"name": "", "type": "Virtual Machine"}')
|
||||||
|
]
|
||||||
|
|
||||||
|
usage = [
|
||||||
|
{
|
||||||
|
'service': 'b1.standard',
|
||||||
|
'resource_id': 1,
|
||||||
|
'volume': 1024 * 1024 * 1024,
|
||||||
|
'unit': 'byte',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'service': 'c1.c2r16',
|
||||||
|
'resource_id': 2,
|
||||||
|
'volume': 3600,
|
||||||
|
'unit': 'second',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
jf = jsonfile.JsonFileDriver(self.conf)
|
||||||
|
quotations = jf.get_quotations(
|
||||||
|
'nz-1', 'fake_id', measurements=usage, resources=resources
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
{'total_cost': 0.03},
|
||||||
|
quotations
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('distil.erp.drivers.jsonfile.JsonFileDriver.get_products')
|
||||||
|
def test_get_quotations_with_details(self, mock_get_products):
|
||||||
|
mock_get_products.return_value = {
|
||||||
|
'nz-1': {
|
||||||
|
'Compute': [
|
||||||
|
{
|
||||||
|
'name': 'c1.c2r16', 'description': 'c1.c2r16',
|
||||||
|
'rate': 0.01, 'unit': 'hour'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'Block Storage': [
|
||||||
|
{
|
||||||
|
'name': 'b1.standard', 'description': 'b1.standard',
|
||||||
|
'rate': 0.02, 'unit': 'gigabyte'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Resource(object):
|
||||||
|
def __init__(self, id, info):
|
||||||
|
self.id = id
|
||||||
|
self.info = info
|
||||||
|
|
||||||
|
resources = [
|
||||||
|
Resource(1, '{"name": "volume1", "type": "Volume"}'),
|
||||||
|
Resource(2, '{"name": "instance2", "type": "Virtual Machine"}')
|
||||||
|
]
|
||||||
|
|
||||||
|
usage = [
|
||||||
|
{
|
||||||
|
'service': 'b1.standard',
|
||||||
|
'resource_id': 1,
|
||||||
|
'volume': 1024 * 1024 * 1024,
|
||||||
|
'unit': 'byte',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'service': 'c1.c2r16',
|
||||||
|
'resource_id': 2,
|
||||||
|
'volume': 3600,
|
||||||
|
'unit': 'second',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
jf = jsonfile.JsonFileDriver(self.conf)
|
||||||
|
quotations = jf.get_quotations(
|
||||||
|
'nz-1', 'fake_id', measurements=usage, resources=resources,
|
||||||
|
detailed=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertDictEqual(
|
||||||
|
{
|
||||||
|
'total_cost': 0.03,
|
||||||
|
'details': {
|
||||||
|
'Compute': {
|
||||||
|
'total_cost': 0.01,
|
||||||
|
'breakdown': {
|
||||||
|
'nz-1.c1.c2r16': [
|
||||||
|
{
|
||||||
|
"resource_name": "instance2",
|
||||||
|
"resource_id": 2,
|
||||||
|
"cost": 0.01,
|
||||||
|
"quantity": 1.0,
|
||||||
|
"rate": 0.01,
|
||||||
|
"unit": "hour",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Block Storage': {
|
||||||
|
'total_cost': 0.02,
|
||||||
|
'breakdown': {
|
||||||
|
'nz-1.b1.standard': [
|
||||||
|
{
|
||||||
|
"resource_name": "volume1",
|
||||||
|
"resource_id": 1,
|
||||||
|
"cost": 0.02,
|
||||||
|
"quantity": 1.0,
|
||||||
|
"rate": 0.02,
|
||||||
|
"unit": "gigabyte",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
quotations
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_is_healthy(self):
|
||||||
|
def _fake_load_products():
|
||||||
|
return {'a': 1}
|
||||||
|
jf = jsonfile.JsonFileDriver(self.conf)
|
||||||
|
jf._load_products = _fake_load_products
|
||||||
|
self.assertTrue(jf.is_healthy())
|
||||||
|
|
||||||
|
def test_is_healthy_false(self):
|
||||||
|
def _fake_load_products():
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
jf = jsonfile.JsonFileDriver(self.conf)
|
||||||
|
jf._load_products = _fake_load_products
|
||||||
|
self.assertFalse(jf.is_healthy())
|
104
etc/products.json
Normal file
104
etc/products.json
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"example-region-1":{
|
||||||
|
"compute":[
|
||||||
|
{
|
||||||
|
"name":"c1.c1r05",
|
||||||
|
"unit":"hour",
|
||||||
|
"rate":"0.019",
|
||||||
|
"description":"1 vCPU, 0.5 GB RAM",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"c1.c1r1",
|
||||||
|
"unit":"hour",
|
||||||
|
"rate":"0.039",
|
||||||
|
"description":"1 vCPU, 1 GB RAM",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"c1.c1r2",
|
||||||
|
"unit":"hour",
|
||||||
|
"rate":"0.062",
|
||||||
|
"description":"1 vCPU, 2 GB RAM",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"c1.c1r4",
|
||||||
|
"unit":"hour",
|
||||||
|
"rate":"0.098",
|
||||||
|
"description":"1 vCPU, 4 GB RAM",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"block_storage":[
|
||||||
|
{
|
||||||
|
"name":"b1.standard",
|
||||||
|
"unit":"gigabyte",
|
||||||
|
"rate":"0.000416",
|
||||||
|
"description":"b1.standard"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"object_storage":[
|
||||||
|
{
|
||||||
|
"name":"o1.standard",
|
||||||
|
"unit":"gigabyte",
|
||||||
|
"rate":"0.000275",
|
||||||
|
"description":"Multi-region 3 replicas"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"network":[
|
||||||
|
{
|
||||||
|
"name":"n1.ipv4",
|
||||||
|
"unit":"hour",
|
||||||
|
"rate":"0.006",
|
||||||
|
"description":"Public IPv4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"n1.network",
|
||||||
|
"unit":"hour",
|
||||||
|
"rate":"0.016400",
|
||||||
|
"description":"Network",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"n1.router",
|
||||||
|
"unit":"hour",
|
||||||
|
"rate":"0.017",
|
||||||
|
"description":"Router",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"n1.lb",
|
||||||
|
"unit":"hour",
|
||||||
|
"rate":"0.034",
|
||||||
|
"description":"Load balancer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"n1.vpn",
|
||||||
|
"unit":"hour",
|
||||||
|
"rate":"0.017",
|
||||||
|
"description":"VPN"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data transfer":[
|
||||||
|
{
|
||||||
|
"name":"n1.local",
|
||||||
|
"unit":"gigabyte",
|
||||||
|
"rate":"0",
|
||||||
|
"description":"Between cloud services in the same region"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"n1.inter-region",
|
||||||
|
"unit":"gigabyte",
|
||||||
|
"rate":"0.12",
|
||||||
|
"description":"Between cloud services in different regions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"n1.national",
|
||||||
|
"unit":"gigabyte",
|
||||||
|
"rate":"0.12",
|
||||||
|
"description":"National traffic over the internet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"n1.international",
|
||||||
|
"unit":"gigabyte",
|
||||||
|
"rate":"0.3",
|
||||||
|
"description":"International traffic over the internet"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user