blob: 8354e538fb914533d8fd53c41346e4ef516d38f8 [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#
Andy Bavier200bd272022-06-09 11:15:51 -07005# SPDX-License-Identifier: Apache-2.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 -070029config_file_contents = open(os.getenv('CONFIG_FILE', "./config.json")).read()
Shad Ansari416ccab2022-03-09 19:06:43 -080030# replace 1.1.1.1 with 8.8.8.8
Shad Ansarif390fbb2022-03-17 20:22:46 -070031# config_file_contents = config_file_contents.replace('1.1.1.1', '8.8.8.8')
Hyunsun Moon53097ea2020-09-04 17:20:29 -070032CONF = json.loads(
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -070033 config_file_contents, object_hook=lambda d: namedtuple('X', d.keys())(*d.values())
Hyunsun Moon53097ea2020-09-04 17:20:29 -070034)
35
36logging.basicConfig(
37 filename=CONF.log_file,
38 format='%(asctime)s [%(levelname)s] %(message)s',
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -070039 level=logging.getLevelName(CONF.log_level)
Hyunsun Moon53097ea2020-09-04 17:20:29 -070040)
41
Hyunsun Moon53097ea2020-09-04 17:20:29 -070042
43class State(enum.Enum):
44 error = "-1"
45 disconnected = "0"
46 connected = "1"
47
48 @classmethod
49 def has_value(cls, value):
50 return value in cls._value2member_map_
51
52
53class Modem():
54 log = logging.getLogger('aether_edge_monitoring.Modem')
55
56 read_timeout = 0.1
57
58 def __init__(self, port, baudrate):
59 self.port = port
60 self.baudrate = baudrate
61 self._response = None
62
Don Newtonbd91ae22021-05-11 14:58:18 -070063 def get_modem_port(self):
64 cmd = "ls " + CONF.modem.port
65 sp = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,
66 stderr=subprocess.PIPE, universal_newlines=True)
Shad Ansari2a376d72022-03-10 14:01:32 -080067 sp.wait()
Don Newtonbd91ae22021-05-11 14:58:18 -070068 ret,err = sp.communicate()
69 if err != "" :
Shad Ansarib234ff82022-02-17 22:14:35 -080070 self.log.error("unable to find serial port " + err)
Don Newtonbd91ae22021-05-11 14:58:18 -070071
72 ret = ret.replace(CONF.modem.port,"").strip()
Shad Ansarib234ff82022-02-17 22:14:35 -080073 self.log.info("Modem.get_modem_port found " + ret)
Don Newtonbd91ae22021-05-11 14:58:18 -070074 return ret
75
Hyunsun Moon53097ea2020-09-04 17:20:29 -070076 def connect(self):
Don Newtonbd91ae22021-05-11 14:58:18 -070077 self.port=self.get_modem_port()
Shad Ansarib234ff82022-02-17 22:14:35 -080078 self.log.info("modem.connect Port: %s, BaudRate: %i",self.port,self.baudrate)
Hyunsun Moon53097ea2020-09-04 17:20:29 -070079 self.serial = serial.Serial(
80 port=self.port,
81 baudrate=self.baudrate,
82 timeout=1)
83
84 def _write(self, command):
85 if self.serial.inWaiting() > 0:
86 self.serial.flushInput()
87
88 self._response = b""
89
90 self.serial.write(bytearray(command + "\r", "ascii"))
91 read = self.serial.inWaiting()
92 while True:
Shad Ansarib234ff82022-02-17 22:14:35 -080093 self.log.debug("Waiting for write to complete...")
Hyunsun Moon53097ea2020-09-04 17:20:29 -070094 if read > 0:
95 self._response += self.serial.read(read)
96 else:
97 time.sleep(self.read_timeout)
98 read = self.serial.inWaiting()
99 if read == 0:
100 break
Shad Ansarib234ff82022-02-17 22:14:35 -0800101 self.log.debug("Write complete...")
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700102 return self._response.decode("ascii").replace('\r\n', ' ')
103
104 def write(self, command, wait_resp=True):
105 response = self._write(command)
106 self.log.debug("%s: %s", command, response)
107
108 if wait_resp and "ERROR" in response:
109 return False, None
110 return True, response
111
Shad Ansari2a376d72022-03-10 14:01:32 -0800112 def get_state(self, counters):
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700113 success, result = self.write('AT+CGATT?')
114 if not success or 'CGATT:' not in result:
Shad Ansari2a376d72022-03-10 14:01:32 -0800115 logging.error("AT+CGATT modem cmd failed")
116 counters['modem_cgatt_error'] += 1
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700117 return State.error
118 state = result.split('CGATT:')[1].split(' ')[0]
119 return State(state)
120
121 def close(self):
122 self.serial.close()
123
124
Shad Ansari2a376d72022-03-10 14:01:32 -0800125def get_control_plane_state(modem, counters, dongle_stats=None):
Shad Ansari341a1c92022-03-02 09:14:40 -0800126 if not modem and dongle_stats:
Shad Ansari2a376d72022-03-10 14:01:32 -0800127 if dongle_stats['Connection'] == 'Connected':
Shad Ansari341a1c92022-03-02 09:14:40 -0800128 return State.connected
129 else:
Shad Ansari2a376d72022-03-10 14:01:32 -0800130 logging.error("Dongle not connected: {}".format(dongle_stats['Connection']))
131 counters['dongle_connect_error'] += 1
Shad Ansari341a1c92022-03-02 09:14:40 -0800132 return State.disconnected
Shad Ansarib234ff82022-02-17 22:14:35 -0800133
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700134 # Disable radio fuction
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700135 # "echo" works more stable than serial for this action
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700136 try:
Don Newtonbd91ae22021-05-11 14:58:18 -0700137 logging.debug("echo 'AT+CFUN=0' > " + modem.port)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700138 subprocess.check_output(
Don Newtonbd91ae22021-05-11 14:58:18 -0700139 "echo 'AT+CFUN=0' > " + modem.port, shell=True)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700140 except subprocess.CalledProcessError as e:
Shad Ansari2a376d72022-03-10 14:01:32 -0800141 logging.error("Write 'AT+CFUN=0' failed: {}".format(e))
142 counters['modem_cfun0_error'] += 1
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700143 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700144
145 # Wait until the modem is fully disconnected
146 retry = 0
147 state = None
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700148 while retry < CONF.detach_timeout:
Shad Ansari2a376d72022-03-10 14:01:32 -0800149 state = modem.get_state(counters)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700150 if state is State.disconnected:
151 break
152 time.sleep(1)
153 retry += 1
154
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700155 if state is not State.disconnected:
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700156 logging.error("Failed to disconnect")
157 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700158
159 time.sleep(2)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700160 # Enable radio function
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700161 # "echo" works more stable than serial for this action
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700162 try:
Don Newtonbd91ae22021-05-11 14:58:18 -0700163 logging.debug("echo 'AT+CFUN=1' > " + modem.port)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700164 subprocess.check_output(
Don Newtonbd91ae22021-05-11 14:58:18 -0700165 "echo 'AT+CFUN=1' > " + modem.port, shell=True)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700166 except subprocess.CalledProcessError as e:
Shad Ansari2a376d72022-03-10 14:01:32 -0800167 logging.error("Write 'AT+CFUN=1' failed: {}".format(e))
168 counters['modem_cfun1_error'] += 1
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700169 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700170
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700171 # Wait attach_timeout sec for the modem to be fully connected
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700172 retry = 0
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700173 while retry < CONF.attach_timeout:
Shad Ansari2a376d72022-03-10 14:01:32 -0800174 state = modem.get_state(counters)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700175 if state is State.connected:
176 break
177 time.sleep(1)
178 retry += 1
179 # CGATT sometimes returns None
180 if state is State.error:
181 state = State.disconnected
182
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700183 return state
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700184
185
Shad Ansari2a376d72022-03-10 14:01:32 -0800186def dry_run_ping_test(counters):
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800187 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 -0800188 return do_ping(CONF.ips.dry_run, 10)
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800189 else: # run default user plane test
190 try:
191 subprocess.check_output(
Shad Ansaria6ce0772022-03-15 21:34:51 -0700192 "ping -I {} -c 3 {} >/dev/null 2>&1".format(CONF.modem.iface, CONF.ips.dns),
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800193 shell=True)
Shad Ansari2a376d72022-03-10 14:01:32 -0800194 return None, True
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800195 except subprocess.CalledProcessError as e:
Shad Ansari2a376d72022-03-10 14:01:32 -0800196 logging.warning("Ping failed for {}: {}".format(CONF.ips.dns, e))
197 return None, False
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700198
199
Shad Ansari2a376d72022-03-10 14:01:32 -0800200def do_ping(ip, count):
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800201 '''
202 Runs the ping test
203 Input: IP to ping, # times to ping
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700204 Returns: Transmitted packets
205 Received packets
206 Median ping ms
207 Min ping ms
208 Avg ping ms
209 Max ping ms
210 Std Dev ping ms
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800211 '''
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700212 result = {'transmitted': 0,
213 'received': 0,
214 'median': 0.0,
215 'min': 0.0,
Jeremy Ronquilloa944fbc2021-03-30 10:57:45 -0700216 'avg': 0.0,
217 'max': 0.0,
218 'stddev': 0.0}
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700219 if not ip:
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700220 return result, True
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800221 try:
Shad Ansari2a376d72022-03-10 14:01:32 -0800222 logging.debug("Pinging {}".format(ip))
Shad Ansaria6ce0772022-03-15 21:34:51 -0700223 pingCmd = "ping -I {} -c {} {}".format(CONF.modem.iface, str(count), ip)
224 pingOutput = subprocess.check_output(pingCmd, shell=True).decode("UTF-8").split()
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700225 result['transmitted'] = int(pingOutput[-15])
226 result['received'] = int(pingOutput[-12])
227 if result['received'] > 0:
228 pingValues = []
229
230 # Hack for getting all ping values for median
231 for word in pingOutput:
232 if "time=" in word:
233 pingValues.append(float(word.split("=")[1]))
234 result['median'] = round(median(pingValues), 3)
235
236 pingResult = pingOutput[-2].split('/')
237 result['min'] = float(pingResult[0])
238 result['avg'] = float(pingResult[1])
239 result['max'] = float(pingResult[2])
240 result['stddev'] = float(pingResult[3])
241 else:
242 logging.error("No packets received during ping " + ip)
243 return result, False
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800244 except Exception as e:
245 logging.error("Ping test failed for " + ip + ": %s", e)
Shad Ansaria6ce0772022-03-15 21:34:51 -0700246 traceback.print_exc()
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700247 return result, False
248 return result, True
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800249
250
Shad Ansari2a376d72022-03-10 14:01:32 -0800251def ping_test(modem, dry_run_latency=None):
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800252 '''
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700253 Prepares the ping test.
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700254 Runs ping tests from 'ips' entry in config.json in order.
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800255 Note: 'dry_run' is not run here; it is run during the user plane test.
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800256 '''
257 speedtest_ping = {}
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700258 status = True
259 ping_test_passed = True
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700260
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800261 if dry_run_latency:
262 speedtest_ping["dry_run"] = dry_run_latency
263
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700264 for i in range(0, len(CONF.ips)):
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800265 if CONF.ips._fields[i] == "dry_run":
266 continue
Shad Ansarib3d54752022-02-28 12:32:58 -0800267 count = 10
Shad Ansari2a376d72022-03-10 14:01:32 -0800268 speedtest_ping[CONF.ips._fields[i]], status = do_ping(CONF.ips[i], count)
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700269 if not status:
270 ping_test_passed = False
Shad Ansari2a376d72022-03-10 14:01:32 -0800271 logging.error("Ping failed: {}".format(CONF.ips[i]))
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700272 return speedtest_ping, ping_test_passed
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800273
Shad Ansari2a376d72022-03-10 14:01:32 -0800274def run_iperf_test(ip, port, time_duration, is_downlink, counters):
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700275 '''
276 Runs iperf test to specified IP in the config file.
277 - Runs for 10 seconds (10 iterations)
278 - Retrieves downlink and uplink test results from json output
279 '''
280 result = 0.0
Jeremy Ronquilloc03ba682021-10-06 10:27:09 -0700281 if not ip or port == 0:
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700282 return result
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700283 maxRetries = 2
Jeremy Ronquillo79c3e672021-09-03 12:54:55 -0700284 err = None
Jeremy Ronquillo012ac662021-08-06 12:13:43 -0700285 for _ in range(0, maxRetries):
286 try:
287 iperfResult = json.loads(subprocess.check_output(
288 "iperf3 -c " + ip +
289 " -p " + str(port) +
290 " -t " + str(time_duration) +
291 (" -R " if is_downlink else "") +
292 " --json", shell=True).decode("UTF-8"))
293 received_mbps = iperfResult['end']['sum_received']['bits_per_second'] / 1000000
294 sent_mbps = iperfResult['end']['sum_sent']['bits_per_second'] / 1000000.0
295 result = received_mbps if is_downlink else sent_mbps
296 return result
297 except Exception as e:
Jeremy Ronquillo79c3e672021-09-03 12:54:55 -0700298 err = e
Shad Ansari2a376d72022-03-10 14:01:32 -0800299 counters['iperf_error'] += 1
Jeremy Ronquillo012ac662021-08-06 12:13:43 -0700300 time.sleep(5)
301 pass
Jeremy Ronquillo79c3e672021-09-03 12:54:55 -0700302 logging.error("After " + str(maxRetries) + " retries, iperf test failed for " + ip + ": %s", err)
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700303 return result
304
305
Shad Ansarif390fbb2022-03-17 20:22:46 -0700306def update_iperf_stats(iperf_stats, counters):
307 global cycles
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800308 global hour_iperf_scheduled_time_last_ran
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800309
Shad Ansarif390fbb2022-03-17 20:22:46 -0700310 if cycles != 0:
311 if "iperf_schedule" in CONF._fields and len(CONF.iperf_schedule) > 0:
312 if int(time.strftime("%H")) not in CONF.iperf_schedule: # not in the schedule
313 hour_iperf_scheduled_time_last_ran = -1
314 return
315 elif int(time.strftime("%H")) == hour_iperf_scheduled_time_last_ran: # already ran this hour
316 return
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800317 hour_iperf_scheduled_time_last_ran = int(time.strftime("%H"))
318
Shad Ansarif390fbb2022-03-17 20:22:46 -0700319 iperf_stats['cluster']['downlink'] = run_iperf_test(CONF.ips.iperf_server, CONF.iperf_port, 10, True, counters)
320 iperf_stats['cluster']['uplink'] = run_iperf_test(CONF.ips.iperf_server, CONF.iperf_port, 10, False, counters)
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700321
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800322
Shad Ansari2a376d72022-03-10 14:01:32 -0800323def get_signal_quality(modem, counters, dongle_stats=None):
Shad Ansari341a1c92022-03-02 09:14:40 -0800324 if not modem and dongle_stats:
325 if dongle_stats['RSRQ'] != '' and dongle_stats['RSRP'] != '':
326 rsrq = int((float(dongle_stats['RSRQ']) + 19.5) * 2)
327 rsrp = int(float(dongle_stats['RSRP']) + 140)
328 return {'rsrq': rsrq, 'rsrp': rsrp}
329 else:
Shad Ansari2a376d72022-03-10 14:01:32 -0800330 counters['dongle_rsrp_rsrq_error'] += 1
Shad Ansari341a1c92022-03-02 09:14:40 -0800331 return {'rsrq': 0, 'rsrp': 0}
332
333 # Fall back to modem cmds
Shad Ansarib234ff82022-02-17 22:14:35 -0800334
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700335 success, result = modem.write('AT+CESQ')
Don Newtonbd91ae22021-05-11 14:58:18 -0700336 logging.debug("get_signal_quality success %i result %s",success,result)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700337 if not success or 'CESQ: ' not in result:
338 logging.error("Failed to get signal quality")
Shad Ansari2a376d72022-03-10 14:01:32 -0800339 counters['modem_cesq_error'] += 1
Don Newtonbd91ae22021-05-11 14:58:18 -0700340 return {'rsrq':0, 'rsrp':0}
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700341
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700342 tmp_rsrq = result.split('CESQ:')[1].split(',')[4]
343 tmp_rsrp = result.split('CESQ:')[1].split(',')[5]
344
345 rsrq = int(tmp_rsrq.strip())
346 rsrp = int(tmp_rsrp.strip().split(' ')[0])
347 result = {
Shad Ansaria6ce0772022-03-15 21:34:51 -0700348 'rsrq': 0 if rsrq == 255 else rsrq,
349 'rsrp': 0 if rsrp == 255 else rsrp
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700350 }
351
352 return result
353
Shad Ansarib234ff82022-02-17 22:14:35 -0800354
Shad Ansari2a376d72022-03-10 14:01:32 -0800355def get_dongle_stats(counters):
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800356 result = {'SuccessfulFetch' : False}
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800357 if "report_in_band" in CONF._fields:
358 result['inBandReporting'] = CONF.report_in_band
359 else:
360 result['inBandReporting'] = False
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800361 XMLkeys = ["MAC",
362 "PLMNStatus",
363 "UICCStatus",
364 "IMEI",
365 "IMSI",
366 "PLMNSelected",
367 "MCC",
368 "MNC",
369 "PhyCellID",
370 "CellGlobalID",
371 "Band",
372 "EARFCN",
373 "BandWidth",
Shad Ansarib234ff82022-02-17 22:14:35 -0800374 "RSRP",
375 "RSRQ",
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800376 "ServCellState",
377 "Connection",
378 "IPv4Addr"]
379 dongleStatsXML = None
380 try:
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800381 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 -0800382 except Exception as e:
383 logging.error("Failed to fetch dongle stats from URL: " + str(e))
Shad Ansari2a376d72022-03-10 14:01:32 -0800384 counters['dongle_read_error'] += 1
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800385 return result
386 try:
387 for key in XMLkeys:
388 try:
389 result[key] = dongleStatsXML.find(key).text
Shad Ansari2a376d72022-03-10 14:01:32 -0800390 except AttributeError:
391 logging.error("Failed to find " + key + " in XML")
392 counters['dongle_read_error'] += 1
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800393 result[key] = ""
394 result["SuccessfulFetch"] = True
395 except Exception as e:
396 logging.error("Failed to fetch dongle stats from XML: " + str(e))
Shad Ansari2a376d72022-03-10 14:01:32 -0800397 counters['dongle_read_error'] += 1
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800398 return result
399 return result
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700400
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800401
Shad Ansarif390fbb2022-03-17 20:22:46 -0700402def report_status(counters, signal_quality, dongle_stats, cp_state=None, up_state=None, speedtest_ping=None, iperf_stats=None):
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700403 report = {
404 'name': CONF.edge_name,
405 'status': {
406 'control_plane': "disconnected",
407 'user_plane': "disconnected"
408 },
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800409 'dongle_stats': {
410 'SuccessfulFetch' : False
411 },
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700412 'speedtest': {
413 'ping': {
414 'dns': {
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700415 'transmitted' : 0,
416 'received' : 0,
417 'median' : 0.0,
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700418 'min': 0.0,
419 'avg': 0.0,
420 'max': 0.0,
421 'stddev': 0.0
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700422 }
423 }
424 },
425 'signal_quality': {
426 'rsrq': 0,
427 'rsrp': 0
428 }
429 }
430
431 if cp_state is not None:
432 report['status']['control_plane'] = cp_state.name
433 if up_state is not None:
434 report['status']['user_plane'] = up_state.name
435 if speedtest_ping is not None:
436 report['speedtest']['ping'] = speedtest_ping
Shad Ansarif390fbb2022-03-17 20:22:46 -0700437 if iperf_stats is not None:
438 report['speedtest']['iperf'] = iperf_stats
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700439 report['signal_quality'] = signal_quality
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800440 report['dongle_stats'] = dongle_stats
Shad Ansari2a376d72022-03-10 14:01:32 -0800441 report['counters'] = counters
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700442
443 logging.info("Sending report %s", report)
Don Newtonbd91ae22021-05-11 14:58:18 -0700444 global cycles
445 cycles += 1
446 logging.info("Number of cycles since modem restart %i",cycles)
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800447
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700448 try:
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800449 interface = None
450 report_via_modem = "report_in_band" in CONF._fields and CONF.report_in_band and \
451 "iface" in CONF.modem._fields and CONF.modem.iface
452 report_via_given_iface = "report_iface" in CONF._fields and CONF.report_iface
453
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800454 c = pycurl.Curl()
455 c.setopt(pycurl.URL, CONF.report_url)
456 c.setopt(pycurl.POST, True)
457 c.setopt(pycurl.HTTPHEADER, ['Content-Type: application/json'])
458 c.setopt(pycurl.TIMEOUT, 10)
459 c.setopt(pycurl.POSTFIELDS, json.dumps(report))
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800460 c.setopt(pycurl.WRITEFUNCTION, lambda x: None) # don't output to console
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800461
462 if report_via_modem: # report in-band
463 interface = CONF.modem.iface
464 c.setopt(pycurl.INTERFACE, interface)
465 elif report_via_given_iface: # report over given interface
466 interface = CONF.report_iface
467 c.setopt(pycurl.INTERFACE, interface)
Hyunsun Moon9f668e02022-03-11 13:12:57 -0800468 else:
469 interface = "default"
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800470
471 try:
472 c.perform()
473 logging.info("Report sent via " + interface + "!")
474 except Exception as e:
Shad Ansaria6ce0772022-03-15 21:34:51 -0700475 logging.error("Failed to send report in-band: " + str(e))
476 counters['report_send_error'] += 1
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800477 if report_via_modem and report_via_given_iface:
Shad Ansaria6ce0772022-03-15 21:34:51 -0700478 logging.warning("Attempting to send report via " + str(CONF.report_iface) + ".")
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800479 interface = CONF.report_iface
480 c.setopt(pycurl.INTERFACE, interface)
481 c.perform()
482 logging.info("Report sent via " + interface + "!")
483 else:
Shad Ansaria6ce0772022-03-15 21:34:51 -0700484 logging.error("Failed to send report out-of-band: " + str(e))
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800485 c.close()
486 except Exception as e:
487 logging.error("Failed to send report: " + str(e))
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800488 c.close()
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800489
Don Newtonbd91ae22021-05-11 14:58:18 -0700490def reset_usb():
Jeremy Ronquillo82a14612021-10-08 12:08:20 -0700491 try:
492 # Attempt to run uhubctl
493 if (int(subprocess.call("which uhubctl",shell=True)) == 0):
494 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
495 ret = subprocess.call(cmd,shell=True)
496 logging.info("Shutting down usb hub 2 results %s" , ret)
497 time.sleep(10)# let power down process settle out
498 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
499 ret = subprocess.call(cmd,shell=True)
500 logging.info("Starting up usb hub 2 results %s" , ret)
501 time.sleep(10) #allow dbus to finish
502 global cycles
503 cycles = 0
504 else:
505 reboot(120)
506 except Exception as e:
507 logging.error("Failed to run uhubctl: %s", e)
508 reboot(120)
509
510def reboot(delay):
511 logging.error("Failed to run uhubctl. Reboot system in " + str(delay) + " second(s).")
512 time.sleep(delay)
513 subprocess.check_output("sudo shutdown -r now", shell=True)
Don Newtonbd91ae22021-05-11 14:58:18 -0700514
Shad Ansarif390fbb2022-03-17 20:22:46 -0700515def init_iperf_stats():
516 return {'cluster': {'downlink': 0.0, 'uplink': 0.0}}
517
Shad Ansari2a376d72022-03-10 14:01:32 -0800518def init_counters():
519 return {
520 'dongle_read_error': 0,
521 'dongle_connect_error': 0,
522 'dongle_rsrp_rsrq_error': 0,
523 'modem_cfun0_error': 0,
524 'modem_cfun1_error': 0,
525 'modem_cgatt_error': 0,
526 'modem_cesq_error': 0,
527 'dry_run_ping_error': 0,
528 'ping_error': 0,
Shad Ansaria6ce0772022-03-15 21:34:51 -0700529 'iperf_error': 0,
530 'report_send_error': 0
Shad Ansari2a376d72022-03-10 14:01:32 -0800531 }
532
Shad Ansaria6ce0772022-03-15 21:34:51 -0700533def clear_counters(counters):
534 for x in counters:
535 counters[x] = 0
536
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700537def main():
Don Newtonbd91ae22021-05-11 14:58:18 -0700538 global cycles
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800539 global hour_iperf_scheduled_time_last_ran
Don Newtonbd91ae22021-05-11 14:58:18 -0700540 cycles = 0
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800541 hour_iperf_scheduled_time_last_ran = -1
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800542
Shad Ansaria6ce0772022-03-15 21:34:51 -0700543 counters = init_counters()
Shad Ansarif390fbb2022-03-17 20:22:46 -0700544 iperf_stats = init_iperf_stats()
Shad Ansaria6ce0772022-03-15 21:34:51 -0700545
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800546 try:
547 if "report_in_band" in CONF._fields and \
548 "iface" in CONF.modem._fields and CONF.modem.iface:
549 if CONF.report_in_band: # need to add default gateway if reporting in-band
550 subprocess.check_output("sudo route add default gw " + CONF.modem.ip_addr + " " + CONF.modem.iface + " || true", shell=True)
551 else:
552 subprocess.check_output("sudo route del default gw " + CONF.modem.ip_addr + " " + CONF.modem.iface + " || true", shell=True)
553 except Exception as e:
554 logging.error("Failed to change default route for modem: " + str(e))
555
Don Newtonbd91ae22021-05-11 14:58:18 -0700556 for ip in CONF.ips:
557 if not ip:
558 continue
559 try:
560 subprocess.check_output("sudo ip route replace {}/32 via {}".format(
561 ip, CONF.modem.ip_addr), shell=True)
562 except subprocess.CalledProcessError as e:
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800563 logging.error("Failed to add routes: " + str(e.returncode) + str(e.output))
Jeremy Ronquillo82a14612021-10-08 12:08:20 -0700564 time.sleep(10) # Sleep for 10 seconds before retry
Don Newtonbd91ae22021-05-11 14:58:18 -0700565 sys.exit(1)
566
Shad Ansari341a1c92022-03-02 09:14:40 -0800567 if USE_MODEM_CMDS:
568 modem = Modem(CONF.modem.port, CONF.modem.baud)
569 try:
570 modem.connect()
571 except serial.serialutil.SerialException as e:
572 logging.error("Failed to connect the modem for %s", e)
Hyunsun Moonf4242372020-10-04 23:32:38 -0700573 sys.exit(1)
Shad Ansari341a1c92022-03-02 09:14:40 -0800574 else:
575 modem = None
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700576
Shad Ansari341a1c92022-03-02 09:14:40 -0800577 connect_retries = 0
578 while True:
579 dongle_retries = 0
Shad Ansari2a376d72022-03-10 14:01:32 -0800580 dongle_stats = get_dongle_stats(counters)
Shad Ansari341a1c92022-03-02 09:14:40 -0800581 while not dongle_stats['SuccessfulFetch']:
Shad Ansari341a1c92022-03-02 09:14:40 -0800582 dongle_retries += 1
583 if dongle_retries > 10:
Shad Ansari2a376d72022-03-10 14:01:32 -0800584 logging.warning("Rebooting Pi: dongle not readable")
Shad Ansari341a1c92022-03-02 09:14:40 -0800585 os.system("shutdown /r /t 0")
586 sys.exit(1)
Shad Ansari2a376d72022-03-10 14:01:32 -0800587 dongle_stats = get_dongle_stats(counters)
Shad Ansari341a1c92022-03-02 09:14:40 -0800588
Shad Ansari2a376d72022-03-10 14:01:32 -0800589 cp_state = get_control_plane_state(modem, counters, dongle_stats)
Shad Ansari341a1c92022-03-02 09:14:40 -0800590
591 if cp_state != State.connected:
592 logging.error("Dongle not connected")
593 connect_retries += 1
594 if connect_retries > 10:
Shad Ansari2a376d72022-03-10 14:01:32 -0800595 logging.warning("Rebooting Pi: dongle not connected")
Shad Ansari341a1c92022-03-02 09:14:40 -0800596 os.system("shutdown /r /t 0")
597 sys.exit(1)
598
Shad Ansari2a376d72022-03-10 14:01:32 -0800599 signal_quality = get_signal_quality(modem, counters, dongle_stats)
600
601 dry_run_ping_latency, dry_run_ping_result = dry_run_ping_test(counters)
602 if not dry_run_ping_result:
Shad Ansari341a1c92022-03-02 09:14:40 -0800603 logging.error("Dry run ping failed")
Shad Ansari2a376d72022-03-10 14:01:32 -0800604 counters['dry_run_ping_error'] += 1
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700605
Shad Ansari2a376d72022-03-10 14:01:32 -0800606 ping_latency, ping_result = ping_test(modem, dry_run_ping_latency)
607 if not ping_result:
608 logging.error("Ping test failed")
609 counters['ping_error'] += 1
Shad Ansari341a1c92022-03-02 09:14:40 -0800610
Shad Ansari2a376d72022-03-10 14:01:32 -0800611 # If either of the ping tests pass, then declare user plane connected
612 if dry_run_ping_result or ping_result:
613 up_state = State.connected
Hyunsun Moon9f668e02022-03-11 13:12:57 -0800614 else:
615 up_state = State.disconnected
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700616
Shad Ansarif390fbb2022-03-17 20:22:46 -0700617 update_iperf_stats(iperf_stats, counters)
Shad Ansari2a376d72022-03-10 14:01:32 -0800618
Shad Ansarif390fbb2022-03-17 20:22:46 -0700619 report_status(counters, signal_quality, dongle_stats, cp_state, up_state, ping_latency, iperf_stats)
Shad Ansari927a7ff2022-03-17 11:11:49 -0700620 # counters = clear_counters(counters)
Shad Ansari341a1c92022-03-02 09:14:40 -0800621 time.sleep(CONF.report_interval)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700622
623 modem.close()
624
625
626if __name__ == "__main__":
627 main()