diff --git a/distil/erp/drivers/odoo.py b/distil/erp/drivers/odoo.py index 54cbb77..aefc3cd 100644 --- a/distil/erp/drivers/odoo.py +++ b/distil/erp/drivers/odoo.py @@ -14,7 +14,9 @@ # limitations under the License. import collections +import copy from decimal import Decimal +import itertools import json import re @@ -372,6 +374,19 @@ class OdooDriver(driver.BaseDriver): 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. @@ -401,7 +416,12 @@ class OdooDriver(driver.BaseDriver): cost_details = {} odoo_region = self.region_mapping.get(region, region).upper() - resources = {row.id: json.loads(row.info) for row in resources} + + 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 @@ -411,15 +431,25 @@ class OdooDriver(driver.BaseDriver): products = self.get_products()[region] service_mapping = self._get_service_mapping(products) + # Find windows VM usage entries + windows_vm_entries = [] for entry in measurements: - service_name = entry.get('service') - volume = entry.get('volume') - unit = entry.get('unit') - res_id = entry.get('resource_id') + (service_name, service_type, _, _, resource, + resource_type) = self._get_entry_info(entry, resources_info, + service_mapping) - # resource_type is the type defined in meter_mappings.yml. - resource_type = resources[res_id]['type'] - service_type = service_mapping.get(service_name, resource_type) + if (service_type == COMPUTE_CATEGORY + and resource_type == 'Virtual Machine' + and resource.get('os_distro') == 'windows'): + new_entry = copy.deepcopy(entry) + setattr(new_entry, 'service', '%s-windows' % service_name) + windows_vm_entries.append(new_entry) + + for entry in itertools.chain(measurements, windows_vm_entries): + (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] = { @@ -436,10 +466,10 @@ class OdooDriver(driver.BaseDriver): 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) + 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 @@ -455,7 +485,7 @@ class OdooDriver(driver.BaseDriver): odoo_service_name ].append( { - "resource_name": resources[res_id].get('name', ''), + "resource_name": resource.get('name', ''), "resource_id": res_id, "cost": cost, "quantity": round(volume, 3), @@ -465,8 +495,10 @@ class OdooDriver(driver.BaseDriver): } ) - result = {'total_cost': round(float(total_cost), - constants.PRICE_DIGITS)} + result = { + 'total_cost': round(float(total_cost), constants.PRICE_DIGITS) + } + if detailed: result.update({'details': cost_details}) diff --git a/distil/tests/unit/erp/drivers/test_odoo.py b/distil/tests/unit/erp/drivers/test_odoo.py index b20448e..992c78a 100644 --- a/distil/tests/unit/erp/drivers/test_odoo.py +++ b/distil/tests/unit/erp/drivers/test_odoo.py @@ -399,6 +399,116 @@ class TestOdooDriver(base.DistilTestCase): quotations ) + @mock.patch('odoorpc.ODOO') + @mock.patch('distil.erp.drivers.odoo.OdooDriver.get_products') + def test_get_quotations_with_details_windows_vm(self, mock_get_products, + mock_odoo): + mock_get_products.return_value = { + 'nz_1': { + 'Compute': [ + { + 'name': 'c1.c2r16', 'description': 'c1.c2r16', + 'rate': 0.01, 'unit': 'hour' + }, + { + 'name': 'c1.c2r16-windows', + 'description': 'c1.c2r16-windows', + 'rate': 0.02, '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", ' + '"os_distro": "windows"}' + ) + ] + + class Usage(object): + def __init__(self, service, resource_id, volume, unit): + self.service = service + self.resource_id = resource_id + self.volume = volume + self.unit = unit + + def get(self, attr): + return getattr(self, attr) + + usage = [ + Usage('b1.standard', 1, 1024 * 1024 * 1024, 'byte'), + Usage('c1.c2r16', 2, 3600, 'second') + ] + + odoodriver = odoo.OdooDriver(self.conf) + quotations = odoodriver.get_quotations( + 'nz_1', 'fake_id', measurements=usage, resources=resources, + detailed=True + ) + + self.assertDictEqual( + { + 'total_cost': 0.05, + 'details': { + 'Compute': { + 'total_cost': 0.03, + 'breakdown': { + 'NZ-1.c1.c2r16': [ + { + "resource_name": "instance2", + "resource_id": 2, + "cost": 0.01, + "quantity": 1.0, + "rate": 0.01, + "unit": "hour", + } + ], + 'NZ-1.c1.c2r16-windows': [ + { + "resource_name": "instance2", + "resource_id": 2, + "cost": 0.02, + "quantity": 1.0, + "rate": 0.02, + "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 + ) + @mock.patch('odoorpc.ODOO') def test_get_credits(self, mock_odoo): fake_credits = [{'create_uid': [182, 'OpenStack Testing'],