AETHER-1259 Add signal quality metrics to monitoring server

In addition to the signal quality update,
- Fix the license
- Add .gitignore

Change-Id: I73484aaf2a1047e66026743262ea713c2121c8d2
diff --git a/edge-monitoring/.gitignore b/edge-monitoring/.gitignore
new file mode 100644
index 0000000..c39a109
--- /dev/null
+++ b/edge-monitoring/.gitignore
@@ -0,0 +1,6 @@
+# Copyright 2020-present Open Networking Foundation
+#
+# SPDX-License-Identifier: LicenseRef-ONF-Member-Only-1.0
+
+*.pyc
+__pycache__/
diff --git a/edge-monitoring/VERSION b/edge-monitoring/VERSION
index 7d85683..d1d899f 100644
--- a/edge-monitoring/VERSION
+++ b/edge-monitoring/VERSION
@@ -1 +1 @@
-0.5.4
+0.5.5
diff --git a/edge-monitoring/edge_monitoring_server.py b/edge-monitoring/edge_monitoring_server.py
index 20ddc94..16bfe9e 100755
--- a/edge-monitoring/edge_monitoring_server.py
+++ b/edge-monitoring/edge_monitoring_server.py
@@ -2,17 +2,7 @@
 
 # Copyright 2020-present Open Networking Foundation
 #
-# 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.
+# SPDX-License-Identifier: LicenseRef-ONF-Member-Only-1.0
 
 import os
 import time
@@ -51,7 +41,11 @@
                 }
             }
         },
-        'last_update': time.time(),
+        'signal_quality': {
+            'rsrq': 0,
+            'rsrp': 0
+        },
+        'last_update': time.time()
     }
 ]
 
@@ -84,6 +78,12 @@
 ping_dns_max = prom.Gauge("aetheredge_ping_dns_test_max","Last ping test maximum value",["name"])
 ping_dns_stddev = prom.Gauge("aetheredge_ping_dns_test_stddev","Last ping test standard deviation",["name"])
 
+# Signal quality metrics in CESQ format not dB
+# RSRQ: >=53 excellent, 43 ~ 53 good, 33 ~ 43 mid, <=33 bad, 0 no signal
+# RSRP: >=20 excellent, 10 ~ 20 good, 0 ~ 10 mid, 0 no signal
+signal_quality_rsrq = prom.Gauge("aetheredge_signal_quality_rsrq", "Quality of the received signal", ["name"])
+signal_quality_rsrp = prom.Gauge("aetheredge_signal_quality_rsrp", "Power of the received signal", ["name"])
+
 # Other metrics
 last_update = prom.Gauge("aetheredge_last_update", "Last reported test result", ["name"])
 maint_window = prom.Gauge("aetheredge_in_maintenance_window", "Currently in a maintenance window", ["name"])
@@ -145,15 +145,12 @@
                                                 'avg': 0.0,
                                                 'max': 0.0,
                                                 'stddev': 0.0}
+            edge.pop('signal_quality', None)
 
 def remove_edge_from_metrics(name):
     try:
         cp_status.remove(name)
         up_status.remove(name)
-        ping_dns_min.remove(name)
-        ping_dns_avg.remove(name)
-        ping_dns_max.remove(name)
-        ping_dns_stddev.remove(name)
         last_update.remove(name)
         e2e_tests_ok.remove(name)
         connect_test_ok.remove(name)
@@ -163,6 +160,20 @@
         pass
 
     try:
+        ping_dns_min.remove(name)
+        ping_dns_avg.remove(name)
+        ping_dns_max.remove(name)
+        ping_dns_stddev.remove(name)
+    except:
+        pass
+
+    try:
+        signal_quality_rsrq.remove(name)
+        signal_quality_rsrp.remove(name)
+    except:
+        pass
+
+    try:
         maint_window.remove(name)
     except:
         pass
@@ -215,6 +226,9 @@
             ping_dns_max.labels(edge['name']).set(ping_dns_max_result)
             ping_dns_stddev.labels(edge['name']).set(ping_dns_stddev_result)
 
+        if 'signal_quality' in edge.keys():
+            signal_quality_rsrq.labels(edge['name']).set(edge['signal_quality']['rsrq'])
+            signal_quality_rsrp.labels(edge['name']).set(edge['signal_quality']['rsrp'])
 
     res.append(prom.generate_latest(cp_status))
     res.append(prom.generate_latest(up_status))
