diff --git a/.coveragerc b/.coveragerc index eb665db..4099efa 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,6 +5,7 @@ omit = .tox/* distil/tests/* distil/api/web.py + distil/api/helpers.py distil/auth.py distil/database.py distil/helpers.py diff --git a/distil/db/api.py b/distil/db/api.py index c8d666d..4b57339 100644 --- a/distil/db/api.py +++ b/distil/db/api.py @@ -88,6 +88,7 @@ def usage_get(project_id, start_at, end_at): return IMPL.usage_get(project_id, start_at, end_at) +# NOTE(lingxian): This method is not used anywhere but for testing purpose. def usage_add(project_id, resource_id, samples, unit, start_at, end_at): """If a tenant exists does nothing, @@ -101,9 +102,9 @@ def usages_add(project_id, resources, usage_entries, last_collect): return IMPL.usages_add(project_id, resources, usage_entries, last_collect) -def resource_add(project_id, resource_id, resource_type, rawdata, metadata): - return IMPL.resource_add(project_id, resource_id, resource_type, - rawdata, metadata) +# NOTE(lingxian): This method is not used anywhere but for testing purpose. +def resource_add(project_id, resource_id, resource_info): + return IMPL.resource_add(project_id, resource_id, resource_info) def project_add(values, last_collect=None): diff --git a/distil/db/sqlalchemy/api.py b/distil/db/sqlalchemy/api.py index 6d00cbc..391687d 100644 --- a/distil/db/sqlalchemy/api.py +++ b/distil/db/sqlalchemy/api.py @@ -207,8 +207,10 @@ def usage_add(project_id, resource_id, samples, unit, volume=volume, unit=unit, resource_id=resource_id, - project_id=project_id, - start_at=start_at, end_at=end_at) + tenant_id=project_id, + start=start_at, + end=end_at, + created=datetime.utcnow()) resource_ref.save(session=session) except sa.exc.InvalidRequestError as e: # FIXME(flwang): I assume there should be a DBDuplicateEntry error @@ -271,11 +273,12 @@ def usages_add(project_id, resources, usage_entries, last_collect): ) -def resource_add(project_id, resource_id, resource_type, raw, metadata): +def resource_add(project_id, resource_id, resource_info): session = get_session() - metadata = _merge_resource_metadata({'type': resource_type}, raw, metadata) - resource_ref = Resource(id=resource_id, project_id=project_id, - resource_type=resource_type, meta_data=metadata) + resource_ref = Resource( + id=resource_id, tenant_id=project_id, info=json.dumps(resource_info), + created=datetime.utcnow() + ) try: resource_ref.save(session=session) @@ -299,28 +302,6 @@ def resource_get_by_ids(project_id, resource_ids): return query.all() -def _merge_resource_metadata(md_dict, entry, md_def): - """Strips metadata from the entry as defined in the config, - and merges it with the given metadata dict. - """ - for field, parameters in md_def.iteritems(): - for _, source in enumerate(parameters['sources']): - try: - value = entry['resource_metadata'][source] - if 'template' in parameters: - md_dict[field] = parameters['template'] % value - break - else: - md_dict[field] = value - break - except KeyError: - # Just means we haven't found the right value yet. - # Or value isn't present. - pass - - return md_dict - - def get_project_locks(project_id): session = get_session() diff --git a/distil/tests/unit/api/base.py b/distil/tests/unit/api/base.py new file mode 100644 index 0000000..f903780 --- /dev/null +++ b/distil/tests/unit/api/base.py @@ -0,0 +1,36 @@ +# Copyright (C) 2017 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 flask + +from distil.api import v2 as api_v2 +from distil.common import api +from distil.tests.unit import base + + +class APITest(base.DistilWithDbTestCase): + def setUp(self): + super(APITest, self).setUp() + + self.app = flask.Flask(__name__) + + @self.app.route('/', methods=['GET']) + def version_list(): + return api.render({ + "versions": [ + {"id": "v2", "status": "CURRENT"} + ]}) + + self.app.register_blueprint(api_v2.rest, url_prefix="/v2") + self.client = self.app.test_client() diff --git a/distil/tests/unit/api/test_api.py b/distil/tests/unit/api/test_api.py new file mode 100644 index 0000000..05d90d9 --- /dev/null +++ b/distil/tests/unit/api/test_api.py @@ -0,0 +1,226 @@ +# Copyright (C) 2017 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. +from datetime import date +from datetime import datetime +import json + +from flask import url_for +import mock +from oslo_policy import policy as cpolicy + +from distil.api import acl +from distil.common import constants +from distil.db import api as db_api +from distil.tests.unit.api import base + + +class TestAPI(base.APITest): + @classmethod + def setUpClass(cls): + acl.setup_policy() + + def _setup_policy(self, policy): + rules = cpolicy.Rules.from_dict(policy) + acl.ENFORCER.set_rules(rules, use_conf=False) + + self.addCleanup(acl.ENFORCER.clear) + + def test_get_versions(self): + ret = self.client.get('/') + + self.assertEqual( + {'versions': [{'id': 'v2', 'status': 'CURRENT'}]}, + json.loads(ret.data) + ) + + @mock.patch('distil.erp.drivers.odoo.OdooDriver.get_products') + @mock.patch('odoorpc.ODOO') + def test_products_get_without_regions(self, mock_odoo, + mock_odoo_get_products): + mock_odoo_get_products.return_value = [] + + ret = self.client.get('/v2/products') + + self.assertEqual({'products': []}, json.loads(ret.data)) + + @mock.patch('distil.erp.drivers.odoo.OdooDriver.get_products') + @mock.patch('odoorpc.ODOO') + @mock.patch('distil.common.openstack.get_regions') + def test_products_get_with_regions(self, mock_regions, mock_odoo, + mock_odoo_get_products): + class Region(object): + def __init__(self, id): + self.id = id + + mock_regions.return_value = [Region('nz_1'), Region('nz_2')] + mock_odoo_get_products.return_value = [] + + ret = self.client.get('/v2/products?regions=nz_1,nz_2') + + mock_odoo_get_products.assert_called_once_with(['nz_1', 'nz_2']) + self.assertEqual({'products': []}, json.loads(ret.data)) + + @mock.patch('distil.common.openstack.get_regions') + def test_products_get_with_invalid_regions(self, mock_regions): + class Region(object): + def __init__(self, id): + self.id = id + + mock_regions.return_value = [Region('nz_1'), Region('nz_2')] + + ret = self.client.get('/v2/products?regions=nz_1,nz_3') + + self.assertEqual(404, ret.status_code) + + def test_measurements_get(self): + default_project = 'tenant_1' + start = '2014-06-01T00:00:00' + end = '2014-07-01T00:00:00' + res_id = 'instance_1' + + db_api.project_add( + { + 'id': default_project, + 'name': 'default_project', + 'description': 'project for test' + } + ) + db_api.resource_add( + default_project, res_id, {'type': 'Virtual Machine'} + ) + db_api.usage_add( + default_project, res_id, {'instance': 100}, 'hour', + datetime.strptime(start, constants.iso_time), + datetime.strptime(end, constants.iso_time), + ) + + with self.app.test_request_context(): + url = url_for( + 'v2.measurements_get', + project_id=default_project, + start=start, + end=end + ) + + self._setup_policy({"rating:measurements:get": ""}) + ret = self.client.get(url, headers={'X-Tenant-Id': default_project}) + + self.assertEqual( + { + 'measurements': { + 'start': '2014-06-01 00:00:00', + 'end': '2014-07-01 00:00:00', + 'project_name': 'default_project', + 'project_id': default_project, + 'resources': { + res_id: { + 'type': 'Virtual Machine', + 'services': [{ + 'name': 'instance', + 'volume': '100.00', + 'unit': 'hour' + }] + } + } + } + }, + json.loads(ret.data) + ) + + @mock.patch('distil.erp.drivers.odoo.OdooDriver.get_invoices') + @mock.patch('odoorpc.ODOO') + def test_invoices_get(self, mock_odoo, mock_get_invoices): + default_project = 'tenant_1' + start = '2014-06-01T00:00:00' + end = '2014-07-01T00:00:00' + + db_api.project_add( + { + 'id': default_project, + 'name': 'default_project', + 'description': 'project for test' + } + ) + + mock_get_invoices.return_value = {} + + with self.app.test_request_context(): + url = url_for( + 'v2.invoices_get', + project_id=default_project, + start=start, + end=end + ) + + self._setup_policy({"rating:invoices:get": ""}) + ret = self.client.get(url, headers={'X-Tenant-Id': default_project}) + + self.assertEqual( + { + 'start': '2014-06-01 00:00:00', + 'end': '2014-07-01 00:00:00', + 'project_name': 'default_project', + 'project_id': default_project, + 'invoices': {} + }, + json.loads(ret.data) + ) + + @mock.patch('distil.erp.drivers.odoo.OdooDriver.get_quotations') + @mock.patch('odoorpc.ODOO') + def test_quotations_get(self, mock_odoo, mock_get_quotations): + self.override_config('keystone_authtoken', region_name='region-1') + + default_project = 'tenant_1' + res_id = 'instance_1' + today = date.today() + datetime_today = datetime(today.year, today.month, today.day) + + db_api.project_add( + { + 'id': default_project, + 'name': 'default_project', + 'description': 'project for test' + } + ) + db_api.resource_add( + default_project, res_id, {'type': 'Virtual Machine'} + ) + db_api.usage_add( + default_project, res_id, {'instance': 100}, 'hour', + datetime_today, + datetime_today, + ) + + mock_get_quotations.return_value = {} + + with self.app.test_request_context(): + url = url_for( + 'v2.quotations_get', + project_id=default_project, + ) + + self._setup_policy({"rating:quotations:get": ""}) + ret = self.client.get(url, headers={'X-Tenant-Id': default_project}) + + self.assertEqual( + { + 'start': str(datetime(today.year, today.month, 1)), + 'end': str(datetime_today), + 'project_name': 'default_project', + 'project_id': default_project, + 'quotations': {str(today): {}} + }, + json.loads(ret.data) + ) diff --git a/distil/tests/unit/base.py b/distil/tests/unit/base.py index fd1af08..240321a 100644 --- a/distil/tests/unit/base.py +++ b/distil/tests/unit/base.py @@ -25,8 +25,8 @@ from distil import context from distil import config from distil.db import api as db_api -class DistilTestCase(base.BaseTestCase): +class DistilTestCase(base.BaseTestCase): config_file = None def setUp(self): diff --git a/test-requirements.txt b/test-requirements.txt index 10069e2..5114b7c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,3 +13,4 @@ testscenarios>=0.4 testtools>=0.9.34 WebTest>=2.0 # MIT mock>=2.0 # BSD +Flask>=0.10,!=0.11,<1.0 # BSD