blob: bcd8b069ef74f04bc17eb4087ed29ccf5addcade [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")
Jeremy Ronquilloc03ba682021-10-06 10:27:09 -070033config_file_contents = config_file_contents.replace("\"speedtest_ping_dns\": \"1.1.1.1\",", "")
Hyunsun Moon53097ea2020-09-04 17:20:29 -070034CONF = json.loads(
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -070035 config_file_contents, object_hook=lambda d: namedtuple('X', d.keys())(*d.values())
Hyunsun Moon53097ea2020-09-04 17:20:29 -070036)
37
38logging.basicConfig(
39 filename=CONF.log_file,
40 format='%(asctime)s [%(levelname)s] %(message)s',
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -070041 level=logging.getLevelName(CONF.log_level)
Hyunsun Moon53097ea2020-09-04 17:20:29 -070042)
43
Hyunsun Moon53097ea2020-09-04 17:20:29 -070044
45class State(enum.Enum):
46 error = "-1"
47 disconnected = "0"
48 connected = "1"
49
50 @classmethod
51 def has_value(cls, value):
52 return value in cls._value2member_map_
53
54
55class Modem():
56 log = logging.getLogger('aether_edge_monitoring.Modem')
57
58 read_timeout = 0.1
59
60 def __init__(self, port, baudrate):
61 self.port = port
62 self.baudrate = baudrate
63 self._response = None
64
Don Newtonbd91ae22021-05-11 14:58:18 -070065 def get_modem_port(self):
66 cmd = "ls " + CONF.modem.port
67 sp = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,
68 stderr=subprocess.PIPE, universal_newlines=True)
69 rc = sp.wait()
70 ret,err = sp.communicate()
71 if err != "" :
Shad Ansarib234ff82022-02-17 22:14:35 -080072 self.log.error("unable to find serial port " + err)
Don Newtonbd91ae22021-05-11 14:58:18 -070073
74 ret = ret.replace(CONF.modem.port,"").strip()
Shad Ansarib234ff82022-02-17 22:14:35 -080075 self.log.info("Modem.get_modem_port found " + ret)
Don Newtonbd91ae22021-05-11 14:58:18 -070076 return ret
77
Hyunsun Moon53097ea2020-09-04 17:20:29 -070078 def connect(self):
Don Newtonbd91ae22021-05-11 14:58:18 -070079 self.port=self.get_modem_port()
Shad Ansarib234ff82022-02-17 22:14:35 -080080 self.log.info("modem.connect Port: %s, BaudRate: %i",self.port,self.baudrate)
Hyunsun Moon53097ea2020-09-04 17:20:29 -070081 self.serial = serial.Serial(
82 port=self.port,
83 baudrate=self.baudrate,
84 timeout=1)
85
86 def _write(self, command):
87 if self.serial.inWaiting() > 0:
88 self.serial.flushInput()
89
90 self._response = b""
91
92 self.serial.write(bytearray(command + "\r", "ascii"))
93 read = self.serial.inWaiting()
94 while True:
Shad Ansarib234ff82022-02-17 22:14:35 -080095 self.log.debug("Waiting for write to complete...")
Hyunsun Moon53097ea2020-09-04 17:20:29 -070096 if read > 0:
97 self._response += self.serial.read(read)
98 else:
99 time.sleep(self.read_timeout)
100 read = self.serial.inWaiting()
101 if read == 0:
102 break
Shad Ansarib234ff82022-02-17 22:14:35 -0800103 self.log.debug("Write complete...")
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700104 return self._response.decode("ascii").replace('\r\n', ' ')
105
106 def write(self, command, wait_resp=True):
107 response = self._write(command)
108 self.log.debug("%s: %s", command, response)
109
110 if wait_resp and "ERROR" in response:
111 return False, None
112 return True, response
113
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700114 def get_state(self):
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700115 success, result = self.write('AT+CGATT?')
116 if not success or 'CGATT:' not in result:
117 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 Ansarib234ff82022-02-17 22:14:35 -0800125def get_control_plane_state(modem, dongle_stats=None):
Shad Ansari341a1c92022-03-02 09:14:40 -0800126 if not modem and dongle_stats:
127 if dongle_stats and dongle_stats['Connection'] == 'Connected':
128 return State.connected
129 else:
130 return State.disconnected
Shad Ansarib234ff82022-02-17 22:14:35 -0800131
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700132 # Disable radio fuction
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700133 # "echo" works more stable than serial for this action
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700134 try:
Don Newtonbd91ae22021-05-11 14:58:18 -0700135 logging.debug("echo 'AT+CFUN=0' > " + modem.port)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700136 subprocess.check_output(
Don Newtonbd91ae22021-05-11 14:58:18 -0700137 "echo 'AT+CFUN=0' > " + modem.port, shell=True)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700138 except subprocess.CalledProcessError as e:
139 logging.error("Write 'AT+CFUN=0' failed")
140 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700141
142 # Wait until the modem is fully disconnected
143 retry = 0
144 state = None
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700145 while retry < CONF.detach_timeout:
146 state = modem.get_state()
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700147 if state is State.disconnected:
148 break
149 time.sleep(1)
150 retry += 1
151
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700152 if state is not State.disconnected:
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700153 logging.error("Failed to disconnect")
154 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700155
156 time.sleep(2)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700157 # Enable radio function
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700158 # "echo" works more stable than serial for this action
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700159 try:
Don Newtonbd91ae22021-05-11 14:58:18 -0700160 logging.debug("echo 'AT+CFUN=1' > " + modem.port)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700161 subprocess.check_output(
Don Newtonbd91ae22021-05-11 14:58:18 -0700162 "echo 'AT+CFUN=1' > " + modem.port, shell=True)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700163 except subprocess.CalledProcessError as e:
164 logging.error("Write 'AT+CFUN=1' failed")
165 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700166
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700167 # Wait attach_timeout sec for the modem to be fully connected
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700168 retry = 0
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700169 while retry < CONF.attach_timeout:
170 state = modem.get_state()
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700171 if state is State.connected:
172 break
173 time.sleep(1)
174 retry += 1
175 # CGATT sometimes returns None
176 if state is State.error:
177 state = State.disconnected
178
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700179 return state
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700180
181
Shad Ansari341a1c92022-03-02 09:14:40 -0800182def get_user_plane_state():
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800183 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 -0800184 dry_run_latency, dry_run_passed = run_ping_test(CONF.ips.dry_run, 10)
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800185 if dry_run_passed:
186 return State.connected, dry_run_latency
187 else:
188 logging.warning("User plane test failed")
189 return State.disconnected, dry_run_latency
190 else: # run default user plane test
191 try:
192 subprocess.check_output(
193 "ping -c 3 " + CONF.ips.dns + ">/dev/null 2>&1",
194 shell=True)
195 return State.connected, None
196 except subprocess.CalledProcessError as e:
197 logging.warning("User plane test failed")
198 return State.disconnected, None
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700199
200
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800201def run_ping_test(ip, count):
202 '''
203 Runs the ping test
204 Input: IP to ping, # times to ping
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700205 Returns: Transmitted packets
206 Received packets
207 Median ping ms
208 Min ping ms
209 Avg ping ms
210 Max ping ms
211 Std Dev ping ms
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800212 '''
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700213 result = {'transmitted': 0,
214 'received': 0,
215 'median': 0.0,
216 'min': 0.0,
Jeremy Ronquilloa944fbc2021-03-30 10:57:45 -0700217 'avg': 0.0,
218 'max': 0.0,
219 'stddev': 0.0}
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700220 if not ip:
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700221 return result, True
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800222 try:
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700223 pingOutput = subprocess.check_output(
224 "ping -c " + str(count) + " " + \
225 ip, shell=True).decode("UTF-8").split()
226 result['transmitted'] = int(pingOutput[-15])
227 result['received'] = int(pingOutput[-12])
228 if result['received'] > 0:
229 pingValues = []
230
231 # Hack for getting all ping values for median
232 for word in pingOutput:
233 if "time=" in word:
234 pingValues.append(float(word.split("=")[1]))
235 result['median'] = round(median(pingValues), 3)
236
237 pingResult = pingOutput[-2].split('/')
238 result['min'] = float(pingResult[0])
239 result['avg'] = float(pingResult[1])
240 result['max'] = float(pingResult[2])
241 result['stddev'] = float(pingResult[3])
242 else:
243 logging.error("No packets received during ping " + ip)
244 return result, False
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800245 except Exception as e:
246 logging.error("Ping test failed for " + ip + ": %s", e)
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700247 return result, False
248 return result, True
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800249
250
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800251def get_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
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700268 speedtest_ping[CONF.ips._fields[i]], status = run_ping_test(CONF.ips[i], count)
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700269 if not status:
270 ping_test_passed = False
271 logging.error("Ping test failed. Not running further tests.")
272 return speedtest_ping, ping_test_passed
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800273
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700274def run_iperf_test(ip, port, time_duration, is_downlink):
275 '''
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
Jeremy Ronquillo012ac662021-08-06 12:13:43 -0700299 time.sleep(5)
300 pass
Jeremy Ronquillo79c3e672021-09-03 12:54:55 -0700301 logging.error("After " + str(maxRetries) + " retries, iperf test failed for " + ip + ": %s", err)
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700302 return result
303
304
305def get_iperf_test(modem):
306 '''
307 Prepares the iperf test.
308 '''
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800309 global hour_iperf_scheduled_time_last_ran
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700310 speedtest_iperf = {}
311 speedtest_iperf['cluster'] = {}
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800312
313 if "iperf_schedule" in CONF._fields and len(CONF.iperf_schedule) > 0:
314 if int(time.strftime("%H")) not in CONF.iperf_schedule: # not in the schedule
315 hour_iperf_scheduled_time_last_ran = -1
316 return None
317 elif int(time.strftime("%H")) == hour_iperf_scheduled_time_last_ran: # already ran this hour
318 return None
319 hour_iperf_scheduled_time_last_ran = int(time.strftime("%H"))
320
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700321 speedtest_iperf['cluster']['downlink'] = run_iperf_test(CONF.ips.iperf_server, CONF.iperf_port, 10, True)
322 speedtest_iperf['cluster']['uplink'] = run_iperf_test(CONF.ips.iperf_server, CONF.iperf_port, 10, False)
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700323
324 return speedtest_iperf
325
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800326
Shad Ansarib234ff82022-02-17 22:14:35 -0800327def get_signal_quality(modem, dongle_stats=None):
Shad Ansari341a1c92022-03-02 09:14:40 -0800328 if not modem and dongle_stats:
329 if dongle_stats['RSRQ'] != '' and dongle_stats['RSRP'] != '':
330 rsrq = int((float(dongle_stats['RSRQ']) + 19.5) * 2)
331 rsrp = int(float(dongle_stats['RSRP']) + 140)
332 return {'rsrq': rsrq, 'rsrp': rsrp}
333 else:
334 return {'rsrq': 0, 'rsrp': 0}
335
336 # Fall back to modem cmds
Shad Ansarib234ff82022-02-17 22:14:35 -0800337
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700338 success, result = modem.write('AT+CESQ')
Don Newtonbd91ae22021-05-11 14:58:18 -0700339 logging.debug("get_signal_quality success %i result %s",success,result)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700340 if not success or 'CESQ: ' not in result:
341 logging.error("Failed to get signal quality")
Don Newtonbd91ae22021-05-11 14:58:18 -0700342 return {'rsrq':0, 'rsrp':0}
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700343
344 logging.debug("%s", result)
345 tmp_rsrq = result.split('CESQ:')[1].split(',')[4]
346 tmp_rsrp = result.split('CESQ:')[1].split(',')[5]
347
348 rsrq = int(tmp_rsrq.strip())
349 rsrp = int(tmp_rsrp.strip().split(' ')[0])
350 result = {
351 'rsrq': 0 if rsrq is 255 else rsrq,
352 'rsrp': 0 if rsrp is 255 else rsrp
353 }
354
355 return result
356
Shad Ansarib234ff82022-02-17 22:14:35 -0800357
358def get_dongle_stats():
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800359 result = {'SuccessfulFetch' : False}
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800360 if "report_in_band" in CONF._fields:
361 result['inBandReporting'] = CONF.report_in_band
362 else:
363 result['inBandReporting'] = False
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800364 XMLkeys = ["MAC",
365 "PLMNStatus",
366 "UICCStatus",
367 "IMEI",
368 "IMSI",
369 "PLMNSelected",
370 "MCC",
371 "MNC",
372 "PhyCellID",
373 "CellGlobalID",
374 "Band",
375 "EARFCN",
376 "BandWidth",
Shad Ansarib234ff82022-02-17 22:14:35 -0800377 "RSRP",
378 "RSRQ",
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800379 "ServCellState",
380 "Connection",
381 "IPv4Addr"]
382 dongleStatsXML = None
383 try:
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800384 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 -0800385 except Exception as e:
386 logging.error("Failed to fetch dongle stats from URL: " + str(e))
387 return result
388 try:
389 for key in XMLkeys:
390 try:
391 result[key] = dongleStatsXML.find(key).text
392 except AttributeError as e:
393 logging.warn("Failed to find " + key + " in XML.")
394 result[key] = ""
395 result["SuccessfulFetch"] = True
396 except Exception as e:
397 logging.error("Failed to fetch dongle stats from XML: " + str(e))
398 return result
399 return result
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700400
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800401
402def 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 -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 }
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700423 },
424 'iperf': {
425 'cluster': {
426 'downlink': 0.0,
427 'uplink': 0.0
428 }
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700429 }
430 },
431 'signal_quality': {
432 'rsrq': 0,
433 'rsrp': 0
434 }
435 }
436
437 if cp_state is not None:
438 report['status']['control_plane'] = cp_state.name
439 if up_state is not None:
440 report['status']['user_plane'] = up_state.name
441 if speedtest_ping is not None:
442 report['speedtest']['ping'] = speedtest_ping
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700443 if speedtest_iperf is not None:
444 report['speedtest']['iperf'] = speedtest_iperf
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700445 report['signal_quality'] = signal_quality
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800446 report['dongle_stats'] = dongle_stats
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700447
448 logging.info("Sending report %s", report)
Don Newtonbd91ae22021-05-11 14:58:18 -0700449 global cycles
450 cycles += 1
451 logging.info("Number of cycles since modem restart %i",cycles)
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800452
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700453 try:
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800454 interface = None
455 report_via_modem = "report_in_band" in CONF._fields and CONF.report_in_band and \
456 "iface" in CONF.modem._fields and CONF.modem.iface
457 report_via_given_iface = "report_iface" in CONF._fields and CONF.report_iface
458
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800459 c = pycurl.Curl()
460 c.setopt(pycurl.URL, CONF.report_url)
461 c.setopt(pycurl.POST, True)
462 c.setopt(pycurl.HTTPHEADER, ['Content-Type: application/json'])
463 c.setopt(pycurl.TIMEOUT, 10)
464 c.setopt(pycurl.POSTFIELDS, json.dumps(report))
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800465 c.setopt(pycurl.WRITEFUNCTION, lambda x: None) # don't output to console
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800466
467 if report_via_modem: # report in-band
468 interface = CONF.modem.iface
469 c.setopt(pycurl.INTERFACE, interface)
470 elif report_via_given_iface: # report over given interface
471 interface = CONF.report_iface
472 c.setopt(pycurl.INTERFACE, interface)
473 # else, reports over default interface
474
475 try:
476 c.perform()
477 logging.info("Report sent via " + interface + "!")
478 except Exception as e:
479 if report_via_modem and report_via_given_iface:
480 logging.warning("Sending report via modem failed. Attempting to send report via " + str(CONF.report_iface) + ".")
481 interface = CONF.report_iface
482 c.setopt(pycurl.INTERFACE, interface)
483 c.perform()
484 logging.info("Report sent via " + interface + "!")
485 else:
486 logging.error("Failed to send report: " + str(e))
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800487 c.close()
488 except Exception as e:
489 logging.error("Failed to send report: " + str(e))
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800490 c.close()
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800491
Don Newtonbd91ae22021-05-11 14:58:18 -0700492def reset_usb():
Jeremy Ronquillo82a14612021-10-08 12:08:20 -0700493 try:
494 # Attempt to run uhubctl
495 if (int(subprocess.call("which uhubctl",shell=True)) == 0):
496 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
497 ret = subprocess.call(cmd,shell=True)
498 logging.info("Shutting down usb hub 2 results %s" , ret)
499 time.sleep(10)# let power down process settle out
500 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
501 ret = subprocess.call(cmd,shell=True)
502 logging.info("Starting up usb hub 2 results %s" , ret)
503 time.sleep(10) #allow dbus to finish
504 global cycles
505 cycles = 0
506 else:
507 reboot(120)
508 except Exception as e:
509 logging.error("Failed to run uhubctl: %s", e)
510 reboot(120)
511
512def reboot(delay):
513 logging.error("Failed to run uhubctl. Reboot system in " + str(delay) + " second(s).")
514 time.sleep(delay)
515 subprocess.check_output("sudo shutdown -r now", shell=True)
Don Newtonbd91ae22021-05-11 14:58:18 -0700516
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700517def main():
Don Newtonbd91ae22021-05-11 14:58:18 -0700518 global cycles
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800519 global hour_iperf_scheduled_time_last_ran
Don Newtonbd91ae22021-05-11 14:58:18 -0700520 cycles = 0
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800521 hour_iperf_scheduled_time_last_ran = -1
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800522
523 try:
524 if "report_in_band" in CONF._fields and \
525 "iface" in CONF.modem._fields and CONF.modem.iface:
526 if CONF.report_in_band: # need to add default gateway if reporting in-band
527 subprocess.check_output("sudo route add default gw " + CONF.modem.ip_addr + " " + CONF.modem.iface + " || true", shell=True)
528 else:
529 subprocess.check_output("sudo route del default gw " + CONF.modem.ip_addr + " " + CONF.modem.iface + " || true", shell=True)
530 except Exception as e:
531 logging.error("Failed to change default route for modem: " + str(e))
532
Don Newtonbd91ae22021-05-11 14:58:18 -0700533 for ip in CONF.ips:
534 if not ip:
535 continue
536 try:
537 subprocess.check_output("sudo ip route replace {}/32 via {}".format(
538 ip, CONF.modem.ip_addr), shell=True)
539 except subprocess.CalledProcessError as e:
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800540 logging.error("Failed to add routes: " + str(e.returncode) + str(e.output))
Jeremy Ronquillo82a14612021-10-08 12:08:20 -0700541 time.sleep(10) # Sleep for 10 seconds before retry
Don Newtonbd91ae22021-05-11 14:58:18 -0700542 sys.exit(1)
543
Shad Ansari341a1c92022-03-02 09:14:40 -0800544 if USE_MODEM_CMDS:
545 modem = Modem(CONF.modem.port, CONF.modem.baud)
546 try:
547 modem.connect()
548 except serial.serialutil.SerialException as e:
549 logging.error("Failed to connect the modem for %s", e)
Hyunsun Moonf4242372020-10-04 23:32:38 -0700550 sys.exit(1)
Shad Ansari341a1c92022-03-02 09:14:40 -0800551 else:
552 modem = None
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700553
Shad Ansari341a1c92022-03-02 09:14:40 -0800554 connect_retries = 0
555 while True:
556 dongle_retries = 0
557 dongle_stats = get_dongle_stats()
558 while not dongle_stats['SuccessfulFetch']:
559 log.error("Dongle error")
560 dongle_retries += 1
561 if dongle_retries > 10:
562 logging.warn("Rebooting Pi")
563 os.system("shutdown /r /t 0")
564 sys.exit(1)
565 dongle_stats = get_dongle_stats()
566
567 cp_state = get_control_plane_state(modem, dongle_stats)
568
569 if cp_state != State.connected:
570 logging.error("Dongle not connected")
571 connect_retries += 1
572 if connect_retries > 10:
573 os.system("shutdown /r /t 0")
574 sys.exit(1)
575
576 signal_quality = get_signal_quality(modem, dongle_stats)
577 up_state, dry_run_latency = get_user_plane_state()
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700578 if up_state is State.disconnected:
Shad Ansari341a1c92022-03-02 09:14:40 -0800579 logging.error("Dry run ping failed")
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700580
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800581 speedtest_ping, speedtest_status = get_ping_test(modem, dry_run_latency)
Shad Ansari341a1c92022-03-02 09:14:40 -0800582 if not speedtest_status:
583 logging.error("Ping tests failed")
584
585 speedtest_iperf = get_iperf_test(modem)
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700586
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800587 report_status(signal_quality, dongle_stats, cp_state, up_state, speedtest_ping, speedtest_iperf)
Shad Ansari341a1c92022-03-02 09:14:40 -0800588 time.sleep(CONF.report_interval)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700589
590 modem.close()
591
592
593if __name__ == "__main__":
594 main()