Repository: EricssonResearch/arms

Issue:
Fetching and evaluating of DU failure reports
In total two things have to be implemented to fulfill this issue:

- A small script that starts a server and provides the DU link failure. For simplification, the DU failure simply equals the letter _A_. For more details see the example failure report we got from Ericsson.
- A python class that fetches the information from the server and returns the boolean value True if the error message _A_ was received. If an internal error occurred, the boolean value False shall be returned.

In addition, tests should show that the python class works as intended.

Comments:

Patch:
diff --git a/arms/misc/socket.py b/arms/misc/socket.py
index 0ca3fd3..a04eb19 100644
--- a/arms/misc/socket.py
+++ b/arms/misc/socket.py
@@ -25,9 +25,10 @@ class SocketClient:
         server.
 
         Return:
-            - True, if a setting entry belonging to the client (client_name)
-            could be found and no data type violation occurred.
-            - False, if not.
+			- [#1, #2]
+				#1: True, if a setting entry belonging to the client (client_name)
+					could be found and no data type violation occurred. False, if not.
+				#2: True, if a connection could be made. False, if not.
         """
         try:
             c = config.var.data['socket'][self.name]
@@ -38,11 +39,11 @@ class SocketClient:
                              'socket {} in the configuration file ('
                              'arms/config/config.json). Hence, no '
                              'connection could be made.'.format(self.name))
-            return False
+            return [False, False]
 
         if isinstance(host, str) and isinstance(port, int):
             ok = self.connect(host, port)
-            return ok
+            return [True, ok]
         else:
             log.socket.error('Data type violation. The host number has to be '
                              'provided as a string and the port number as an '
@@ -50,7 +51,7 @@ class SocketClient:
                              'arms/config/config.json). In consequence, the '
                              'socket {} could not connect to the server.'
                              .format(self.name))
-            return False
+            return [False, False]
 
     def connect(self, host, port):
         """Establishes a connection to a server.
diff --git a/arms/units/abb.py b/arms/units/abb.py
index 5d39fa1..76c3cb0 100644
--- a/arms/units/abb.py
+++ b/arms/units/abb.py
@@ -23,6 +23,11 @@ class ABB:
     def __exit__(self, exc_type, exc_val, exc_tb):
         self.close()
 
+    def connect(self):
+        """Connects to the socket server."""
+        ok, connected = self.client.get_config_and_connect()
+        return ok, connected
+
     def close(self):
         """Close connection."""
         case = 0
diff --git a/arms/units/alarms.py b/arms/units/alarms.py
new file mode 100644
index 0000000..8d3ff68
--- /dev/null
+++ b/arms/units/alarms.py
@@ -0,0 +1,46 @@
+"""Utilities related to receiving and evaluating of DU failure reports."""
+
+import requests
+import time
+from arms.config import config
+from arms.utils import log
+
+
+class Alarms:
+
+    """Receiving and evaluating of DU failure reports."""
+
+    def listen(self):
+        """Connects with the web server 'alarms' specified in the settings
+        (arms/config/config.json -> web -> alarms) and waits for an error
+        message.
+
+        Return:
+            -   True, if a hardware link failure was reported.
+            -   False, if an internal error occurred.
+        """
+
+        try:
+            host = config.var.data['web']['alarms']['host']
+            port = config.var.data['web']['alarms']['port']
+            url = config.var.data['web']['alarms']['url']
+            webpage = host + ":" + str(port) + url
+            response = requests.get(webpage).text
+        except (TypeError, KeyError):
+            log.alarms.critical("Could not find (appropriate) settings in the "
+                                "configuration file (arms/config/config.json) "
+                                "to reach the website responsible for sending "
+                                "failure reports. Hence, this service will be "
+                                "closed.")
+            return False
+        except requests.exceptions.RequestException:
+            time.sleep(60)
+            return self.listen()
+
+        if response == "A":
+            log.alarms.info("The error code ({}) was received. The healing "
+                            "process will be started.".format(response))
+            return True
+        else:
+            time.sleep(60)
+            return self.listen()
diff --git a/arms/utils/log.py b/arms/utils/log.py
index 5c1a8b6..72a7217 100644
--- a/arms/utils/log.py
+++ b/arms/utils/log.py
@@ -71,6 +71,7 @@ def get_logger(name):
 
 # The different loggers used (alphabetical order).
 abb = get_logger('abb')
+alarms = get_logger('alarms')
 app = get_logger('app')
 ard_log = get_logger('ard_log')
 camera = get_logger('camera')
diff --git a/other/alarms/alarmSample.txt b/other/alarms/alarmSample.txt
new file mode 100644
index 0000000..f45e1a4
--- /dev/null
+++ b/other/alarms/alarmSample.txt
@@ -0,0 +1,7 @@
+===================================================================================================================================================================================================================
+Date & Time (Local) S Specific Problem                      				MO (Cause/AdditionalInfo)
+===================================================================================================================================================================================================================
+2018-11-22 15:52:19 M Gigabit Ethernet Link Fault         				Subrack=1,Slot=1,PlugInUnit=1,ExchangeTerminalIp=1,GigaBitEthernet=1 (Autonegotiation Failed to Meet Minimum Requirements.)
+2018-11-22 16:03:19 M Network Synch Time from GPS Missing 				Subrack=1,Slot=1,PlugInUnit=1,TimingUnit=1,GpsSyncRef=1 (Cause: Loss of signal)
+2018-11-23 10:23:52 m Link Failure RiLink=1 (No signal detected (link start time-out)   ManagedElement=1,Equipment=1,Subrack=1,Slot=1,PlugInUnit=1,RiPort=A (transportType=NOT_SET)
+
diff --git a/other/alarms/alservice_rest.py b/other/alarms/alservice_rest.py
new file mode 100644
index 0000000..e501c44
--- /dev/null
+++ b/other/alarms/alservice_rest.py
@@ -0,0 +1,21 @@
+from flask import Flask
+import os
+import sys
+
+app = Flask(__name__)
+
+@app.route('/alarms')
+def alarms():
+
+	with open(os.getcwd() + '/alarmSample.txt', 'r') as f:
+		lines = f.readlines()
+		f.close()
+	for line in lines:
+		if "Link Failure" in line:
+			return line[line.find("RiPort") + 7:line.find("RiPort") + 8]
+
+	return "no alarm"
+
+
+if __name__ == '__main__':
+	app.run(host='0.0.0.0', port=8000)
diff --git a/requirements.txt b/requirements.txt
index a6b2e32..cd20793 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
 # List of files to be installed using pip install.
+requests
 pytest
\ No newline at end of file


Test patch:
diff --git a/tests/unit/test_alarms.py b/tests/unit/test_alarms.py
new file mode 100644
index 0000000..094319a
--- /dev/null
+++ b/tests/unit/test_alarms.py
@@ -0,0 +1,90 @@
+"""Tests for arms.units.alarms.Alarms.listen"""
+
+import pytest
+import requests
+from unittest import mock
+from arms.config import config
+from arms.units import alarms
+
+
+class DotDict(dict):
+    """Access the keys of a dictionary via a dot notation.
+
+    Example:
+        val = Map({'text': "No error"})
+        val.text -> "No error"
+    """
+    __getattr__ = dict.get
+    __setattr__ = dict.__setitem__
+    __delattr__ = dict.__delitem__
+
+
+@mock.patch.object(alarms.requests, 'get', return_value=DotDict({'text': "A"}))
+def test_listen_alarm(mock_requests):
+    """WHILE the webpage information for 'alarms' could be found in the
+    settings (see details below), WHILE the requested server is online,
+    the method 'listen' shall fetch the information from the server and return
+    the boolean value True if the received error message equals the letter 'A'.
+
+    Note that the following config entry is needed:
+        arms.config.config.json -> web -> alarms -> host, port, url
+    """
+    alarm = alarms.Alarms()
+    config_data = {"web": {"alarms": {"host": "a", "port": "b", "url": "c"}}}
+    with mock.patch.object(config.var, 'data', config_data):
+        assert alarm.listen() is True
+        mock_requests.assert_called()
+
+
+@pytest.mark.parametrize("config_data", [
+    None,
+    {},
+    ({"color": "blue"}),
+    ({"web": {"color": "blue"}}),
+    ({"web": {"alarms": {"color": "blue"}}}),
+    ({"web": {"alarms": {"host": "a", "color": "b", "url": "c"}}}),
+])
+def test_listen_dict_error(config_data):
+    """IF no webpage information for 'alarms' could be found or if they are
+    incomplete, THEN the boolean value False shall be returned.
+    """
+    alarm = alarms.Alarms()
+    with mock.patch.object(config.var, 'data', config_data):
+        assert alarm.listen() is False
+
+
+@mock.patch.object(alarms.requests, 'get', return_value=None,
+                   side_effect=requests.exceptions.RequestException)
+@mock.patch('time.sleep', return_value=None)
+def test_listen_request_error(mock_time, mock_requests):
+    """IF the requested server is offline, WHILE the webpage information for
+    'alarms' are available, the system shall sleep for awhile and try to
+    fetch the information from the server again.
+    """
+    alarm = alarms.Alarms()
+    listen = alarm.listen
+    config_data = {"web": {"alarms": {"host": "a", "port": "b", "url": "c"}}}
+    with mock.patch.object(config.var, 'data', config_data):
+        with mock.patch.object(alarms.Alarms, 'listen', return_value="loop"):
+            assert listen() == "loop"
+            mock_requests.assert_called()
+            mock_time.assert_called()
+
+
+@mock.patch.object(alarms.requests, 'get',
+                   return_value=DotDict({'text': "No error"}))
+@mock.patch('time.sleep', return_value=None)
+def test_listen_no_alarm(mock_time, mock_requests):
+    """WHILE the webpage information for 'alarms' could be found in the
+    settings, WHILE the requested server is online, the method 'listen' shall
+    sleep awhile and make a request again if the fetched information does not
+    equal the letter 'A'.
+    """
+    alarm = alarms.Alarms()
+    listen = alarm.listen
+    config_data = {"web": {"alarms": {"host": "a", "port": "b", "url": "c"}}}
+    with mock.patch.object(config.var, 'data', config_data):
+        with mock.patch.object(alarms.Alarms, 'listen', return_value="loop"):
+            assert listen() == "loop"
+            mock_requests.assert_called()
+            mock_time.assert_called()
diff --git a/tests/unit/test_socket.py b/tests/unit/test_socket.py
index a1a9b3c..a56d6e7 100644
--- a/tests/unit/test_socket.py
+++ b/tests/unit/test_socket.py
@@ -39,11 +39,12 @@ def test_get_config_and_connect():
     """
     client = SocketClient("test_socket")
     data = {"socket": {"test_socket": {"host": host, "port": port}}}
-    client.connect = mock.MagicMock()
+    mock_connect = mock.MagicMock()
+    client.connect = mock_connect
 
     with mock.patch.object(config.var, 'data', data):
         client.get_config_and_connect()
-        client.connect.assert_called_with(host, port)
+        mock_connect.assert_called_with(host, port)
 
 
 config_none = None
@@ -65,16 +66,17 @@ def test_get_config_and_connect_fail(config_data):
     """WHEN the function 'get_config_and_connect' is called, IF the config
     entry containing the host name and port number to the specified client
     (client_name) is of the wrong format or does not exist, THEN no attempt
-    to connect with the respective server shall be made and the boolean value
-    False shall be returned.
+    to connect with the respective server shall be made and the boolean values
+    [False, False] shall be returned.
     """
     client = SocketClient("test_socket")
-    client.connect = mock.MagicMock()
+    mock_connect = mock.MagicMock()
+    client.connect = mock_connect
 
     with mock.patch.object(config.var, 'data', config_data):
         ok = client.get_config_and_connect()
-        client.connect.assert_not_called()
-        assert ok is False
+        mock_connect.assert_not_called()
+        assert ok == [False, False]
 
 
 def test_connect():
@@ -185,7 +187,7 @@ class SocketServer:
         """Closes the socket and the connection to the client."""
         if self.conn is not None:
             self.conn.close()
-            
+
         if self.sock is not None:
             self.sock.close()