diff --git a/devstack/settings b/devstack/settings index cf6286389..b4032ae02 100644 --- a/devstack/settings +++ b/devstack/settings @@ -24,7 +24,7 @@ VITRAGE_USE_MOD_WSGI=${VITRAGE_USE_MOD_WSGI:-${ENABLE_HTTPD_MOD_WSGI_SERVICES}} # Toggle for deploying Vitrage with/without nagios VITRAGE_USE_NAGIOS=$(trueorfalse False VITRAGE_USE_NAGIOS) -VITRAGE_DEFAULT_DATASOURCES=${VITRAGE_DEFAULT_DATASOURCES:-nova.host,nova.instance,nova.zone,nagios,static_physical,aodh,cinder.volume,neutron.network,neutron.port,heat.stack} +VITRAGE_DEFAULT_DATASOURCES=${VITRAGE_DEFAULT_DATASOURCES:-nova.host,nova.instance,nova.zone,nagios,static_physical,aodh,cinder.volume,neutron.network,neutron.port,heat.stack,doctor} # Tell Tempest this project is present diff --git a/etc/vitrage/policy.json b/etc/vitrage/policy.json index 051c69f40..e1d8ec4b3 100644 --- a/etc/vitrage/policy.json +++ b/etc/vitrage/policy.json @@ -9,5 +9,6 @@ "get rca:all_tenants": "role:admin", "template validate": "", "template list": "", - "template show": "" + "template show": "", + "event post": "" } \ No newline at end of file diff --git a/vitrage/api/controllers/v1/event.py b/vitrage/api/controllers/v1/event.py new file mode 100644 index 000000000..49fa9997d --- /dev/null +++ b/vitrage/api/controllers/v1/event.py @@ -0,0 +1,53 @@ +# Copyright 2017 - Nokia Corporation +# +# 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 pecan + +from oslo_log import log +from pecan.core import abort + +from vitrage.api.controllers.rest import RootRestController +from vitrage.api.policy import enforce +from vitrage.i18n import _LI + + +LOG = log.getLogger(__name__) + + +class EventController(RootRestController): + + @pecan.expose('json') + def post(self, **kwargs): + LOG.info(_LI('Post event called with args: %s') % kwargs) + + enforce("event post", pecan.request.headers, + pecan.request.enforcer, {}) + + event_time = kwargs['time'] + event_type = kwargs['type'] + details = kwargs['details'] + + self.post_event(event_time, event_type, details) + + @staticmethod + def post_event(event_time, event_type, details): + try: + pecan.request.client.call(pecan.request.context, + 'post', + event_time=event_time, + event_type=event_type, + details=details) + except Exception as e: + LOG.exception('Failed to post an event', e) + abort(404, str(e)) diff --git a/vitrage/api/controllers/v1/root.py b/vitrage/api/controllers/v1/root.py index cd50744dc..148712030 100644 --- a/vitrage/api/controllers/v1/root.py +++ b/vitrage/api/controllers/v1/root.py @@ -11,6 +11,7 @@ # under the License. from vitrage.api.controllers.v1 import alarm +from vitrage.api.controllers.v1 import event from vitrage.api.controllers.v1 import rca from vitrage.api.controllers.v1 import resource from vitrage.api.controllers.v1 import template @@ -23,3 +24,4 @@ class V1Controller(object): alarm = alarm.AlarmsController() rca = rca.RCAController() template = template.TemplateController() + event = event.EventController() diff --git a/vitrage/api_handler/apis/event.py b/vitrage/api_handler/apis/event.py new file mode 100644 index 000000000..18f709ccb --- /dev/null +++ b/vitrage/api_handler/apis/event.py @@ -0,0 +1,62 @@ +# Copyright 2017 - Nokia Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from datetime import datetime +import json +from oslo_log import log +import oslo_messaging +from oslo_utils import uuidutils +import socket + +from vitrage.api_handler.apis.base import EntityGraphApisBase +from vitrage.common.constants import EventProperties +from vitrage.messaging import get_transport + +LOG = log.getLogger(__name__) + + +class EventApis(EntityGraphApisBase): + + def __init__(self, conf): + self.conf = conf + self._init_oslo_notifier() + + def post(self, ctx, event_time, event_type, details): + try: + event = {EventProperties.TYPE: event_type, + EventProperties.TIME: event_time, + EventProperties.DETAILS: json.loads(details)} + + self.oslo_notifier.info( + ctxt={'message_id': uuidutils.generate_uuid(), + 'publisher_id': self.publisher, + 'timestamp': datetime.utcnow()}, + event_type=event_type, + payload=event) + except Exception as e: + LOG.warn('Failed to post event %s. Exception: %s', + event_type, e) + + def _init_oslo_notifier(self): + self.oslo_notifier = None + try: + self.publisher = 'api_%s' % socket.gethostname() + + self.oslo_notifier = oslo_messaging.Notifier( + get_transport(self.conf), + driver='messagingv2', + publisher_id=self.publisher, + topic='vitrage_notifications') + except Exception as e: + LOG.info('Failed to initialize oslo notifier %s', str(e)) diff --git a/vitrage/api_handler/service.py b/vitrage/api_handler/service.py index 970e360b1..c139cca48 100644 --- a/vitrage/api_handler/service.py +++ b/vitrage/api_handler/service.py @@ -18,6 +18,7 @@ import oslo_messaging from oslo_service import service as os_service from vitrage.api_handler.apis.alarm import AlarmApis +from vitrage.api_handler.apis.event import EventApis from vitrage.api_handler.apis.rca import RcaApis from vitrage.api_handler.apis.template import TemplateApis from vitrage.api_handler.apis.topology import TopologyApis @@ -50,7 +51,8 @@ class VitrageApiHandlerService(os_service.Service): endpoints = [TopologyApis(self.entity_graph, self.conf), AlarmApis(self.entity_graph, self.conf), RcaApis(self.entity_graph, self.conf), - TemplateApis(self.scenario_repo.templates)] + TemplateApis(self.scenario_repo.templates), + EventApis(self.conf)] server = vitrage_rpc.get_server(target, endpoints, transport) diff --git a/vitrage/common/constants.py b/vitrage/common/constants.py index c28d47cc6..c38ae0e5d 100644 --- a/vitrage/common/constants.py +++ b/vitrage/common/constants.py @@ -129,3 +129,10 @@ class TopologyFields(object): RELATIONSHIP_TYPE = 'relationship_type' SOURCE = 'source' TARGET = 'target' + + +class EventProperties(object): + TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%f' + TYPE = 'type' + TIME = 'time' + DETAILS = 'details' diff --git a/vitrage/datasources/doctor/driver.py b/vitrage/datasources/doctor/driver.py index 5996a6fd6..edef76b1b 100644 --- a/vitrage/datasources/doctor/driver.py +++ b/vitrage/datasources/doctor/driver.py @@ -17,6 +17,7 @@ from oslo_log import log from vitrage.common.constants import DatasourceAction from vitrage.common.constants import DatasourceProperties as DSProps +from vitrage.common.constants import EventProperties as EventProps from vitrage.datasources.alarm_driver_base import AlarmDriverBase from vitrage.datasources.doctor import DOCTOR_DATASOURCE from vitrage.datasources.doctor.properties import DoctorDetails @@ -40,7 +41,7 @@ class DoctorDriver(AlarmDriverBase): return DOCTOR_DATASOURCE def _alarm_key(self, alarm): - return self.AlarmKey(alarm_name=alarm[DoctorProps.TYPE], + return self.AlarmKey(alarm_name=alarm[EventProps.TYPE], hostname=get_detail(alarm, DoctorDetails.HOSTNAME)) @@ -49,12 +50,12 @@ class DoctorDriver(AlarmDriverBase): get_detail(alarm, DoctorDetails.STATUS) != DoctorStatus.UP def _is_valid(self, alarm): - if not alarm or DoctorProps.TIME not in alarm or \ - DoctorProps.TYPE not in alarm or \ - DoctorProps.DETAILS not in alarm: + if not alarm or EventProps.TIME not in alarm or \ + EventProps.TYPE not in alarm or \ + EventProps.DETAILS not in alarm: return False - details = alarm[DoctorProps.DETAILS] + details = alarm[EventProps.DETAILS] return DoctorDetails.STATUS in details and \ DoctorDetails.SEVERITY in details and \ DoctorDetails.HOSTNAME in details @@ -91,17 +92,21 @@ class DoctorDriver(AlarmDriverBase): """ + LOG.debug('Going to enrich event: %s', str(event)) + event[DSProps.EVENT_TYPE] = event_type old_alarm = self._old_alarm(event) if old_alarm and not self._status_changed(old_alarm, event): event[DoctorProps.UPDATE_TIME] = old_alarm[DoctorProps.UPDATE_TIME] else: - event[DoctorProps.UPDATE_TIME] = event[DoctorProps.TIME] + event[DoctorProps.UPDATE_TIME] = event[EventProps.TIME] event = self._filter_and_cache_alarm(event, old_alarm, self._filter_get_erroneous, - event[DoctorProps.TIME]) + event[EventProps.TIME]) + + LOG.debug('Enriched event: %s', str(event)) if event: return DoctorDriver.make_pickleable([event], DOCTOR_DATASOURCE, diff --git a/vitrage/datasources/doctor/properties.py b/vitrage/datasources/doctor/properties.py index 582f218c8..a69fa3b1d 100644 --- a/vitrage/datasources/doctor/properties.py +++ b/vitrage/datasources/doctor/properties.py @@ -12,15 +12,13 @@ # License for the specific language governing permissions and limitations # under the License. +from vitrage.common.constants import EventProperties as EventProps + class DoctorProperties(object): - TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%f' HOST_DOWN = 'compute.host.down' HOST_TYPE = 'nova.host' - TYPE = 'type' - TIME = 'time' UPDATE_TIME = 'update_time' - DETAILS = 'details' class DoctorDetails(object): @@ -36,7 +34,7 @@ class DoctorStatus(object): def get_detail(alarm, detail): - return alarm[DoctorProperties.DETAILS][detail] if \ - alarm and DoctorProperties.DETAILS in alarm and \ - detail in alarm[DoctorProperties.DETAILS] \ + return alarm[EventProps.DETAILS][detail] if \ + alarm and EventProps.DETAILS in alarm and \ + detail in alarm[EventProps.DETAILS] \ else None diff --git a/vitrage/datasources/doctor/transformer.py b/vitrage/datasources/doctor/transformer.py index 4e267a72d..8ba2431a4 100644 --- a/vitrage/datasources/doctor/transformer.py +++ b/vitrage/datasources/doctor/transformer.py @@ -17,6 +17,7 @@ from oslo_log import log as logging from vitrage.common.constants import DatasourceProperties as DSProps from vitrage.common.constants import EdgeLabel from vitrage.common.constants import EntityCategory +from vitrage.common.constants import EventProperties as EventProps from vitrage.common.constants import VertexProperties as VProps from vitrage.datasources.alarm_transformer_base import AlarmTransformerBase from vitrage.datasources.doctor import DOCTOR_DATASOURCE @@ -44,9 +45,9 @@ class DoctorTransformer(AlarmTransformerBase): def _create_update_entity_vertex(self, entity_event): self._unify_time_format(entity_event) - details = entity_event.get(DoctorProps.DETAILS, {}) - details[VProps.NAME] = entity_event[DoctorProps.TYPE] - details[DoctorProps.TIME] = entity_event[DoctorProps.TIME] + details = entity_event.get(EventProps.DETAILS, {}) + details[VProps.NAME] = entity_event[EventProps.TYPE] + details[EventProps.TIME] = entity_event[EventProps.TIME] return graph_utils.create_vertex( self._create_entity_key(entity_event), @@ -69,7 +70,7 @@ class DoctorTransformer(AlarmTransformerBase): return tbase.build_key(( EntityCategory.ALARM, entity_event[DSProps.ENTITY_TYPE], - entity_event[DoctorProps.TYPE], + entity_event[EventProps.TYPE], get_detail(entity_event, DoctorDetails.HOSTNAME))) def get_type(self): @@ -89,7 +90,7 @@ class DoctorTransformer(AlarmTransformerBase): @staticmethod def _unify_time_format(entity_event): DoctorTransformer._unify_prop_time_format(entity_event, - DoctorProps.TIME) + EventProps.TIME) DoctorTransformer._unify_prop_time_format(entity_event, DoctorProps.UPDATE_TIME) @@ -97,5 +98,5 @@ class DoctorTransformer(AlarmTransformerBase): def _unify_prop_time_format(entity_event, prop): entity_event[prop] = change_time_str_format( entity_event[prop], - DoctorProps.TIME_FORMAT, + EventProps.TIME_FORMAT, tbase.TIMESTAMP_FORMAT) diff --git a/vitrage/tests/unit/datasources/doctor/test_doctor_driver.py b/vitrage/tests/unit/datasources/doctor/test_doctor_driver.py index 1297946c3..494d26d73 100644 --- a/vitrage/tests/unit/datasources/doctor/test_doctor_driver.py +++ b/vitrage/tests/unit/datasources/doctor/test_doctor_driver.py @@ -16,6 +16,7 @@ from datetime import datetime from oslo_config import cfg from vitrage.common.constants import DatasourceProperties as DSProps +from vitrage.common.constants import EventProperties as EventProps from vitrage.datasources.doctor.driver import DoctorDriver from vitrage.datasources.doctor.properties import DoctorDetails from vitrage.datasources.doctor.properties import DoctorProperties \ @@ -107,9 +108,9 @@ class DoctorDriverTest(base.BaseTest): if status: details[DoctorDetails.STATUS] = status - update_vals = {DoctorProps.DETAILS: details} + update_vals = {EventProps.DETAILS: details} if time: - update_vals[DoctorProps.TIME] = time + update_vals[EventProps.TIME] = time generators = mock_driver.simple_doctor_alarm_generators( update_vals=update_vals) @@ -125,9 +126,9 @@ class DoctorDriverTest(base.BaseTest): expected_update_date): self.assertIsNotNone(event, 'No event returned') self.assertEqual(expected_hostname, - event[DoctorProps.DETAILS][DoctorDetails.HOSTNAME]) + event[EventProps.DETAILS][DoctorDetails.HOSTNAME]) self.assertEqual(expected_status, - event[DoctorProps.DETAILS][DoctorDetails.STATUS]) - self.assertEqual(expected_sample_date, event[DoctorProps.TIME]) + event[EventProps.DETAILS][DoctorDetails.STATUS]) + self.assertEqual(expected_sample_date, event[EventProps.TIME]) self.assertEqual(expected_update_date, event[DoctorProps.UPDATE_TIME]) self.assertEqual(expected_event_type, event[DSProps.EVENT_TYPE]) diff --git a/vitrage/tests/unit/datasources/doctor/test_doctor_transformer.py b/vitrage/tests/unit/datasources/doctor/test_doctor_transformer.py index ddabd24ce..00521e407 100644 --- a/vitrage/tests/unit/datasources/doctor/test_doctor_transformer.py +++ b/vitrage/tests/unit/datasources/doctor/test_doctor_transformer.py @@ -17,6 +17,7 @@ from oslo_config import cfg from oslo_log import log as logging from vitrage.common.constants import DatasourceProperties as DSProps +from vitrage.common.constants import EventProperties as EventProps from vitrage.common.constants import UpdateMethod from vitrage.datasources.doctor import DOCTOR_DATASOURCE from vitrage.datasources.doctor.properties import DoctorDetails @@ -94,7 +95,7 @@ class DoctorTransformerTest(BaseAlarmTransformerTest): def _validate_vertex_props(self, vertex, event): self._validate_alarm_vertex_props(vertex, - event[DoctorProps.TYPE], + event[EventProps.TYPE], DOCTOR_DATASOURCE, event[DSProps.SAMPLE_DATE]) @@ -106,9 +107,9 @@ class DoctorTransformerTest(BaseAlarmTransformerTest): if status: details[DoctorDetails.STATUS] = status - update_vals = {DoctorProps.DETAILS: details} + update_vals = {EventProps.DETAILS: details} if time: - update_vals[DoctorProps.TIME] = time + update_vals[EventProps.TIME] = time update_vals[DoctorProps.UPDATE_TIME] = time generators = mock_transformer.simple_doctor_alarm_generators( diff --git a/vitrage_tempest_tests/tests/api/event/__init__.py b/vitrage_tempest_tests/tests/api/event/__init__.py new file mode 100644 index 000000000..bf9f61d74 --- /dev/null +++ b/vitrage_tempest_tests/tests/api/event/__init__.py @@ -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' diff --git a/vitrage_tempest_tests/tests/api/event/test_events.py b/vitrage_tempest_tests/tests/api/event/test_events.py new file mode 100644 index 000000000..1d81cbdef --- /dev/null +++ b/vitrage_tempest_tests/tests/api/event/test_events.py @@ -0,0 +1,83 @@ +# 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 datetime import datetime +from oslo_log import log as logging + +from vitrage.common.constants import DatasourceProperties as DSProps +from vitrage.common.constants import EntityCategory +from vitrage.common.constants import EventProperties as EventProps +from vitrage.common.constants import VertexProperties as VProps +from vitrage_tempest_tests.tests.api.base import BaseApiTest + +LOG = logging.getLogger(__name__) + + +class TestEvents(BaseApiTest): + """Test class for Vitrage event API""" + + @classmethod + def setUpClass(cls): + super(TestEvents, cls).setUpClass() + + def test_send_doctor_event(self): + """Sending an event in Doctor format should result in an alarm""" + try: + # post an event to the message bus + event_time = datetime.now().isoformat() + event_type = 'compute.host.down' + details = { + 'hostname': 'host123', + 'source': 'sample_monitor', + 'cause': 'another alarm', + 'severity': 'critical', + 'status': 'down', + 'monitor_id': 'sample monitor', + 'monitor_event_id': '456', + } + + self.vitrage_client.event.post(event_time, event_type, details) + + # list all alarms + api_alarms = self.vitrage_client.alarms.list(vitrage_id=None) + + # expect to get a 'host down alarm', generated by Doctor datasource + self.assertIsNotNone(api_alarms, 'Expected host down alarm') + self.assertEqual(1, len(api_alarms), 'Expected host down alarm') + alarm = api_alarms[0] + + self._wait_for_status(2, + self._check_alarm, + alarm=alarm, + event_time=event_time, + event_type=event_type, + details=details) + + except Exception as e: + LOG.exception(e) + raise + finally: + # do what? + LOG.warning('done') + + def _check_alarm(self, alarm, event_time, event_type, details): + LOG.info('alarm = %s', str(alarm)) + self.assertEqual(EntityCategory.ALARM, alarm[VProps.CATEGORY]) + self.assertEqual(event_type, alarm[VProps.NAME]) + self.assertEqual(event_time, alarm[EventProps.TIME]) + self.assertEqual(event_type, alarm[DSProps.ENTITY_TYPE]) + self.assertEqual(details['status'], alarm[VProps.STATE]) + self.assertFalse(alarm[VProps.IS_DELETED]) + self.assertFalse(alarm[VProps.IS_PLACEHOLDER])