AETHER-455 Add edge monitoring agent for Sercomm CBRS dongle

Also enhanced ADB based agent and moved it under agent_adb folder

Change-Id: I146c70d5d249f584a006ef90fbc17a4636dacac5
diff --git a/.gitreview b/.gitreview
new file mode 100644
index 0000000..d1989d6
--- /dev/null
+++ b/.gitreview
@@ -0,0 +1,9 @@
+# SPDX-FileCopyrightText: 2020 Open Networking Foundation <info@opennetworking.org>
+#
+# SPDX-License-Identifier: LicenseRef-ONF-Member-Only-1.0
+
+[gerrit]
+host=gerrit.opencord.org
+port=29418
+project=aether-monitoring.git
+defaultremote=origin
diff --git a/edge-monitoring/agent_adb/config.json b/edge-monitoring/agent_adb/config.json
new file mode 100644
index 0000000..8c91c94
--- /dev/null
+++ b/edge-monitoring/agent_adb/config.json
@@ -0,0 +1,12 @@
+{
+    "edge_name": "production-edge-example",
+    "adb": {
+        "path": "/usr/local/bin/adb",
+        "apn_mode_toggle_location": {
+            "x": "550",
+            "y": "700"
+        }
+    },
+    "report_url": "https://aether.onlab.us/edges",
+    "report_interval": 180
+}
diff --git a/edge-monitoring/edge_monitoring_agent.py b/edge-monitoring/agent_adb/edge_monitoring_agent_adb.py
similarity index 86%
rename from edge-monitoring/edge_monitoring_agent.py
rename to edge-monitoring/agent_adb/edge_monitoring_agent_adb.py
index bcfd5b5..f26f5d1 100644
--- a/edge-monitoring/edge_monitoring_agent.py
+++ b/edge-monitoring/agent_adb/edge_monitoring_agent_adb.py
@@ -71,8 +71,8 @@
 
 
 def _run_adb_shell(adb, command):
-    result = adb.shell_command(command)
-    if adb.lastFailed():
+    result, error = adb.shell_command(command)
+    if error is not None:
         err = "[ERROR]: " + command + " failed"
         return False, err
     time.sleep(2)
@@ -80,15 +80,10 @@
     return True, result
 
 
-def get_control_plane_state():
+def get_control_plane_state(adb):
     '''
     check aether control plane works by toggling airplane mode
     '''
-    adb = ADB()
-    if adb.set_adb_path(CONF.adb.path) is False:
-        err = "[ERROR]: " + CONF.adb.path + " not found"
-        return State.error, err
-
     # get the current airplane mode
     success, result = _run_adb_shell(adb, ADB_GET_COMMANDS['apn_mode'])
     if not success or result is None:
@@ -121,15 +116,10 @@
     return State(state), None
 
 
-def get_user_plane_state():
+def get_user_plane_state(adb):
     '''
     checks aether user plane connectivity with ping to 8.8.8.8
     '''
-    adb = ADB()
-    if adb.set_adb_path(CONF.adb.path) is False:
-        err = "[ERROR]: " + CONF.adb.path + " not found"
-        return State.error, err
-
     success, result = _run_adb_shell(adb, ADB_GET_COMMANDS['ping_result'])
     if not success or result is None:
         return State.error, result
@@ -142,24 +132,32 @@
     '''
     report the aether network state to the monitoring server
     '''
-    response = requests.post(CONF.report_url, json=edge_status)
-    return requests.codes.ok,
-    if response == requests.codes.ok:
-        print("[INFO]: reported the status")
-    else:
-        response.raise_for_status()
+    try:
+        result = requests.post(CONF.report_url, json=edge_status)
+    except requests.exceptions.ConnectionError:
+        pass
 
 
 def run():
+    adb = ADB()
+    if adb.set_adb_path(CONF.adb.path) is False:
+        err = "[ERROR]: " + CONF.adb.path + " not found"
+        return State.error, err
+
+    dev = adb.get_devices()
+    if len(dev) == 0:
+      return State.error, "Device not found"
+    adb.set_target_device(dev[0])
+
     while True:
-        cp_state, err = get_control_plane_state()
-        up_state, err = get_user_plane_state()
+        cp_state, err = get_control_plane_state(adb)
+        up_state, err = get_user_plane_state(adb)
 
         edge_status['status']['control_plane'] = cp_state.name
         edge_status['status']['user_plane'] = up_state.name
 
         report_aether_network_state()
-        time.sleep(600)
+        time.sleep(CONF.report_interval)
 
 
 def main():
