| # ================================================================================== |
| # 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. |
| The alarm target host and port are set by environment variables. The alarm |
| message contents comply with the JSON schema in file alarm-schema.json. |
| """ |
| |
| from ctypes import c_void_p |
| from enum import Enum, auto |
| import json |
| import os |
| import time |
| from mdclogpy import Logger |
| from ricxappframe.rmr import rmr |
| from ricxappframe.alarm.exceptions import InitFailed |
| |
| ############## |
| # PRIVATE API |
| ############## |
| |
| mdc_logger = Logger(name=__name__) |
| RETRIES = 4 |
| |
| ############## |
| # PUBLIC API |
| ############## |
| |
| # constants |
| RIC_ALARM_UPDATE = 110 # message type |
| ALARM_MGR_SERVICE_NAME_ENV = "ALARM_MGR_SERVICE_NAME" |
| ALARM_MGR_SERVICE_PORT_ENV = "ALARM_MGR_SERVICE_PORT" |
| |
| # 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 directly to an Alarm Adapter. Requires environment variables |
| ALARM_MGR_SERVICE_NAME and ALARM_MGR_SERVICE_PORT with the destination host |
| (service) name and port number; raises an exception if not found. |
| |
| Parameters |
| ---------- |
| vctx: ctypes c_void_p |
| Pointer to RMR context obtained by initializing RMR. |
| The context is used to allocate space and send messages. |
| |
| 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 |
| service = os.environ.get(ALARM_MGR_SERVICE_NAME_ENV, None) |
| port = os.environ.get(ALARM_MGR_SERVICE_PORT_ENV, None) |
| if service is None or port is None: |
| mdc_logger.error("init: missing env var(s) {0}, {1}".format(ALARM_MGR_SERVICE_NAME_ENV, ALARM_MGR_SERVICE_PORT_ENV)) |
| raise InitFailed |
| target = "{0}:{1}".format(service, port) |
| self._wormhole_id = rmr.rmr_wh_open(self.vctx, target.encode('utf-8')) |
| if rmr.rmr_wh_state(self.vctx, self._wormhole_id) != rmr.RMR_OK: |
| mdc_logger.error("init: failed to open wormhole to target {}".format(target)) |
| raise InitFailed |
| |
| 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 the 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 to the wormhole initialized at start. |
| |
| 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_wh_send_msg(self.vctx, self._wormhole_id, 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) |