blob: f66947109a7a7ccefc9dccb2c59c4bfb107ad4d2 [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#
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -07005# SPDX-License-Identifier: LicenseRef-ONF-Member-Only-1.0
Hyunsun Moon53097ea2020-09-04 17:20:29 -07006
7import sys
8import os
9import json
10import logging
11import enum
12import requests
13import time
14import serial
Jeremy Ronquillod996b512021-02-13 13:45:47 -080015import subprocess
Hyunsun Moon53097ea2020-09-04 17:20:29 -070016from collections import namedtuple
17
18'''
Jeremy Ronquilloa944fbc2021-03-30 10:57:45 -070019"Simple" script that checks Aether network operational status periodically
Hyunsun Moon53097ea2020-09-04 17:20:29 -070020by controlling the attached 4G/LTE modem with AT commands and
21report the result to the central monitoring server.
22'''
23
24
25CONF = json.loads(
26 open(os.getenv('CONFIG_FILE', "./config.json")).read(),
27 object_hook=lambda d: namedtuple('X', d.keys())(*d.values())
28)
29
30logging.basicConfig(
31 filename=CONF.log_file,
32 format='%(asctime)s [%(levelname)s] %(message)s',
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -070033 level=logging.getLevelName(CONF.log_level)
Hyunsun Moon53097ea2020-09-04 17:20:29 -070034)
35
Hyunsun Moon53097ea2020-09-04 17:20:29 -070036
37class State(enum.Enum):
38 error = "-1"
39 disconnected = "0"
40 connected = "1"
41
42 @classmethod
43 def has_value(cls, value):
44 return value in cls._value2member_map_
45
46
47class Modem():
48 log = logging.getLogger('aether_edge_monitoring.Modem')
49
50 read_timeout = 0.1
51
52 def __init__(self, port, baudrate):
53 self.port = port
54 self.baudrate = baudrate
55 self._response = None
56
57 def connect(self):
58 self.serial = serial.Serial(
59 port=self.port,
60 baudrate=self.baudrate,
61 timeout=1)
62
63 def _write(self, command):
64 if self.serial.inWaiting() > 0:
65 self.serial.flushInput()
66
67 self._response = b""
68
69 self.serial.write(bytearray(command + "\r", "ascii"))
70 read = self.serial.inWaiting()
71 while True:
72 if read > 0:
73 self._response += self.serial.read(read)
74 else:
75 time.sleep(self.read_timeout)
76 read = self.serial.inWaiting()
77 if read == 0:
78 break
79 return self._response.decode("ascii").replace('\r\n', ' ')
80
81 def write(self, command, wait_resp=True):
82 response = self._write(command)
83 self.log.debug("%s: %s", command, response)
84
85 if wait_resp and "ERROR" in response:
86 return False, None
87 return True, response
88
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -070089 def get_state(self):
Hyunsun Moon53097ea2020-09-04 17:20:29 -070090 success, result = self.write('AT+CGATT?')
91 if not success or 'CGATT:' not in result:
92 return State.error
93 state = result.split('CGATT:')[1].split(' ')[0]
94 return State(state)
95
96 def close(self):
97 self.serial.close()
98
99
100def get_control_plane_state(modem):
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700101 # Disable radio fuction
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700102 # "echo" works more stable than serial for this action
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700103 try:
104 logging.debug("echo 'AT+CFUN=0' > " + CONF.modem.port)
105 subprocess.check_output(
106 "echo 'AT+CFUN=0' > " + CONF.modem.port, shell=True)
107 except subprocess.CalledProcessError as e:
108 logging.error("Write 'AT+CFUN=0' failed")
109 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700110
111 # Wait until the modem is fully disconnected
112 retry = 0
113 state = None
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700114 while retry < CONF.detach_timeout:
115 state = modem.get_state()
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700116 if state is State.disconnected:
117 break
118 time.sleep(1)
119 retry += 1
120
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700121 if state is not State.disconnected:
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700122 logging.error("Failed to disconnect")
123 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700124
125 time.sleep(2)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700126 # Enable radio function
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700127 # "echo" works more stable than serial for this action
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700128 try:
129 logging.debug("echo 'AT+CFUN=1' > " + CONF.modem.port)
130 subprocess.check_output(
131 "echo 'AT+CFUN=1' > " + CONF.modem.port, shell=True)
132 except subprocess.CalledProcessError as e:
133 logging.error("Write 'AT+CFUN=1' failed")
134 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700135
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700136 # Wait attach_timeout sec for the modem to be fully connected
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700137 retry = 0
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700138 while retry < CONF.attach_timeout:
139 state = modem.get_state()
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700140 if state is State.connected:
141 break
142 time.sleep(1)
143 retry += 1
144 # CGATT sometimes returns None
145 if state is State.error:
146 state = State.disconnected
147
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700148 return state
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700149
150
151def get_user_plane_state(modem):
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700152 try:
153 subprocess.check_output(
154 "ping -c 3 " + CONF.ips.user_plane_ping_test + ">/dev/null 2>&1",
155 shell=True)
156 return State.connected
157 except subprocess.CalledProcessError as e:
158 logging.warning("User plane test failed")
159 return State.disconnected
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700160
161
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800162def run_ping_test(ip, count):
163 '''
164 Runs the ping test
165 Input: IP to ping, # times to ping
166 Returns: dict of the min/avg/max/stddev numbers from the ping command result
167 '''
Jeremy Ronquilloa944fbc2021-03-30 10:57:45 -0700168 result = {'min': 0.0,
169 'avg': 0.0,
170 'max': 0.0,
171 'stddev': 0.0}
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800172 try:
173 pingResult = subprocess.check_output(
174 "ping -c " + str(count) + " " + ip + \
175 " | tail -1 | awk '{print $4}'",
176 shell=True).decode("UTF-8").split("/")
177 result = {'min': float(pingResult[0]),
178 'avg': float(pingResult[1]),
179 'max': float(pingResult[2]),
180 'stddev': float(pingResult[3])}
181 except Exception as e:
182 logging.error("Ping test failed for " + ip + ": %s", e)
183 return result
184
185
186def get_ping_test(modem):
187 '''
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700188 Prepares the ping test.
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800189 Each ping test result saves the min/avg/max/stddev to dict.
190 1) Performs ping test to Google Public DNS for 10 iterations.
191 2) # TODO: Performs ping to device on network.
192 '''
193 speedtest_ping = {}
Jeremy Ronquilloa944fbc2021-03-30 10:57:45 -0700194 speedtest_ping['dns'] = run_ping_test(CONF.ips.speedtest_ping_dns, 10)
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800195 return speedtest_ping
196
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700197def run_iperf_test(ip, port, time_duration, is_downlink):
198 '''
199 Runs iperf test to specified IP in the config file.
200 - Runs for 10 seconds (10 iterations)
201 - Retrieves downlink and uplink test results from json output
202 '''
203 result = 0.0
204 if not ip:
205 return result
206 try:
207 iperfResult = json.loads(subprocess.check_output(
208 "iperf3 -c " + ip +
209 " -p " + str(port) +
210 " -t " + str(time_duration) +
211 (" -R " if is_downlink else "") +
212 " --json", shell=True).decode("UTF-8"))
213 received_mbps = iperfResult['end']['sum_received']['bits_per_second'] / 1000000
214 sent_mbps = iperfResult['end']['sum_sent']['bits_per_second'] / 1000000.0
215 result = received_mbps if is_downlink else sent_mbps
216 except Exception as e:
217 logging.error("iperf test failed for " + ip + ": %s", e)
218 return result
219
220
221def get_iperf_test(modem):
222 '''
223 Prepares the iperf test.
224 '''
225 speedtest_iperf = {}
226 speedtest_iperf['cluster'] = {}
227 speedtest_iperf['cluster']['downlink'] = run_iperf_test(CONF.ips.speedtest_iperf, CONF.iperf_port, 10, True)
228 speedtest_iperf['cluster']['uplink'] = run_iperf_test(CONF.ips.speedtest_iperf, CONF.iperf_port, 10, False)
229
230 return speedtest_iperf
231
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800232
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700233def get_signal_quality(modem):
234 success, result = modem.write('AT+CESQ')
235 if not success or 'CESQ: ' not in result:
236 logging.error("Failed to get signal quality")
Hyunsun Moon4c804ea2021-04-13 11:56:40 -0700237 return { 'rsrq': 0, 'rsrp': 0 }
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700238
239 logging.debug("%s", result)
240 tmp_rsrq = result.split('CESQ:')[1].split(',')[4]
241 tmp_rsrp = result.split('CESQ:')[1].split(',')[5]
242
243 rsrq = int(tmp_rsrq.strip())
244 rsrp = int(tmp_rsrp.strip().split(' ')[0])
245 result = {
246 'rsrq': 0 if rsrq is 255 else rsrq,
247 'rsrp': 0 if rsrp is 255 else rsrp
248 }
249
250 return result
251
252
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700253def report_status(signal_quality, cp_state=None, up_state=None, speedtest_ping=None, speedtest_iperf=None):
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700254 report = {
255 'name': CONF.edge_name,
256 'status': {
257 'control_plane': "disconnected",
258 'user_plane': "disconnected"
259 },
260 'speedtest': {
261 'ping': {
262 'dns': {
263 'min': 0.0,
264 'avg': 0.0,
265 'max': 0.0,
266 'stddev': 0.0
267 }
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700268 },
269 'iperf': {
270 'cluster': {
271 'downlink': 0.0,
272 'uplink': 0.0
273 }
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700274 }
275 },
276 'signal_quality': {
277 'rsrq': 0,
278 'rsrp': 0
279 }
280 }
281
282 if cp_state is not None:
283 report['status']['control_plane'] = cp_state.name
284 if up_state is not None:
285 report['status']['user_plane'] = up_state.name
286 if speedtest_ping is not None:
287 report['speedtest']['ping'] = speedtest_ping
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700288 if speedtest_iperf is not None:
289 report['speedtest']['iperf'] = speedtest_iperf
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700290 report['signal_quality'] = signal_quality
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700291
292 logging.info("Sending report %s", report)
293 try:
294 result = requests.post(CONF.report_url, json=report)
295 except requests.exceptions.ConnectionError:
296 logging.error("Failed to report for %s", e)
297 pass
298 try:
299 result.raise_for_status()
300 except requests.exceptions.HTTPError as e:
301 logging.error("Failed to report for %s", e)
302 pass
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700303 time.sleep(CONF.report_interval)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700304
305
306def main():
307 modem = Modem(CONF.modem.port, CONF.modem.baud)
308 try:
309 modem.connect()
310 except serial.serialutil.SerialException as e:
311 logging.error("Failed to connect the modem for %s", e)
Hyunsun Moon2b7d3e12021-01-04 13:30:19 -0800312 sys.exit(1)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700313
Hyunsun Moonb980ed32021-04-11 19:30:06 -0700314 for ip in CONF.ips:
315 if not ip:
316 continue
317 try:
318 subprocess.check_output("sudo ip route replace {}/32 via {}".format(
319 ip, CONF.modem.ip_addr), shell=True)
320 logging.info("Added route for test address: %s", ip)
321 except subprocess.CalledProcessError as e:
322 logging.error("Failed to add routes %s", e.output)
323 sys.exit(1)
324
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700325 while True:
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700326 signal_quality = get_signal_quality(modem)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700327
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700328 cp_state = get_control_plane_state(modem)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700329 if cp_state is State.error:
330 logging.error("Modem is in error state.")
Hyunsun Moonf4242372020-10-04 23:32:38 -0700331 sys.exit(1)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700332 if cp_state is State.disconnected:
333 # Failed to attach, don't need to run other tests
334 report_status(signal_quality)
335 continue
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700336
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700337 up_state = get_user_plane_state(modem)
338 if up_state is State.disconnected:
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700339 # Basic user plane test failed, don't need to run the rest of tests
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700340 report_status(signal_quality, cp_state)
341 continue
342
343 speedtest_ping = get_ping_test(modem)
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700344 speedtest_iperf = get_iperf_test(modem)
345
346 report_status(signal_quality, cp_state, up_state, speedtest_ping, speedtest_iperf)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700347
348 modem.close()
349
350
351if __name__ == "__main__":
352 main()