@@ -228,6 +242,8 @@
     res.append(prom.generate_latest(ping_test_ok))
     res.append(prom.generate_latest(e2e_tests_ok))
     res.append(prom.generate_latest(e2e_tests_down))
+    res.append(prom.generate_latest(signal_quality_rsrq))
+    res.append(prom.generate_latest(signal_quality_rsrp))
 
     return Response(res, mimetype="text/plain")
 
@@ -286,6 +302,8 @@
             'ping': request.json['speedtest']['ping']
         }
 
+    if 'signal_quality' in request.json:
+        req_edge['signal_quality'] = request.json['signal_quality']
 
     edge = [edge for edge in edges if edge['name'] == req_edge['name']]
     if len(edge) == 0:
@@ -295,6 +313,8 @@
         edge[0]['status']['control_plane'] = req_edge['status']['control_plane']
         edge[0]['status']['user_plane'] = req_edge['status']['user_plane']
         edge[0]['speedtest']['ping'] = req_edge['speedtest']['ping']
+        if 'signal_quality' in req_edge.keys():
+            edge[0]['signal_quality'] = req_edge['signal_quality']
         edge[0]['last_update'] = req_edge['last_update']
 
     return jsonify({'edge': req_edge}), 201
diff --git a/edge-monitoring/test_edge_monitoring_server.py b/edge-monitoring/test_edge_monitoring_server.py
index a7e8cf2..fa980cd 100755
--- a/edge-monitoring/test_edge_monitoring_server.py
+++ b/edge-monitoring/test_edge_monitoring_server.py
@@ -2,17 +2,7 @@
 
 # Copyright 2020-present Open Networking Foundation
 #
-# 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.
+# SPDX-License-Identifier: LicenseRef-ONF-Member-Only-1.0
 
 import unittest
 import edge_monitoring_server as ems
@@ -38,6 +28,19 @@
             }
         }
     },
+    'signal_quality': {
+        'rsrq': 30,
+        'rsrp': 80
+    },
+    'last_update': time.time()
+}
+
+test_edge_status_only = {
+    'name': 'ace-menlo-pixel',
+    'status': {
+        'control_plane': 'connected',
+        'user_plane': 'connected'
+    },
     'last_update': time.time()
 }
 
@@ -47,6 +50,29 @@
         'control_plane': 'connected',
         'user_plane': 'connected'
     },
+    'signal_quality': {
+        'rsrq': 30,
+        'rsrp': 80
+    },
+    'last_update': time.time()
+}
+
+test_edge_no_signal_quality = {
+    'name': 'ace-menlo-pixel',
+    'status': {
+        'control_plane': 'connected',
+        'user_plane': 'connected'
+    },
+    'speedtest': {
+        'ping': {
+            'dns': {
+                'min': 2.0,
+                'avg': 4.0,
+                'max': 6.0,
+                'stddev': 1.0
+            }
+        }
+    },
     'last_update': time.time()
 }
 
@@ -69,6 +95,34 @@
 class TestEdgeMonitoringServer(unittest.TestCase):
     def setUp(self):
         self.app = ems.app.test_client()
+        self.emulated_time = time.mktime(time.strptime("2021-04-05 00:00:00", "%Y-%m-%d %H:%M:%S"))
+        self.time_method = time.time
+        time.time = self._get_time
+
+    def tearDown(self):
+        time.time = self.time_method
+
+    def _get_time(self):
+        return self.emulated_time
+
+    def _assert_status_metrics_exist(self, data):
+        self.assertTrue('aetheredge_status_control_plane{name="ace-menlo-pixel"} 2.0' in data)
+        self.assertTrue('aetheredge_status_user_plane{name="ace-menlo-pixel"} 2.0' in data)
+        self.assertTrue('aetheredge_last_update{name="ace-menlo-pixel"}' in data)
+        self.assertTrue('aetheredge_connect_test_ok{name="ace-menlo-pixel"} 1.0' in data)
+        self.assertTrue('aetheredge_ping_test_ok{name="ace-menlo-pixel"} 1.0' in data)
+        self.assertTrue('aetheredge_e2e_tests_ok{name="ace-menlo-pixel"} 1.0' in data)
+        self.assertTrue('aetheredge_e2e_tests_down{name="ace-menlo-pixel"} 0.0' in data)
+
+    def _assert_speedtest_metrics_exist(self, data):
+        self.assertTrue('aetheredge_ping_dns_test_min{name="ace-menlo-pixel"} 2.0' in data)
+        self.assertTrue('aetheredge_ping_dns_test_avg{name="ace-menlo-pixel"} 4.0' in data)
+        self.assertTrue('aetheredge_ping_dns_test_max{name="ace-menlo-pixel"} 6.0' in data)
+        self.assertTrue('aetheredge_ping_dns_test_stddev{name="ace-menlo-pixel"} 1.0' in data)
+
+    def _assert_signal_quality_metrics_exist(self, data):
+        self.assertTrue('aetheredge_signal_quality_rsrq{name="ace-menlo-pixel"} 30' in data)
+        self.assertTrue('aetheredge_signal_quality_rsrp{name="ace-menlo-pixel"} 80' in data)
 
     def test_match_location(self):
         event = MyEvent(location = "ace-menlo-pixel-production")
