Add the PATCH method for the monitoring API 83/12783/3
authorZhang Rong(Jon) <rong.zhang@windriver.com>
Thu, 25 Apr 2024 15:12:19 +0000 (23:12 +0800)
committerZhang Rong(Jon) <rong.zhang@windriver.com>
Sun, 28 Apr 2024 09:12:37 +0000 (17:12 +0800)
Since the specification release, a new API has been added to the
monitoring interface. Which is an acknowledged operation for the
alarm event.

This commit will support SMO requests the O2IMS API to
acknowledge and clear the alarm event.

Test Plan:
1. Request the API to acknowledge the alarm.
2. After ack the alarm, it was requested the stx clear
   its alarm.

Issue-ID: INF-449

Change-Id: Ic2cb07cbc0784793d1b1de637b7c8a6c29d3f6a2
Signed-off-by: Zhang Rong(Jon) <rong.zhang@windriver.com>
12 files changed:
o2app/entrypoints/redis_eventconsumer.py
o2app/service/handlers.py
o2ims/adapter/alarm_repository.py
o2ims/adapter/clients/fault_client.py
o2ims/domain/alarm_repo.py
o2ims/domain/commands.py
o2ims/domain/events.py
o2ims/service/command/purge_alarm_handler.py [new file with mode: 0644]
o2ims/service/event/alarm_event.py
o2ims/views/alarm_route.py
o2ims/views/alarm_view.py
tests/conftest.py

index c95133a..439619b 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2021 Wind River Systems, Inc.
+# Copyright (C) 2021-2024 Wind River Systems, Inc.
 #
 #  Licensed under the Apache License, Version 2.0 (the "License");
 #  you may not use this file except in compliance with the License.
@@ -49,6 +49,7 @@ def main():
     pubsub.subscribe('DmsChanged')
     pubsub.subscribe('ResourceChanged')
     pubsub.subscribe('AlarmEventChanged')
+    pubsub.subscribe('AlarmEventPurged')
 
     for m in pubsub.listen():
         try:
@@ -138,6 +139,17 @@ def handle_changed(m, bus):
             eventtype=data['notificationEventType'],
             updatetime=data['updatetime']))
         bus.handle(cmd)
+    elif channel == 'AlarmEventPurged':
+        datastr = m['data']
+        data = json.loads(datastr)
+        logger.info('AlarmEventPurged with cmd:{}'.format(data))
+        ref = api_monitoring_base + \
+            monitor_api_version + '/alarms/' + data['id']
+        cmd = imscmd.PurgeAlarmEvent(data=AlarmEvent2SMO(
+            id=data['id'], ref=ref,
+            eventtype=data['notificationEventType'],
+            updatetime=data['updatetime']))
+        bus.handle(cmd)
     else:
         logger.info("unhandled:{}".format(channel))
 
index 9b4f49d..4ef0ef8 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2021 Wind River Systems, Inc.
+# Copyright (C) 2021-2024 Wind River Systems, Inc.
 #
 #  Licensed under the Apache License, Version 2.0 (the "License");
 #  you may not use this file except in compliance with the License.
@@ -30,7 +30,7 @@ from o2ims.service.auditor import ocloud_handler, dms_handler, \
     pserver_dev_handler, agg_compute_handler, agg_network_handler,\
     agg_storage_handler, agg_undefined_handler
 from o2ims.service.command import notify_handler, registration_handler,\
-    notify_alarm_handler
+    notify_alarm_handler, purge_alarm_handler
 from o2ims.service.event import ocloud_event, resource_event, \
     resource_pool_event, alarm_event, dms_event, resource_type_event
 
@@ -63,6 +63,8 @@ EVENT_HANDLERS = {
                                  notify_resourcepool_change],
     events.AlarmEventChanged: [alarm_event.\
                                notify_alarm_event_change],
+    events.AlarmEventPurged: [alarm_event.\
+                               notify_alarm_event_purge],
 }  # type: Dict[Type[events.Event], Callable]
 
 
@@ -95,4 +97,5 @@ COMMAND_HANDLERS = {
     commands.PubMessage2SMO: notify_handler.notify_change_to_smo,
     commands.PubAlarm2SMO: notify_alarm_handler.notify_alarm_to_smo,
     commands.Register2SMO: registration_handler.registry_to_smo,
+    commands.PurgeAlarmEvent: purge_alarm_handler.purge_alarm_event,
 }  # type: Dict[Type[commands.Command], Callable]
