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/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