@@ -153,13 +207,10 @@
         response = self.app.get('/edges/metrics')
         data = response.get_data(as_text=True)
         print(data)
-        self.assertTrue('aetheredge_status_control_plane{name="ace-menlo-pixel"} 2.0' in data)
-        self.assertTrue('aetheredge_status_user_plane{name="ace-menlo-pixel"} 2.0' in data)
-        self.assertTrue('aetheredge_last_update{name="ace-menlo-pixel"}' in data)
-        self.assertTrue('aetheredge_connect_test_ok{name="ace-menlo-pixel"} 1.0' in data)
-        self.assertTrue('aetheredge_ping_test_ok{name="ace-menlo-pixel"} 1.0' in data)
-        self.assertTrue('aetheredge_e2e_tests_ok{name="ace-menlo-pixel"} 1.0' in data)
-        self.assertTrue('aetheredge_e2e_tests_down{name="ace-menlo-pixel"} 0.0' in data)
+
+        self._assert_status_metrics_exist(data)
+        self._assert_speedtest_metrics_exist(data)
+        self._assert_signal_quality_metrics_exist(data)
 
         response = self.app.delete('/edges/ace-menlo-pixel')
         data = json.loads(response.get_data(as_text=True))
@@ -186,13 +237,10 @@
         response = self.app.get('/edges/metrics')
         data = response.get_data(as_text=True)
         print(data)
-        self.assertTrue('aetheredge_status_control_plane{name="ace-menlo-pixel"} 2.0' in data)
-        self.assertTrue('aetheredge_status_user_plane{name="ace-menlo-pixel"} 2.0' in data)
-        self.assertTrue('aetheredge_last_update{name="ace-menlo-pixel"}' in data)
-        self.assertTrue('aetheredge_connect_test_ok{name="ace-menlo-pixel"} 1.0' in data)
-        self.assertTrue('aetheredge_ping_test_ok{name="ace-menlo-pixel"} 1.0' in data)
-        self.assertTrue('aetheredge_e2e_tests_ok{name="ace-menlo-pixel"} 1.0' in data)
-        self.assertTrue('aetheredge_e2e_tests_down{name="ace-menlo-pixel"} 0.0' in data)
+
+        self._assert_status_metrics_exist(data)
+        self._assert_speedtest_metrics_exist(data)
+        self._assert_signal_quality_metrics_exist(data)
 
         response = self.app.delete('/testresults/ace-menlo-pixel')
         data = json.loads(response.get_data(as_text=True))
@@ -216,10 +264,25 @@
         data = response.get_data(as_text=True)
         print(data)
 
-        self.assertTrue('aetheredge_ping_dns_test_min{name="ace-menlo-pixel"} ' + str(test_edge['speedtest']['ping']['dns']['min']) in data)
-        self.assertTrue('aetheredge_ping_dns_test_avg{name="ace-menlo-pixel"} ' + str(test_edge['speedtest']['ping']['dns']['avg']) in data)
-        self.assertTrue('aetheredge_ping_dns_test_max{name="ace-menlo-pixel"} ' + str(test_edge['speedtest']['ping']['dns']['max']) in data)
-        self.assertTrue('aetheredge_ping_dns_test_stddev{name="ace-menlo-pixel"} ' + str(test_edge['speedtest']['ping']['dns']['stddev']) in data)
+        self._assert_speedtest_metrics_exist(data)
+
+        response = self.app.delete('/testresults/ace-menlo-pixel')
+        data = json.loads(response.get_data(as_text=True))
+        self.assertEqual(data['result'], True)
+
+    def test_backwards_compatible_status_only(self):
+        response = self.app.post('/testresults', json=test_edge_status_only)
+        data = json.loads(response.get_data(as_text=True))
+        self.assertEqual(data['edge']['name'], 'ace-menlo-pixel')
+
+        response = self.app.get('/edges/metrics')
+        data = response.get_data(as_text=True)
+        print(data)
+
+        self.assertFalse('aetheredge_signal_quality_rsrq{name="ace-menlo-pixel"}' in data)
+        self.assertFalse('aetheredge_signal_quality_rsrp{name="ace-menlo-pixel"}' in data)
+
+        self._assert_status_metrics_exist(data)
 
         response = self.app.delete('/testresults/ace-menlo-pixel')
         data = json.loads(response.get_data(as_text=True))
