Skip to content
This repository was archived by the owner on Jan 24, 2018. It is now read-only.

Commit 59dd6ea

Browse files
lasleyYannickB
authored andcommitted
[WIP] Create metric and billing interface outline (#173)
* Create metric and billing interface outline * Playing around with data structures * [IMP] sale_clouder: Finish module * Fix docstrings * Implement unimplemented methods * Fix bugs found * Add test coverage * Add README [IMP] clouder_metric: Finish module * Fix/Add docstrings * Add test coverage * Fix logic * Add README * [IMP] clouder_metric: Fixes per PR * Update license to AGPL * Add model `_description` * Clean up lint issues * [IMP] sale_clouder: Fixes per PR * Update license to AGPL * Add model `_description` * Clean up lint issues * Improve usage metric aggregation logic
1 parent d723f44 commit 59dd6ea

27 files changed

+849
-0
lines changed

.travis.yml

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ addons:
2222
- python-simplejson
2323
- python-serial
2424
- python-yaml
25+
- unixodbc-dev
2526

2627
env:
2728
global:

clouder_metric/README.md

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
2+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
3+
:alt: License: AGPL-3
4+
5+
==============
6+
Clouder Metric
7+
==============
8+
9+
This module provides models to ingest and store usage metrics from ELK about
10+
running instances.
11+
12+
Installation
13+
============
14+
15+
* Install module as normal
16+
17+
18+
Bug Tracker
19+
===========
20+
21+
Bugs are tracked on `GitHub Issues
22+
<https://github.com/clouder-community/clouder>`_.
23+
24+
Contributors
25+
------------
26+
27+
* Dave Lasley <dave@laslabs.com>
28+
* Ted Salmon <tsalmon@laslabs.com>

clouder_metric/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2016 LasLabs Inc.
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
from . import models

clouder_metric/__manifest__.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2016 LasLabs Inc.
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
{
5+
"name": "Clouder - Metrics",
6+
"summary": "Provides Usage Metric Interface for Clouder",
7+
"version": "10.0.1.0.0",
8+
"category": "Clouder",
9+
"website": "https://github.com/clouder-community/clouder",
10+
"author": "LasLabs",
11+
"license": "AGPL-3",
12+
"application": False,
13+
"installable": True,
14+
"depends": [
15+
'base_external_dbsource',
16+
"clouder",
17+
"contract_variable_quantity",
18+
"sale",
19+
],
20+
"data": [
21+
"security/ir.model.access.csv",
22+
],
23+
}

clouder_metric/models/__init__.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2016 LasLabs Inc.
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
from . import clouder_metric_type
6+
from . import clouder_metric_interface
7+
from . import clouder_metric_value
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2016 LasLabs Inc.
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
from odoo import api, fields, models
6+
7+
8+
class ClouderMetricInterface(models.Model):
9+
""" It provides a common interface for Clouder Usage metrics.
10+
11+
This object receives all attributes from ``clouder.metric.type``.
12+
"""
13+
14+
_name = 'clouder.metric.interface'
15+
_description = 'Clouder Metric Interfaces'
16+
_inherits = {'clouder.metric.type': 'type_id'}
17+
18+
type_id = fields.Many2one(
19+
string='Metric Type',
20+
comodel_name='clouder.metric.type',
21+
required=True,
22+
ondelete='restrict',
23+
)
24+
metric_value_ids = fields.One2many(
25+
string='Metric Values',
26+
comodel_name='clouder.metric.value',
27+
inverse_name='interface_id',
28+
)
29+
metric_model = fields.Selection(
30+
related='type_id.metric_model',
31+
)
32+
metric_ref = fields.Integer(
33+
required=True,
34+
)
35+
source_id = fields.Many2one(
36+
string='Metric Source',
37+
comodel_name='base.external.dbsource',
38+
domain="[('connector', '=', type_id.connector_type)]",
39+
required=True,
40+
)
41+
cron_id = fields.Many2one(
42+
string='Scheduled Task',
43+
comodel_name='ir.cron',
44+
domain="[('model', '=', _name)]",
45+
context="""{
46+
'default_model': _name,
47+
'default_name': '[Clouder Metric] %s' % display_name,
48+
}""",
49+
)
50+
interval_number = fields.Integer(
51+
related='cron_id.interval_number',
52+
)
53+
interval_type = fields.Selection(
54+
related='cron_id.interval_type',
55+
)
56+
query_code = fields.Text()
57+
58+
@property
59+
@api.multi
60+
def metric_id(self):
61+
self.ensure_one()
62+
return self.env[self.metric_model].browse(
63+
self.metric_ref,
64+
)
65+
66+
@api.multi
67+
def name_get(self):
68+
return [
69+
(r.id, '%s - %s' % (r.type_id.name, r.metric_id.id)) for r in self
70+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2016 LasLabs Inc.
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
from odoo import _, api, fields, models
6+
from odoo.tools import safe_eval
7+
from odoo.exceptions import ValidationError, UserError
8+
9+
10+
class ClouderMetricType(models.Model):
11+
""" It provides context for usage metric types """
12+
13+
_name = 'clouder.metric.type'
14+
_description = 'Clouder Metric Types'
15+
16+
name = fields.Char()
17+
code = fields.Char()
18+
metric_model = fields.Selection(
19+
selection=lambda s: s._get_metric_models(),
20+
required=True,
21+
help='Clouder entity type that this metric is related to.',
22+
)
23+
uom_id = fields.Many2one(
24+
string='Unit of Measure',
25+
comodel_name='product.uom',
26+
required=True,
27+
)
28+
connector_type = fields.Selection(
29+
selection=lambda s: s.env['base.external.dbsource'].CONNECTORS,
30+
)
31+
metric_code = fields.Text(
32+
required=True,
33+
default=lambda s: s._default_query_code(),
34+
help='Python code to use as query for metric.'
35+
)
36+
37+
@api.model
38+
def _default_query_code(self):
39+
return _("# Python code. \n"
40+
"Use `value = my_value` to specify the final calculated "
41+
" metric value. This is required. \n"
42+
"Optionally use ``uom = product_uom_record`` to change the "
43+
"units that the metric is being measured in. \n"
44+
"You should also add `date_start` and `date_end`, which "
45+
"are `datetime` values to signify the date of occurrence of "
46+
"the metric value in question. \n"
47+
"# You can use the following variables: \n"
48+
"# - self: browse_record of the current ID Category \n"
49+
"# - interface: browse_record of the Metrics Interface. \n"
50+
"# - metric_model: Name of the metric model type. \n")
51+
52+
@api.model
53+
def _get_metric_models(self):
54+
""" Returns a selection of available metric models
55+
Returns:
56+
list: Additional metric models
57+
"""
58+
return [
59+
('clouder.base', 'Base'),
60+
('clouder.service', 'Service'),
61+
]
62+
63+
@api.multi
64+
def _get_query_code_context(self, interface):
65+
""" Returns a query context for use
66+
Args:
67+
interface (clouder.metric.interface): The interface to use
68+
Returns:
69+
dict: Dict with the context for the given iface and model
70+
"""
71+
self.ensure_one()
72+
return {
73+
'interface': interface,
74+
'metric_model': self.metric_model,
75+
'self': self,
76+
}
77+
78+
@api.model
79+
def save_metric_value(self, metric_interfaces):
80+
""" Saves a metric value from the given interface
81+
Args:
82+
metric_interfaces (clouder.metric.interface): The interface to use
83+
Returns:
84+
None
85+
"""
86+
for iface in metric_interfaces:
87+
eval_context = iface.type_id._get_query_code_context(iface)
88+
try:
89+
safe_eval(
90+
iface.query_code,
91+
eval_context,
92+
mode='exec',
93+
nocopy=True,
94+
)
95+
except Exception as e:
96+
raise UserError(_(
97+
'Error while evaluating metrics query:'
98+
'\n %s \n %s' % (iface.name, e),
99+
))
100+
if eval_context.get('value') is None:
101+
raise ValidationError(_(
102+
'Metrics query did not set the `value` variable, which '
103+
'is used to indicate the value that should be saved for '
104+
'the query.',
105+
))
106+
uom = eval_context.get('uom') or iface.uom_id
107+
iface.write({
108+
'metric_value_ids': [(0, 0, {
109+
'value': eval_context['value'],
110+
'date_start': eval_context.get('date_start'),
111+
'date_end': eval_context.get('date_end'),
112+
'uom_id': uom.id,
113+
})],
114+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2016 LasLabs Inc.
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
from odoo import fields, models
6+
7+
8+
class ClouderMetricValue(models.Model):
9+
""" It provides a record of metric values used in billing. """
10+
11+
_name = 'clouder.metric.value'
12+
_description = 'Clouder Metric Values'
13+
14+
interface_id = fields.Many2one(
15+
string='Interface',
16+
comodel_name='clouder.metric.interface',
17+
required=True,
18+
)
19+
value = fields.Float(
20+
required=True,
21+
)
22+
uom_id = fields.Many2one(
23+
string='Unit of Measure',
24+
comodel_name='product.uom',
25+
)
26+
date_start = fields.Datetime(
27+
string='Metric Start',
28+
)
29+
date_end = fields.Datetime(
30+
string='Metric End',
31+
)
32+
date_create = fields.Datetime(
33+
string='Creation Time',
34+
default=lambda s: fields.Datetime.now(),
35+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2+
access_clouder_metric_type,access_clouder_metric_type,model_clouder_metric_type,clouder.group_clouder_user,1,0,0,0
3+
access_clouder_metric_interface,access_clouder_metric_interface,model_clouder_metric_interface,clouder.group_clouder_user,1,0,0,0
4+
access_clouder_metric_value,access_clouder_metric_value,model_clouder_metric_value,clouder.group_clouder_user,1,0,0,0

clouder_metric/tests/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2017 LasLabs Inc.
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
from . import test_clouder_metric_interface
6+
from . import test_clouder_metric_type

clouder_metric/tests/common.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2017 LasLabs Inc.
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
from odoo.tests.common import TransactionCase
6+
7+
8+
class TestCommon(TransactionCase):
9+
10+
def setUp(self):
11+
super(TestCommon, self).setUp()
12+
self.metric_type = self.env['clouder.metric.type'].create({
13+
'name': 'Test Metric',
14+
'code': 'TEST',
15+
'metric_model': 'clouder.base',
16+
'uom_id': self.env.ref('product.uom_categ_wtime').id,
17+
})
18+
self.metric_interface = self.env['clouder.metric.interface'].create({
19+
'type_id': self.metric_type.id,
20+
'metric_ref': 7,
21+
'source_id': self.env.ref(
22+
'base_external_dbsource.demo_postgre').id,
23+
'query_code': 'print True',
24+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2017 LasLabs Inc.
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
from .common import TestCommon
6+
7+
8+
class TestClouderMetricInterface(TestCommon):
9+
10+
def test_metric_id(self):
11+
""" It should test to see that at least one metric_id is returned """
12+
self.assertTrue(len(self.metric_interface.metric_id) == 1)
13+
14+
def test_name_get(self):
15+
""" It should return the right name """
16+
exp = [
17+
(self.metric_interface.id, 'Test Metric - 7')
18+
]
19+
self.assertEqual(exp, self.metric_interface.name_get())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2017 LasLabs Inc.
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
from odoo.exceptions import UserError, ValidationError
6+
7+
from .common import TestCommon
8+
9+
10+
class TestClouderMetricType(TestCommon):
11+
12+
def test_get_metric_models(self):
13+
""" It should have the correct metric model types """
14+
exp = [
15+
('clouder.base', 'Base'),
16+
('clouder.service', 'Service'),
17+
]
18+
self.assertEquals(exp, self.metric_type._get_metric_models())
19+
20+
def test_save_metric_value_usererror(self):
21+
""" It should raise UserError when a bad query is supplied """
22+
with self.assertRaises(UserError):
23+
self.metric_type.save_metric_value(self.metric_interface)
24+
25+
def test_save_metric_value_validationerror(self):
26+
""" It should raise ValidationError when no value is supplied """
27+
self.metric_interface.query_code = 'test = 0'
28+
with self.assertRaises(ValidationError):
29+
self.metric_type.save_metric_value(self.metric_interface)
30+
31+
def test_save_metric_value(self):
32+
""" It should verify that the right metric values are saved """
33+
self.metric_interface.query_code = 'value = 100'
34+
self.metric_type.save_metric_value(self.metric_interface)
35+
self.assertTrue(
36+
self.metric_interface.metric_value_ids.mapped('value') == [100.0]
37+
)

oca_dependencies.txt

+3
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
connector
2+
contract
3+
# Pending https://github.com/OCA/server-tools/pull/660
4+
server-tools https://github.com/laslabs/server-tools feature/10.0/base_external_dbsource_elasticsearch-v10

0 commit comments

Comments
 (0)