adding machine learning service
at this first step, the service includes one plugin,this plugin use jaccard correlation in order to measure correlation between a pair of alarms. once in configured amount of alarms this service generates a report and saves it to configured directory. Change-Id: I2428dcf05365e6b68fb0e86ccd82554c82b94c2f
This commit is contained in:
parent
01297e9288
commit
0b506a745d
@ -291,6 +291,7 @@ function start_vitrage {
|
|||||||
run_process vitrage-collector "$VITRAGE_BIN_DIR/vitrage-collector --config-file $VITRAGE_CONF"
|
run_process vitrage-collector "$VITRAGE_BIN_DIR/vitrage-collector --config-file $VITRAGE_CONF"
|
||||||
run_process vitrage-graph "$VITRAGE_BIN_DIR/vitrage-graph --config-file $VITRAGE_CONF"
|
run_process vitrage-graph "$VITRAGE_BIN_DIR/vitrage-graph --config-file $VITRAGE_CONF"
|
||||||
run_process vitrage-notifier "$VITRAGE_BIN_DIR/vitrage-notifier --config-file $VITRAGE_CONF"
|
run_process vitrage-notifier "$VITRAGE_BIN_DIR/vitrage-notifier --config-file $VITRAGE_CONF"
|
||||||
|
run_process vitrage-ml "$VITRAGE_BIN_DIR/vitrage-ml --config-file $VITRAGE_CONF"
|
||||||
}
|
}
|
||||||
|
|
||||||
# stop_vitrage() - Stop running processes
|
# stop_vitrage() - Stop running processes
|
||||||
|
@ -7,6 +7,8 @@ enable_service vitrage-graph
|
|||||||
enable_service vitrage-notifier
|
enable_service vitrage-notifier
|
||||||
# Collector
|
# Collector
|
||||||
enable_service vitrage-collector
|
enable_service vitrage-collector
|
||||||
|
# machine_learning
|
||||||
|
enable_service vitrage-ml
|
||||||
|
|
||||||
|
|
||||||
# Default directories
|
# Default directories
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- A new service was added to Vitrage, the machine learning service.
|
||||||
|
Together with it, the first machine learning plugin was added - the
|
||||||
|
Jaccard Correlation plugin. This plugin listens to rabbit MQ
|
||||||
|
receiving messages about creation and deletion of alarms, and
|
||||||
|
learns the correlation between each pair of alarms, in order to
|
||||||
|
recommend on creation of new templates in the future.
|
@ -29,6 +29,7 @@ console_scripts =
|
|||||||
vitrage-graph = vitrage.cli.graph:main
|
vitrage-graph = vitrage.cli.graph:main
|
||||||
vitrage-notifier = vitrage.cli.notifier:main
|
vitrage-notifier = vitrage.cli.notifier:main
|
||||||
vitrage-collector = vitrage.cli.collector:main
|
vitrage-collector = vitrage.cli.collector:main
|
||||||
|
vitrage-ml = vitrage.cli.machine_learning:main
|
||||||
|
|
||||||
vitrage.entity_graph =
|
vitrage.entity_graph =
|
||||||
networkx = vitrage.graph.driver.networkx_graph:NXGraph
|
networkx = vitrage.graph.driver.networkx_graph:NXGraph
|
||||||
|
30
vitrage/cli/machine_learning.py
Normal file
30
vitrage/cli/machine_learning.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Copyright 2017 - Nokia
|
||||||
|
#
|
||||||
|
# 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 oslo_service import service as os_service
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from vitrage.machine_learning.service import MachineLearningService
|
||||||
|
from vitrage import service
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
conf = service.prepare_service()
|
||||||
|
launcher = os_service.ServiceLauncher(conf)
|
||||||
|
launcher.launch_service(MachineLearningService(conf))
|
||||||
|
launcher.wait()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
@ -112,6 +112,8 @@ class GraphAction(object):
|
|||||||
class NotifierEventTypes(object):
|
class NotifierEventTypes(object):
|
||||||
ACTIVATE_DEDUCED_ALARM_EVENT = 'vitrage.deduced_alarm.activate'
|
ACTIVATE_DEDUCED_ALARM_EVENT = 'vitrage.deduced_alarm.activate'
|
||||||
DEACTIVATE_DEDUCED_ALARM_EVENT = 'vitrage.deduced_alarm.deactivate'
|
DEACTIVATE_DEDUCED_ALARM_EVENT = 'vitrage.deduced_alarm.deactivate'
|
||||||
|
ACTIVATE_ALARM_EVENT = 'vitrage.alarm.activate'
|
||||||
|
DEACTIVATE_ALARM_EVENT = 'vitrage.alarm.deactivate'
|
||||||
ACTIVATE_MARK_DOWN_EVENT = 'vitrage.mark_down.activate'
|
ACTIVATE_MARK_DOWN_EVENT = 'vitrage.mark_down.activate'
|
||||||
DEACTIVATE_MARK_DOWN_EVENT = 'vitrage.mark_down.deactivate'
|
DEACTIVATE_MARK_DOWN_EVENT = 'vitrage.mark_down.deactivate'
|
||||||
EXECUTE_EXTERNAL_ACTION = 'vitrage.execute_external_action'
|
EXECUTE_EXTERNAL_ACTION = 'vitrage.execute_external_action'
|
||||||
|
@ -28,25 +28,42 @@ class GraphNotifier(object):
|
|||||||
"""Allows writing to message bus"""
|
"""Allows writing to message bus"""
|
||||||
def __init__(self, conf):
|
def __init__(self, conf):
|
||||||
self.oslo_notifier = None
|
self.oslo_notifier = None
|
||||||
try:
|
topics = self._get_topics(conf)
|
||||||
topic = conf.entity_graph.notifier_topic
|
if not topics:
|
||||||
notifier_plugins = conf.notifiers
|
LOG.info('Graph Notifier is disabled')
|
||||||
if not topic or not notifier_plugins:
|
return
|
||||||
LOG.info('Graph Notifier is disabled')
|
self.oslo_notifier = oslo_messaging.Notifier(
|
||||||
return
|
get_transport(conf),
|
||||||
|
driver='messagingv2',
|
||||||
self.oslo_notifier = oslo_messaging.Notifier(
|
publisher_id='vitrage.graph',
|
||||||
get_transport(conf),
|
topics=topics)
|
||||||
driver='messagingv2',
|
|
||||||
publisher_id='vitrage.graph',
|
|
||||||
topics=[topic])
|
|
||||||
except Exception as e:
|
|
||||||
LOG.info('Graph Notifier - missing configuration %s' % str(e))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled(self):
|
def enabled(self):
|
||||||
return self.oslo_notifier is not None
|
return self.oslo_notifier is not None
|
||||||
|
|
||||||
|
def _get_topics(self, conf):
|
||||||
|
topics = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
notifier_topic = conf.entity_graph.notifier_topic
|
||||||
|
notifier_plugins = conf.notifiers
|
||||||
|
if notifier_topic and notifier_plugins:
|
||||||
|
topics.append(notifier_topic)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.info('Graph Notifier - missing configuration %s' % str(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
machine_learning_topic = \
|
||||||
|
conf.machine_learning.machine_learning_topic
|
||||||
|
machine_learning_plugins = conf.machine_learning.plugins
|
||||||
|
if machine_learning_topic and machine_learning_plugins:
|
||||||
|
topics.append(machine_learning_topic)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.info('Machine Learning - missing configuration %s' % str(e))
|
||||||
|
|
||||||
|
return topics
|
||||||
|
|
||||||
def notify_when_applicable(self, before, current, is_vertex, graph):
|
def notify_when_applicable(self, before, current, is_vertex, graph):
|
||||||
"""Callback subscribed to driver.graph updates
|
"""Callback subscribed to driver.graph updates
|
||||||
|
|
||||||
@ -64,9 +81,9 @@ class GraphNotifier(object):
|
|||||||
|
|
||||||
# in case the vertex point to some resource add the resource to the
|
# in case the vertex point to some resource add the resource to the
|
||||||
# notification (useful for deduce alarm notifications)
|
# notification (useful for deduce alarm notifications)
|
||||||
if current.get(VProps.RESOURCE_ID):
|
if current.get(VProps.VITRAGE_RESOURCE_ID):
|
||||||
current.properties[VProps.RESOURCE] = graph.get_vertex(
|
current.properties[VProps.RESOURCE] = graph.get_vertex(
|
||||||
current.get(VProps.RESOURCE_ID))
|
current.get(VProps.VITRAGE_RESOURCE_ID))
|
||||||
|
|
||||||
LOG.info('notification_types : %s', str(notification_types))
|
LOG.info('notification_types : %s', str(notification_types))
|
||||||
LOG.info('notification properties : %s', current.properties)
|
LOG.info('notification properties : %s', current.properties)
|
||||||
@ -99,6 +116,9 @@ def _get_notification_type(before, current, is_vertex):
|
|||||||
notification_type(_is_active_deduced_alarm,
|
notification_type(_is_active_deduced_alarm,
|
||||||
NotifierEventTypes.ACTIVATE_DEDUCED_ALARM_EVENT,
|
NotifierEventTypes.ACTIVATE_DEDUCED_ALARM_EVENT,
|
||||||
NotifierEventTypes.DEACTIVATE_DEDUCED_ALARM_EVENT),
|
NotifierEventTypes.DEACTIVATE_DEDUCED_ALARM_EVENT),
|
||||||
|
notification_type(_is_active_alarm,
|
||||||
|
NotifierEventTypes.ACTIVATE_ALARM_EVENT,
|
||||||
|
NotifierEventTypes.DEACTIVATE_ALARM_EVENT),
|
||||||
notification_type(_is_marked_down,
|
notification_type(_is_marked_down,
|
||||||
NotifierEventTypes.ACTIVATE_MARK_DOWN_EVENT,
|
NotifierEventTypes.ACTIVATE_MARK_DOWN_EVENT,
|
||||||
NotifierEventTypes.DEACTIVATE_MARK_DOWN_EVENT),
|
NotifierEventTypes.DEACTIVATE_MARK_DOWN_EVENT),
|
||||||
@ -115,6 +135,12 @@ def _is_active_deduced_alarm(vertex):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _is_active_alarm(vertex):
|
||||||
|
if vertex and vertex.get(VProps.VITRAGE_CATEGORY) == EntityCategory.ALARM:
|
||||||
|
return _is_relevant_vertex(vertex)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _is_marked_down(vertex):
|
def _is_marked_down(vertex):
|
||||||
if not vertex:
|
if not vertex:
|
||||||
return False
|
return False
|
||||||
|
28
vitrage/machine_learning/__init__.py
Normal file
28
vitrage/machine_learning/__init__.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Copyright 2017 - Nokia
|
||||||
|
#
|
||||||
|
# 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 oslo_config import cfg
|
||||||
|
|
||||||
|
OPTS = [
|
||||||
|
cfg.ListOpt('plugins',
|
||||||
|
help='Names of enabled machine learning plugins '
|
||||||
|
'(example jaccard_correlation)'),
|
||||||
|
cfg.ListOpt('plugins_path',
|
||||||
|
default=['vitrage.machine_learning.plugins'],
|
||||||
|
help='list of base path for notifiers'),
|
||||||
|
cfg.StrOpt('machine_learning_topic',
|
||||||
|
default='vitrage.machine_learning',
|
||||||
|
help='The topic that vitrage-graph uses for graph '
|
||||||
|
'machine learning messages.'),
|
||||||
|
]
|
15
vitrage/machine_learning/plugins/__init__.py
Normal file
15
vitrage/machine_learning/plugins/__init__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright 2017 - Nokia
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
__author__ = 'stack'
|
32
vitrage/machine_learning/plugins/base.py
Normal file
32
vitrage/machine_learning/plugins/base.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Copyright 2017 - Nokia
|
||||||
|
#
|
||||||
|
# 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 abc
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
|
class MachineLearningBase(object):
|
||||||
|
|
||||||
|
def __init__(self, conf):
|
||||||
|
self.conf = conf
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def process_event(self, data, event_type):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_plugin_name():
|
||||||
|
pass
|
@ -0,0 +1,34 @@
|
|||||||
|
# Copyright 2017 - Nokia
|
||||||
|
#
|
||||||
|
# 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 oslo_config import cfg
|
||||||
|
|
||||||
|
OPTS = [
|
||||||
|
cfg.StrOpt('plugin_path',
|
||||||
|
default='vitrage.machine_learning.plugins.'
|
||||||
|
'jaccard_correlation.alarm_processor.'
|
||||||
|
'AlarmDataProcessor',
|
||||||
|
help='jaccard_correlation class path',
|
||||||
|
required=True),
|
||||||
|
cfg.IntOpt('num_of_events_to_flush', default=1000,
|
||||||
|
help='the amount of events flushes'),
|
||||||
|
cfg.StrOpt('output_folder', default='/tmp',
|
||||||
|
help='folder to write all reports to'),
|
||||||
|
cfg.FloatOpt('correlation_threshold', default=0,
|
||||||
|
help='threshold of interesting correlations'),
|
||||||
|
cfg.FloatOpt('high_corr_score', default=0.9,
|
||||||
|
help='high correlation lower limit'),
|
||||||
|
cfg.FloatOpt('med_corr_score', default=0.5,
|
||||||
|
help='medium correlation lower limit'),
|
||||||
|
]
|
@ -0,0 +1,59 @@
|
|||||||
|
# Copyright 2017 - Nokia
|
||||||
|
#
|
||||||
|
# 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 collections import namedtuple
|
||||||
|
from oslo_log import log
|
||||||
|
import pickle
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
AccumulatedData = namedtuple('AccumulatedData', ['activity', 'intersection'])
|
||||||
|
|
||||||
|
ACTIVITY_PATH = "/tmp/alarms_activity.txt"
|
||||||
|
INTERSECT_PATH = "/tmp/alarms_intersections.txt"
|
||||||
|
|
||||||
|
|
||||||
|
def load_data():
|
||||||
|
try:
|
||||||
|
with open(ACTIVITY_PATH, 'rb') as activity_f:
|
||||||
|
alarms_activity = pickle.load(activity_f)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.info('Cannot load alarms_activity - %s', e)
|
||||||
|
return AccumulatedData({}, {})
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(INTERSECT_PATH, 'rb') as intersect_f:
|
||||||
|
alarms_intersect = pickle.load(intersect_f)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.info('Cannot load alarms_intersect - %s', e)
|
||||||
|
return AccumulatedData({}, {})
|
||||||
|
|
||||||
|
return AccumulatedData(alarms_activity, alarms_intersect)
|
||||||
|
|
||||||
|
|
||||||
|
def save_accumulated_data(data_manager):
|
||||||
|
activity = data_manager.alarms_activity
|
||||||
|
intersects = data_manager.alarms_intersects
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(ACTIVITY_PATH, 'wb') as activity_f:
|
||||||
|
pickle.dump(activity, activity_f)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception('Cannot save alarms_intersect - %s', e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(INTERSECT_PATH, 'wb') as intersect_f:
|
||||||
|
pickle.dump(intersects, intersect_f)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception('Cannot save alarms_intersect - %s', e)
|
@ -0,0 +1,80 @@
|
|||||||
|
# Copyright 2017 - Nokia
|
||||||
|
#
|
||||||
|
# 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 datetime
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AlarmDataAccumulator(object):
|
||||||
|
|
||||||
|
def __init__(self, accumulated_data):
|
||||||
|
self.active_start_times = {}
|
||||||
|
self.alarms_activity = accumulated_data.activity
|
||||||
|
# TODO(annarez): exclude intersections between deduced and it's cause
|
||||||
|
self.alarms_intersects = accumulated_data.intersection
|
||||||
|
|
||||||
|
def append_active(self, alarm_id, timestamp):
|
||||||
|
|
||||||
|
if alarm_id in self.active_start_times:
|
||||||
|
LOG.debug("Active alarm {} was started twice. Second time at {}".
|
||||||
|
format(alarm_id, str(timestamp)))
|
||||||
|
return
|
||||||
|
|
||||||
|
self.active_start_times[alarm_id] = timestamp
|
||||||
|
|
||||||
|
def append_inactive(self, alarm_id, end_time):
|
||||||
|
|
||||||
|
if alarm_id not in self.active_start_times:
|
||||||
|
LOG.debug("Alarm {} at {} was deactivated without being active".
|
||||||
|
format(alarm_id, str(end_time)))
|
||||||
|
return
|
||||||
|
|
||||||
|
alarm_duration = end_time - self.active_start_times[alarm_id]
|
||||||
|
self.alarms_activity[alarm_id] = alarm_duration + \
|
||||||
|
self.alarms_activity.get(alarm_id, datetime.timedelta(0))
|
||||||
|
|
||||||
|
start_time = self.active_start_times[alarm_id]
|
||||||
|
|
||||||
|
del self.active_start_times[alarm_id]
|
||||||
|
self.append_intersect(alarm_id, start_time, end_time)
|
||||||
|
|
||||||
|
def append_intersect(self, alarm_id, start_time, end_time):
|
||||||
|
|
||||||
|
for active_alarm in self.active_start_times.items():
|
||||||
|
key = frozenset([alarm_id, active_alarm[0]])
|
||||||
|
active_alarm_service = active_alarm[1]
|
||||||
|
self.alarms_intersects[key] = \
|
||||||
|
self.alarms_intersects.get(key, datetime.timedelta(0)) + \
|
||||||
|
self.calc_intersect(start_time, end_time, active_alarm_service)
|
||||||
|
|
||||||
|
def calc_intersect(self, inactive_start, inactive_end, active_start):
|
||||||
|
return inactive_end - max(inactive_start, active_start)
|
||||||
|
|
||||||
|
def flush_accumulations(self):
|
||||||
|
"""flush all active alarms
|
||||||
|
|
||||||
|
empty the data from active_start_times and re-enter all the
|
||||||
|
currently-active alarms, as if they started now
|
||||||
|
"""
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
active_alarms = list(self.active_start_times)
|
||||||
|
|
||||||
|
for active_alarm in active_alarms:
|
||||||
|
self.append_inactive(active_alarm, now)
|
||||||
|
|
||||||
|
for flushed_active_alarm in active_alarms:
|
||||||
|
self.append_active(flushed_active_alarm, now)
|
@ -0,0 +1,95 @@
|
|||||||
|
# Copyright 2017 - Nokia
|
||||||
|
#
|
||||||
|
# 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 collections import namedtuple
|
||||||
|
from datetime import datetime
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from vitrage.common.constants import NotifierEventTypes
|
||||||
|
from vitrage.common.constants import VertexProperties as VProps
|
||||||
|
from vitrage.datasources.alarm_properties import AlarmProperties as AlarmProps
|
||||||
|
from vitrage.datasources.transformer_base import TIMESTAMP_FORMAT
|
||||||
|
from vitrage.machine_learning.plugins.base import MachineLearningBase
|
||||||
|
import vitrage.machine_learning.plugins.jaccard_correlation.\
|
||||||
|
accumulation_persistor_utils as APersistor
|
||||||
|
from vitrage.machine_learning.plugins.jaccard_correlation.\
|
||||||
|
alarm_data_accumulator import AlarmDataAccumulator as ADAcummulator
|
||||||
|
from vitrage.machine_learning.plugins.jaccard_correlation.correlation_manager\
|
||||||
|
import CorrelationManager as CM
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
AlarmID = namedtuple('AlarmID', [VProps.VITRAGE_RESOURCE_ID,
|
||||||
|
VProps.VITRAGE_RESOURCE_TYPE,
|
||||||
|
VProps.NAME])
|
||||||
|
|
||||||
|
|
||||||
|
class AlarmDataProcessor(MachineLearningBase):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_plugin_name():
|
||||||
|
return 'jaccard_correlation'
|
||||||
|
|
||||||
|
def __init__(self, conf):
|
||||||
|
super(AlarmDataProcessor, self).__init__(conf)
|
||||||
|
self.data_manager = ADAcummulator(APersistor.load_data())
|
||||||
|
self.correlation_manager = CM(conf)
|
||||||
|
self.num_of_events_to_flush = \
|
||||||
|
conf.jaccard_correlation.num_of_events_to_flush
|
||||||
|
self.event_counter = 0
|
||||||
|
|
||||||
|
def process_event(self, data, event_type):
|
||||||
|
|
||||||
|
if event_type == NotifierEventTypes.ACTIVATE_ALARM_EVENT \
|
||||||
|
or event_type == NotifierEventTypes.DEACTIVATE_ALARM_EVENT:
|
||||||
|
|
||||||
|
# TODO(annarez): handle alarms from collectd
|
||||||
|
if data[VProps.VITRAGE_TYPE] == 'collectd':
|
||||||
|
return
|
||||||
|
|
||||||
|
self._update_data_accumulator(data)
|
||||||
|
self.event_counter += 1
|
||||||
|
|
||||||
|
# flush all data once num_of_events_to_flush is achieved
|
||||||
|
if self.event_counter == self.num_of_events_to_flush:
|
||||||
|
LOG.debug("Persisting: {}".format(str(data)))
|
||||||
|
self.data_manager.flush_accumulations()
|
||||||
|
APersistor.save_accumulated_data(self.data_manager)
|
||||||
|
self.correlation_manager.output_correlations(self.data_manager)
|
||||||
|
self.event_counter = 0
|
||||||
|
|
||||||
|
def _update_data_accumulator(self, data):
|
||||||
|
|
||||||
|
alarm_name = data[VProps.RAWTEXT] if data.get(VProps.RAWTEXT) else \
|
||||||
|
data[VProps.NAME]
|
||||||
|
|
||||||
|
alarm_id, timestamp = \
|
||||||
|
self._get_alarm_id_and_timestamp(data, alarm_name)
|
||||||
|
|
||||||
|
if data[VProps.STATE] == AlarmProps.ACTIVE_STATE:
|
||||||
|
self.data_manager.append_active(alarm_id, timestamp)
|
||||||
|
else:
|
||||||
|
self.data_manager.append_inactive(alarm_id, timestamp)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_alarm_id_and_timestamp(data, alarm_name):
|
||||||
|
|
||||||
|
alarm_id = AlarmID(data.get(VProps.VITRAGE_RESOURCE_ID),
|
||||||
|
data.get(VProps.VITRAGE_RESOURCE_TYPE),
|
||||||
|
alarm_name)
|
||||||
|
|
||||||
|
timestamp = datetime.strptime(data[VProps.UPDATE_TIMESTAMP],
|
||||||
|
TIMESTAMP_FORMAT)
|
||||||
|
|
||||||
|
return alarm_id, timestamp
|
@ -0,0 +1,84 @@
|
|||||||
|
# Copyright 2017 - Nokia
|
||||||
|
#
|
||||||
|
# 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 collections import defaultdict
|
||||||
|
from itertools import groupby
|
||||||
|
from operator import itemgetter
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CorrelationPriorities(object):
|
||||||
|
HIGH = "HIGH"
|
||||||
|
MEDIUM = "MEDIUM"
|
||||||
|
LOW = "LOW"
|
||||||
|
|
||||||
|
|
||||||
|
class AlarmsProperties(object):
|
||||||
|
ALARM1_RESOURCE_ID = 0
|
||||||
|
ALARM1_RESOURCE_TYPE = 1
|
||||||
|
ALARM1_NAME = 2
|
||||||
|
ALARM2_RESOURCE_ID = 3
|
||||||
|
ALARM2_RESOURCE_TYPE = 4
|
||||||
|
ALARM2_NAME = 5
|
||||||
|
OFFSET_DELTA = 6
|
||||||
|
CORR_SCORE = 7
|
||||||
|
|
||||||
|
|
||||||
|
class CorrelationCollection(object):
|
||||||
|
|
||||||
|
def __init__(self, high_corr_score, med_corr_score):
|
||||||
|
self.correlation_list = []
|
||||||
|
self.high_corr_score = high_corr_score
|
||||||
|
self.med_corr_score = med_corr_score
|
||||||
|
|
||||||
|
def set(self, alarm_1, alarm_2, offset_delta, correlation_score):
|
||||||
|
|
||||||
|
self.correlation_list.append(alarm_1 + alarm_2 +
|
||||||
|
(offset_delta, correlation_score))
|
||||||
|
|
||||||
|
def get_aggregated(self):
|
||||||
|
|
||||||
|
pairs_dict = defaultdict(list)
|
||||||
|
|
||||||
|
for alarm_pair in self.correlation_list:
|
||||||
|
if ((alarm_pair[AlarmsProperties.ALARM1_RESOURCE_TYPE],
|
||||||
|
alarm_pair[AlarmsProperties.ALARM1_NAME],
|
||||||
|
alarm_pair[AlarmsProperties.ALARM2_RESOURCE_TYPE],
|
||||||
|
alarm_pair[AlarmsProperties.ALARM2_NAME])) in pairs_dict:
|
||||||
|
|
||||||
|
pairs_dict[alarm_pair[AlarmsProperties.ALARM1_RESOURCE_TYPE],
|
||||||
|
alarm_pair[AlarmsProperties.ALARM1_NAME],
|
||||||
|
alarm_pair[AlarmsProperties.ALARM2_RESOURCE_TYPE],
|
||||||
|
alarm_pair[AlarmsProperties.ALARM2_NAME]]\
|
||||||
|
.append(alarm_pair[AlarmsProperties.CORR_SCORE])
|
||||||
|
else:
|
||||||
|
pairs_dict[alarm_pair[AlarmsProperties.ALARM2_RESOURCE_TYPE],
|
||||||
|
alarm_pair[AlarmsProperties.ALARM2_NAME],
|
||||||
|
alarm_pair[AlarmsProperties.ALARM1_RESOURCE_TYPE],
|
||||||
|
alarm_pair[AlarmsProperties.ALARM1_NAME]].\
|
||||||
|
append(alarm_pair[AlarmsProperties.CORR_SCORE])
|
||||||
|
|
||||||
|
results = [(key, sum((pairs_dict[key])) / float(len(pairs_dict[key])))
|
||||||
|
for key in pairs_dict]
|
||||||
|
|
||||||
|
categorize = lambda x: CorrelationPriorities.HIGH \
|
||||||
|
if x[1] >= self.high_corr_score \
|
||||||
|
else CorrelationPriorities.MEDIUM \
|
||||||
|
if x[1] >= self.med_corr_score else CorrelationPriorities.LOW
|
||||||
|
|
||||||
|
return [(key, [(x, y) for x, y in group]) for
|
||||||
|
key, group in groupby(sorted(results, key=itemgetter(1)),
|
||||||
|
key=categorize)]
|
@ -0,0 +1,114 @@
|
|||||||
|
# Copyright 2017 - Nokia
|
||||||
|
#
|
||||||
|
# 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 os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from vitrage.machine_learning.plugins.jaccard_correlation.\
|
||||||
|
correlation_collection import CorrelationCollection as CCollection
|
||||||
|
from vitrage.machine_learning.plugins.jaccard_correlation.\
|
||||||
|
correlation_collection import CorrelationPriorities as CPriorities
|
||||||
|
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CorrelationManager(object):
|
||||||
|
|
||||||
|
def __init__(self, conf):
|
||||||
|
self.high_corr_score = conf.jaccard_correlation.high_corr_score
|
||||||
|
self.med_corr_score = conf.jaccard_correlation.med_corr_score
|
||||||
|
self.correlation_threshold = \
|
||||||
|
conf.jaccard_correlation.correlation_threshold
|
||||||
|
self.output_folder = conf.jaccard_correlation.output_folder
|
||||||
|
self.last_written_file = ""
|
||||||
|
self.correlation_table = CCollection(self.high_corr_score,
|
||||||
|
self.med_corr_score)
|
||||||
|
|
||||||
|
def output_correlations(self, accumulated_data):
|
||||||
|
now = int(time.time())
|
||||||
|
report = self._generate_report(accumulated_data)
|
||||||
|
|
||||||
|
self._dump_correlations(str(now) + "_correlations.out", dict(report))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _jaccard_score(alarm_1, alarm_2, accumulated_data):
|
||||||
|
key = frozenset([alarm_1, alarm_2])
|
||||||
|
intersect = accumulated_data.alarms_intersects.get(key)
|
||||||
|
|
||||||
|
if not intersect:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
a1_time = accumulated_data.alarms_activity.get(alarm_1)
|
||||||
|
a2_time = accumulated_data.alarms_activity.get(alarm_2)
|
||||||
|
if not a1_time or not a2_time:
|
||||||
|
LOG.error("One of the alarms given has never been active")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
combined = a1_time + a2_time - intersect
|
||||||
|
# jaccard is intersection / union of alarm times
|
||||||
|
return intersect.total_seconds() / combined.total_seconds()
|
||||||
|
|
||||||
|
def _generate_report(self, accumulated_data):
|
||||||
|
|
||||||
|
for alarm_1, alarm_2 in accumulated_data.alarms_intersects.keys():
|
||||||
|
|
||||||
|
jacc_score = \
|
||||||
|
self._jaccard_score(alarm_1, alarm_2, accumulated_data)
|
||||||
|
|
||||||
|
if jacc_score >= self.correlation_threshold:
|
||||||
|
self.correlation_table.set(alarm_1, alarm_2, 0, jacc_score)
|
||||||
|
|
||||||
|
# mean correlations divided to HIGH, MEDIUM and LOW correlation scores
|
||||||
|
report = self.correlation_table.get_aggregated()
|
||||||
|
return report
|
||||||
|
|
||||||
|
def _dump_correlations(self, output_path, alarms):
|
||||||
|
|
||||||
|
new_file_name = "{}/{}".format(self.output_folder, output_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(new_file_name, 'w') as f:
|
||||||
|
LOG.info("Correlation manager wrote a new file: {}/{}".format
|
||||||
|
(self.output_folder, output_path))
|
||||||
|
|
||||||
|
for correlation_level, correlation_bar \
|
||||||
|
in [(CPriorities.HIGH,
|
||||||
|
" (> {})".format(self.high_corr_score)),
|
||||||
|
(CPriorities.MEDIUM,
|
||||||
|
" (> {})".format(self.med_corr_score)),
|
||||||
|
(CPriorities.LOW,
|
||||||
|
"(< {})".format(self.med_corr_score))]:
|
||||||
|
if correlation_level in alarms:
|
||||||
|
|
||||||
|
title = correlation_level + \
|
||||||
|
" correlation" + correlation_bar + ":"
|
||||||
|
|
||||||
|
f.write("\n" + title + "\n" +
|
||||||
|
("-" * len(title)) + "\n")
|
||||||
|
|
||||||
|
for alarm in alarms[correlation_level]:
|
||||||
|
f.write("alarm " + alarm[0][1] + " on " +
|
||||||
|
alarm[0][0] + " <-> alarm " +
|
||||||
|
alarm[0][3] + " on " + alarm[0][2] +
|
||||||
|
" with score " + str(alarm[1]) + "\n")
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception('Cannot save correlations - %s', e)
|
||||||
|
|
||||||
|
if os.path.isfile(self.last_written_file):
|
||||||
|
os.remove(self.last_written_file)
|
||||||
|
|
||||||
|
self.last_written_file = new_file_name
|
85
vitrage/machine_learning/service.py
Normal file
85
vitrage/machine_learning/service.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# Copyright 2017 - Nokia
|
||||||
|
#
|
||||||
|
# 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 oslo_log import log
|
||||||
|
import oslo_messaging as oslo_m
|
||||||
|
from oslo_service import service as os_service
|
||||||
|
from oslo_utils import importutils
|
||||||
|
|
||||||
|
from vitrage import messaging
|
||||||
|
from vitrage.opts import register_opts
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MachineLearningService(os_service.Service):
|
||||||
|
|
||||||
|
def __init__(self, conf):
|
||||||
|
super(MachineLearningService, self).__init__()
|
||||||
|
self.conf = conf
|
||||||
|
self.machine_learning_plugins = self.get_machine_learning_plugins(conf)
|
||||||
|
transport = messaging.get_transport(conf)
|
||||||
|
target = \
|
||||||
|
oslo_m.Target(topic=conf.machine_learning.machine_learning_topic)
|
||||||
|
self.listener = messaging.get_notification_listener(
|
||||||
|
transport, [target],
|
||||||
|
[VitrageEventEndpoint(self.machine_learning_plugins)])
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
LOG.info("Vitrage Machine Learning Service - Starting...")
|
||||||
|
|
||||||
|
super(MachineLearningService, self).start()
|
||||||
|
self.listener.start()
|
||||||
|
|
||||||
|
LOG.info("Vitrage Machine Learning Service - Started!")
|
||||||
|
|
||||||
|
def stop(self, graceful=False):
|
||||||
|
LOG.info("Vitrage Machine Learning Service - Stopping...")
|
||||||
|
|
||||||
|
self.listener.stop()
|
||||||
|
self.listener.wait()
|
||||||
|
super(MachineLearningService, self).stop(graceful)
|
||||||
|
|
||||||
|
LOG.info("Vitrage Machine Learning Service - Stopped!")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_machine_learning_plugins(conf):
|
||||||
|
machine_learning_plugins = []
|
||||||
|
machine_learning_plugins_names = \
|
||||||
|
conf.machine_learning.plugins
|
||||||
|
if not machine_learning_plugins_names:
|
||||||
|
LOG.info('There are no Machine Learning plugins in configuration')
|
||||||
|
return []
|
||||||
|
for machine_learning_plugin_name in machine_learning_plugins_names:
|
||||||
|
register_opts(conf, machine_learning_plugin_name,
|
||||||
|
conf.machine_learning.plugins_path)
|
||||||
|
LOG.info('Machine Learning plugin %s started',
|
||||||
|
machine_learning_plugin_name)
|
||||||
|
machine_learning_plugins.append(importutils.import_object(
|
||||||
|
conf[machine_learning_plugin_name].plugin_path,
|
||||||
|
conf))
|
||||||
|
return machine_learning_plugins
|
||||||
|
|
||||||
|
|
||||||
|
class VitrageEventEndpoint(object):
|
||||||
|
|
||||||
|
def __init__(self, machine_learning_plugins):
|
||||||
|
self.machine_learning_plugins = machine_learning_plugins
|
||||||
|
|
||||||
|
def info(self, ctxt, publisher_id, event_type, payload, metadata):
|
||||||
|
"""Endpoint for alarm notifications"""
|
||||||
|
LOG.info('Vitrage Event Info: event_type %s', event_type)
|
||||||
|
LOG.info('Vitrage Event Info: payload %s', payload)
|
||||||
|
for plugin in self.machine_learning_plugins:
|
||||||
|
plugin.process_event(payload, event_type)
|
@ -23,6 +23,8 @@ import vitrage.datasources
|
|||||||
import vitrage.entity_graph.consistency
|
import vitrage.entity_graph.consistency
|
||||||
import vitrage.evaluator
|
import vitrage.evaluator
|
||||||
import vitrage.keystone_client
|
import vitrage.keystone_client
|
||||||
|
import vitrage.machine_learning
|
||||||
|
import vitrage.machine_learning.plugins.jaccard_correlation
|
||||||
import vitrage.notifier
|
import vitrage.notifier
|
||||||
import vitrage.notifier.plugins.snmp
|
import vitrage.notifier.plugins.snmp
|
||||||
import vitrage.os_clients
|
import vitrage.os_clients
|
||||||
@ -44,6 +46,10 @@ def list_opts():
|
|||||||
('consistency', vitrage.entity_graph.consistency.OPTS),
|
('consistency', vitrage.entity_graph.consistency.OPTS),
|
||||||
('entity_graph', vitrage.entity_graph.OPTS),
|
('entity_graph', vitrage.entity_graph.OPTS),
|
||||||
('service_credentials', vitrage.keystone_client.OPTS),
|
('service_credentials', vitrage.keystone_client.OPTS),
|
||||||
|
('machine_learning',
|
||||||
|
vitrage.machine_learning.OPTS),
|
||||||
|
('jaccard_correlation',
|
||||||
|
vitrage.machine_learning.plugins.jaccard_correlation.OPTS),
|
||||||
('snmp', vitrage.notifier.plugins.snmp.OPTS),
|
('snmp', vitrage.notifier.plugins.snmp.OPTS),
|
||||||
('DEFAULT', itertools.chain(
|
('DEFAULT', itertools.chain(
|
||||||
vitrage.os_clients.OPTS,
|
vitrage.os_clients.OPTS,
|
||||||
|
15
vitrage/tests/unit/machine_learning/__init__.py
Normal file
15
vitrage/tests/unit/machine_learning/__init__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright 2017 - Alcatel-Lucent
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
__author__ = 'stack'
|
@ -0,0 +1,15 @@
|
|||||||
|
# Copyright 2017 - Nokia
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
__author__ = 'stack'
|
@ -0,0 +1,386 @@
|
|||||||
|
# Copyright 2017 - Nokia
|
||||||
|
#
|
||||||
|
# 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 datetime
|
||||||
|
import os.path
|
||||||
|
from oslo_config import cfg
|
||||||
|
import time
|
||||||
|
|
||||||
|
from vitrage.common.constants import EntityCategory
|
||||||
|
from vitrage.common.constants import VertexProperties as VProps
|
||||||
|
from vitrage.datasources.aodh import AODH_DATASOURCE
|
||||||
|
from vitrage.datasources.transformer_base import TIMESTAMP_FORMAT
|
||||||
|
from vitrage.datasources.zabbix import ZABBIX_DATASOURCE
|
||||||
|
from vitrage.evaluator.actions import evaluator_event_transformer as evaluator
|
||||||
|
from vitrage.graph import Vertex
|
||||||
|
from vitrage.machine_learning.plugins.jaccard_correlation.\
|
||||||
|
accumulation_persistor_utils import AccumulatedData as AData
|
||||||
|
from vitrage.machine_learning.plugins.jaccard_correlation.\
|
||||||
|
alarm_data_accumulator import AlarmDataAccumulator as ADAccumulator
|
||||||
|
from vitrage.machine_learning.plugins.jaccard_correlation.\
|
||||||
|
alarm_processor import AlarmDataProcessor as ADProcessor
|
||||||
|
from vitrage.machine_learning.plugins.jaccard_correlation.\
|
||||||
|
alarm_processor import AlarmID
|
||||||
|
from vitrage.machine_learning.plugins.jaccard_correlation.\
|
||||||
|
correlation_collection import CorrelationCollection as CCollection
|
||||||
|
from vitrage.machine_learning.plugins.jaccard_correlation.correlation_manager \
|
||||||
|
import CorrelationManager as CManager
|
||||||
|
from vitrage.machine_learning.plugins.jaccard_correlation.\
|
||||||
|
correlation_collection import CorrelationPriorities as CPriorities
|
||||||
|
from vitrage.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
ACTIVE_TIMESTAMP = datetime.datetime.utcnow()
|
||||||
|
ACTIVE_TIMESTAMP = ACTIVE_TIMESTAMP.strftime(TIMESTAMP_FORMAT)
|
||||||
|
INACTIVE_TIMESTAMP = \
|
||||||
|
datetime.datetime.utcnow() + datetime.timedelta(minutes=10)
|
||||||
|
INACTIVE_TIMESTAMP = INACTIVE_TIMESTAMP.strftime(TIMESTAMP_FORMAT)
|
||||||
|
|
||||||
|
DEDUCED_ALARM_1 = Vertex('111', {
|
||||||
|
VProps.VITRAGE_CATEGORY: EntityCategory.ALARM,
|
||||||
|
VProps.VITRAGE_TYPE: evaluator.VITRAGE_DATASOURCE,
|
||||||
|
VProps.VITRAGE_IS_DELETED: False,
|
||||||
|
VProps.VITRAGE_IS_PLACEHOLDER: False,
|
||||||
|
VProps.NAME: 'deduced_alarm_1',
|
||||||
|
VProps.UPDATE_TIMESTAMP: ACTIVE_TIMESTAMP,
|
||||||
|
VProps.VITRAGE_RESOURCE_ID: '111',
|
||||||
|
VProps.VITRAGE_RESOURCE_TYPE: 'resource_111',
|
||||||
|
})
|
||||||
|
|
||||||
|
AODH_ALARM_1 = Vertex('11', {
|
||||||
|
VProps.VITRAGE_CATEGORY: EntityCategory.ALARM,
|
||||||
|
VProps.VITRAGE_TYPE: AODH_DATASOURCE,
|
||||||
|
VProps.VITRAGE_IS_DELETED: False,
|
||||||
|
VProps.VITRAGE_IS_PLACEHOLDER: False,
|
||||||
|
VProps.NAME: 'aodh_alarm_1',
|
||||||
|
VProps.UPDATE_TIMESTAMP: ACTIVE_TIMESTAMP,
|
||||||
|
VProps.VITRAGE_RESOURCE_ID: '11',
|
||||||
|
VProps.VITRAGE_RESOURCE_TYPE: 'resource_11',
|
||||||
|
})
|
||||||
|
|
||||||
|
ZABBIX_ALARM_1 = Vertex('1111', {
|
||||||
|
VProps.VITRAGE_CATEGORY: EntityCategory.ALARM,
|
||||||
|
VProps.VITRAGE_TYPE: ZABBIX_DATASOURCE,
|
||||||
|
VProps.VITRAGE_IS_DELETED: False,
|
||||||
|
VProps.VITRAGE_IS_PLACEHOLDER: True,
|
||||||
|
VProps.NAME: 'zabbix_alarm_1 {}',
|
||||||
|
VProps.RAWTEXT: 'zabbix_alarm_1',
|
||||||
|
VProps.UPDATE_TIMESTAMP: ACTIVE_TIMESTAMP,
|
||||||
|
VProps.VITRAGE_RESOURCE_ID: '1111',
|
||||||
|
VProps.VITRAGE_RESOURCE_TYPE: 'resource_1111',
|
||||||
|
})
|
||||||
|
|
||||||
|
ZABBIX_ALARM_2 = Vertex('2222', {
|
||||||
|
VProps.VITRAGE_CATEGORY: EntityCategory.ALARM,
|
||||||
|
VProps.VITRAGE_TYPE: ZABBIX_DATASOURCE,
|
||||||
|
VProps.VITRAGE_IS_DELETED: False,
|
||||||
|
VProps.VITRAGE_IS_PLACEHOLDER: True,
|
||||||
|
VProps.NAME: 'zabbix_alarm_2 {}',
|
||||||
|
VProps.RAWTEXT: 'zabbix_alarm_2',
|
||||||
|
VProps.UPDATE_TIMESTAMP: ACTIVE_TIMESTAMP,
|
||||||
|
VProps.VITRAGE_RESOURCE_ID: '2222',
|
||||||
|
VProps.VITRAGE_RESOURCE_TYPE: 'resource_2222',
|
||||||
|
})
|
||||||
|
|
||||||
|
DELETED_DEDUCED_ALARM_1 = Vertex('111', {
|
||||||
|
VProps.VITRAGE_CATEGORY: EntityCategory.ALARM,
|
||||||
|
VProps.VITRAGE_TYPE: evaluator.VITRAGE_DATASOURCE,
|
||||||
|
VProps.VITRAGE_IS_DELETED: True,
|
||||||
|
VProps.VITRAGE_IS_PLACEHOLDER: False,
|
||||||
|
VProps.NAME: 'deduced_alarm_1',
|
||||||
|
VProps.UPDATE_TIMESTAMP: INACTIVE_TIMESTAMP,
|
||||||
|
VProps.VITRAGE_RESOURCE_ID: '111',
|
||||||
|
VProps.VITRAGE_RESOURCE_TYPE: 'resource_111',
|
||||||
|
})
|
||||||
|
|
||||||
|
DELETED_AODH_ALARM_1 = Vertex('11', {
|
||||||
|
VProps.VITRAGE_CATEGORY: EntityCategory.ALARM,
|
||||||
|
VProps.VITRAGE_TYPE: evaluator.VITRAGE_DATASOURCE,
|
||||||
|
VProps.VITRAGE_IS_DELETED: True,
|
||||||
|
VProps.VITRAGE_IS_PLACEHOLDER: False,
|
||||||
|
VProps.NAME: 'aodh_alarm_1',
|
||||||
|
VProps.UPDATE_TIMESTAMP: INACTIVE_TIMESTAMP,
|
||||||
|
VProps.VITRAGE_RESOURCE_ID: '11',
|
||||||
|
VProps.VITRAGE_RESOURCE_TYPE: 'resource_11',
|
||||||
|
})
|
||||||
|
|
||||||
|
DELETED_ZABBIX_ALARM_1 = Vertex('1111', {
|
||||||
|
VProps.VITRAGE_CATEGORY: EntityCategory.ALARM,
|
||||||
|
VProps.VITRAGE_TYPE: ZABBIX_DATASOURCE,
|
||||||
|
VProps.VITRAGE_IS_DELETED: True,
|
||||||
|
VProps.VITRAGE_IS_PLACEHOLDER: False,
|
||||||
|
VProps.NAME: 'zabbix_alarm_1 {}',
|
||||||
|
VProps.RAWTEXT: 'zabbix_alarm_1',
|
||||||
|
VProps.UPDATE_TIMESTAMP: INACTIVE_TIMESTAMP,
|
||||||
|
VProps.VITRAGE_RESOURCE_ID: '1111',
|
||||||
|
VProps.VITRAGE_RESOURCE_TYPE: 'resource_1111',
|
||||||
|
})
|
||||||
|
|
||||||
|
DELETED_ZABBIX_ALARM_2 = Vertex('2222', {
|
||||||
|
VProps.VITRAGE_CATEGORY: EntityCategory.ALARM,
|
||||||
|
VProps.VITRAGE_TYPE: ZABBIX_DATASOURCE,
|
||||||
|
VProps.VITRAGE_IS_DELETED: True,
|
||||||
|
VProps.VITRAGE_IS_PLACEHOLDER: False,
|
||||||
|
VProps.NAME: 'zabbix_alarm_2 {}',
|
||||||
|
VProps.RAWTEXT: 'zabbix_alarm_2',
|
||||||
|
VProps.UPDATE_TIMESTAMP: INACTIVE_TIMESTAMP,
|
||||||
|
VProps.VITRAGE_RESOURCE_ID: '2222',
|
||||||
|
VProps.VITRAGE_RESOURCE_TYPE: 'resource_2222',
|
||||||
|
})
|
||||||
|
|
||||||
|
ACTIVE_ALARMS = [DEDUCED_ALARM_1, AODH_ALARM_1,
|
||||||
|
ZABBIX_ALARM_1, ZABBIX_ALARM_2]
|
||||||
|
|
||||||
|
INACTIVE_ALARMS = [DELETED_DEDUCED_ALARM_1, DELETED_AODH_ALARM_1,
|
||||||
|
DELETED_ZABBIX_ALARM_1, DELETED_ZABBIX_ALARM_2]
|
||||||
|
|
||||||
|
|
||||||
|
class JaccardCorrelationTest(base.BaseTest):
|
||||||
|
OPTS = [
|
||||||
|
cfg.StrOpt('output_folder', default='/tmp',
|
||||||
|
help='folder to write all reports to'),
|
||||||
|
cfg.FloatOpt('correlation_threshold', default=0,
|
||||||
|
help='threshold of interesting correlations'),
|
||||||
|
cfg.FloatOpt('high_corr_score', default=0.9,
|
||||||
|
help='high correlation lower limit'),
|
||||||
|
cfg.FloatOpt('med_corr_score', default=0.5,
|
||||||
|
help='medium correlation lower limit'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
|
||||||
|
cls.conf = cfg.ConfigOpts()
|
||||||
|
cls.conf.register_opts(cls.OPTS, group='jaccard_correlation')
|
||||||
|
|
||||||
|
cls.data_manager = ADAccumulator(AData({}, {}))
|
||||||
|
cls.collection = \
|
||||||
|
CCollection(cls.conf.jaccard_correlation.high_corr_score,
|
||||||
|
cls.conf.jaccard_correlation.med_corr_score)
|
||||||
|
cls.correlation_manager = CManager(cls.conf)
|
||||||
|
cls.activate_timestamps = {}
|
||||||
|
cls.inactivate_timestamps = {}
|
||||||
|
cls.alarm_ids = cls._setup_expected_active_alarms_ids()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _setup_expected_active_alarms_ids():
|
||||||
|
alarm_ids = []
|
||||||
|
for alarm in ACTIVE_ALARMS:
|
||||||
|
alarm_name = alarm[VProps.RAWTEXT] if alarm.get(VProps.RAWTEXT) \
|
||||||
|
else alarm[VProps.NAME]
|
||||||
|
alarm_id = AlarmID(alarm.get(VProps.VITRAGE_RESOURCE_ID),
|
||||||
|
alarm.get(VProps.VITRAGE_RESOURCE_TYPE),
|
||||||
|
alarm_name)
|
||||||
|
alarm_ids.append(alarm_id)
|
||||||
|
|
||||||
|
return alarm_ids
|
||||||
|
|
||||||
|
def test_jaccard_correlation(self):
|
||||||
|
self._test_alarm_data_accumulations()
|
||||||
|
self._test_correlation_collection()
|
||||||
|
self._test_correlation_manager()
|
||||||
|
|
||||||
|
def _test_alarm_data_accumulations(self):
|
||||||
|
self._test_append_active()
|
||||||
|
self._test_flush_accumulations()
|
||||||
|
self._test_append_inactive()
|
||||||
|
|
||||||
|
def _test_append_active(self):
|
||||||
|
|
||||||
|
expected_active_start_dict = {}
|
||||||
|
real_alarm_ids = []
|
||||||
|
|
||||||
|
for alarm in ACTIVE_ALARMS:
|
||||||
|
|
||||||
|
alarm_name = alarm[VProps.RAWTEXT] if alarm.get(VProps.RAWTEXT) \
|
||||||
|
else alarm[VProps.NAME]
|
||||||
|
|
||||||
|
alarm_id, timestamp = ADProcessor.\
|
||||||
|
_get_alarm_id_and_timestamp(alarm, alarm_name)
|
||||||
|
|
||||||
|
self.activate_timestamps[alarm_id] = timestamp
|
||||||
|
expected_active_start_dict[alarm_id] = \
|
||||||
|
datetime.datetime.strptime(alarm.get(VProps.UPDATE_TIMESTAMP),
|
||||||
|
TIMESTAMP_FORMAT)
|
||||||
|
|
||||||
|
real_alarm_ids.append(alarm_id)
|
||||||
|
self.data_manager.append_active(alarm_id, timestamp)
|
||||||
|
|
||||||
|
# assert all alarm ids are right
|
||||||
|
for i in range(len(self.alarm_ids)):
|
||||||
|
self.assertEqual(self.alarm_ids[i], real_alarm_ids[i], '')
|
||||||
|
|
||||||
|
self.assertEqual(expected_active_start_dict,
|
||||||
|
self.data_manager.active_start_times)
|
||||||
|
|
||||||
|
self.assertEqual({}, self.data_manager.alarms_activity)
|
||||||
|
self.assertEqual({}, self.data_manager.alarms_intersects)
|
||||||
|
|
||||||
|
def _test_flush_accumulations(self):
|
||||||
|
|
||||||
|
prev_active_start_dict = self.data_manager.active_start_times
|
||||||
|
|
||||||
|
time.sleep(2)
|
||||||
|
self.data_manager.flush_accumulations()
|
||||||
|
|
||||||
|
self.assertEqual(prev_active_start_dict,
|
||||||
|
self.data_manager.active_start_times)
|
||||||
|
|
||||||
|
expected_activity_dict_len = len(ACTIVE_ALARMS)
|
||||||
|
self.assertEqual(expected_activity_dict_len,
|
||||||
|
len(self.data_manager.alarms_activity))
|
||||||
|
|
||||||
|
# choose 2
|
||||||
|
expected_intersections_dict_len = \
|
||||||
|
(len(ACTIVE_ALARMS) * (len(ACTIVE_ALARMS) - 1)) / 2
|
||||||
|
self.assertEqual(expected_intersections_dict_len,
|
||||||
|
len(self.data_manager.alarms_intersects))
|
||||||
|
|
||||||
|
def _test_append_inactive(self):
|
||||||
|
deleted_alarm_ids = []
|
||||||
|
|
||||||
|
for alarm in INACTIVE_ALARMS:
|
||||||
|
alarm_name = alarm[VProps.RAWTEXT] if alarm.get(VProps.RAWTEXT) \
|
||||||
|
else alarm[VProps.NAME]
|
||||||
|
|
||||||
|
alarm_id, timestamp = ADProcessor.\
|
||||||
|
_get_alarm_id_and_timestamp(alarm, alarm_name)
|
||||||
|
|
||||||
|
expected_alarm_id = \
|
||||||
|
AlarmID(alarm.get(VProps.VITRAGE_RESOURCE_ID),
|
||||||
|
alarm.get(VProps.VITRAGE_RESOURCE_TYPE),
|
||||||
|
alarm_name)
|
||||||
|
|
||||||
|
self.assertEqual(expected_alarm_id, alarm_id, '')
|
||||||
|
|
||||||
|
self.inactivate_timestamps[alarm_id] = timestamp
|
||||||
|
deleted_alarm_ids.append(alarm_id)
|
||||||
|
|
||||||
|
self.data_manager.append_inactive(alarm_id, timestamp)
|
||||||
|
|
||||||
|
# assert all deleted alarms has same alarm ids as activated alarms
|
||||||
|
self.assertEqual(self.alarm_ids, deleted_alarm_ids)
|
||||||
|
|
||||||
|
# all alarm are inactive at this moment
|
||||||
|
expected_active_start_dict = {}
|
||||||
|
self.assertEqual(expected_active_start_dict,
|
||||||
|
self.data_manager.active_start_times)
|
||||||
|
|
||||||
|
expected_activity_dict = {}
|
||||||
|
|
||||||
|
for alarm_id in self.alarm_ids:
|
||||||
|
expected_activity_dict[alarm_id] = \
|
||||||
|
self.inactivate_timestamps[alarm_id]\
|
||||||
|
- self.activate_timestamps[alarm_id]
|
||||||
|
|
||||||
|
self.assertEqual(expected_activity_dict,
|
||||||
|
self.data_manager.alarms_activity)
|
||||||
|
|
||||||
|
expected_intersections_dict = {}
|
||||||
|
|
||||||
|
# all alarms started and ended at the same time,
|
||||||
|
# intersection time equals activity time
|
||||||
|
for alarm_id1 in self.alarm_ids:
|
||||||
|
for alarm_id2 in self.alarm_ids:
|
||||||
|
# exclude intersection of alarm with itself
|
||||||
|
if alarm_id1 != alarm_id2:
|
||||||
|
key = frozenset([alarm_id1, alarm_id2])
|
||||||
|
expected_intersections_dict[key] = \
|
||||||
|
self.inactivate_timestamps[alarm_id]\
|
||||||
|
- self.activate_timestamps[alarm_id]
|
||||||
|
|
||||||
|
self.assertEqual(expected_intersections_dict,
|
||||||
|
self.data_manager.alarms_intersects)
|
||||||
|
|
||||||
|
def _test_correlation_collection(self):
|
||||||
|
self._test_correlation_list()
|
||||||
|
self._test_correlations_aggregation()
|
||||||
|
self.collection = CCollection(0.9, 0.5)
|
||||||
|
|
||||||
|
def _test_correlation_list(self):
|
||||||
|
offset_delta = 0
|
||||||
|
high_correlation = 0.9
|
||||||
|
med_correlation = 0.7
|
||||||
|
low_correlation = 0.4
|
||||||
|
|
||||||
|
correlations = [high_correlation, med_correlation, low_correlation]
|
||||||
|
alarms_pairs = []
|
||||||
|
cnt = 0
|
||||||
|
|
||||||
|
seen_pairs = []
|
||||||
|
|
||||||
|
for alarm1 in self.alarm_ids:
|
||||||
|
for alarm2 in self.alarm_ids:
|
||||||
|
if alarm1 != alarm2 \
|
||||||
|
and frozenset([alarm1, alarm2]) not in seen_pairs:
|
||||||
|
seen_pairs.append(frozenset([alarm1, alarm2]))
|
||||||
|
correlation = correlations[cnt % 3]
|
||||||
|
alarms_pairs.append((alarm1 + alarm2 +
|
||||||
|
(offset_delta, correlation)))
|
||||||
|
|
||||||
|
self.collection.set(alarm1, alarm2, offset_delta,
|
||||||
|
correlation)
|
||||||
|
cnt += 1
|
||||||
|
|
||||||
|
self.assertEqual(alarms_pairs, self.collection.correlation_list)
|
||||||
|
|
||||||
|
def _test_correlations_aggregation(self):
|
||||||
|
|
||||||
|
aggregated = self.collection.get_aggregated()
|
||||||
|
cnt_high_correlations = 0
|
||||||
|
cnt_med_correlations = 0
|
||||||
|
cnt_low_correlations = 0
|
||||||
|
|
||||||
|
for correlation_level in aggregated:
|
||||||
|
if correlation_level[0] == CPriorities.HIGH:
|
||||||
|
cnt_high_correlations = len(correlation_level[1])
|
||||||
|
if correlation_level[0] == CPriorities.MEDIUM:
|
||||||
|
cnt_med_correlations = len(correlation_level[1])
|
||||||
|
if correlation_level[0] == CPriorities.LOW:
|
||||||
|
cnt_low_correlations = len(correlation_level[1])
|
||||||
|
|
||||||
|
self.assertEqual(cnt_high_correlations, 2, '')
|
||||||
|
self.assertEqual(cnt_med_correlations, 2, '')
|
||||||
|
self.assertEqual(cnt_low_correlations, 2, '')
|
||||||
|
|
||||||
|
def _test_correlation_manager(self):
|
||||||
|
report = []
|
||||||
|
self._test_generate_report(report)
|
||||||
|
self._test_dump_correlations(report)
|
||||||
|
|
||||||
|
def _test_generate_report(self, report):
|
||||||
|
self.data_manager.flush_accumulations()
|
||||||
|
report.extend(self.correlation_manager.
|
||||||
|
_generate_report(self.data_manager))
|
||||||
|
|
||||||
|
# all intersects correlations are 1
|
||||||
|
self.assertEqual(CPriorities.HIGH, report[0][0])
|
||||||
|
self.assertEqual(len(self.data_manager.alarms_intersects),
|
||||||
|
len(report[0][1]))
|
||||||
|
|
||||||
|
def _test_dump_correlations(self, report):
|
||||||
|
now = int(time.time())
|
||||||
|
|
||||||
|
self.correlation_manager.\
|
||||||
|
_dump_correlations(str(now) + "_correlations_test.out",
|
||||||
|
report)
|
||||||
|
|
||||||
|
file_path = self.conf.jaccard_correlation.output_folder + '/' + \
|
||||||
|
str(now) + "_correlations_test.out"
|
||||||
|
is_file = os.path.isfile(file_path)
|
||||||
|
|
||||||
|
self.assertEqual(is_file, True)
|
||||||
|
|
||||||
|
if os.path.isfile(file_path):
|
||||||
|
os.remove(file_path)
|
Loading…
Reference in New Issue
Block a user