blob: cdfab63fb48fb8e3fbd1db37b5537f1a50d97077 [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 '''
188 Each ping test result saves the min/avg/max/stddev to dict.
189 1) Performs ping test to Google Public DNS for 10 iterations.
190 2) # TODO: Performs ping to device on network.
191 '''
192 speedtest_ping = {}
Jeremy Ronquilloa944fbc2021-03-30 10:57:45 -0700193 speedtest_ping['dns'] = run_ping_test(CONF.ips.speedtest_ping_dns, 10)
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800194 return speedtest_ping
195
196
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700197def get_signal_quality(modem):
198 success, result = modem.write('AT+CESQ')
199 if not success or 'CESQ: ' not in result:
200 logging.error("Failed to get signal quality")
201 return 0, 0
202
203 logging.debug("%s", result)
204 tmp_rsrq = result.split('CESQ:')[1].split(',')[4]
205 tmp_rsrp = result.split('CESQ:')[1].split(',')[5]
206
207 rsrq = int(tmp_rsrq.strip())
208 rsrp = int(tmp_rsrp.strip().split(' ')[0])
209 result = {
210 'rsrq': 0 if rsrq is 255 else rsrq,
211 'rsrp': 0 if rsrp is 255 else rsrp
212 }
213
214 return result
215
216
217def report_status(signal_quality, cp_state=None, up_state=None, speedtest_ping=None):
218 report = {
219 'name': CONF.edge_name,
220 'status': {
221 'control_plane': "disconnected",
222 'user_plane': "disconnected"
223 },
224 'speedtest': {
225 'ping': {
226 'dns': {
227 'min': 0.0,
228 'avg': 0.0,
229 'max': 0.0,
230 'stddev': 0.0
231 }
232 }
233 },
234 'signal_quality': {
235 'rsrq': 0,
236 'rsrp': 0
237 }
238 }
239
240 if cp_state is not None:
241 report['status']['control_plane'] = cp_state.name
242 if up_state is not None:
243 report['status']['user_plane'] = up_state.name
244 if speedtest_ping is not None:
245 report['speedtest']['ping'] = speedtest_ping
246 report['signal_quality'] = signal_quality
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700247
248 logging.info("Sending report %s", report)
249 try:
250 result = requests.post(CONF.report_url, json=report)
251 except requests.exceptions.ConnectionError:
252 logging.error("Failed to report for %s", e)
253 pass
254 try:
255 result.raise_for_status()
256 except requests.exceptions.HTTPError as e:
257 logging.error("Failed to report for %s", e)
258 pass
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700259 time.sleep(CONF.report_interval)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700260
261
262def main():
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700263 for ip in CONF.ips:
264 try:
265 subprocess.check_output("sudo ip route replace {}/32 via {}".format(
266 ip, CONF.modem.ip_addr), shell=True)
267 except subprocess.CalledProcessError as e:
268 logging.error("Failed to add routes", e.returncode, e.output)
269 sys.exit(1)
270
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700271 modem = Modem(CONF.modem.port, CONF.modem.baud)
272 try:
273 modem.connect()
274 except serial.serialutil.SerialException as e:
275 logging.error("Failed to connect the modem for %s", e)
Hyunsun Moon2b7d3e12021-01-04 13:30:19 -0800276 sys.exit(1)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700277
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700278 while True:
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700279 signal_quality = get_signal_quality(modem)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700280
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700281 cp_state = get_control_plane_state(modem)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700282 if cp_state is State.error:
283 logging.error("Modem is in error state.")
Hyunsun Moonf4242372020-10-04 23:32:38 -0700284 sys.exit(1)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700285 if cp_state is State.disconnected:
286 # Failed to attach, don't need to run other tests
287 report_status(signal_quality)
288 continue
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700289
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700290 up_state = get_user_plane_state(modem)
291 if up_state is State.disconnected:
292 # Basic user plan test failed, don't need to run the rest of tests
293 report_status(signal_quality, cp_state)
294 continue
295
296 speedtest_ping = get_ping_test(modem)
297 report_status(signal_quality, cp_state, up_state, speedtest_ping)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700298
299 modem.close()
300
301
302if __name__ == "__main__":
303 main()