blob: b7691531fb3afc8b7278ff4aee0b143161c3a90f [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 time
14import serial
Jeremy Ronquillod996b512021-02-13 13:45:47 -080015import subprocess
Jeremy Ronquilloef17e362021-11-08 10:56:42 -080016import time
Hyunsun Moon53097ea2020-09-04 17:20:29 -070017from collections import namedtuple
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -070018from statistics import median
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -080019import xml.etree.ElementTree as ET
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
35config_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)
71 rc = sp.wait()
72 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
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700116 def get_state(self):
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700117 success, result = self.write('AT+CGATT?')
118 if not success or 'CGATT:' not in result:
119 return State.error
120 state = result.split('CGATT:')[1].split(' ')[0]
121 return State(state)
122
123 def close(self):
124 self.serial.close()
125
126
Shad Ansarib234ff82022-02-17 22:14:35 -0800127def get_control_plane_state(modem, dongle_stats=None):
Shad Ansari341a1c92022-03-02 09:14:40 -0800128 if not modem and dongle_stats:
129 if dongle_stats and dongle_stats['Connection'] == 'Connected':
130 return State.connected
131 else:
132 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:
141 logging.error("Write 'AT+CFUN=0' failed")
142 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700143
144 # Wait until the modem is fully disconnected
145 retry = 0
146 state = None
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700147 while retry < CONF.detach_timeout:
148 state = modem.get_state()
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700149 if state is State.disconnected:
150 break
151 time.sleep(1)
152 retry += 1
153
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700154 if state is not State.disconnected:
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700155 logging.error("Failed to disconnect")
156 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700157
158 time.sleep(2)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700159 # Enable radio function
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700160 # "echo" works more stable than serial for this action
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700161 try:
Don Newtonbd91ae22021-05-11 14:58:18 -0700162 logging.debug("echo 'AT+CFUN=1' > " + modem.port)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700163 subprocess.check_output(
Don Newtonbd91ae22021-05-11 14:58:18 -0700164 "echo 'AT+CFUN=1' > " + modem.port, shell=True)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700165 except subprocess.CalledProcessError as e:
166 logging.error("Write 'AT+CFUN=1' failed")
167 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700168
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700169 # Wait attach_timeout sec for the modem to be fully connected
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700170 retry = 0
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700171 while retry < CONF.attach_timeout:
172 state = modem.get_state()
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700173 if state is State.connected:
174 break
175 time.sleep(1)
176 retry += 1
177 # CGATT sometimes returns None
178 if state is State.error:
179 state = State.disconnected
180
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700181 return state
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700182
183
Shad Ansari341a1c92022-03-02 09:14:40 -0800184def get_user_plane_state():
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800185 if "dry_run" in CONF.ips._fields and CONF.ips.dry_run: # run dry_run latency test as user plane test
Shad Ansarib3d54752022-02-28 12:32:58 -0800186 dry_run_latency, dry_run_passed = run_ping_test(CONF.ips.dry_run, 10)
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800187 if dry_run_passed:
188 return State.connected, dry_run_latency
189 else:
Shad Ansari416ccab2022-03-09 19:06:43 -0800190 logging.warning("Ping failed: {}".format(CONF.ips.dry_run))
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800191 return State.disconnected, dry_run_latency
192 else: # run default user plane test
193 try:
194 subprocess.check_output(
195 "ping -c 3 " + CONF.ips.dns + ">/dev/null 2>&1",
196 shell=True)
197 return State.connected, None
198 except subprocess.CalledProcessError as e:
Shad Ansari416ccab2022-03-09 19:06:43 -0800199 logging.warning("Ping failed: {}".format(CONF.ips.dns))
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800200 return State.disconnected, None
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700201
202
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800203def run_ping_test(ip, count):
204 '''
205 Runs the ping test
206 Input: IP to ping, # times to ping
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700207 Returns: Transmitted packets
208 Received packets
209 Median ping ms
210 Min ping ms
211 Avg ping ms
212 Max ping ms
213 Std Dev ping ms
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800214 '''
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700215 result = {'transmitted': 0,
216 'received': 0,
217 'median': 0.0,
218 'min': 0.0,
Jeremy Ronquilloa944fbc2021-03-30 10:57:45 -0700219 'avg': 0.0,
220 'max': 0.0,
221 'stddev': 0.0}
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700222 if not ip:
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700223 return result, True
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800224 try:
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700225 pingOutput = subprocess.check_output(
226 "ping -c " + str(count) + " " + \
227 ip, shell=True).decode("UTF-8").split()
228 result['transmitted'] = int(pingOutput[-15])
229 result['received'] = int(pingOutput[-12])
230 if result['received'] > 0:
231 pingValues = []
232
233 # Hack for getting all ping values for median
234 for word in pingOutput:
235 if "time=" in word:
236 pingValues.append(float(word.split("=")[1]))
237 result['median'] = round(median(pingValues), 3)
238
239 pingResult = pingOutput[-2].split('/')
240 result['min'] = float(pingResult[0])
241 result['avg'] = float(pingResult[1])
242 result['max'] = float(pingResult[2])
243 result['stddev'] = float(pingResult[3])
244 else:
245 logging.error("No packets received during ping " + ip)
246 return result, False
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800247 except Exception as e:
248 logging.error("Ping test failed for " + ip + ": %s", e)
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700249 return result, False
250 return result, True
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800251
252
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800253def get_ping_test(modem, dry_run_latency=None):
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800254 '''
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700255 Prepares the ping test.
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700256 Runs ping tests from 'ips' entry in config.json in order.
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800257 Note: 'dry_run' is not run here; it is run during the user plane test.
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800258 '''
259 speedtest_ping = {}
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700260 status = True
261 ping_test_passed = True
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700262
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800263 if dry_run_latency:
264 speedtest_ping["dry_run"] = dry_run_latency
265
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700266 for i in range(0, len(CONF.ips)):
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800267 if CONF.ips._fields[i] == "dry_run":
268 continue
Shad Ansarib3d54752022-02-28 12:32:58 -0800269 count = 10
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700270 speedtest_ping[CONF.ips._fields[i]], status = run_ping_test(CONF.ips[i], count)
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700271 if not status:
272 ping_test_passed = False
273 logging.error("Ping test failed. Not running further tests.")
274 return speedtest_ping, ping_test_passed
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800275
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700276def run_iperf_test(ip, port, time_duration, is_downlink):
277 '''
278 Runs iperf test to specified IP in the config file.
279 - Runs for 10 seconds (10 iterations)
280 - Retrieves downlink and uplink test results from json output
281 '''
282 result = 0.0
Jeremy Ronquilloc03ba682021-10-06 10:27:09 -0700283 if not ip or port == 0:
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700284 return result
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700285 maxRetries = 2
Jeremy Ronquillo79c3e672021-09-03 12:54:55 -0700286 err = None
Jeremy Ronquillo012ac662021-08-06 12:13:43 -0700287 for _ in range(0, maxRetries):
288 try:
289 iperfResult = json.loads(subprocess.check_output(
290 "iperf3 -c " + ip +
291 " -p " + str(port) +
292 " -t " + str(time_duration) +
293 (" -R " if is_downlink else "") +
294 " --json", shell=True).decode("UTF-8"))
295 received_mbps = iperfResult['end']['sum_received']['bits_per_second'] / 1000000
296 sent_mbps = iperfResult['end']['sum_sent']['bits_per_second'] / 1000000.0
297 result = received_mbps if is_downlink else sent_mbps
298 return result
299 except Exception as e:
Jeremy Ronquillo79c3e672021-09-03 12:54:55 -0700300 err = e
Jeremy Ronquillo012ac662021-08-06 12:13:43 -0700301 time.sleep(5)
302 pass
Jeremy Ronquillo79c3e672021-09-03 12:54:55 -0700303 logging.error("After " + str(maxRetries) + " retries, iperf test failed for " + ip + ": %s", err)
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700304 return result
305
306
307def get_iperf_test(modem):
308 '''
309 Prepares the iperf test.
310 '''
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800311 global hour_iperf_scheduled_time_last_ran
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700312 speedtest_iperf = {}
313 speedtest_iperf['cluster'] = {}
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800314
315 if "iperf_schedule" in CONF._fields and len(CONF.iperf_schedule) > 0:
316 if int(time.strftime("%H")) not in CONF.iperf_schedule: # not in the schedule
317 hour_iperf_scheduled_time_last_ran = -1
318 return None
319 elif int(time.strftime("%H")) == hour_iperf_scheduled_time_last_ran: # already ran this hour
320 return None
321 hour_iperf_scheduled_time_last_ran = int(time.strftime("%H"))
322
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700323 speedtest_iperf['cluster']['downlink'] = run_iperf_test(CONF.ips.iperf_server, CONF.iperf_port, 10, True)
324 speedtest_iperf['cluster']['uplink'] = run_iperf_test(CONF.ips.iperf_server, CONF.iperf_port, 10, False)
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700325
326 return speedtest_iperf
327
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800328
Shad Ansarib234ff82022-02-17 22:14:35 -0800329def get_signal_quality(modem, dongle_stats=None):
Shad Ansari341a1c92022-03-02 09:14:40 -0800330 if not modem and dongle_stats:
331 if dongle_stats['RSRQ'] != '' and dongle_stats['RSRP'] != '':
332 rsrq = int((float(dongle_stats['RSRQ']) + 19.5) * 2)
333 rsrp = int(float(dongle_stats['RSRP']) + 140)
334 return {'rsrq': rsrq, 'rsrp': rsrp}
335 else:
336 return {'rsrq': 0, 'rsrp': 0}
337
338 # Fall back to modem cmds
Shad Ansarib234ff82022-02-17 22:14:35 -0800339
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700340 success, result = modem.write('AT+CESQ')
Don Newtonbd91ae22021-05-11 14:58:18 -0700341 logging.debug("get_signal_quality success %i result %s",success,result)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700342 if not success or 'CESQ: ' not in result:
343 logging.error("Failed to get signal quality")
Don Newtonbd91ae22021-05-11 14:58:18 -0700344 return {'rsrq':0, 'rsrp':0}
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700345
346 logging.debug("%s", result)
347 tmp_rsrq = result.split('CESQ:')[1].split(',')[4]
348 tmp_rsrp = result.split('CESQ:')[1].split(',')[5]
349
350 rsrq = int(tmp_rsrq.strip())
351 rsrp = int(tmp_rsrp.strip().split(' ')[0])
352 result = {
353 'rsrq': 0 if rsrq is 255 else rsrq,
354 'rsrp': 0 if rsrp is 255 else rsrp
355 }
356
357 return result
358
Shad Ansarib234ff82022-02-17 22:14:35 -0800359
360def get_dongle_stats():
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800361 result = {'SuccessfulFetch' : False}
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800362 if "report_in_band" in CONF._fields:
363 result['inBandReporting'] = CONF.report_in_band
364 else:
365 result['inBandReporting'] = False
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800366 XMLkeys = ["MAC",
367 "PLMNStatus",
368 "UICCStatus",
369 "IMEI",
370 "IMSI",
371 "PLMNSelected",
372 "MCC",
373 "MNC",
374 "PhyCellID",
375 "CellGlobalID",
376 "Band",
377 "EARFCN",
378 "BandWidth",
Shad Ansarib234ff82022-02-17 22:14:35 -0800379 "RSRP",
380 "RSRQ",
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800381 "ServCellState",
382 "Connection",
383 "IPv4Addr"]
384 dongleStatsXML = None
385 try:
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800386 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 -0800387 except Exception as e:
388 logging.error("Failed to fetch dongle stats from URL: " + str(e))
389 return result
390 try:
391 for key in XMLkeys:
392 try:
393 result[key] = dongleStatsXML.find(key).text
394 except AttributeError as e:
395 logging.warn("Failed to find " + key + " in XML.")
396 result[key] = ""
397 result["SuccessfulFetch"] = True
398 except Exception as e:
399 logging.error("Failed to fetch dongle stats from XML: " + str(e))
400 return result
401 return result
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700402
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800403
404def report_status(signal_quality, dongle_stats, cp_state=None, up_state=None, speedtest_ping=None, speedtest_iperf=None):
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700405 report = {
406 'name': CONF.edge_name,
407 'status': {
408 'control_plane': "disconnected",
409 'user_plane': "disconnected"
410 },
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800411 'dongle_stats': {
412 'SuccessfulFetch' : False
413 },
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700414 'speedtest': {
415 'ping': {
416 'dns': {
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700417 'transmitted' : 0,
418 'received' : 0,
419 'median' : 0.0,
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700420 'min': 0.0,
421 'avg': 0.0,
422 'max': 0.0,
423 'stddev': 0.0
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700424 }
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700425 },
426 'iperf': {
427 'cluster': {
428 'downlink': 0.0,
429 'uplink': 0.0
430 }
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700431 }
432 },
433 'signal_quality': {
434 'rsrq': 0,
435 'rsrp': 0
436 }
437 }
438
439 if cp_state is not None:
440 report['status']['control_plane'] = cp_state.name
441 if up_state is not None:
442 report['status']['user_plane'] = up_state.name
443 if speedtest_ping is not None:
444 report['speedtest']['ping'] = speedtest_ping
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700445 if speedtest_iperf is not None:
446 report['speedtest']['iperf'] = speedtest_iperf
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700447 report['signal_quality'] = signal_quality
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800448 report['dongle_stats'] = dongle_stats
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700449
450 logging.info("Sending report %s", report)
Don Newtonbd91ae22021-05-11 14:58:18 -0700451 global cycles
452 cycles += 1
453 logging.info("Number of cycles since modem restart %i",cycles)
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800454
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700455 try:
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800456 interface = None
457 report_via_modem = "report_in_band" in CONF._fields and CONF.report_in_band and \
458 "iface" in CONF.modem._fields and CONF.modem.iface
459 report_via_given_iface = "report_iface" in CONF._fields and CONF.report_iface
460
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800461 c = pycurl.Curl()
462 c.setopt(pycurl.URL, CONF.report_url)
463 c.setopt(pycurl.POST, True)
464 c.setopt(pycurl.HTTPHEADER, ['Content-Type: application/json'])
465 c.setopt(pycurl.TIMEOUT, 10)
466 c.setopt(pycurl.POSTFIELDS, json.dumps(report))
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800467 c.setopt(pycurl.WRITEFUNCTION, lambda x: None) # don't output to console
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800468
469 if report_via_modem: # report in-band
470 interface = CONF.modem.iface
471 c.setopt(pycurl.INTERFACE, interface)
472 elif report_via_given_iface: # report over given interface
473 interface = CONF.report_iface
474 c.setopt(pycurl.INTERFACE, interface)
475 # else, reports over default interface
476
477 try:
478 c.perform()
479 logging.info("Report sent via " + interface + "!")
480 except Exception as e:
481 if report_via_modem and report_via_given_iface:
482 logging.warning("Sending report via modem failed. Attempting to send report via " + str(CONF.report_iface) + ".")
483 interface = CONF.report_iface
484 c.setopt(pycurl.INTERFACE, interface)
485 c.perform()
486 logging.info("Report sent via " + interface + "!")
487 else:
488 logging.error("Failed to send report: " + str(e))
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800489 c.close()
490 except Exception as e:
491 logging.error("Failed to send report: " + str(e))
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800492 c.close()
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800493
Don Newtonbd91ae22021-05-11 14:58:18 -0700494def reset_usb():
Jeremy Ronquillo82a14612021-10-08 12:08:20 -0700495 try:
496 # Attempt to run uhubctl
497 if (int(subprocess.call("which uhubctl",shell=True)) == 0):
498 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
499 ret = subprocess.call(cmd,shell=True)
500 logging.info("Shutting down usb hub 2 results %s" , ret)
501 time.sleep(10)# let power down process settle out
502 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
503 ret = subprocess.call(cmd,shell=True)
504 logging.info("Starting up usb hub 2 results %s" , ret)
505 time.sleep(10) #allow dbus to finish
506 global cycles
507 cycles = 0
508 else:
509 reboot(120)
510 except Exception as e:
511 logging.error("Failed to run uhubctl: %s", e)
512 reboot(120)
513
514def reboot(delay):
515 logging.error("Failed to run uhubctl. Reboot system in " + str(delay) + " second(s).")
516 time.sleep(delay)
517 subprocess.check_output("sudo shutdown -r now", shell=True)
Don Newtonbd91ae22021-05-11 14:58:18 -0700518
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700519def main():
Don Newtonbd91ae22021-05-11 14:58:18 -0700520 global cycles
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800521 global hour_iperf_scheduled_time_last_ran
Don Newtonbd91ae22021-05-11 14:58:18 -0700522 cycles = 0
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800523 hour_iperf_scheduled_time_last_ran = -1
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800524
525 try:
526 if "report_in_band" in CONF._fields and \
527 "iface" in CONF.modem._fields and CONF.modem.iface:
528 if CONF.report_in_band: # need to add default gateway if reporting in-band
529 subprocess.check_output("sudo route add default gw " + CONF.modem.ip_addr + " " + CONF.modem.iface + " || true", shell=True)
530 else:
531 subprocess.check_output("sudo route del default gw " + CONF.modem.ip_addr + " " + CONF.modem.iface + " || true", shell=True)
532 except Exception as e:
533 logging.error("Failed to change default route for modem: " + str(e))
534
Don Newtonbd91ae22021-05-11 14:58:18 -0700535 for ip in CONF.ips:
536 if not ip:
537 continue
538 try:
539 subprocess.check_output("sudo ip route replace {}/32 via {}".format(
540 ip, CONF.modem.ip_addr), shell=True)
541 except subprocess.CalledProcessError as e:
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800542 logging.error("Failed to add routes: " + str(e.returncode) + str(e.output))
Jeremy Ronquillo82a14612021-10-08 12:08:20 -0700543 time.sleep(10) # Sleep for 10 seconds before retry
Don Newtonbd91ae22021-05-11 14:58:18 -0700544 sys.exit(1)
545
Shad Ansari341a1c92022-03-02 09:14:40 -0800546 if USE_MODEM_CMDS:
547 modem = Modem(CONF.modem.port, CONF.modem.baud)
548 try:
549 modem.connect()
550 except serial.serialutil.SerialException as e:
551 logging.error("Failed to connect the modem for %s", e)
Hyunsun Moonf4242372020-10-04 23:32:38 -0700552 sys.exit(1)
Shad Ansari341a1c92022-03-02 09:14:40 -0800553 else:
554 modem = None
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700555
Shad Ansari341a1c92022-03-02 09:14:40 -0800556 connect_retries = 0
557 while True:
558 dongle_retries = 0
559 dongle_stats = get_dongle_stats()
560 while not dongle_stats['SuccessfulFetch']:
561 log.error("Dongle error")
562 dongle_retries += 1
563 if dongle_retries > 10:
564 logging.warn("Rebooting Pi")
565 os.system("shutdown /r /t 0")
566 sys.exit(1)
567 dongle_stats = get_dongle_stats()
568
569 cp_state = get_control_plane_state(modem, dongle_stats)
570
571 if cp_state != State.connected:
572 logging.error("Dongle not connected")
573 connect_retries += 1
574 if connect_retries > 10:
575 os.system("shutdown /r /t 0")
576 sys.exit(1)
577
578 signal_quality = get_signal_quality(modem, dongle_stats)
579 up_state, dry_run_latency = get_user_plane_state()
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700580 if up_state is State.disconnected:
Shad Ansari341a1c92022-03-02 09:14:40 -0800581 logging.error("Dry run ping failed")
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700582
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800583 speedtest_ping, speedtest_status = get_ping_test(modem, dry_run_latency)
Shad Ansari341a1c92022-03-02 09:14:40 -0800584 if not speedtest_status:
585 logging.error("Ping tests failed")
586
587 speedtest_iperf = get_iperf_test(modem)
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700588
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800589 report_status(signal_quality, dongle_stats, cp_state, up_state, speedtest_ping, speedtest_iperf)
Shad Ansari341a1c92022-03-02 09:14:40 -0800590 time.sleep(CONF.report_interval)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700591
592 modem.close()
593
594
595if __name__ == "__main__":
596 main()