blob: c1a804299b400714b9d13a1b2ee7c321ee462b27 [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
Jeremy Ronquillo8d108652021-11-22 17:34:58 -080012import pycurl
Hyunsun Moon53097ea2020-09-04 17:20:29 -070013import serial
Jeremy Ronquillod996b512021-02-13 13:45:47 -080014import subprocess
Jeremy Ronquilloef17e362021-11-08 10:56:42 -080015import time
Hyunsun Moon53097ea2020-09-04 17:20:29 -070016from collections import namedtuple
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -070017from statistics import median
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -080018import xml.etree.ElementTree as ET
Shad Ansaria6ce0772022-03-15 21:34:51 -070019import traceback
Hyunsun Moon53097ea2020-09-04 17:20:29 -070020
21'''
Jeremy Ronquilloa944fbc2021-03-30 10:57:45 -070022"Simple" script that checks Aether network operational status periodically
Hyunsun Moon53097ea2020-09-04 17:20:29 -070023by controlling the attached 4G/LTE modem with AT commands and
24report the result to the central monitoring server.
25'''
26
Shad Ansari341a1c92022-03-02 09:14:40 -080027USE_MODEM_CMDS = False
28
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -070029# Parse config with backwards compatibility with config.json pre 0.6.6
30config_file_contents = open(os.getenv('CONFIG_FILE', "./config.json")).read()
31config_file_contents = config_file_contents.replace("user_plane_ping_test", "dns")
32config_file_contents = config_file_contents.replace("speedtest_iperf", "iperf_server")
Shad Ansari416ccab2022-03-09 19:06:43 -080033config_file_contents = config_file_contents.replace("\"speedtest_ping_dns\": \"8.8.8.8\",", "")
34# replace 1.1.1.1 with 8.8.8.8
Shad Ansari2a376d72022-03-10 14:01:32 -080035config_file_contents = config_file_contents.replace('1.1.1.1', '8.8.8.8')
Hyunsun Moon53097ea2020-09-04 17:20:29 -070036CONF = json.loads(
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -070037 config_file_contents, object_hook=lambda d: namedtuple('X', d.keys())(*d.values())
Hyunsun Moon53097ea2020-09-04 17:20:29 -070038)
39
40logging.basicConfig(
41 filename=CONF.log_file,
42 format='%(asctime)s [%(levelname)s] %(message)s',
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -070043 level=logging.getLevelName(CONF.log_level)
Hyunsun Moon53097ea2020-09-04 17:20:29 -070044)
45
Hyunsun Moon53097ea2020-09-04 17:20:29 -070046
47class State(enum.Enum):
48 error = "-1"
49 disconnected = "0"
50 connected = "1"
51
52 @classmethod
53 def has_value(cls, value):
54 return value in cls._value2member_map_
55
56
57class Modem():
58 log = logging.getLogger('aether_edge_monitoring.Modem')
59
60 read_timeout = 0.1
61
62 def __init__(self, port, baudrate):
63 self.port = port
64 self.baudrate = baudrate
65 self._response = None
66
Don Newtonbd91ae22021-05-11 14:58:18 -070067 def get_modem_port(self):
68 cmd = "ls " + CONF.modem.port
69 sp = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,
70 stderr=subprocess.PIPE, universal_newlines=True)
Shad Ansari2a376d72022-03-10 14:01:32 -080071 sp.wait()
Don Newtonbd91ae22021-05-11 14:58:18 -070072 ret,err = sp.communicate()
73 if err != "" :
Shad Ansarib234ff82022-02-17 22:14:35 -080074 self.log.error("unable to find serial port " + err)
Don Newtonbd91ae22021-05-11 14:58:18 -070075
76 ret = ret.replace(CONF.modem.port,"").strip()
Shad Ansarib234ff82022-02-17 22:14:35 -080077 self.log.info("Modem.get_modem_port found " + ret)
Don Newtonbd91ae22021-05-11 14:58:18 -070078 return ret
79
Hyunsun Moon53097ea2020-09-04 17:20:29 -070080 def connect(self):
Don Newtonbd91ae22021-05-11 14:58:18 -070081 self.port=self.get_modem_port()
Shad Ansarib234ff82022-02-17 22:14:35 -080082 self.log.info("modem.connect Port: %s, BaudRate: %i",self.port,self.baudrate)
Hyunsun Moon53097ea2020-09-04 17:20:29 -070083 self.serial = serial.Serial(
84 port=self.port,
85 baudrate=self.baudrate,
86 timeout=1)
87
88 def _write(self, command):
89 if self.serial.inWaiting() > 0:
90 self.serial.flushInput()
91
92 self._response = b""
93
94 self.serial.write(bytearray(command + "\r", "ascii"))
95 read = self.serial.inWaiting()
96 while True:
Shad Ansarib234ff82022-02-17 22:14:35 -080097 self.log.debug("Waiting for write to complete...")
Hyunsun Moon53097ea2020-09-04 17:20:29 -070098 if read > 0:
99 self._response += self.serial.read(read)
100 else:
101 time.sleep(self.read_timeout)
102 read = self.serial.inWaiting()
103 if read == 0:
104 break
Shad Ansarib234ff82022-02-17 22:14:35 -0800105 self.log.debug("Write complete...")
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700106 return self._response.decode("ascii").replace('\r\n', ' ')
107
108 def write(self, command, wait_resp=True):
109 response = self._write(command)
110 self.log.debug("%s: %s", command, response)
111
112 if wait_resp and "ERROR" in response:
113 return False, None
114 return True, response
115
Shad Ansari2a376d72022-03-10 14:01:32 -0800116 def get_state(self, counters):
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700117 success, result = self.write('AT+CGATT?')
118 if not success or 'CGATT:' not in result:
Shad Ansari2a376d72022-03-10 14:01:32 -0800119 logging.error("AT+CGATT modem cmd failed")
120 counters['modem_cgatt_error'] += 1
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700121 return State.error
122 state = result.split('CGATT:')[1].split(' ')[0]
123 return State(state)
124
125 def close(self):
126 self.serial.close()
127
128
Shad Ansari2a376d72022-03-10 14:01:32 -0800129def get_control_plane_state(modem, counters, dongle_stats=None):
Shad Ansari341a1c92022-03-02 09:14:40 -0800130 if not modem and dongle_stats:
Shad Ansari2a376d72022-03-10 14:01:32 -0800131 if dongle_stats['Connection'] == 'Connected':
Shad Ansari341a1c92022-03-02 09:14:40 -0800132 return State.connected
133 else:
Shad Ansari2a376d72022-03-10 14:01:32 -0800134 logging.error("Dongle not connected: {}".format(dongle_stats['Connection']))
135 counters['dongle_connect_error'] += 1
Shad Ansari341a1c92022-03-02 09:14:40 -0800136 return State.disconnected
Shad Ansarib234ff82022-02-17 22:14:35 -0800137
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700138 # Disable radio fuction
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700139 # "echo" works more stable than serial for this action
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700140 try:
Don Newtonbd91ae22021-05-11 14:58:18 -0700141 logging.debug("echo 'AT+CFUN=0' > " + modem.port)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700142 subprocess.check_output(
Don Newtonbd91ae22021-05-11 14:58:18 -0700143 "echo 'AT+CFUN=0' > " + modem.port, shell=True)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700144 except subprocess.CalledProcessError as e:
Shad Ansari2a376d72022-03-10 14:01:32 -0800145 logging.error("Write 'AT+CFUN=0' failed: {}".format(e))
146 counters['modem_cfun0_error'] += 1
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700147 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700148
149 # Wait until the modem is fully disconnected
150 retry = 0
151 state = None
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700152 while retry < CONF.detach_timeout:
Shad Ansari2a376d72022-03-10 14:01:32 -0800153 state = modem.get_state(counters)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700154 if state is State.disconnected:
155 break
156 time.sleep(1)
157 retry += 1
158
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700159 if state is not State.disconnected:
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700160 logging.error("Failed to disconnect")
161 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700162
163 time.sleep(2)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700164 # Enable radio function
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700165 # "echo" works more stable than serial for this action
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700166 try:
Don Newtonbd91ae22021-05-11 14:58:18 -0700167 logging.debug("echo 'AT+CFUN=1' > " + modem.port)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700168 subprocess.check_output(
Don Newtonbd91ae22021-05-11 14:58:18 -0700169 "echo 'AT+CFUN=1' > " + modem.port, shell=True)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700170 except subprocess.CalledProcessError as e:
Shad Ansari2a376d72022-03-10 14:01:32 -0800171 logging.error("Write 'AT+CFUN=1' failed: {}".format(e))
172 counters['modem_cfun1_error'] += 1
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700173 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700174
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700175 # Wait attach_timeout sec for the modem to be fully connected
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700176 retry = 0
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700177 while retry < CONF.attach_timeout:
Shad Ansari2a376d72022-03-10 14:01:32 -0800178 state = modem.get_state(counters)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700179 if state is State.connected:
180 break
181 time.sleep(1)
182 retry += 1
183 # CGATT sometimes returns None
184 if state is State.error:
185 state = State.disconnected
186
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700187 return state
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700188
189
Shad Ansari2a376d72022-03-10 14:01:32 -0800190def dry_run_ping_test(counters):
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800191 if "dry_run" in CONF.ips._fields and CONF.ips.dry_run: # run dry_run latency test as user plane test
Shad Ansari2a376d72022-03-10 14:01:32 -0800192 return do_ping(CONF.ips.dry_run, 10)
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800193 else: # run default user plane test
194 try:
195 subprocess.check_output(
Shad Ansaria6ce0772022-03-15 21:34:51 -0700196 "ping -I {} -c 3 {} >/dev/null 2>&1".format(CONF.modem.iface, CONF.ips.dns),
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800197 shell=True)
Shad Ansari2a376d72022-03-10 14:01:32 -0800198 return None, True
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800199 except subprocess.CalledProcessError as e:
Shad Ansari2a376d72022-03-10 14:01:32 -0800200 logging.warning("Ping failed for {}: {}".format(CONF.ips.dns, e))
201 return None, False
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700202
203
Shad Ansari2a376d72022-03-10 14:01:32 -0800204def do_ping(ip, count):
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800205 '''
206 Runs the ping test
207 Input: IP to ping, # times to ping
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700208 Returns: Transmitted packets
209 Received packets
210 Median ping ms
211 Min ping ms
212 Avg ping ms
213 Max ping ms
214 Std Dev ping ms
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800215 '''
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700216 result = {'transmitted': 0,
217 'received': 0,
218 'median': 0.0,
219 'min': 0.0,
Jeremy Ronquilloa944fbc2021-03-30 10:57:45 -0700220 'avg': 0.0,
221 'max': 0.0,
222 'stddev': 0.0}
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700223 if not ip:
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700224 return result, True
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800225 try:
Shad Ansari2a376d72022-03-10 14:01:32 -0800226 logging.debug("Pinging {}".format(ip))
Shad Ansaria6ce0772022-03-15 21:34:51 -0700227 pingCmd = "ping -I {} -c {} {}".format(CONF.modem.iface, str(count), ip)
228 pingOutput = subprocess.check_output(pingCmd, shell=True).decode("UTF-8").split()
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700229 result['transmitted'] = int(pingOutput[-15])
230 result['received'] = int(pingOutput[-12])
231 if result['received'] > 0:
232 pingValues = []
233
234 # Hack for getting all ping values for median
235 for word in pingOutput:
236 if "time=" in word:
237 pingValues.append(float(word.split("=")[1]))
238 result['median'] = round(median(pingValues), 3)
239
240 pingResult = pingOutput[-2].split('/')
241 result['min'] = float(pingResult[0])
242 result['avg'] = float(pingResult[1])
243 result['max'] = float(pingResult[2])
244 result['stddev'] = float(pingResult[3])
245 else:
246 logging.error("No packets received during ping " + ip)
247 return result, False
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800248 except Exception as e:
249 logging.error("Ping test failed for " + ip + ": %s", e)
Shad Ansaria6ce0772022-03-15 21:34:51 -0700250 traceback.print_exc()
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700251 return result, False
252 return result, True
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800253
254
Shad Ansari2a376d72022-03-10 14:01:32 -0800255def ping_test(modem, dry_run_latency=None):
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800256 '''
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700257 Prepares the ping test.
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700258 Runs ping tests from 'ips' entry in config.json in order.
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800259 Note: 'dry_run' is not run here; it is run during the user plane test.
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800260 '''
261 speedtest_ping = {}
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700262 status = True
263 ping_test_passed = True
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700264
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800265 if dry_run_latency:
266 speedtest_ping["dry_run"] = dry_run_latency
267
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700268 for i in range(0, len(CONF.ips)):
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800269 if CONF.ips._fields[i] == "dry_run":
270 continue
Shad Ansarib3d54752022-02-28 12:32:58 -0800271 count = 10
Shad Ansari2a376d72022-03-10 14:01:32 -0800272 speedtest_ping[CONF.ips._fields[i]], status = do_ping(CONF.ips[i], count)
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700273 if not status:
274 ping_test_passed = False
Shad Ansari2a376d72022-03-10 14:01:32 -0800275 logging.error("Ping failed: {}".format(CONF.ips[i]))
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700276 return speedtest_ping, ping_test_passed
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800277
Shad Ansari2a376d72022-03-10 14:01:32 -0800278def run_iperf_test(ip, port, time_duration, is_downlink, counters):
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700279 '''
280 Runs iperf test to specified IP in the config file.
281 - Runs for 10 seconds (10 iterations)
282 - Retrieves downlink and uplink test results from json output
283 '''
284 result = 0.0
Jeremy Ronquilloc03ba682021-10-06 10:27:09 -0700285 if not ip or port == 0:
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700286 return result
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700287 maxRetries = 2
Jeremy Ronquillo79c3e672021-09-03 12:54:55 -0700288 err = None
Jeremy Ronquillo012ac662021-08-06 12:13:43 -0700289 for _ in range(0, maxRetries):
290 try:
291 iperfResult = json.loads(subprocess.check_output(
292 "iperf3 -c " + ip +
293 " -p " + str(port) +
294 " -t " + str(time_duration) +
295 (" -R " if is_downlink else "") +
296 " --json", shell=True).decode("UTF-8"))
297 received_mbps = iperfResult['end']['sum_received']['bits_per_second'] / 1000000
298 sent_mbps = iperfResult['end']['sum_sent']['bits_per_second'] / 1000000.0
299 result = received_mbps if is_downlink else sent_mbps
300 return result
301 except Exception as e:
Jeremy Ronquillo79c3e672021-09-03 12:54:55 -0700302 err = e
Shad Ansari2a376d72022-03-10 14:01:32 -0800303 counters['iperf_error'] += 1
Jeremy Ronquillo012ac662021-08-06 12:13:43 -0700304 time.sleep(5)
305 pass
Jeremy Ronquillo79c3e672021-09-03 12:54:55 -0700306 logging.error("After " + str(maxRetries) + " retries, iperf test failed for " + ip + ": %s", err)
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700307 return result
308
309
Shad Ansari2a376d72022-03-10 14:01:32 -0800310def iperf_test(counters):
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700311 '''
312 Prepares the iperf test.
313 '''
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800314 global hour_iperf_scheduled_time_last_ran
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700315 speedtest_iperf = {}
316 speedtest_iperf['cluster'] = {}
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800317
318 if "iperf_schedule" in CONF._fields and len(CONF.iperf_schedule) > 0:
319 if int(time.strftime("%H")) not in CONF.iperf_schedule: # not in the schedule
320 hour_iperf_scheduled_time_last_ran = -1
321 return None
322 elif int(time.strftime("%H")) == hour_iperf_scheduled_time_last_ran: # already ran this hour
323 return None
324 hour_iperf_scheduled_time_last_ran = int(time.strftime("%H"))
325
Shad Ansari2a376d72022-03-10 14:01:32 -0800326 speedtest_iperf['cluster']['downlink'] = run_iperf_test(CONF.ips.iperf_server, CONF.iperf_port, 10, True, counters)
327 speedtest_iperf['cluster']['uplink'] = run_iperf_test(CONF.ips.iperf_server, CONF.iperf_port, 10, False, counters)
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700328
329 return speedtest_iperf
330
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800331
Shad Ansari2a376d72022-03-10 14:01:32 -0800332def get_signal_quality(modem, counters, dongle_stats=None):
Shad Ansari341a1c92022-03-02 09:14:40 -0800333 if not modem and dongle_stats:
334 if dongle_stats['RSRQ'] != '' and dongle_stats['RSRP'] != '':
335 rsrq = int((float(dongle_stats['RSRQ']) + 19.5) * 2)
336 rsrp = int(float(dongle_stats['RSRP']) + 140)
337 return {'rsrq': rsrq, 'rsrp': rsrp}
338 else:
Shad Ansari2a376d72022-03-10 14:01:32 -0800339 counters['dongle_rsrp_rsrq_error'] += 1
Shad Ansari341a1c92022-03-02 09:14:40 -0800340 return {'rsrq': 0, 'rsrp': 0}
341
342 # Fall back to modem cmds
Shad Ansarib234ff82022-02-17 22:14:35 -0800343
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700344 success, result = modem.write('AT+CESQ')
Don Newtonbd91ae22021-05-11 14:58:18 -0700345 logging.debug("get_signal_quality success %i result %s",success,result)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700346 if not success or 'CESQ: ' not in result:
347 logging.error("Failed to get signal quality")
Shad Ansari2a376d72022-03-10 14:01:32 -0800348 counters['modem_cesq_error'] += 1
Don Newtonbd91ae22021-05-11 14:58:18 -0700349 return {'rsrq':0, 'rsrp':0}
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700350
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700351 tmp_rsrq = result.split('CESQ:')[1].split(',')[4]
352 tmp_rsrp = result.split('CESQ:')[1].split(',')[5]
353
354 rsrq = int(tmp_rsrq.strip())
355 rsrp = int(tmp_rsrp.strip().split(' ')[0])
356 result = {
Shad Ansaria6ce0772022-03-15 21:34:51 -0700357 'rsrq': 0 if rsrq == 255 else rsrq,
358 'rsrp': 0 if rsrp == 255 else rsrp
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700359 }
360
361 return result
362
Shad Ansarib234ff82022-02-17 22:14:35 -0800363
Shad Ansari2a376d72022-03-10 14:01:32 -0800364def get_dongle_stats(counters):
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800365 result = {'SuccessfulFetch' : False}
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800366 if "report_in_band" in CONF._fields:
367 result['inBandReporting'] = CONF.report_in_band
368 else:
369 result['inBandReporting'] = False
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800370 XMLkeys = ["MAC",
371 "PLMNStatus",
372 "UICCStatus",
373 "IMEI",
374 "IMSI",
375 "PLMNSelected",
376 "MCC",
377 "MNC",
378 "PhyCellID",
379 "CellGlobalID",
380 "Band",
381 "EARFCN",
382 "BandWidth",
Shad Ansarib234ff82022-02-17 22:14:35 -0800383 "RSRP",
384 "RSRQ",
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800385 "ServCellState",
386 "Connection",
387 "IPv4Addr"]
388 dongleStatsXML = None
389 try:
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800390 dongleStatsXML = ET.fromstring(subprocess.check_output("curl -u admin:admin -s 'http://192.168.0.1:8080/cgi-bin/ltestatus.cgi?Command=Status'", shell=True).decode("UTF-8"))
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800391 except Exception as e:
392 logging.error("Failed to fetch dongle stats from URL: " + str(e))
Shad Ansari2a376d72022-03-10 14:01:32 -0800393 counters['dongle_read_error'] += 1
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800394 return result
395 try:
396 for key in XMLkeys:
397 try:
398 result[key] = dongleStatsXML.find(key).text
Shad Ansari2a376d72022-03-10 14:01:32 -0800399 except AttributeError:
400 logging.error("Failed to find " + key + " in XML")
401 counters['dongle_read_error'] += 1
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800402 result[key] = ""
403 result["SuccessfulFetch"] = True
404 except Exception as e:
405 logging.error("Failed to fetch dongle stats from XML: " + str(e))
Shad Ansari2a376d72022-03-10 14:01:32 -0800406 counters['dongle_read_error'] += 1
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800407 return result
408 return result
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700409
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800410
Shad Ansari2a376d72022-03-10 14:01:32 -0800411def report_status(counters, signal_quality, dongle_stats, cp_state=None, up_state=None, speedtest_ping=None, speedtest_iperf=None):
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700412 report = {
413 'name': CONF.edge_name,
414 'status': {
415 'control_plane': "disconnected",
416 'user_plane': "disconnected"
417 },
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800418 'dongle_stats': {
419 'SuccessfulFetch' : False
420 },
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700421 'speedtest': {
422 'ping': {
423 'dns': {
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700424 'transmitted' : 0,
425 'received' : 0,
426 'median' : 0.0,
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700427 'min': 0.0,
428 'avg': 0.0,
429 'max': 0.0,
430 'stddev': 0.0
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700431 }
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700432 },
433 'iperf': {
434 'cluster': {
435 'downlink': 0.0,
436 'uplink': 0.0
437 }
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700438 }
439 },
440 'signal_quality': {
441 'rsrq': 0,
442 'rsrp': 0
443 }
444 }
445
446 if cp_state is not None:
447 report['status']['control_plane'] = cp_state.name
448 if up_state is not None:
449 report['status']['user_plane'] = up_state.name
450 if speedtest_ping is not None:
451 report['speedtest']['ping'] = speedtest_ping
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700452 if speedtest_iperf is not None:
453 report['speedtest']['iperf'] = speedtest_iperf
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700454 report['signal_quality'] = signal_quality
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800455 report['dongle_stats'] = dongle_stats
Shad Ansari2a376d72022-03-10 14:01:32 -0800456 report['counters'] = counters
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700457
458 logging.info("Sending report %s", report)
Don Newtonbd91ae22021-05-11 14:58:18 -0700459 global cycles
460 cycles += 1
461 logging.info("Number of cycles since modem restart %i",cycles)
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800462
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700463 try:
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800464 interface = None
465 report_via_modem = "report_in_band" in CONF._fields and CONF.report_in_band and \
466 "iface" in CONF.modem._fields and CONF.modem.iface
467 report_via_given_iface = "report_iface" in CONF._fields and CONF.report_iface
468
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800469 c = pycurl.Curl()
470 c.setopt(pycurl.URL, CONF.report_url)
471 c.setopt(pycurl.POST, True)
472 c.setopt(pycurl.HTTPHEADER, ['Content-Type: application/json'])
473 c.setopt(pycurl.TIMEOUT, 10)
474 c.setopt(pycurl.POSTFIELDS, json.dumps(report))
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800475 c.setopt(pycurl.WRITEFUNCTION, lambda x: None) # don't output to console
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800476
477 if report_via_modem: # report in-band
478 interface = CONF.modem.iface
479 c.setopt(pycurl.INTERFACE, interface)
480 elif report_via_given_iface: # report over given interface
481 interface = CONF.report_iface
482 c.setopt(pycurl.INTERFACE, interface)
Hyunsun Moon9f668e02022-03-11 13:12:57 -0800483 else:
484 interface = "default"
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800485
486 try:
487 c.perform()
488 logging.info("Report sent via " + interface + "!")
489 except Exception as e:
Shad Ansaria6ce0772022-03-15 21:34:51 -0700490 logging.error("Failed to send report in-band: " + str(e))
491 counters['report_send_error'] += 1
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800492 if report_via_modem and report_via_given_iface:
Shad Ansaria6ce0772022-03-15 21:34:51 -0700493 logging.warning("Attempting to send report via " + str(CONF.report_iface) + ".")
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800494 interface = CONF.report_iface
495 c.setopt(pycurl.INTERFACE, interface)
496 c.perform()
497 logging.info("Report sent via " + interface + "!")
498 else:
Shad Ansaria6ce0772022-03-15 21:34:51 -0700499 logging.error("Failed to send report out-of-band: " + str(e))
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800500 c.close()
501 except Exception as e:
502 logging.error("Failed to send report: " + str(e))
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800503 c.close()
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800504
Don Newtonbd91ae22021-05-11 14:58:18 -0700505def reset_usb():
Jeremy Ronquillo82a14612021-10-08 12:08:20 -0700506 try:
507 # Attempt to run uhubctl
508 if (int(subprocess.call("which uhubctl",shell=True)) == 0):
509 cmd = "/usr/sbin/uhubctl -a 0 -l 2" # -a 0 = action is shutdown -l 2 location = bus 2 on pi controls power to all hubs
510 ret = subprocess.call(cmd,shell=True)
511 logging.info("Shutting down usb hub 2 results %s" , ret)
512 time.sleep(10)# let power down process settle out
513 cmd = "/usr/sbin/uhubctl -a 1 -l 2" # -a 1 = action is start -l 2 location = bus 2 on pi controls power to all hubs
514 ret = subprocess.call(cmd,shell=True)
515 logging.info("Starting up usb hub 2 results %s" , ret)
516 time.sleep(10) #allow dbus to finish
517 global cycles
518 cycles = 0
519 else:
520 reboot(120)
521 except Exception as e:
522 logging.error("Failed to run uhubctl: %s", e)
523 reboot(120)
524
525def reboot(delay):
526 logging.error("Failed to run uhubctl. Reboot system in " + str(delay) + " second(s).")
527 time.sleep(delay)
528 subprocess.check_output("sudo shutdown -r now", shell=True)
Don Newtonbd91ae22021-05-11 14:58:18 -0700529
Shad Ansari2a376d72022-03-10 14:01:32 -0800530def init_counters():
531 return {
532 'dongle_read_error': 0,
533 'dongle_connect_error': 0,
534 'dongle_rsrp_rsrq_error': 0,
535 'modem_cfun0_error': 0,
536 'modem_cfun1_error': 0,
537 'modem_cgatt_error': 0,
538 'modem_cesq_error': 0,
539 'dry_run_ping_error': 0,
540 'ping_error': 0,
Shad Ansaria6ce0772022-03-15 21:34:51 -0700541 'iperf_error': 0,
542 'report_send_error': 0
Shad Ansari2a376d72022-03-10 14:01:32 -0800543 }
544
Shad Ansaria6ce0772022-03-15 21:34:51 -0700545def clear_counters(counters):
546 for x in counters:
547 counters[x] = 0
548
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700549def main():
Don Newtonbd91ae22021-05-11 14:58:18 -0700550 global cycles
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800551 global hour_iperf_scheduled_time_last_ran
Don Newtonbd91ae22021-05-11 14:58:18 -0700552 cycles = 0
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800553 hour_iperf_scheduled_time_last_ran = -1
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800554
Shad Ansaria6ce0772022-03-15 21:34:51 -0700555 counters = init_counters()
556
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800557 try:
558 if "report_in_band" in CONF._fields and \
559 "iface" in CONF.modem._fields and CONF.modem.iface:
560 if CONF.report_in_band: # need to add default gateway if reporting in-band
561 subprocess.check_output("sudo route add default gw " + CONF.modem.ip_addr + " " + CONF.modem.iface + " || true", shell=True)
562 else:
563 subprocess.check_output("sudo route del default gw " + CONF.modem.ip_addr + " " + CONF.modem.iface + " || true", shell=True)
564 except Exception as e:
565 logging.error("Failed to change default route for modem: " + str(e))
566
Don Newtonbd91ae22021-05-11 14:58:18 -0700567 for ip in CONF.ips:
568 if not ip:
569 continue
570 try:
571 subprocess.check_output("sudo ip route replace {}/32 via {}".format(
572 ip, CONF.modem.ip_addr), shell=True)
573 except subprocess.CalledProcessError as e:
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800574 logging.error("Failed to add routes: " + str(e.returncode) + str(e.output))
Jeremy Ronquillo82a14612021-10-08 12:08:20 -0700575 time.sleep(10) # Sleep for 10 seconds before retry
Don Newtonbd91ae22021-05-11 14:58:18 -0700576 sys.exit(1)
577
Shad Ansari341a1c92022-03-02 09:14:40 -0800578 if USE_MODEM_CMDS:
579 modem = Modem(CONF.modem.port, CONF.modem.baud)
580 try:
581 modem.connect()
582 except serial.serialutil.SerialException as e:
583 logging.error("Failed to connect the modem for %s", e)
Hyunsun Moonf4242372020-10-04 23:32:38 -0700584 sys.exit(1)
Shad Ansari341a1c92022-03-02 09:14:40 -0800585 else:
586 modem = None
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700587
Shad Ansari341a1c92022-03-02 09:14:40 -0800588 connect_retries = 0
589 while True:
590 dongle_retries = 0
Shad Ansari2a376d72022-03-10 14:01:32 -0800591 dongle_stats = get_dongle_stats(counters)
Shad Ansari341a1c92022-03-02 09:14:40 -0800592 while not dongle_stats['SuccessfulFetch']:
Shad Ansari341a1c92022-03-02 09:14:40 -0800593 dongle_retries += 1
594 if dongle_retries > 10:
Shad Ansari2a376d72022-03-10 14:01:32 -0800595 logging.warning("Rebooting Pi: dongle not readable")
Shad Ansari341a1c92022-03-02 09:14:40 -0800596 os.system("shutdown /r /t 0")
597 sys.exit(1)
Shad Ansari2a376d72022-03-10 14:01:32 -0800598 dongle_stats = get_dongle_stats(counters)
Shad Ansari341a1c92022-03-02 09:14:40 -0800599
Shad Ansari2a376d72022-03-10 14:01:32 -0800600 cp_state = get_control_plane_state(modem, counters, dongle_stats)
Shad Ansari341a1c92022-03-02 09:14:40 -0800601
602 if cp_state != State.connected:
603 logging.error("Dongle not connected")
604 connect_retries += 1
605 if connect_retries > 10:
Shad Ansari2a376d72022-03-10 14:01:32 -0800606 logging.warning("Rebooting Pi: dongle not connected")
Shad Ansari341a1c92022-03-02 09:14:40 -0800607 os.system("shutdown /r /t 0")
608 sys.exit(1)
609
Shad Ansari2a376d72022-03-10 14:01:32 -0800610 signal_quality = get_signal_quality(modem, counters, dongle_stats)
611
612 dry_run_ping_latency, dry_run_ping_result = dry_run_ping_test(counters)
613 if not dry_run_ping_result:
Shad Ansari341a1c92022-03-02 09:14:40 -0800614 logging.error("Dry run ping failed")
Shad Ansari2a376d72022-03-10 14:01:32 -0800615 counters['dry_run_ping_error'] += 1
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700616
Shad Ansari2a376d72022-03-10 14:01:32 -0800617 ping_latency, ping_result = ping_test(modem, dry_run_ping_latency)
618 if not ping_result:
619 logging.error("Ping test failed")
620 counters['ping_error'] += 1
Shad Ansari341a1c92022-03-02 09:14:40 -0800621
Shad Ansari2a376d72022-03-10 14:01:32 -0800622 # If either of the ping tests pass, then declare user plane connected
623 if dry_run_ping_result or ping_result:
624 up_state = State.connected
Hyunsun Moon9f668e02022-03-11 13:12:57 -0800625 else:
626 up_state = State.disconnected
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700627
Shad Ansari2a376d72022-03-10 14:01:32 -0800628 speedtest_iperf = iperf_test(counters)
629
630 report_status(counters, signal_quality, dongle_stats, cp_state, up_state, ping_latency, speedtest_iperf)
Shad Ansaria6ce0772022-03-15 21:34:51 -0700631 counters = clear_counters(counters)
Shad Ansari341a1c92022-03-02 09:14:40 -0800632 time.sleep(CONF.report_interval)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700633
634 modem.close()
635
636
637if __name__ == "__main__":
638 main()