Make synchronization work.

This commit is contained in:
Albert Cervera i Areny 2014-12-11 14:11:13 +01:00
parent 6b446ce807
commit a7449bb2b8
4 changed files with 290 additions and 63 deletions

View file

@ -19,6 +19,7 @@ def register():
ResultFloat, ResultFloat,
ResultChar, ResultChar,
AssetPartyNotification, AssetPartyNotification,
SynchroMapping,
Asset, Asset,
StateTypeParty, StateTypeParty,
Party, Party,

View file

@ -1,20 +1,35 @@
# The COPYRIGHT file at the top level of this repository contains the full # The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms. # copyright notices and license terms.
import sys import sys
import ast #import ast
import xmlrpclib
from datetime import datetime from datetime import datetime
import random
import string
import hashlib
from itertools import izip, chain
from decimal import Decimal
import logging
try:
import bcrypt
except ImportError:
bcrypt = None
from trytond.tools import safe_eval from trytond.tools import safe_eval
from trytond.model import ModelSQL, ModelView, fields from trytond.model import ModelSQL, ModelView, fields
from trytond.pool import Pool, PoolMeta from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval from trytond.pyson import Eval
from trytond.config import config
from trytond.rpc import RPC
from trytond.transaction import Transaction
__all__ = ['CheckType', 'ResultType', 'StateType', 'StateIndicator', __all__ = ['CheckType', 'ResultType', 'StateType', 'StateIndicator',
'StateIndicatorLine', 'Scheduler', 'CheckPlan', 'StateIndicatorCheckPlan', 'StateIndicatorLine', 'Scheduler', 'CheckPlan', 'StateIndicatorCheckPlan',
'Check', 'State', 'ResultInteger', 'ResultFloat', 'ResultChar', 'Check', 'State', 'ResultInteger', 'ResultFloat', 'ResultChar',
'AssetPartyNotification', 'Asset', 'StateTypeParty', 'Party', 'AssetPartyNotification', 'SynchroMapping', 'Asset', 'StateTypeParty',
'RelationType'] 'Party', 'RelationType']
__metaclass__ = PoolMeta __metaclass__ = PoolMeta
@ -131,6 +146,43 @@ class CheckPlan(ModelSQL, ModelView):
def default_active(): def default_active():
return True return True
@staticmethod
def get_indicators(plan, type_, value):
states_to_create = []
for indicator in plan.indicators:
if indicator.result_type != type_:
continue
state_type = None
for line in indicator.lines:
#ast.literal_eval(indicator.expression)
if safe_eval(line.expression, {
'value': value,
}):
state_type = line.state_type
break
if not state_type:
state_type = indicator.default_state_type
states_to_create.append({
'indicator': indicator.id,
'state': state_type.id,
'value': unicode(value),
})
return states_to_create
@classmethod
def create_indicators(cls, checks):
State = Pool().get('monitoring.state')
to_create = []
for check in checks:
for result in chain(check.integer_results, check.float_results,
check.char_results):
vals = cls.get_indicators(check.plan, result.type,
result.value)
for state in vals:
state['check'] = check.id
to_create += vals
State.create(to_create)
@classmethod @classmethod
@ModelView.button @ModelView.button
@ModelView.button @ModelView.button
@ -179,33 +231,7 @@ class CheckPlan(ModelSQL, ModelView):
% (t.type, result['result'])) % (t.type, result['result']))
continue continue
states_to_create = [] states_to_create = cls.get_indicators(plan, t, value)
for indicator in plan.indicators:
if indicator.result_type != t:
continue
state_type = None
for line in indicator.lines:
#ast.literal_eval(indicator.expression)
if safe_eval(line.expression, {
'value': value,
}):
state_type = line.state_type
break
if not state_type:
state_type = indicator.default_state_type
states_to_create.append({
'indicator': indicator.id,
'state': state_type.id,
'value': unicode(value),
})
# TODO: Should be improved to take into account previous
# state and notify if state is ok again
# Maybe standard triggers will be enough by now
#for party in asset.notification_parties:
#if state_type in party.notification_types:
#Template.render_and_send(configuration.email_template.id, [notification])
to_create.append({ to_create.append({
'timestamp': datetime.now(), 'timestamp': datetime.now(),
@ -251,7 +277,12 @@ class CheckPlan(ModelSQL, ModelView):
""" """
if self.attributes and name in self.attributes: if self.attributes and name in self.attributes:
return self.attributes[name] return self.attributes[name]
return self.monitored_asset.get_attribute(name) value = None
if self.monitored_asset:
value = self.monitored_asset.get_attribute(name)
if value is None:
value = self.monitoring_asset.get_attribute(name)
return value
class StateIndicatorCheckPlan(ModelSQL, ModelView): class StateIndicatorCheckPlan(ModelSQL, ModelView):
@ -306,8 +337,6 @@ class StateIndicatorCheckPlan(ModelSQL, ModelView):
return state.color return state.color
def get_asset(self, name): def get_asset(self, name):
# TODO: Should probably be replaced by monitored_asset but it is not
# required
asset = getattr(self.plan, name) asset = getattr(self.plan, name)
if asset: if asset:
return asset.id return asset.id
@ -345,7 +374,8 @@ class State(ModelSQL, ModelView):
'Monitoring State' 'Monitoring State'
__name__ = 'monitoring.state' __name__ = 'monitoring.state'
_rec_name = 'check' _rec_name = 'check'
check = fields.Many2One('monitoring.check', 'Check', required=True) check = fields.Many2One('monitoring.check', 'Check', required=True,
ondelete='CASCADE')
indicator = fields.Many2One('monitoring.state.indicator', 'Indicator', indicator = fields.Many2One('monitoring.state.indicator', 'Indicator',
required=True) required=True)
monitoring_asset = fields.Function(fields.Many2One('asset', monitoring_asset = fields.Function(fields.Many2One('asset',
@ -375,7 +405,8 @@ class State(ModelSQL, ModelView):
class ResultInteger(ModelSQL, ModelView): class ResultInteger(ModelSQL, ModelView):
'Monitoring Result Integer' 'Monitoring Result Integer'
__name__ = 'monitoring.result.integer' __name__ = 'monitoring.result.integer'
check = fields.Many2One('monitoring.check', 'Check', required=True) check = fields.Many2One('monitoring.check', 'Check', required=True,
ondelete='CASCADE')
type = fields.Many2One('monitoring.result.type', 'Type', required=True) type = fields.Many2One('monitoring.result.type', 'Type', required=True)
value = fields.Integer('Value') value = fields.Integer('Value')
uom = fields.Many2One('product.uom', 'UoM', required=True) uom = fields.Many2One('product.uom', 'UoM', required=True)
@ -384,7 +415,8 @@ class ResultInteger(ModelSQL, ModelView):
class ResultFloat(ModelSQL, ModelView): class ResultFloat(ModelSQL, ModelView):
'Monitoring Result Float' 'Monitoring Result Float'
__name__ = 'monitoring.result.float' __name__ = 'monitoring.result.float'
check = fields.Many2One('monitoring.check', 'Check', required=True) check = fields.Many2One('monitoring.check', 'Check', required=True,
ondelete='CASCADE')
type = fields.Many2One('monitoring.result.type', 'Type', required=True) type = fields.Many2One('monitoring.result.type', 'Type', required=True)
value = fields.Float('Value') value = fields.Float('Value')
uom = fields.Many2One('product.uom', 'UoM', required=True) uom = fields.Many2One('product.uom', 'UoM', required=True)
@ -393,7 +425,8 @@ class ResultFloat(ModelSQL, ModelView):
class ResultChar(ModelSQL, ModelView): class ResultChar(ModelSQL, ModelView):
'Monitoring Result Char' 'Monitoring Result Char'
__name__ = 'monitoring.result.char' __name__ = 'monitoring.result.char'
check = fields.Many2One('monitoring.check', 'Check', required=True) check = fields.Many2One('monitoring.check', 'Check', required=True,
ondelete='CASCADE')
type = fields.Many2One('monitoring.result.type', 'Type', required=True) type = fields.Many2One('monitoring.result.type', 'Type', required=True)
value = fields.Char('Value') value = fields.Char('Value')
@ -405,6 +438,22 @@ class AssetPartyNotification(ModelSQL):
party = fields.Many2One('party.party', 'Party', required=True) party = fields.Many2One('party.party', 'Party', required=True)
class SynchroMapping(ModelSQL):
'Synchronization Mapping'
__name__ = 'synchro.mapping'
local_id = fields.Integer('Local ID', required=True)
remote_id = fields.Integer('Remote ID', required=True)
model = fields.Char('Model Name', required=True)
@classmethod
def __setup__(cls):
super(SynchroMapping, cls).__setup__()
cls._sql_constraints += [
('remote_id_model_uniq', 'UNIQUE(remote_id, model)',
'remote_id and model must be unique.')
]
class Asset: class Asset:
__name__ = 'asset' __name__ = 'asset'
plans = fields.One2Many('monitoring.check.plan', 'monitoring_asset', plans = fields.One2Many('monitoring.check.plan', 'monitoring_asset',
@ -420,6 +469,14 @@ class Asset:
password = fields.Function(fields.Char('Password'), getter='get_password', password = fields.Function(fields.Char('Password'), getter='get_password',
setter='set_password') setter='set_password')
@classmethod
def __setup__(cls):
super(Asset, cls).__setup__()
cls.__rpc__.update({
'update_remote_checks': RPC(readonly=False),
'fetch_remote_assets': RPC(),
})
def get_attribute(self, name, browsed=None): def get_attribute(self, name, browsed=None):
""" """
Returns the value of the given attribute. Returns the value of the given attribute.
@ -463,8 +520,6 @@ class Asset:
@classmethod @classmethod
def _get_login(cls, login): def _get_login(cls, login):
if result:
return result
cursor = Transaction().cursor cursor = Transaction().cursor
table = cls.__table__() table = cls.__table__()
cursor.execute(*table.select(table.id, table.password_hash, cursor.execute(*table.select(table.id, table.password_hash,
@ -539,11 +594,21 @@ class Asset:
return hash_ == bcrypt.hashpw(password, hash_) return hash_ == bcrypt.hashpw(password, hash_)
@staticmethod @staticmethod
def object_to_dict(obj): def object_to_dict(obj, mappings=None, model_data=False):
ModelData = Pool().get('ir.model.data')
if mappings is None:
mappings = {}
res = {} res = {}
fields = [name for name, field in obj._fields.iteritems()
if isinstance(field, fields.Field)]
res['id'] = obj.id res['id'] = obj.id
if model_data:
records = ModelData.search([
('db_id', '=', obj.id),
('model', '=', obj.__name__),
])
value = None
if records:
value = (records[0].module, records[0].fs_id)
res['__model_data__'] = value
for name, field in obj._fields.iteritems(): for name, field in obj._fields.iteritems():
value = getattr(obj, name) value = getattr(obj, name)
if isinstance(field, (fields.Function, fields.One2Many, if isinstance(field, (fields.Function, fields.One2Many,
@ -554,35 +619,82 @@ class Asset:
elif isinstance(field, fields.Reference) and value: elif isinstance(field, fields.Reference) and value:
# TODO: Reference fields # TODO: Reference fields
value = '' value = ''
if name in mappings and value:
remote, = SynchroMapping.search([
('local_id', '=', value),
('model', '=', mappings[name]),
])
value = remote.remote_id
res[name] = value res[name] = value
return res return res
@staticmethod @staticmethod
def export_objects(objects): def export_objects(objects, mappings=None, model_data=False):
res = [] res = []
for obj in objects: for obj in objects:
res.append(cls.object_to_dict(obj)) res.append(Asset.object_to_dict(obj, mappings=mappings,
model_data=model_data))
return res return res
@staticmethod @staticmethod
def dict_to_object(record, cls): def dict_to_object(record, cls, overrides=None, mappings=None):
SynchroMapping = Pool().get('synchro.mapping')
if overrides is None:
overrides = {}
if mappings is None:
mappings = {}
obj = cls() obj = cls()
for name, value in record.iteritems(): for name, value in record.iteritems():
if name == '__model_data__':
continue
value = overrides.get(name, value)
if name in mappings and value:
local, = SynchroMapping.search([
('remote_id', '=', value),
('model', '=', mappings[name]),
])
value = local.local_id
setattr(obj, name, value) setattr(obj, name, value)
return obj return obj
@staticmethod @staticmethod
def import_objects(records, cls): def import_objects(records, cls, overrides=None, mappings=None):
res = [] SynchroMapping = Pool().get('synchro.mapping')
for record in records: ModelData = Pool().get('ir.model.data')
res.append(cls.dict_to_object(record, cls))
return res
to_create = []
new_records = []
map_records = []
for record in records:
if '__model_data__' in record:
value = record['__model_data__']
map_records.append({
'local_id': ModelData.get_id(value[0], value[1]),
'remote_id': record['id'],
'model': cls.__name__,
})
continue
to_create.append(Asset.dict_to_object(record, cls, overrides, mappings))
new_records.append(record)
local_objects = cls.create([x._save_values for x in to_create])
for local, remote in izip(local_objects, new_records):
map_records.append({
'local_id': local.id,
'remote_id': remote['id'],
'model': cls.__name__,
})
SynchroMapping.create(map_records)
return local_objects
@classmethod
def fetch_remote_assets(cls, login, password): def fetch_remote_assets(cls, login, password):
AssetRelationAll = Pool().get('asset.relation.all') AssetRelationAll = Pool().get('asset.relation.all')
ResultType = Pool().get('monitoring.result.type')
asset_id = cls.get_login(login, password) asset_id = cls.get_login(login, password)
if not asset_id: if not asset_id:
logger.getLogger('monitoring').error('No asset found for login %s' %
login)
return return
asset = cls(asset_id) asset = cls(asset_id)
@ -601,22 +713,25 @@ class Asset:
# assets.append(relation.from_) # assets.append(relation.from_)
plans = [] plans = []
schedulers = set() schedulers = set()
types = set() check_types = set()
for asset in assets: for asset in assets:
for plan in asset.plans: for plan in asset.plans:
plans.append(plan) plans.append(plan)
schedulers.add(plan.scheduler) schedulers.add(plan.scheduler)
# Types should be available in the remote host check_types.add(plan.type)
#types.add(plan.type)
result_types = ResultType.search([])
data = {} data = {}
data['schedulers'] = cls.export_objects(list(schedulers)) data['schedulers'] = cls.export_objects(list(schedulers))
# Types should be available in the remote host data['check_types'] = cls.export_objects(list(check_types),
#data['types'] = cls.export_objects(list(types)) model_data=True)
data['result_types'] = cls.export_objects(result_types, model_data=True)
data['plans'] = cls.export_objects(plans) data['plans'] = cls.export_objects(plans)
data['assets'] = cls.export_objects(assets) data['assets'] = cls.export_objects(assets)
return data return data
@classmethod
def update_remote_checks(cls, login, password, data): def update_remote_checks(cls, login, password, data):
if not cls.get_login(login, password): if not cls.get_login(login, password):
return return
@ -626,15 +741,109 @@ class Asset:
IntegerResult = pool.get('monitoring.result.integer') IntegerResult = pool.get('monitoring.result.integer')
FloatResult = pool.get('monitoring.result.float') FloatResult = pool.get('monitoring.result.float')
CharResult = pool.get('monitoring.result.char') CharResult = pool.get('monitoring.result.char')
CheckPlan = pool.get('monitoring.check.plan')
objs = cls.import_objects(data['checks'], Check) # TODO: Result and Check types need to be properly synchronized
Check.save(objs) # Now they work only if modules were installed in the same
cls.import_objects(data['integer_results'], IntegerResult) # order on both ends.
IntegerResult.save(objs) checks = cls.import_objects(data['checks'], Check)
cls.import_objects(data['float_results'], FloatResult) cls.import_objects(data['integer_results'], IntegerResult,
FloatResult.save(objs) mappings={
cls.import_objects(data['char_results'], CharResult) 'check': 'monitoring.check',
CharResult.save(objs) })
cls.import_objects(data['float_results'], FloatResult,
mappings={
'check': 'monitoring.check',
})
cls.import_objects(data['char_results'], CharResult,
mappings={
'check': 'monitoring.check',
})
CheckPlan.create_indicators(Check.browse([x.id for x in checks]))
@classmethod
def sync(cls):
pool = Pool()
Check = pool.get('monitoring.check')
IntegerResult = pool.get('monitoring.result.integer')
FloatResult = pool.get('monitoring.result.float')
CharResult = pool.get('monitoring.result.char')
Plan = pool.get('monitoring.check.plan')
Scheduler = pool.get('monitoring.scheduler')
Asset = pool.get('asset')
Product = pool.get('product.product')
Template = pool.get('product.template')
SynchroMapping = pool.get('synchro.mapping')
ModelData = pool.get('ir.model.data')
CheckType = pool.get('monitoring.check.type')
ResultType = pool.get('monitoring.result.type')
data = {}
checks = Check.search([])
data['checks'] = cls.export_objects(checks, mappings={
'plan': 'monitoring.check.plan',
'monitoring_asset': 'asset',
'monitored_asset': 'asset',
})
integers = IntegerResult.search([])
data['integer_results'] = cls.export_objects(integers)
floats = FloatResult.search([])
data['float_results'] = cls.export_objects(floats)
chars = CharResult.search([])
data['char_results'] = cls.export_objects(chars)
uri = config.get('monitoring', 'uri')
username = config.get('monitoring', 'username')
password = config.get('monitoring', 'password')
server = xmlrpclib.ServerProxy(uri, allow_none=True)
context = server.model.res.user.get_preferences(True, {})
server.model.asset.update_remote_checks(username, password, data, context)
data = server.model.asset.fetch_remote_assets(username, password, context)
Check.delete(checks)
IntegerResult.delete(checks)
FloatResult.delete(checks)
CharResult.delete(checks)
Plan.delete(Plan.search([]))
Scheduler.delete(Scheduler.search([]))
Asset.delete(Asset.search([]))
SynchroMapping.delete(SynchroMapping.search([]))
# TODO: Maybe create a product with the module and deactivate it by
# default. The problem would be if another module adds required fields.
asset_product = Product.search([
('type', '=', 'assets'),
('code', '=', 'monitoring'),
], limit=1)
if not asset_product:
asset_product = Template.create([{
'name': 'Monitoring Asset',
'type': 'assets',
'list_price': Decimal(0),
'cost_price': Decimal(0),
'default_uom': ModelData.get_id('product', 'uom_unit'),
'products': [('create', [{
'code': 'monitoring',
}])]
}])
asset_product = Product.search([
('type', '=', 'assets'),
('code', '=', 'monitoring'),
], limit=1)
asset_product = asset_product[0]
cls.import_objects(data['assets'], Asset, overrides={
'product': asset_product.id,
})
cls.import_objects(data['schedulers'], Scheduler)
cls.import_objects(data['plans'], Plan, mappings={
'monitoring_asset': 'asset',
'monitored_asset': 'asset',
'scheduler': 'monitoring.scheduler',
})
cls.import_objects(data['check_types'], CheckType)
cls.import_objects(data['result_types'], ResultType)
class StateTypeParty(ModelSQL): class StateTypeParty(ModelSQL):

View file

@ -790,6 +790,19 @@
<field name="function">check_all</field> <field name="function">check_all</field>
</record> </record>
<record model="ir.cron" id="cron_slave">
<field name="name">Monitoring Slave</field>
<field name="request_user" ref="res.user_admin"/>
<field name="user" ref="user_monitoring"/>
<field name="active" eval="True"/>
<field name="interval_number">1</field>
<field name="interval_type">minutes</field>
<field name="number_calls">-1</field>
<field name="repeat_missed" eval="False"/>
<field name="model">asset</field>
<field name="function">sync</field>
</record>
<!-- State Types --> <!-- State Types -->
<record model="monitoring.state.type" id="state_type_ok"> <record model="monitoring.state.type" id="state_type_ok">
<field name="name">Ok</field> <field name="name">Ok</field>

View file

@ -4,6 +4,10 @@
<data> <data>
<xpath expr="/form/notebook/page[@id='relations']" position="after"> <xpath expr="/form/notebook/page[@id='relations']" position="after">
<page string="Monitoring" id="monitoring"> <page string="Monitoring" id="monitoring">
<label name="login"/>
<field name="login"/>
<label name="password"/>
<field name="password" widget="password"/>
<field name="plans" colspan="4"/> <field name="plans" colspan="4"/>
<field name="notification_parties" colspan="4"/> <field name="notification_parties" colspan="4"/>
<field name="states" colspan="4"/> <field name="states" colspan="4"/>