Add alarm API module ricxappframe.alarm.alarm
[ric-plt/xapp-frame-py.git] / ricxappframe / alarm / alarm.py
diff --git a/ricxappframe/alarm/alarm.py b/ricxappframe/alarm/alarm.py
new file mode 100644 (file)
index 0000000..9a3ba2d
--- /dev/null
@@ -0,0 +1,307 @@
+# ==================================================================================
+#       Copyright (c) 2020 AT&T Intellectual Property.
+#       Copyright (c) 2020 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.
+# ==================================================================================
+"""
+Provides classes and methods to define, raise, reraise and clear alarms.
+All actions are implemented by sending RMR messages to the Alarm Adapter
+that comply with the JSON schema in file alarm-schema.json.
+"""
+
+from ctypes import c_void_p
+from enum import Enum, auto
+import json
+import time
+from mdclogpy import Logger
+from ricxappframe.rmr import rmr
+
+##############
+# PRIVATE API
+##############
+
+mdc_logger = Logger(name=__name__)
+RETRIES = 4
+
+##############
+# PUBLIC API
+##############
+
+# constants
+RIC_ALARM_UPDATE = 13111
+# RIC_ALARM_QUERY = 13112 # TBD
+
+# Publish dict keys as constants for convenience of client code.
+# Mixed lower/upper casing to comply with the Adapter JSON requirements.
+KEY_ALARM = "alarm"
+KEY_MANAGED_OBJECT_ID = "managedObjectId"
+KEY_APPLICATION_ID = "applicationId"
+KEY_SPECIFIC_PROBLEM = "specificProblem"
+KEY_PERCEIVED_SEVERITY = "perceivedSeverity"
+KEY_ADDITIONAL_INFO = "additionalInfo"
+KEY_IDENTIFYING_INFO = "identifyingInfo"
+KEY_ALARM_ACTION = "AlarmAction"
+KEY_ALARM_TIME = "AlarmTime"
+
+
+class AlarmAction(Enum):
+    """
+    Action to perform at the Alarm Adapter
+    """
+    RAISE = auto()
+    CLEAR = auto()
+    CLEARALL = auto()
+
+
+class AlarmSeverity(Enum):
+    """
+    Severity of an alarm
+    """
+    UNSPECIFIED = auto()
+    CRITICAL = auto()
+    MAJOR = auto()
+    MINOR = auto()
+    WARNING = auto()
+    CLEARED = auto()
+    DEFAULT = auto()
+
+
+class AlarmDetail(dict):
+    """
+    An alarm that can be raised or cleared.
+
+    Parameters
+    ----------
+    managed_object_id: str
+        The name of the managed object that is the cause of the fault (required)
+
+    application_id: str
+        The name of the process that raised the alarm (required)
+
+    specific_problem: int
+        The problem that is the cause of the alarm
+
+    perceived_severity: AlarmSeverity
+        The severity of the alarm, a value from the enum.
+
+    identifying_info: str
+        Identifying additional information, which is part of alarm identity
+
+    additional_info: str
+        Additional information given by the application (optional)
+    """
+    # pylint: disable=too-many-arguments
+    def __init__(self,
+                 managed_object_id: str,
+                 application_id: str,
+                 specific_problem: int,
+                 perceived_severity: AlarmSeverity,
+                 identifying_info: str,
+                 additional_info: str = ""):
+        """
+        Creates an object with the specified items.
+        """
+        dict.__init__(self)
+        self[KEY_MANAGED_OBJECT_ID] = managed_object_id
+        self[KEY_APPLICATION_ID] = application_id
+        self[KEY_SPECIFIC_PROBLEM] = specific_problem
+        self[KEY_PERCEIVED_SEVERITY] = perceived_severity.name
+        self[KEY_IDENTIFYING_INFO] = identifying_info
+        self[KEY_ADDITIONAL_INFO] = additional_info
+
+
+class AlarmManager:
+    """
+    Provides an API for an Xapp to raise and clear alarms by sending messages
+    via RMR, which should route the messages to an Alarm Adapter.
+
+    Parameters
+    ----------
+    vctx: ctypes c_void_p
+        Pointer to RMR context obtained by initializing RMR.
+        The context is used to allocate space and send messages.
+        The RMR routing table must have a destination for message
+        type RIC_ALARM_UPDATE as defined in this module.
+
+    managed_object_id: str
+        The name of the managed object that raises alarms
+
+    application_id: str
+        The name of the process that raises alarms
+    """
+    def __init__(self,
+                 vctx: c_void_p,
+                 managed_object_id: str,
+                 application_id: str):
+        """
+        Creates an alarm manager.
+        """
+        self.vctx = vctx
+        self.managed_object_id = managed_object_id
+        self.application_id = application_id
+
+    def create_alarm(self,
+                     specific_problem: int,
+                     perceived_severity: AlarmSeverity,
+                     identifying_info: str,
+                     additional_info: str = ""):
+        """
+        Convenience method that creates an alarm instance, an AlarmDetail object,
+        using cached values for managed object ID and application ID.
+
+        Parameters
+        ----------
+        specific_problem: int
+            The problem that is the cause of the alarm
+
+        perceived_severity: AlarmSeverity
+            The severity of the alarm, a value from the enum.
+
+        identifying_info: str
+            Identifying additional information, which is part of alarm identity
+
+        additional_info: str
+            Additional information given by the application (optional)
+
+        Returns
+        -------
+        AlarmDetail
+        """
+        return AlarmDetail(managed_object_id=self.managed_object_id,
+                           application_id=self.application_id,
+                           specific_problem=specific_problem, perceived_severity=perceived_severity,
+                           identifying_info=identifying_info, additional_info=additional_info)
+
+    @staticmethod
+    def _create_alarm_message(alarm: AlarmDetail, action: AlarmAction):
+        """
+        Creates a dict with the specified alarm detail plus action and time.
+        Uses the current system time in milliseconds since the Epoch.
+
+        Parameters
+        ----------
+        detail: AlarmDetail
+            The alarm details.
+
+        action: AlarmAction
+            The action to perform at the Alarm Adapter on this alarm.
+        """
+        return {
+            **alarm,
+            KEY_ALARM_ACTION: action.name,
+            KEY_ALARM_TIME: int(round(time.time() * 1000))
+        }
+
+    def _rmr_send_alarm(self, msg: dict):
+        """
+        Serializes the dict and sends the result via RMR using a predefined message type.
+
+        Parameters
+        ----------
+        msg: dict
+            Dictionary with alarm message to encode and send
+
+        Returns
+        -------
+        bool
+            True if the send succeeded (possibly with retries), False otherwise
+        """
+        payload = json.dumps(msg).encode()
+        mdc_logger.debug("_rmr_send_alarm: payload is {}".format(payload))
+        sbuf = rmr.rmr_alloc_msg(vctx=self.vctx, size=len(payload), payload=payload,
+                                 mtype=RIC_ALARM_UPDATE, gen_transaction_id=True)
+
+        for _ in range(0, RETRIES):
+            sbuf = rmr.rmr_send_msg(self.vctx, sbuf)
+            post_send_summary = rmr.message_summary(sbuf)
+            mdc_logger.debug("_rmr_send_alarm: try {0} result is {1}".format(_, post_send_summary[rmr.RMR_MS_MSG_STATE]))
+            # stop trying if RMR does not indicate retry
+            if post_send_summary[rmr.RMR_MS_MSG_STATE] != rmr.RMR_ERR_RETRY:
+                break
+
+        rmr.rmr_free_msg(sbuf)
+        if post_send_summary[rmr.RMR_MS_MSG_STATE] != rmr.RMR_OK:
+            mdc_logger.warning("_rmr_send_alarm: failed after {} retries".format(RETRIES))
+            return False
+
+        return True
+
+    def raise_alarm(self, detail: AlarmDetail):
+        """
+        Builds and sends a message to the AlarmAdapter to raise an alarm
+        with the specified detail.
+
+        Parameters
+        ----------
+        detail: AlarmDetail
+            Alarm to raise
+
+        Returns
+        -------
+        bool
+            True if the send succeeded (possibly with retries), False otherwise
+        """
+        msg = self._create_alarm_message(detail, AlarmAction.RAISE)
+        return self._rmr_send_alarm(msg)
+
+    def clear_alarm(self, detail: AlarmDetail):
+        """
+        Builds and sends a message to the AlarmAdapter to clear the alarm
+        with the specified detail.
+
+        Parameters
+        ----------
+        detail: AlarmDetail
+            Alarm to clear
+
+        Returns
+        -------
+        bool
+            True if the send succeeded (possibly with retries), False otherwise
+        """
+        msg = self._create_alarm_message(detail, AlarmAction.CLEAR)
+        return self._rmr_send_alarm(msg)
+
+    def reraise_alarm(self, detail: AlarmDetail):
+        """
+        Builds and sends a message to the AlarmAdapter to clear the alarm with the
+        the specified detail, then builds and sends a message to raise the alarm again.
+
+        Parameters
+        ----------
+        detail: AlarmDetail
+            Alarm to clear and raise again.
+
+        Returns
+        -------
+        bool
+            True if the send succeeded (possibly with retries), False otherwise
+        """
+        success = self.clear_alarm(detail)
+        if success:
+            success = self.raise_alarm(detail)
+        return success
+
+    def clear_all_alarms(self):
+        """
+        Builds and sends a message to the AlarmAdapter to clear all alarms.
+
+        Returns
+        -------
+        bool
+            True if the send succeeded (possibly with retries), False otherwise
+        """
+        detail = self.create_alarm(0, AlarmSeverity.DEFAULT, "", "")
+        msg = self._create_alarm_message(detail, AlarmAction.CLEARALL)
+        return self._rmr_send_alarm(msg)