Add the PATCH method for the monitoring API 98/12898/7
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>
Sat, 25 May 2024 06:43:47 +0000 (14:43 +0800)
In the specification release R003-v05.00, a new API has been
added to the monitoring interface, which is an acknowledge/clear
operation for the alarm event.

This commit implements the O2IMS API to support SMO's requests of
alarm event acknowledgment or clearing.
The acknowledge operation will update the alarmEventRecord object,
mark the acknowledged field as True.
The clear operation will call the fault client asynchronously to
delete the alarm of the O-Cloud.

Test Plan:
1. Request the API to acknowledge/clear the alarm.
2. Request with both "alarmAcknowledged" and "perceivedSeverity"
   attribute failed since it only supports one, not both.
3. Request with "perceivedSeverity" only support "5" as expected
   which is "CLEARED".
4. After SMO requests the alarm clear to IMS via IMS monitoring API,
   FM(Fault Management) client is requested to delete the alarm
   from the O-Cloud.

Issue-ID: INF-449

Change-Id: I71419a9d467687d6708bfd3e9cd201dbce5e36c5
Signed-off-by: Zhang Rong(Jon) <rong.zhang@windriver.com>
13 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_obj.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_dto.py
o2ims/views/alarm_route.py
o2ims/views/alarm_view.py
tests/conftest.py

index c95133a..db39daa 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.
 
 import redis
 import json
+
 from o2app import bootstrap
-from o2common.config import config
 from o2common.adapter.notifications import SmoNotifications
+from o2common.config import config
+from o2common.helper import o2logging
 from o2dms.domain import commands
 from o2ims.domain import commands as imscmd
 from o2ims.domain.subscription_obj import Message2SMO, RegistrationMessage
 from o2ims.domain.alarm_obj import AlarmEvent2SMO
 
-from o2common.helper import o2logging
 logger = o2logging.get_logger(__name__)
 
 r = redis.Redis(**config.get_redis_host_and_port())
@@ -49,6 +50,7 @@ def main():
     pubsub.subscribe('DmsChanged')
     pubsub.subscribe('ResourceChanged')
     pubsub.subscribe('AlarmEventChanged')
+    pubsub.subscribe('AlarmEventPurged')
 
     for m in pubsub.listen():
         try:
@@ -138,6 +140,14 @@ 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))
+        cmd = imscmd.PurgeAlarmEvent(data=AlarmEvent2SMO(
+            id=data['id'], 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..5986ad5 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.
@@ -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 989edfc..74abe57 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.
@@ -97,11 +97,15 @@ class StxAlarmClient(BaseClient):
     def _set_stx_client(self):
         self.driver.setFaultClient(self._pool_id)
 
+    def delete(self, id) -> alarmModel.FaultGenericModel:
+        return self.driver.deleteAlarm(id)
+
 
 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)
@@ -112,6 +116,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):
@@ -212,6 +222,12 @@ class StxFaultClientImp(object):
         return alarmModel.FaultGenericModel(
             alarmModel.EventTypeEnum.ALARM, self._alarmconverter(alarm))
 
