-
Notifications
You must be signed in to change notification settings - Fork 46
[WIP] Create metric and billing interface outline #173
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ addons: | |
- python-simplejson | ||
- python-serial | ||
- python-yaml | ||
- unixodbc-dev | ||
|
||
env: | ||
global: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg | ||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html | ||
:alt: License: AGPL-3 | ||
|
||
============== | ||
Clouder Metric | ||
============== | ||
|
||
This module provides models to ingest and store usage metrics from ELK about | ||
running instances. | ||
|
||
Installation | ||
============ | ||
|
||
* Install module as normal | ||
|
||
|
||
Bug Tracker | ||
=========== | ||
|
||
Bugs are tracked on `GitHub Issues | ||
<https://github.com/clouder-community/clouder>`_. | ||
|
||
Contributors | ||
------------ | ||
|
||
* Dave Lasley <dave@laslabs.com> | ||
* Ted Salmon <tsalmon@laslabs.com> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright 2016 LasLabs Inc. | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
from . import models |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# -*- coding: utf-8 -*- | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please make a symbolic link from openerp.py to manifest.py to ensure the module can be installed on Odoo 9 (see my last commit on master) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not so sure about this. What about the changed namespace ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well at the very least the tricks worked on my dev instance, and all other modules now have the symbolic link. Anyway I'll not block the PR for this, so I'm just waiting for your approval. |
||
# Copyright 2016 LasLabs Inc. | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
{ | ||
"name": "Clouder - Metrics", | ||
"summary": "Provides Usage Metric Interface for Clouder", | ||
"version": "10.0.1.0.0", | ||
"category": "Clouder", | ||
"website": "https://github.com/clouder-community/clouder", | ||
"author": "LasLabs", | ||
"license": "AGPL-3", | ||
"application": False, | ||
"installable": True, | ||
"depends": [ | ||
'base_external_dbsource', | ||
"clouder", | ||
"contract_variable_quantity", | ||
"sale", | ||
], | ||
"data": [ | ||
"security/ir.model.access.csv", | ||
], | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright 2016 LasLabs Inc. | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
from . import clouder_metric_type | ||
from . import clouder_metric_interface | ||
from . import clouder_metric_value |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright 2016 LasLabs Inc. | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
from odoo import api, fields, models | ||
|
||
|
||
class ClouderMetricInterface(models.Model): | ||
""" It provides a common interface for Clouder Usage metrics. | ||
|
||
This object receives all attributes from ``clouder.metric.type``. | ||
""" | ||
|
||
_name = 'clouder.metric.interface' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
_description = 'Clouder Metric Interfaces' | ||
_inherits = {'clouder.metric.type': 'type_id'} | ||
|
||
type_id = fields.Many2one( | ||
string='Metric Type', | ||
comodel_name='clouder.metric.type', | ||
required=True, | ||
ondelete='restrict', | ||
) | ||
metric_value_ids = fields.One2many( | ||
string='Metric Values', | ||
comodel_name='clouder.metric.value', | ||
inverse_name='interface_id', | ||
) | ||
metric_model = fields.Selection( | ||
related='type_id.metric_model', | ||
) | ||
metric_ref = fields.Integer( | ||
required=True, | ||
) | ||
source_id = fields.Many2one( | ||
string='Metric Source', | ||
comodel_name='base.external.dbsource', | ||
domain="[('connector', '=', type_id.connector_type)]", | ||
required=True, | ||
) | ||
cron_id = fields.Many2one( | ||
string='Scheduled Task', | ||
comodel_name='ir.cron', | ||
domain="[('model', '=', _name)]", | ||
context="""{ | ||
'default_model': _name, | ||
'default_name': '[Clouder Metric] %s' % display_name, | ||
}""", | ||
) | ||
interval_number = fields.Integer( | ||
related='cron_id.interval_number', | ||
) | ||
interval_type = fields.Selection( | ||
related='cron_id.interval_type', | ||
) | ||
query_code = fields.Text() | ||
|
||
@property | ||
@api.multi | ||
def metric_id(self): | ||
self.ensure_one() | ||
return self.env[self.metric_model].browse( | ||
self.metric_ref, | ||
) | ||
|
||
@api.multi | ||
def name_get(self): | ||
return [ | ||
(r.id, '%s - %s' % (r.type_id.name, r.metric_id.id)) for r in self | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright 2016 LasLabs Inc. | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
from odoo import _, api, fields, models | ||
from odoo.tools import safe_eval | ||
from odoo.exceptions import ValidationError, UserError | ||
|
||
|
||
class ClouderMetricType(models.Model): | ||
""" It provides context for usage metric types """ | ||
|
||
_name = 'clouder.metric.type' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
_description = 'Clouder Metric Types' | ||
|
||
name = fields.Char() | ||
code = fields.Char() | ||
metric_model = fields.Selection( | ||
selection=lambda s: s._get_metric_models(), | ||
required=True, | ||
help='Clouder entity type that this metric is related to.', | ||
) | ||
uom_id = fields.Many2one( | ||
string='Unit of Measure', | ||
comodel_name='product.uom', | ||
required=True, | ||
) | ||
connector_type = fields.Selection( | ||
selection=lambda s: s.env['base.external.dbsource'].CONNECTORS, | ||
) | ||
metric_code = fields.Text( | ||
required=True, | ||
default=lambda s: s._default_query_code(), | ||
help='Python code to use as query for metric.' | ||
) | ||
|
||
@api.model | ||
def _default_query_code(self): | ||
return _("# Python code. \n" | ||
"Use `value = my_value` to specify the final calculated " | ||
" metric value. This is required. \n" | ||
"Optionally use ``uom = product_uom_record`` to change the " | ||
"units that the metric is being measured in. \n" | ||
"You should also add `date_start` and `date_end`, which " | ||
"are `datetime` values to signify the date of occurrence of " | ||
"the metric value in question. \n" | ||
"# You can use the following variables: \n" | ||
"# - self: browse_record of the current ID Category \n" | ||
"# - interface: browse_record of the Metrics Interface. \n" | ||
"# - metric_model: Name of the metric model type. \n") | ||
|
||
@api.model | ||
def _get_metric_models(self): | ||
""" Returns a selection of available metric models | ||
Returns: | ||
list: Additional metric models | ||
""" | ||
return [ | ||
('clouder.base', 'Base'), | ||
('clouder.service', 'Service'), | ||
] | ||
|
||
@api.multi | ||
def _get_query_code_context(self, interface): | ||
""" Returns a query context for use | ||
Args: | ||
interface (clouder.metric.interface): The interface to use | ||
Returns: | ||
dict: Dict with the context for the given iface and model | ||
""" | ||
self.ensure_one() | ||
return { | ||
'interface': interface, | ||
'metric_model': self.metric_model, | ||
'self': self, | ||
} | ||
|
||
@api.model | ||
def save_metric_value(self, metric_interfaces): | ||
""" Saves a metric value from the given interface | ||
Args: | ||
metric_interfaces (clouder.metric.interface): The interface to use | ||
Returns: | ||
None | ||
""" | ||
for iface in metric_interfaces: | ||
eval_context = iface.type_id._get_query_code_context(iface) | ||
try: | ||
safe_eval( | ||
iface.query_code, | ||
eval_context, | ||
mode='exec', | ||
nocopy=True, | ||
) | ||
except Exception as e: | ||
raise UserError(_( | ||
'Error while evaluating metrics query:' | ||
'\n %s \n %s' % (iface.name, e), | ||
)) | ||
if eval_context.get('value') is None: | ||
raise ValidationError(_( | ||
'Metrics query did not set the `value` variable, which ' | ||
'is used to indicate the value that should be saved for ' | ||
'the query.', | ||
)) | ||
uom = eval_context.get('uom') or iface.uom_id | ||
iface.write({ | ||
'metric_value_ids': [(0, 0, { | ||
'value': eval_context['value'], | ||
'date_start': eval_context.get('date_start'), | ||
'date_end': eval_context.get('date_end'), | ||
'uom_id': uom.id, | ||
})], | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright 2016 LasLabs Inc. | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
from odoo import fields, models | ||
|
||
|
||
class ClouderMetricValue(models.Model): | ||
""" It provides a record of metric values used in billing. """ | ||
|
||
_name = 'clouder.metric.value' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
_description = 'Clouder Metric Values' | ||
|
||
interface_id = fields.Many2one( | ||
string='Interface', | ||
comodel_name='clouder.metric.interface', | ||
required=True, | ||
) | ||
value = fields.Float( | ||
required=True, | ||
) | ||
uom_id = fields.Many2one( | ||
string='Unit of Measure', | ||
comodel_name='product.uom', | ||
) | ||
date_start = fields.Datetime( | ||
string='Metric Start', | ||
) | ||
date_end = fields.Datetime( | ||
string='Metric End', | ||
) | ||
date_create = fields.Datetime( | ||
string='Creation Time', | ||
default=lambda s: fields.Datetime.now(), | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink | ||
access_clouder_metric_type,access_clouder_metric_type,model_clouder_metric_type,clouder.group_clouder_user,1,0,0,0 | ||
access_clouder_metric_interface,access_clouder_metric_interface,model_clouder_metric_interface,clouder.group_clouder_user,1,0,0,0 | ||
access_clouder_metric_value,access_clouder_metric_value,model_clouder_metric_value,clouder.group_clouder_user,1,0,0,0 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright 2017 LasLabs Inc. | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
from . import test_clouder_metric_interface | ||
from . import test_clouder_metric_type |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright 2017 LasLabs Inc. | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
from odoo.tests.common import TransactionCase | ||
|
||
|
||
class TestCommon(TransactionCase): | ||
|
||
def setUp(self): | ||
super(TestCommon, self).setUp() | ||
self.metric_type = self.env['clouder.metric.type'].create({ | ||
'name': 'Test Metric', | ||
'code': 'TEST', | ||
'metric_model': 'clouder.base', | ||
'uom_id': self.env.ref('product.uom_categ_wtime').id, | ||
}) | ||
self.metric_interface = self.env['clouder.metric.interface'].create({ | ||
'type_id': self.metric_type.id, | ||
'metric_ref': 7, | ||
'source_id': self.env.ref( | ||
'base_external_dbsource.demo_postgre').id, | ||
'query_code': 'print True', | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright 2017 LasLabs Inc. | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
from .common import TestCommon | ||
|
||
|
||
class TestClouderMetricInterface(TestCommon): | ||
|
||
def test_metric_id(self): | ||
""" It should test to see that at least one metric_id is returned """ | ||
self.assertTrue(len(self.metric_interface.metric_id) == 1) | ||
|
||
def test_name_get(self): | ||
""" It should return the right name """ | ||
exp = [ | ||
(self.metric_interface.id, 'Test Metric - 7') | ||
] | ||
self.assertEqual(exp, self.metric_interface.name_get()) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright 2017 LasLabs Inc. | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). | ||
|
||
from odoo.exceptions import UserError, ValidationError | ||
|
||
from .common import TestCommon | ||
|
||
|
||
class TestClouderMetricType(TestCommon): | ||
|
||
def test_get_metric_models(self): | ||
""" It should have the correct metric model types """ | ||
exp = [ | ||
('clouder.base', 'Base'), | ||
('clouder.service', 'Service'), | ||
] | ||
self.assertEquals(exp, self.metric_type._get_metric_models()) | ||
|
||
def test_save_metric_value_usererror(self): | ||
""" It should raise UserError when a bad query is supplied """ | ||
with self.assertRaises(UserError): | ||
self.metric_type.save_metric_value(self.metric_interface) | ||
|
||
def test_save_metric_value_validationerror(self): | ||
""" It should raise ValidationError when no value is supplied """ | ||
self.metric_interface.query_code = 'test = 0' | ||
with self.assertRaises(ValidationError): | ||
self.metric_type.save_metric_value(self.metric_interface) | ||
|
||
def test_save_metric_value(self): | ||
""" It should verify that the right metric values are saved """ | ||
self.metric_interface.query_code = 'value = 100' | ||
self.metric_type.save_metric_value(self.metric_interface) | ||
self.assertTrue( | ||
self.metric_interface.metric_value_ids.mapped('value') == [100.0] | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,4 @@ | ||
connector | ||
contract | ||
# Pending https://github.com/OCA/server-tools/pull/660 | ||
server-tools https://github.com/laslabs/server-tools feature/10.0/base_external_dbsource_elasticsearch-v10 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change all to LGPL