index 483c73c..ebfe816 100644 (file)
@@ -47,7 +47,7 @@ class AlarmEventRecordSqlAlchemyRepository(AlarmEventRecordRepository):
         return (count, result)
 
     def _update(self, alarm_event_record: alarm_obj.AlarmEventRecord):
-        self.session.add(alarm_event_record)
+        self.session.merge(alarm_event_record)
 
     def _delete(self, alarm_event_record_id):
         self.session.query(alarm_obj.AlarmEventRecord).filter_by(
index 53602b4..c9e1e95 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2022 Wind River Systems, Inc.
+# Copyright (C) 2022-2024 Wind River Systems, Inc.
 #
 #  Licensed under the Apache License, Version 2.0 (the "License");
 #  you may not use this file except in compliance with the License.
@@ -94,9 +94,10 @@ class StxAlarmClient(BaseClient):
 
 
 class StxEventClient(BaseClient):
-    def __init__(self, driver=None):
+    def __init__(self, uow: unit_of_work.AbstractUnitOfWork, driver=None):
         super().__init__()
         self.driver = driver if driver else StxFaultClientImp()
+        self.uow = uow
 
     def _get(self, id) -> alarmModel.FaultGenericModel:
         return self.driver.getEventInfo(id)
@@ -107,6 +108,12 @@ class StxEventClient(BaseClient):
     def _set_stx_client(self):
         self.driver.setFaultClient(self._pool_id)
 
+    def suppression_list(self, alarm_id) -> List[alarmModel.FaultGenericModel]:
+        return self.driver.getSuppressionList(alarm_id)
+
+    def suppress(self, id) -> alarmModel.FaultGenericModel:
+        return self.driver.suppressEvent(id)
+
 
 # internal driver which implement client call to Stx Fault Management instance
 class StxFaultClientImp(object):
@@ -219,6 +226,27 @@ class StxFaultClientImp(object):
         return alarmModel.FaultGenericModel(
             alarmModel.EventTypeEnum.EVENT, self._eventconverter(event))
 
+    def suppressEvent(self, id) -> alarmModel.FaultGenericModel:
+        patch = [dict(path='/' + 'suppression_status', value='suppressed',
+                      op='replace')]
+        event = self.fmclient.event_suppression.update(id, patch)
+        logger.debug('suppressed event id ' + id + ':' + str(event.to_dict()))
+        return alarmModel.FaultGenericModel(
+            alarmModel.EventTypeEnum.EVENT, self._suppression_converter(event))
+
+    def getSuppressionList(self, alarm_id) -> alarmModel.FaultGenericModel:
+        suppression_list = []
+        queryAsArray = []
+        events = self.fmclient.event_suppression.list(q=queryAsArray)
+        for event in events:
+            if event.alarm_id == alarm_id:
+                # logger.debug('suppression event:' + str(event.to_dict()))
+                suppression_list.append(
+                    alarmModel.FaultGenericModel(
+                        alarmModel.EventTypeEnum.EVENT,
+                        self._suppression_converter(event)))
+        return suppression_list
+
     @ staticmethod
     def _alarmconverter(alarm):
         selected_keys = [
@@ -264,6 +292,27 @@ class StxFaultClientImp(object):
                 uuid.NAMESPACE_URL, event.probable_cause)))
         return event
 
+    @ staticmethod
+    def _suppression_converter(event, clear=False):
+        selected_keys = [
+            'alarm_id', 'description', 'suppression_status',
+            'links'
+        ]
+        content = event.to_dict()
+        filtered = dict(
+            filter(lambda item: item[0] in selected_keys, content.items()))
+        setattr(event, 'filtered', filtered)
+        setattr(event, 'uuid', event.uuid)
+        setattr(event, 'alarm_id', event.alarm_id)
+        setattr(event, 'description', event.description)
+        setattr(event, 'suppression_status', event.suppression_status)
+        setattr(event, 'alarm_type', None)
+        setattr(event, 'alarm_def_id', None)
+        setattr(event, 'probable_cause_id', None)
+        setattr(event, 'state', None)
+        setattr(event, 'timestamp', None)
+        return event
+
     @ staticmethod
     def _alarmeventhasher(event, state=''):
         # The event model and the alarm model have different parameter name
index ca6929e..1ef92a4 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2022 Wind River Systems, Inc.
+# Copyright (C) 2022-2024 Wind River Systems, Inc.
 #
 #  Licensed under the Apache License, Version 2.0 (the "License");
 #  you may not use this file except in compliance with the License.
