blob: 07443040d0933c0fe95ce9ba86a55d471dadbd36 [file] [log] [blame]
Hyunsun Moondb72b8f2020-11-02 18:03:39 -08001#!/usr/bin/env python3
Hyunsun Moon53097ea2020-09-04 17:20:29 -07002
3# Copyright 2020-present Open Networking Foundation
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License
16
17import sys
18import os
19import json
20import logging
21import enum
22import requests
23import time
24import serial
Jeremy Ronquillod996b512021-02-13 13:45:47 -080025import subprocess
Hyunsun Moon53097ea2020-09-04 17:20:29 -070026from collections import namedtuple
27
28'''
29Simple script that checks Aether network operational status periodically
30by controlling the attached 4G/LTE modem with AT commands and
31report the result to the central monitoring server.
32'''
33
34
35CONF = json.loads(
36 open(os.getenv('CONFIG_FILE', "./config.json")).read(),
37 object_hook=lambda d: namedtuple('X', d.keys())(*d.values())
38)
39
40logging.basicConfig(
41 filename=CONF.log_file,
42 format='%(asctime)s [%(levelname)s] %(message)s',
Hyunsun Moona5a00602020-11-22 21:47:50 -080043 level=logging.ERROR
Hyunsun Moon53097ea2020-09-04 17:20:29 -070044)
45
46report = {
47 'name': CONF.edge_name,
48 'status': {
49 'control_plane': None,
50 'user_plane': None
Jeremy Ronquillod996b512021-02-13 13:45:47 -080051 },
52 'speedtest': {
53 'ping': {
54 'dns': {
55 'min': None,
56 'avg': None,
57 'max': None,
58 'stddev': None
59 }
60 }
Hyunsun Moon53097ea2020-09-04 17:20:29 -070061 }
62}
63
64
65class State(enum.Enum):
66 error = "-1"
67 disconnected = "0"
68 connected = "1"
69
70 @classmethod
71 def has_value(cls, value):
72 return value in cls._value2member_map_
73
74
75class Modem():
76 log = logging.getLogger('aether_edge_monitoring.Modem')
77
78 read_timeout = 0.1
79
80 def __init__(self, port, baudrate):
81 self.port = port
82 self.baudrate = baudrate
83 self._response = None
84
85 def connect(self):
86 self.serial = serial.Serial(
87 port=self.port,
88 baudrate=self.baudrate,
89 timeout=1)
90
91 def _write(self, command):
92 if self.serial.inWaiting() > 0:
93 self.serial.flushInput()
94
95 self._response = b""
96
97 self.serial.write(bytearray(command + "\r", "ascii"))
98 read = self.serial.inWaiting()
99 while True:
100 if read > 0:
101 self._response += self.serial.read(read)
102 else:
103 time.sleep(self.read_timeout)
104 read = self.serial.inWaiting()
105 if read == 0:
106 break
107 return self._response.decode("ascii").replace('\r\n', ' ')
108
109 def write(self, command, wait_resp=True):
110 response = self._write(command)
111 self.log.debug("%s: %s", command, response)
112
113 if wait_resp and "ERROR" in response:
114 return False, None
115 return True, response
116
117 def is_connected(self):
118 success, result = self.write('AT+CGATT?')
119 if not success or 'CGATT:' not in result:
120 return State.error
121 state = result.split('CGATT:')[1].split(' ')[0]
122 return State(state)
123
124 def close(self):
125 self.serial.close()
126
127
128def get_control_plane_state(modem):
129 # Delete the existing session
130 # "echo" works more stable than serial for this action
131 # success, result = modem.write('AT+CFUN=0')
132 logging.debug("echo 'AT+CFUN=0' > " + CONF.modem.port)
133 success = os.system("echo 'AT+CFUN=0' > " + CONF.modem.port)
134 logging.debug("result: %s", success)
135 if success is not 0:
136 msg = "Write 'AT+CFUN=0' failed"
137 logging.error(msg)
138 return State.error, msg
139
140 # Wait until the modem is fully disconnected
141 retry = 0
142 state = None
143 while retry < 5:
144 state = modem.is_connected()
145 if state is State.disconnected:
146 break
147 time.sleep(1)
148 retry += 1
149
150 # Consider the modem is not responding if disconnection failed
151 if state is not State.disconnected:
152 msg = "Failed to disconnect."
153 logging.error(msg)
154 return State.error, msg
155
156 time.sleep(2)
157 # Create a new session
158 # "echo" works more stable than serial for this action
159 # success, result = modem.write('AT+CGATT=1')
160 logging.debug("echo 'AT+CFUN=1' > " + CONF.modem.port)
161 success = os.system("echo 'AT+CFUN=1' > " + CONF.modem.port)
162 logging.debug("result: %s", success)
163 if success is not 0:
164 msg = "Write 'AT+CFUN=1' failed"
165 logging.error(msg)
166 return State.error, msg
167
168 # Give 10 sec for the modem to be fully connected
169 retry = 0
Hyunsun Moon2b7d3e12021-01-04 13:30:19 -0800170 while retry < 30:
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700171 state = modem.is_connected()
172 if state is State.connected:
173 break
174 time.sleep(1)
175 retry += 1
176 # CGATT sometimes returns None
177 if state is State.error:
178 state = State.disconnected
179
180 time.sleep(2)
181 return state, None
182
183
184def get_user_plane_state(modem):
185 resp = os.system("ping -c 3 " + CONF.ping_to + ">/dev/null 2>&1")
186 return State.connected if resp is 0 else State.disconnected, None
187
188
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800189def run_ping_test(ip, count):
190 '''
191 Runs the ping test
192 Input: IP to ping, # times to ping
193 Returns: dict of the min/avg/max/stddev numbers from the ping command result
194 '''
195 result = {'min': None,
196 'avg': None,
197 'max': None,
198 'stddev': None}
199 try:
200 pingResult = subprocess.check_output(
201 "ping -c " + str(count) + " " + ip + \
202 " | tail -1 | awk '{print $4}'",
203 shell=True).decode("UTF-8").split("/")
204 result = {'min': float(pingResult[0]),
205 'avg': float(pingResult[1]),
206 'max': float(pingResult[2]),
207 'stddev': float(pingResult[3])}
208 except Exception as e:
209 logging.error("Ping test failed for " + ip + ": %s", e)
210 return result
211
212
213def get_ping_test(modem):
214 '''
215 Each ping test result saves the min/avg/max/stddev to dict.
216 1) Performs ping test to Google Public DNS for 10 iterations.
217 2) # TODO: Performs ping to device on network.
218 '''
219 speedtest_ping = {}
220 speedtest_ping['dns'] = run_ping_test("8.8.8.8", 10)
221 return speedtest_ping
222
223
224def report_status(cp_state, up_state, speedtest_ping):
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700225 report['status']['control_plane'] = cp_state.name
226 report['status']['user_plane'] = up_state.name
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800227 report['speedtest']['ping'] = speedtest_ping
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700228
229 logging.info("Sending report %s", report)
230 try:
231 result = requests.post(CONF.report_url, json=report)
232 except requests.exceptions.ConnectionError:
233 logging.error("Failed to report for %s", e)
234 pass
235 try:
236 result.raise_for_status()
237 except requests.exceptions.HTTPError as e:
238 logging.error("Failed to report for %s", e)
239 pass
240
241
242def main():
243 modem = Modem(CONF.modem.port, CONF.modem.baud)
244 try:
245 modem.connect()
246 except serial.serialutil.SerialException as e:
247 logging.error("Failed to connect the modem for %s", e)
Hyunsun Moon2b7d3e12021-01-04 13:30:19 -0800248 sys.exit(1)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700249
250 success = os.system("sudo ip route replace {}/32 via {}".format(
251 CONF.ping_to, CONF.modem.ip_addr))
252 if success is not 0:
253 logging.error("Failed to add test routing.")
254 sys.exit(1)
255
256 while True:
257 cp_state, cp_msg = get_control_plane_state(modem)
258 up_state, up_msg = get_user_plane_state(modem)
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800259 speedtest_ping = get_ping_test(modem)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700260
261 if cp_state is State.error:
262 logging.error("Modem is in error state.")
Hyunsun Moonf4242372020-10-04 23:32:38 -0700263 sys.exit(1)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700264
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800265 report_status(cp_state, up_state, speedtest_ping)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700266 time.sleep(CONF.report_interval)
267
268 modem.close()
269
270
271if __name__ == "__main__":
272 main()