@@ -239,18 +302,54 @@
         self.assertFalse('aetheredge_ping_dns_test_max{name="ace-menlo-pixel"}' in data)
         self.assertFalse('aetheredge_ping_dns_test_stddev{name="ace-menlo-pixel"}' in data)
 
-        self.assertTrue('aetheredge_status_control_plane{name="ace-menlo-pixel"} 2.0' in data)
-        self.assertTrue('aetheredge_status_user_plane{name="ace-menlo-pixel"} 2.0' in data)
-        self.assertTrue('aetheredge_last_update{name="ace-menlo-pixel"}' in data)
-        self.assertTrue('aetheredge_connect_test_ok{name="ace-menlo-pixel"} 1.0' in data)
-        self.assertTrue('aetheredge_ping_test_ok{name="ace-menlo-pixel"} 1.0' in data)
-        self.assertTrue('aetheredge_e2e_tests_ok{name="ace-menlo-pixel"} 1.0' in data)
-        self.assertTrue('aetheredge_e2e_tests_down{name="ace-menlo-pixel"} 0.0' in data)
+        self._assert_status_metrics_exist(data)
+        self._assert_signal_quality_metrics_exist(data)
 
         response = self.app.delete('/testresults/ace-menlo-pixel')
         data = json.loads(response.get_data(as_text=True))
         self.assertEqual(data['result'], True)
 
+    def test_backwards_compatible_no_signal_quality(self):
+        response = self.app.post('/testresults', json=test_edge_no_signal_quality)
+        data = json.loads(response.get_data(as_text=True))
+        self.assertEqual(data['edge']['name'], 'ace-menlo-pixel')
+
+        response = self.app.get('/edges/metrics')
+        data = response.get_data(as_text=True)
+        print(data)
+
+        self.assertFalse('aetheredge_signal_quality_rsrq{name="ace-menlo-pixel"}' in data)
+        self.assertFalse('aetheredge_signal_quality_rsrp{name="ace-menlo-pixel"}' in data)
+
+        self._assert_status_metrics_exist(data)
+        self._assert_speedtest_metrics_exist(data)
+
+        response = self.app.delete('/testresults/ace-menlo-pixel')
+        data = json.loads(response.get_data(as_text=True))
+        self.assertEqual(data['result'], True)
+
+    def test_timeout_stale_result(self):
+        response = self.app.post('/testresults', json=test_edge)
+        data = json.loads(response.get_data(as_text=True))
+        print(json.dumps(data, indent=2))
+
+        self.assertEqual(data['edge']['status']['control_plane'], 'connected')
+        self.assertEqual(data['edge']['status']['user_plane'], 'connected')
+        self.assertEqual(data['edge']['speedtest']['ping']['dns']['avg'], 4.0)
+        self.assertTrue('signal_quality' in data['edge'])
+
+        self.emulated_time += (ems.NO_RESULT_THRESHOLD + 1)
+
+        response = self.app.get('/edges/ace-menlo-pixel')
+        data = json.loads(response.get_data(as_text=True))
+        print(json.dumps(data, indent=2))
+
+        self.assertEqual(data['edge']['status']['control_plane'], 'no result')
+        self.assertEqual(data['edge']['status']['user_plane'], 'no result')
+        self.assertEqual(data['edge']['speedtest']['ping']['dns']['avg'], 0.0)
+        self.assertFalse('signal_quality' in data['edge'])
+
+
 if __name__ == '__main__':
     suite = unittest.TestLoader().loadTestsFromTestCase(TestEdgeMonitoringServer)
     unittest.TextTestRunner(verbosity=2).run(suite)