index 0caca37..9cc51d6 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2021 Wind River Systems, Inc.
+# Copyright (C) 2021-2024 Wind River Systems, Inc.
 #
 #  Licensed under the Apache License, Version 2.0 (the "License");
 #  you may not use this file except in compliance with the License.
@@ -138,3 +138,8 @@ class UpdatePserverAcc(UpdateResource):
 @dataclass
 class UpdateAlarm(UpdateFaultObject):
     parentid: str
+
+
+@dataclass
+class PurgeAlarmEvent(UpdateFaultObject):
+    data: AlarmEvent2SMO
index ede48d2..04e8a08 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2021 Wind River Systems, Inc.
+# Copyright (C) 2021-2024 Wind River Systems, Inc.
 #
 #  Licensed under the Apache License, Version 2.0 (the "License");
 #  you may not use this file except in compliance with the License.
@@ -62,3 +62,10 @@ class AlarmEventChanged(Event):
     id: str
     notificationEventType: AlarmNotificationEventEnum
     updatetime: datetime.now()
+
+
+@dataclass
+class AlarmEventPurged(Event):
+    id: str
+    notificationEventType: AlarmNotificationEventEnum
+    updatetime: datetime.now()
diff --git a/o2ims/service/command/purge_alarm_handler.py b/o2ims/service/command/purge_alarm_handler.py
new file mode 100644 (file)
index 0000000..390cfe2
--- /dev/null
@@ -0,0 +1,51 @@
+# Copyright (C) 2024 Wind River Systems, Inc.
+#
+#  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 json
+
+from o2common.service.unit_of_work import AbstractUnitOfWork
+from o2common.adapter.notifications import AbstractNotifications
+
+from o2ims.adapter.clients.fault_client import StxEventClient
+from o2ims.domain import commands, alarm_obj
+
+from o2common.helper import o2logging
+logger = o2logging.get_logger(__name__)
+
+
+def purge_alarm_event(
+    cmd: commands.PubAlarm2SMO,
+    uow: AbstractUnitOfWork,
+    notifications: AbstractNotifications,
+):
+    logger.debug('In purge_alarm_event')
+    fault_client = StxEventClient(uow)
+    data = cmd.data
+    with uow:
+        alarm_event_record = uow.alarm_event_records.get(data.id)
+        alarm_id = json.loads(alarm_event_record.extensions).get('alarm_id')
+
+        events = fault_client.suppression_list(alarm_id)
+        for event_id in events:
+            event = fault_client.suppress(event_id.id)
+            alarm_event_record.hash = event.hash
+            alarm_event_record.extensions = json.dumps(event.filtered)
+            alarm_event_record.alarmChangedTime = event.updatetime.\
+                strftime("%Y-%m-%dT%H:%M:%S")
+            alarm_event_record.perceivedSeverity = \
+                alarm_obj.PerceivedSeverityEnum.CLEARED
+
+            uow.alarm_event_records.update(alarm_event_record)
+            break
+        uow.commit()
index 5e80668..ff005d6 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2021 Wind River Systems, Inc.
+# Copyright (C) 2021-2024 Wind River Systems, Inc.
 #
 #  Licensed under the Apache License, Version 2.0 (the "License");
 #  you may not use this file except in compliance with the License.