diff --git a/edge-monitoring/agent_adb/requirements.txt b/edge-monitoring/agent_adb/requirements.txt
new file mode 100644
index 0000000..bee016d
--- /dev/null
+++ b/edge-monitoring/agent_adb/requirements.txt
@@ -0,0 +1,3 @@
+requests
+git+git://github.com/sch3m4/pyadb@master#egg=pyadb
+python-daemon
diff --git a/edge-monitoring/agent_modem/config.json b/edge-monitoring/agent_modem/config.json
new file mode 100644
index 0000000..c447bf4
--- /dev/null
+++ b/edge-monitoring/agent_modem/config.json
@@ -0,0 +1,12 @@
+{
+    "edge_name": "production-edge-test",
+    "modem": {
+        "port": "/dev/ttyACM0",
+        "baud": 115200,
+        "ip_addr": "192.168.0.1"
+    },
+    "ping_to": "1.1.1.1",
+    "report_url": "https://aether.onlab.us/edges",
+    "report_interval": 180,
+    "log_file": "/var/log/edge_monitoring_agent.log"
+}
diff --git a/edge-monitoring/agent_modem/edge-mon-agent.service b/edge-monitoring/agent_modem/edge-mon-agent.service
new file mode 100644
index 0000000..1d6ba22
--- /dev/null
+++ b/edge-monitoring/agent_modem/edge-mon-agent.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=Aether Edge Monitoring Agent
+After=multi-user.target
+
+[Service]
+Environment=CONFIG_FILE=/home/pi/aether-monitoring/edge-monitoring/agent_modem/config.json
+Type=simple
+ExecStartPre=/bin/sleep 30
+ExecStart=/usr/bin/python3 /home/pi/aether-monitoring/edge-monitoring/agent_modem/edge_monitoring_agent_modem.py
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/edge-monitoring/agent_modem/edge_monitoring_agent_modem.py b/edge-monitoring/agent_modem/edge_monitoring_agent_modem.py
new file mode 100644
index 0000000..bb41560
--- /dev/null
+++ b/edge-monitoring/agent_modem/edge_monitoring_agent_modem.py
@@ -0,0 +1,224 @@
+#!/usr/bin/env python
+
+# 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
+
+import sys
+import os
+import json
+import logging
+import enum
+import requests
+import time
+import serial
+from collections import namedtuple
+
+'''
+Simple script that checks Aether network operational status periodically
+by controlling the attached 4G/LTE modem with AT commands and
+report the result to the central monitoring server.
+'''
+
+
+CONF = json.loads(
+    open(os.getenv('CONFIG_FILE', "./config.json")).read(),
+    object_hook=lambda d: namedtuple('X', d.keys())(*d.values())
+)
+
+logging.basicConfig(
+    filename=CONF.log_file,
+    format='%(asctime)s [%(levelname)s] %(message)s',
+    level=logging.INFO
+)
+
+report = {
+    'name': CONF.edge_name,
+    'status': {
+        'control_plane': None,
+        'user_plane': None
+    }
+}
+
+
+class State(enum.Enum):
+    error = "-1"
+    disconnected = "0"
+    connected = "1"
+
+    @classmethod
+    def has_value(cls, value):
+        return value in cls._value2member_map_
+
+
+class Modem():
+    log = logging.getLogger('aether_edge_monitoring.Modem')
+
+    read_timeout = 0.1
+
+    def __init__(self, port, baudrate):
+        self.port = port
+        self.baudrate = baudrate
+        self._response = None
+
+    def connect(self):
+        self.serial = serial.Serial(
+            port=self.port,
+            baudrate=self.baudrate,
+            timeout=1)
+
+    def _write(self, command):
+        if self.serial.inWaiting() > 0:
+            self.serial.flushInput()
+
+        self._response = b""
+
+        self.serial.write(bytearray(command + "\r", "ascii"))
+        read = self.serial.inWaiting()
+        while True:
+            if read > 0:
+                self._response += self.serial.read(read)
+            else:
+                time.sleep(self.read_timeout)
+            read = self.serial.inWaiting()
+            if read == 0:
+                break
+        return self._response.decode("ascii").replace('\r\n', ' ')
+
+    def write(self, command, wait_resp=True):
+        response = self._write(command)
+        self.log.debug("%s: %s", command, response)
+
+        if wait_resp and "ERROR" in response:
+            return False, None
+        return True, response
+
+    def is_connected(self):
+        success, result = self.write('AT+CGATT?')
+        if not success or 'CGATT:' not in result:
+            return State.error
+        state = result.split('CGATT:')[1].split(' ')[0]
+        return State(state)
+
+    def close(self):
+        self.serial.close()
+
+
+def get_control_plane_state(modem):
+    # Delete the existing session
+    # "echo" works more stable than serial for this action
+    # success, result = modem.write('AT+CFUN=0')
+    logging.debug("echo 'AT+CFUN=0' > " + CONF.modem.port)
+    success = os.system("echo 'AT+CFUN=0' > " + CONF.modem.port)
+    logging.debug("result: %s", success)
+    if success is not 0:
+        msg = "Write 'AT+CFUN=0' failed"
+        logging.error(msg)
+        return State.error, msg
+
+    # Wait until the modem is fully disconnected
+    retry = 0
+    state = None
+    while retry < 5:
+        state = modem.is_connected()
+        if state is State.disconnected:
+            break
+        time.sleep(1)
+        retry += 1
+
+    # Consider the modem is not responding if disconnection failed
+    if state is not State.disconnected:
+        msg = "Failed to disconnect."
+        logging.error(msg)
+        return State.error, msg
+
+    time.sleep(2)
+    # Create a new session
+    # "echo" works more stable than serial for this action
+    # success, result = modem.write('AT+CGATT=1')
+    logging.debug("echo 'AT+CFUN=1' > " + CONF.modem.port)
+    success = os.system("echo 'AT+CFUN=1' > " + CONF.modem.port)
+    logging.debug("result: %s", success)
+    if success is not 0:
+        msg = "Write 'AT+CFUN=1' failed"
+        logging.error(msg)
+        return State.error, msg
+
+    # Give 10 sec for the modem to be fully connected
+    retry = 0
+    while retry < 10:
+        state = modem.is_connected()
+        if state is State.connected:
+            break
+        time.sleep(1)
+        retry += 1
+    # CGATT sometimes returns None
+    if state is State.error:
+        state = State.disconnected
+
+    time.sleep(2)
+    return state, None
+
+
+def get_user_plane_state(modem):
+    resp = os.system("ping -c 3 " + CONF.ping_to + ">/dev/null 2>&1")
+    return State.connected if resp is 0 else State.disconnected, None
+
+
+def report_status(cp_state, up_state):
+    report['status']['control_plane'] = cp_state.name
+    report['status']['user_plane'] = up_state.name
+
+    logging.info("Sending report %s", report)
+    try:
+        result = requests.post(CONF.report_url, json=report)
+    except requests.exceptions.ConnectionError:
+        logging.error("Failed to report for %s", e)
+        pass
+    try:
+        result.raise_for_status()
+    except requests.exceptions.HTTPError as e:
+        logging.error("Failed to report for %s", e)
+        pass
+
+
+def main():
+    modem = Modem(CONF.modem.port, CONF.modem.baud)
+    try:
+        modem.connect()
+    except serial.serialutil.SerialException as e:
+        logging.error("Failed to connect the modem for %s", e)
+        sys.exit(1)
+
+    success = os.system("sudo ip route replace {}/32 via {}".format(
+        CONF.ping_to, CONF.modem.ip_addr))
+    if success is not 0:
+        logging.error("Failed to add test routing.")
+        sys.exit(1)
+
+    while True:
+        cp_state, cp_msg = get_control_plane_state(modem)
+        up_state, up_msg = get_user_plane_state(modem)
+
+        if cp_state is State.error:
+            logging.error("Modem is in error state.")
+            os.system("sudo shutdown -r now")
+
+        report_status(cp_state, up_state)
+        time.sleep(CONF.report_interval)
+
+    modem.close()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/edge-monitoring/agent_modem/requirements.txt b/edge-monitoring/agent_modem/requirements.txt
new file mode 100644
index 0000000..1b96c7b
--- /dev/null
+++ b/edge-monitoring/agent_modem/requirements.txt
@@ -0,0 +1,2 @@
+requests
+pyserial
diff --git a/edge-monitoring/config.json b/edge-monitoring/config.json
deleted file mode 100644
index 8f8a051..0000000
--- a/edge-monitoring/config.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-    "edge_name": "production-edge-onf-menlo",
-    "adb": {
-        "path": "/usr/local/bin/adb",
-        "apn_mode_toggle_location": {
-            "x": "550",
-            "y": "700"
-        }
-    },
-    "report_url": "https://aether.onlab.us/edges"
-}
diff --git a/edge-monitoring/requirements.txt b/edge-monitoring/requirements.txt
index 7e08052..7b0752e 100644
--- a/edge-monitoring/requirements.txt
+++ b/edge-monitoring/requirements.txt
@@ -1,7 +1,4 @@
 flask
-requests
-git+git://github.com/sch3m4/pyadb@master#egg=pyadb
-python-daemon
 prometheus-client
 pytz
 icalevents