+    def deleteAlarm(self, id) -> alarmModel.FaultGenericModel:
+        alarm = self.fmclient.alarm.delete(id)
+        logger.debug('delete alarm id ' + id + ':' + str(alarm.to_dict()))
+        return alarmModel.FaultGenericModel(
+            alarmModel.EventTypeEnum.ALARM, self._alarmconverter(alarm))
+
     def getEventList(self, **filters) -> List[alarmModel.FaultGenericModel]:
         events = self.fmclient.event_log.list(alarms=True, expand=True)
         [logger.debug(
@@ -226,6 +242,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 = []
+        query_as_array = []
+        events = self.fmclient.event_suppression.list(q=query_as_array)
+        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 = [
@@ -271,6 +308,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 050e7b0..800c6d7 100644 (file)
@@ -125,6 +125,14 @@ class ClearingTypeEnum(str, Enum):
     MANUAL = 'MANUAL'
 
 
+class AlarmEventRecordModifications(AgRoot):
+    def __init__(self, ack: bool = None,
+                 clear: PerceivedSeverityEnum = None) -> None:
+        super().__init__()
+        self.alarmAcknowledged = ack
+        self.perceivedSeverity = clear
+
+
 class AlarmDefinition(AgRoot, Serializer):
     def __init__(self, id: str, name: str, change_type: AlarmChangeTypeEnum,
                  desc: str, prop_action: str, clearing_type: ClearingTypeEnum,
@@ -170,9 +178,8 @@ class AlarmNotificationEventEnum(str, Enum):
 
 class AlarmEvent2SMO(Serializer):
     def __init__(self, eventtype: AlarmNotificationEventEnum,
-                 id: str, ref: str, updatetime: str) -> None:
+                 id: str, updatetime: str) -> None:
         self.notificationEventType = eventtype
-        self.objectRef = ref
         self.id = id
         self.updatetime = updatetime
 
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..335488a
--- /dev/null
@@ -0,0 +1,132 @@
+# 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.adapter.notifications import AbstractNotifications
+from o2common.config import conf
+from o2common.domain.filter import gen_orm_filter
+from o2common.helper import o2logging
+from o2common.service.unit_of_work import AbstractUnitOfWork
+from o2ims.adapter.clients.fault_client import StxAlarmClient
+from o2ims.domain import commands, alarm_obj
+logger = o2logging.get_logger(__name__)
+
+
+def purge_alarm_event(
+    cmd: commands.PubAlarm2SMO,
+    uow: AbstractUnitOfWork,
+    notifications: AbstractNotifications,
+):
+    """
+    Purges an alarm event and notifies relevant subscribers.
+
+    This method performs the following steps:
+    1. Retrieves data from the command object and initializes the fault client.
+    2. Uses the Unit of Work pattern to find and delete the corresponding
+       alarm event record.
+    3. Updates the alarm event record's hash, extensions, changed time,
+       and perceived severity.
+    4. Commits the changes to the database.
+    5. Finds and processes all alarm subscriptions, deciding whether to
+       send notifications based on subscription filters.
+
+    Parameters:
+    - cmd (commands.PubAlarm2SMO): Command object containing the alarm
+      event data.
+    - uow (AbstractUnitOfWork): Unit of Work object for managing
+      database transactions.
+    - notifications (AbstractNotifications): Abstract notifications
+      object for sending notifications.
+
+    Exceptions:
+    - Any exceptions that might occur during database operations or
+      notification sending.
+    """
+    fault_client = StxAlarmClient(uow)
+    data = cmd.data
+    with uow:
+        alarm_event_record = uow.alarm_event_records.get(data.id)
+        alarm = fault_client.delete(alarm_event_record.alarmEventRecordId)
+        alarm_event_record.hash = alarm.hash
+        alarm_event_record.extensions = json.dumps(alarm.filtered)
+        alarm_event_record.alarmChangedTime = alarm.updatetime.\
+            strftime("%Y-%m-%dT%H:%M:%S")
+        alarm_event_record.perceivedSeverity = \
+            alarm_obj.PerceivedSeverityEnum.CLEARED
+
+        uow.alarm_event_records.update(alarm_event_record)
+
+        uow.commit()
+
+        alarm = uow.alarm_event_records.get(data.id)
+        subs = uow.alarm_subscriptions.list()
+        for sub in subs:
+            sub_data = sub.serialize()
+            logger.debug('Alarm Subscription: {}'.format(
+                sub_data['alarmSubscriptionId']))
+
+            if not sub_data.get('filter', None):
+                callback_smo(notifications, sub, data, alarm)
+                continue
+            try:
+                args = gen_orm_filter(alarm_obj.AlarmEventRecord,
+                                      sub_data['filter'])
+            except KeyError:
+                logger.warning(
+                    'Alarm Subscription {} filter {} has wrong attribute '
+                    'name or value. Ignore the filter'.format(
+                        sub_data['alarmSubscriptionId'],
+                        sub_data['filter']))
+                callback_smo(notifications, sub, data, alarm)
+                continue
+            args.append(alarm_obj.AlarmEventRecord.
+                        alarmEventRecordId == data.id)
+            count, _ = uow.alarm_event_records.list_with_count(*args)
+            if count != 0:
+                logger.debug(
+                    'Alarm Event {} skip for subscription {} because of '
+                    'the filter.'
+                    .format(data.id, sub_data['alarmSubscriptionId']))
+                continue
+            callback_smo(notifications, sub, data, alarm)
+
+
+def callback_smo(notifications: AbstractNotifications,
+                 sub: alarm_obj.AlarmSubscription,
+                 msg: alarm_obj.AlarmEvent2SMO,
+                 alarm: alarm_obj.AlarmEventRecord):
+    sub_data = sub.serialize()
+    alarm_data = alarm.serialize()
+    callback = {
+        'globalCloudID': conf.DEFAULT.ocloud_global_id,
+        'consumerSubscriptionId': sub_data['consumerSubscriptionId'],
+        'notificationEventType': msg.notificationEventType,
+        'objectRef': msg.objectRef,
+        'alarmEventRecordId': alarm_data['alarmEventRecordId'],
+        'resourceTypeID': alarm_data['resourceTypeId'],
+        'resourceID': alarm_data['resourceId'],
+        'alarmDefinitionID': alarm_data['alarmDefinitionId'],
+        'probableCauseID': alarm_data['probableCauseId'],
+        'alarmRaisedTime': alarm_data['alarmRaisedTime'],
+        'alarmChangedTime': alarm_data['alarmChangedTime'],
+        'alarmAcknowledgeTime': alarm_data['alarmAcknowledgeTime'],
+        'alarmAcknowledged': alarm_data['alarmAcknowledged'],
+        'perceivedSeverity': alarm_data['perceivedSeverity'],
+        'extensions': json.loads(alarm_data['extensions'])
+    }
+    logger.info('callback URL: {}'.format(sub_data['callback']))
+    logger.debug('callback data: {}'.format(json.dumps(callback)))
+
+    return notifications.send(sub_data['callback'], callback)
index 5e80668..78807c7 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.
@@ -24,7 +24,15 @@ def notify_alarm_event_change(
     event: events.AlarmEventChanged,
     publish: Callable,
 ):
-    logger.debug('In 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,
+):
+    publish("AlarmEventPurged", event)
+    logger.debug("published Alarm Event Purged: {}".format(
+        event.id))
index 3028a84..caef205 100644 (file)
@@ -129,6 +129,22 @@ class AlarmDTO:
         # 'alarmAcknowledgeTime,alarmAcknowledged,extensions}'
     )
 
+    alarm_event_record_patch = api_monitoring_v1.model(
+        "AlarmPatchDto",
+        {
+            'alarmAcknowledged': fields.Boolean(
+                example=True,
+                description='Boolean value indication of a management ' +
+                'system has acknowledged the alarm.'),
+            'perceivedSeverity': fields.String(
+                example='5',
+                description='indicate that the alarm record is requested ' +
+                'to be cleared. Only the value "5" for "CLEARED" is ' +
+                'permitted in a request message content. ')
+        },
+        mask='{alarmAcknowledged,}'
+    )
+
 
 class SubscriptionDTO:
 
index e7a6425..7b0c03e 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.
@@ -19,6 +19,7 @@ from o2common.service.messagebus import MessageBus
 from o2common.views.pagination_route import link_header, PAGE_PARAM
 from o2common.views.route_exception import NotFoundException, \
     BadRequestException
+from o2ims.domain.alarm_obj import PerceivedSeverityEnum
 from o2ims.views import alarm_view
 from o2ims.views.api_ns import api_ims_monitoring as api_monitoring_v1
 from o2ims.views.alarm_dto import AlarmDTO, SubscriptionDTO, \
@@ -130,6 +131,7 @@ class AlarmListRouter(Resource):
 class AlarmGetRouter(Resource):
 
     model = AlarmDTO.alarm_event_record_get
+    patch = AlarmDTO.alarm_event_record_patch
 
     @api_monitoring_v1.doc('Get Alarm Event Record Information')
     @api_monitoring_v1.marshal_with(model)
@@ -140,6 +142,39 @@ 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.expect(patch)
+    @api_monitoring_v1.marshal_with(patch)
+    def patch(self, alarmEventRecordId):
+        data = api_monitoring_v1.payload
+        ack_action = data.get('alarmAcknowledged', None)
+        clear_action = data.get('perceivedSeverity', None)
+
+        ack_is_none = ack_action is None
+        clear_is_none = clear_action is None
+        if (ack_is_none and clear_is_none) or (not ack_is_none and
+                                               not clear_is_none):
+            raise BadRequestException('Either "alarmAcknowledged" or '
+                                      '"perceivedSeverity" shall be included '
+                                      'in a request, but not both.')
+        if ack_action:
+            result = alarm_view.alarm_event_record_ack(alarmEventRecordId,
+                                                       bus.uow)
+            if result is not None:
+                return result
+        elif clear_action:
+            if clear_action != PerceivedSeverityEnum.CLEARED:
+                raise BadRequestException(
+                    'Only the value "5" for "CLEARED" is permitted of '
+                    '"perceivedSeverity".')
+
+            result = alarm_view.alarm_event_record_clear(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..00d4a20 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, AlarmEventRecordModifications, \
+    PerceivedSeverityEnum
 
 from o2common.helper import o2logging
 # from o2common.config import config
@@ -45,6 +49,58 @@ 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)
+        # Check the record does not exist, return None. Otherwise, the
+        # acknowledge request will update the record even if it is
+        # acknowledged.
+        if alarm_event_record is None:
+            return None
+        alarm_event_record.alarmAcknowledged = True
+        alarm_event_record.alarmAcknowledgeTime = datetime.\
+            now().strftime("%Y-%m-%dT%H:%M:%S")
+        uow.alarm_event_records.update(alarm_event_record)
+        uow.commit()
+
+        result = AlarmEventRecordModifications(True)
+    return result
+
+
+def alarm_event_record_clear(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.perceivedSeverity == \
+                PerceivedSeverityEnum.CLEARED:
+            raise BadRequestException(
+                "Alarm Event Record {} has already been marked as CLEARED."
+                .format(alarmEventRecordId))
+        alarm_event_record.events.append(events.AlarmEventPurged(
+            id=alarm_event_record.alarmEventRecordId,
+            notificationEventType=AlarmNotificationEventEnum.CLEAR,
+            updatetime=alarm_event_record.alarmAcknowledgeTime))
+
+        uow.alarm_event_records.update(alarm_event_record)
+        uow.commit()
+
+        result = AlarmEventRecordModifications(
+            clear=PerceivedSeverityEnum.CLEARED)
+    _handle_events(messagebus.MessageBus.get_instance())
+    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