@@ -28,3 +28,13 @@ def notify_alarm_event_change(
     publish("AlarmEventChanged", event)
     logger.debug("published Alarm Event Changed: {}".format(
         event.id))
+
+
+def notify_alarm_event_purge(
+    event: events.AlarmEventPurged,
+    publish: Callable,
+):
+    logger.debug('In notify_alarm_event_purge')
+    publish("AlarmEventPurged", event)
+    logger.debug("published Alarm Event Purged: {}".format(
+        event.id))
index e7a6425..8b0af04 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2021 Wind River Systems, Inc.
+# Copyright (C) 2021-2024 Wind River Systems, Inc.
 #
 #  Licensed under the Apache License, Version 2.0 (the "License");
 #  you may not use this file except in compliance with the License.
@@ -140,6 +140,15 @@ class AlarmGetRouter(Resource):
         raise NotFoundException(
             "Alarm Event Record {} doesn't exist".format(alarmEventRecordId))
 
+    @api_monitoring_v1.doc('Patch Alarm Event Record Information')
+    @api_monitoring_v1.marshal_with(model)
+    def patch(self, alarmEventRecordId):
+        result = alarm_view.alarm_event_record_ack(alarmEventRecordId, bus.uow)
+        if result is not None:
+            return result
+        raise NotFoundException(
+            "Alarm Event Record {} doesn't exist".format(alarmEventRecordId))
+
 
 # ----------  Alarm Subscriptions ---------- #
 @api_monitoring_v1.route("/v1/alarmSubscriptions")
index 83189c1..9f7af04 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2021 Wind River Systems, Inc.
+# Copyright (C) 2021-2024 Wind River Systems, Inc.
 #
 #  Licensed under the Apache License, Version 2.0 (the "License");
 #  you may not use this file except in compliance with the License.
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
+from datetime import datetime
 import uuid as uuid
 
-from o2common.service import unit_of_work
+from o2common.service import unit_of_work, messagebus
 from o2common.views.view import gen_filter, check_filter
 from o2common.views.pagination_view import Pagination
 from o2common.views.route_exception import BadRequestException, \
     NotFoundException
 
+from o2ims.domain import events
 from o2ims.views.alarm_dto import SubscriptionDTO
-from o2ims.domain.alarm_obj import AlarmSubscription, AlarmEventRecord
+from o2ims.domain.alarm_obj import AlarmSubscription, AlarmEventRecord, \
+    AlarmNotificationEventEnum
 
 from o2common.helper import o2logging
 # from o2common.config import config
@@ -45,6 +48,41 @@ def alarm_event_record_one(alarmEventRecordId: str,
         return first.serialize() if first is not None else None
 
 
+def alarm_event_record_ack(alarmEventRecordId: str,
+                           uow: unit_of_work.AbstractUnitOfWork):
+    with uow:
+        alarm_event_record = uow.alarm_event_records.get(alarmEventRecordId)
+        if alarm_event_record is None:
+            return None
+        elif alarm_event_record.alarmAcknowledged == 'true':
+            raise BadRequestException(
+                "Alarm Event Record {} has already been acknowledged."
+                .format(alarmEventRecordId))
+        alarm_event_record.alarmAcknowledged = True
+        alarm_event_record.alarmAcknowledgeTime = datetime.\
+            now().strftime("%Y-%m-%dT%H:%M:%S")
+        bus = messagebus.MessageBus.get_instance()
+        alarm_event_record.events.append(events.AlarmEventPurged(
+            id=alarm_event_record.alarmEventRecordId,
+            notificationEventType=AlarmNotificationEventEnum.ACKNOWLEDGE,
+            updatetime=alarm_event_record.alarmAcknowledgeTime))
+
+        uow.alarm_event_records.update(alarm_event_record)
+        uow.commit()
+
+        result = alarm_event_record.serialize()
+    _handle_events(bus)
+    return result
+
+
+def _handle_events(bus: messagebus.MessageBus):
+    # handle events
+    events = bus.uow.collect_new_events()
+    for event in events:
+        bus.handle(event)
+    return True
+
+
 def subscriptions(uow: unit_of_work.AbstractUnitOfWork, **kwargs):
     pagination = Pagination(**kwargs)
     query_kwargs = pagination.get_pagination()
index b653ae8..447351b 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2022 Wind River Systems, Inc.
+# Copyright (C) 2022-2024 Wind River Systems, Inc.
 #
 #  Licensed under the Apache License, Version 2.0 (the "License");
 #  you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
 # pylint: disable=redefined-outer-name
 import shutil
 import subprocess
+import sys
 import time
 from pathlib import Path
 
@@ -29,6 +30,23 @@ from tenacity import retry, stop_after_delay
 from unittest.mock import MagicMock
 from mock_alchemy.mocking import UnifiedAlchemyMagicMock
 
+# Mock cgtsclient, dcmanagerclient, fmclient
+modules_to_mock = [
+    'cgtsclient',
+    'cgtsclient.client',
+    'cgtsclient.exc',
+    'dcmanagerclient',
+    'dcmanagerclient.api',
+    'dcmanagerclient.api.client',
+    'fmclient',
+    'fmclient.client',
+    'fmclient.common',
+    'fmclient.common.exceptions'
+]
+
+for module_name in modules_to_mock:
+    sys.modules[module_name] = MagicMock()
+
 from o2app.bootstrap import bootstrap
 from o2ims.views import configure_namespace
 from o2app.adapter import unit_of_work