blob: 7b0739cf6f618422a9ff6c9a6d9ce1b0fc5881bb [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
Hyunsun Moon53097ea2020-09-04 17:20:29 -070019
20'''
Jeremy Ronquilloa944fbc2021-03-30 10:57:45 -070021"Simple" script that checks Aether network operational status periodically
Hyunsun Moon53097ea2020-09-04 17:20:29 -070022by controlling the attached 4G/LTE modem with AT commands and
23report the result to the central monitoring server.
24'''
25
Shad Ansari341a1c92022-03-02 09:14:40 -080026USE_MODEM_CMDS = False
27
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -070028# Parse config with backwards compatibility with config.json pre 0.6.6
29config_file_contents = open(os.getenv('CONFIG_FILE', "./config.json")).read()
30config_file_contents = config_file_contents.replace("user_plane_ping_test", "dns")
31config_file_contents = config_file_contents.replace("speedtest_iperf", "iperf_server")
Shad Ansari416ccab2022-03-09 19:06:43 -080032config_file_contents = config_file_contents.replace("\"speedtest_ping_dns\": \"8.8.8.8\",", "")
33# replace 1.1.1.1 with 8.8.8.8
Shad Ansari2a376d72022-03-10 14:01:32 -080034config_file_contents = config_file_contents.replace('1.1.1.1', '8.8.8.8')
Hyunsun Moon53097ea2020-09-04 17:20:29 -070035CONF = json.loads(
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -070036 config_file_contents, object_hook=lambda d: namedtuple('X', d.keys())(*d.values())
Hyunsun Moon53097ea2020-09-04 17:20:29 -070037)
38
39logging.basicConfig(
40 filename=CONF.log_file,
41 format='%(asctime)s [%(levelname)s] %(message)s',
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -070042 level=logging.getLevelName(CONF.log_level)
Hyunsun Moon53097ea2020-09-04 17:20:29 -070043)
44
Hyunsun Moon53097ea2020-09-04 17:20:29 -070045
46class State(enum.Enum):
47 error = "-1"
48 disconnected = "0"
49 connected = "1"
50
51 @classmethod
52 def has_value(cls, value):
53 return value in cls._value2member_map_
54
55
56class Modem():
57 log = logging.getLogger('aether_edge_monitoring.Modem')
58
59 read_timeout = 0.1
60
61 def __init__(self, port, baudrate):
62 self.port = port
63 self.baudrate = baudrate
64 self._response = None
65
Don Newtonbd91ae22021-05-11 14:58:18 -070066 def get_modem_port(self):
67 cmd = "ls " + CONF.modem.port
68 sp = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,
69 stderr=subprocess.PIPE, universal_newlines=True)
Shad Ansari2a376d72022-03-10 14:01:32 -080070 sp.wait()
Don Newtonbd91ae22021-05-11 14:58:18 -070071 ret,err = sp.communicate()
72 if err != "" :
Shad Ansarib234ff82022-02-17 22:14:35 -080073 self.log.error("unable to find serial port " + err)
Don Newtonbd91ae22021-05-11 14:58:18 -070074
75 ret = ret.replace(CONF.modem.port,"").strip()
Shad Ansarib234ff82022-02-17 22:14:35 -080076 self.log.info("Modem.get_modem_port found " + ret)
Don Newtonbd91ae22021-05-11 14:58:18 -070077 return ret
78
Hyunsun Moon53097ea2020-09-04 17:20:29 -070079 def connect(self):
Don Newtonbd91ae22021-05-11 14:58:18 -070080 self.port=self.get_modem_port()
Shad Ansarib234ff82022-02-17 22:14:35 -080081 self.log.info("modem.connect Port: %s, BaudRate: %i",self.port,self.baudrate)
Hyunsun Moon53097ea2020-09-04 17:20:29 -070082 self.serial = serial.Serial(
83 port=self.port,
84 baudrate=self.baudrate,
85 timeout=1)
86
87 def _write(self, command):
88 if self.serial.inWaiting() > 0:
89 self.serial.flushInput()
90
91 self._response = b""
92
93 self.serial.write(bytearray(command + "\r", "ascii"))
94 read = self.serial.inWaiting()
95 while True:
Shad Ansarib234ff82022-02-17 22:14:35 -080096 self.log.debug("Waiting for write to complete...")
Hyunsun Moon53097ea2020-09-04 17:20:29 -070097 if read > 0:
98 self._response += self.serial.read(read)
99 else:
100 time.sleep(self.read_timeout)
101 read = self.serial.inWaiting()
102 if read == 0:
103 break
Shad Ansarib234ff82022-02-17 22:14:35 -0800104 self.log.debug("Write complete...")
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700105 return self._response.decode("ascii").replace('\r\n', ' ')
106
107 def write(self, command, wait_resp=True):
108 response = self._write(command)
109 self.log.debug("%s: %s", command, response)
110
111 if wait_resp and "ERROR" in response:
112 return False, None
113 return True, response
114
Shad Ansari2a376d72022-03-10 14:01:32 -0800115 def get_state(self, counters):
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700116 success, result = self.write('AT+CGATT?')
117 if not success or 'CGATT:' not in result:
Shad Ansari2a376d72022-03-10 14:01:32 -0800118 logging.error("AT+CGATT modem cmd failed")
119 counters['modem_cgatt_error'] += 1
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700120 return State.error
121 state = result.split('CGATT:')[1].split(' ')[0]
122 return State(state)
123
124 def close(self):
125 self.serial.close()
126
127
Shad Ansari2a376d72022-03-10 14:01:32 -0800128def get_control_plane_state(modem, counters, dongle_stats=None):
Shad Ansari341a1c92022-03-02 09:14:40 -0800129 if not modem and dongle_stats:
Shad Ansari2a376d72022-03-10 14:01:32 -0800130 if dongle_stats['Connection'] == 'Connected':
Shad Ansari341a1c92022-03-02 09:14:40 -0800131 return State.connected
132 else:
Shad Ansari2a376d72022-03-10 14:01:32 -0800133 logging.error("Dongle not connected: {}".format(dongle_stats['Connection']))
134 counters['dongle_connect_error'] += 1
Shad Ansari341a1c92022-03-02 09:14:40 -0800135 return State.disconnected
Shad Ansarib234ff82022-02-17 22:14:35 -0800136
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700137 # Disable radio fuction
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700138 # "echo" works more stable than serial for this action
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700139 try:
Don Newtonbd91ae22021-05-11 14:58:18 -0700140 logging.debug("echo 'AT+CFUN=0' > " + modem.port)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700141 subprocess.check_output(
Don Newtonbd91ae22021-05-11 14:58:18 -0700142 "echo 'AT+CFUN=0' > " + modem.port, shell=True)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700143 except subprocess.CalledProcessError as e:
Shad Ansari2a376d72022-03-10 14:01:32 -0800144 logging.error("Write 'AT+CFUN=0' failed: {}".format(e))
145 counters['modem_cfun0_error'] += 1
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700146 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700147
148 # Wait until the modem is fully disconnected
149 retry = 0
150 state = None
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700151 while retry < CONF.detach_timeout:
Shad Ansari2a376d72022-03-10 14:01:32 -0800152 state = modem.get_state(counters)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700153 if state is State.disconnected:
154 break
155 time.sleep(1)
156 retry += 1
157
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700158 if state is not State.disconnected:
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700159 logging.error("Failed to disconnect")
160 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700161
162 time.sleep(2)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700163 # Enable radio function
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700164 # "echo" works more stable than serial for this action
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700165 try:
Don Newtonbd91ae22021-05-11 14:58:18 -0700166 logging.debug("echo 'AT+CFUN=1' > " + modem.port)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700167 subprocess.check_output(
Don Newtonbd91ae22021-05-11 14:58:18 -0700168 "echo 'AT+CFUN=1' > " + modem.port, shell=True)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700169 except subprocess.CalledProcessError as e:
Shad Ansari2a376d72022-03-10 14:01:32 -0800170 logging.error("Write 'AT+CFUN=1' failed: {}".format(e))
171 counters['modem_cfun1_error'] += 1
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700172 return State.error
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700173
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700174 # Wait attach_timeout sec for the modem to be fully connected
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700175 retry = 0
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700176 while retry < CONF.attach_timeout:
Shad Ansari2a376d72022-03-10 14:01:32 -0800177 state = modem.get_state(counters)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700178 if state is State.connected:
179 break
180 time.sleep(1)
181 retry += 1
182 # CGATT sometimes returns None
183 if state is State.error:
184 state = State.disconnected
185
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700186 return state
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700187
188
Shad Ansari2a376d72022-03-10 14:01:32 -0800189def dry_run_ping_test(counters):
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800190 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 -0800191 return do_ping(CONF.ips.dry_run, 10)
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800192 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)
Shad Ansari2a376d72022-03-10 14:01:32 -0800197 return None, True
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800198 except subprocess.CalledProcessError as e:
Shad Ansari2a376d72022-03-10 14:01:32 -0800199 logging.warning("Ping failed for {}: {}".format(CONF.ips.dns, e))
200 return None, False
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700201
202
Shad Ansari2a376d72022-03-10 14:01:32 -0800203def do_ping(ip, count):
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800204 '''
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:
Shad Ansari2a376d72022-03-10 14:01:32 -0800225 logging.debug("Pinging {}".format(ip))
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700226 pingOutput = subprocess.check_output(
227 "ping -c " + str(count) + " " + \
228 ip, shell=True).decode("UTF-8").split()
229 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)
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700250 return result, False
251 return result, True
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800252
253
Shad Ansari2a376d72022-03-10 14:01:32 -0800254def ping_test(modem, dry_run_latency=None):
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800255 '''
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700256 Prepares the ping test.
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700257 Runs ping tests from 'ips' entry in config.json in order.
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800258 Note: 'dry_run' is not run here; it is run during the user plane test.
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800259 '''
260 speedtest_ping = {}
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700261 status = True
262 ping_test_passed = True
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700263
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800264 if dry_run_latency:
265 speedtest_ping["dry_run"] = dry_run_latency
266
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700267 for i in range(0, len(CONF.ips)):
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800268 if CONF.ips._fields[i] == "dry_run":
269 continue
Shad Ansarib3d54752022-02-28 12:32:58 -0800270 count = 10
Shad Ansari2a376d72022-03-10 14:01:32 -0800271 speedtest_ping[CONF.ips._fields[i]], status = do_ping(CONF.ips[i], count)
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700272 if not status:
273 ping_test_passed = False
Shad Ansari2a376d72022-03-10 14:01:32 -0800274 logging.error("Ping failed: {}".format(CONF.ips[i]))
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700275 return speedtest_ping, ping_test_passed
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800276
Shad Ansari2a376d72022-03-10 14:01:32 -0800277def run_iperf_test(ip, port, time_duration, is_downlink, counters):
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700278 '''
279 Runs iperf test to specified IP in the config file.
280 - Runs for 10 seconds (10 iterations)
281 - Retrieves downlink and uplink test results from json output
282 '''
283 result = 0.0
Jeremy Ronquilloc03ba682021-10-06 10:27:09 -0700284 if not ip or port == 0:
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700285 return result
Jeremy Ronquillo115c5e32021-09-30 11:15:56 -0700286 maxRetries = 2
Jeremy Ronquillo79c3e672021-09-03 12:54:55 -0700287 err = None
Jeremy Ronquillo012ac662021-08-06 12:13:43 -0700288 for _ in range(0, maxRetries):
289 try:
290 iperfResult = json.loads(subprocess.check_output(
291 "iperf3 -c " + ip +
292 " -p " + str(port) +
293 " -t " + str(time_duration) +
294 (" -R " if is_downlink else "") +
295 " --json", shell=True).decode("UTF-8"))
296 received_mbps = iperfResult['end']['sum_received']['bits_per_second'] / 1000000
297 sent_mbps = iperfResult['end']['sum_sent']['bits_per_second'] / 1000000.0
298 result = received_mbps if is_downlink else sent_mbps
299 return result
300 except Exception as e:
Jeremy Ronquillo79c3e672021-09-03 12:54:55 -0700301 err = e
Shad Ansari2a376d72022-03-10 14:01:32 -0800302 counters['iperf_error'] += 1
Jeremy Ronquillo012ac662021-08-06 12:13:43 -0700303 time.sleep(5)
304 pass
Jeremy Ronquillo79c3e672021-09-03 12:54:55 -0700305 logging.error("After " + str(maxRetries) + " retries, iperf test failed for " + ip + ": %s", err)
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700306 return result
307
308
Shad Ansari2a376d72022-03-10 14:01:32 -0800309def iperf_test(counters):
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700310 '''
311 Prepares the iperf test.
312 '''
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800313 global hour_iperf_scheduled_time_last_ran
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700314 speedtest_iperf = {}
315 speedtest_iperf['cluster'] = {}
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800316
317 if "iperf_schedule" in CONF._fields and len(CONF.iperf_schedule) > 0:
318 if int(time.strftime("%H")) not in CONF.iperf_schedule: # not in the schedule
319 hour_iperf_scheduled_time_last_ran = -1
320 return None
321 elif int(time.strftime("%H")) == hour_iperf_scheduled_time_last_ran: # already ran this hour
322 return None
323 hour_iperf_scheduled_time_last_ran = int(time.strftime("%H"))
324
Shad Ansari2a376d72022-03-10 14:01:32 -0800325 speedtest_iperf['cluster']['downlink'] = run_iperf_test(CONF.ips.iperf_server, CONF.iperf_port, 10, True, counters)
326 speedtest_iperf['cluster']['uplink'] = run_iperf_test(CONF.ips.iperf_server, CONF.iperf_port, 10, False, counters)
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700327
328 return speedtest_iperf
329
Jeremy Ronquillod996b512021-02-13 13:45:47 -0800330
Shad Ansari2a376d72022-03-10 14:01:32 -0800331def get_signal_quality(modem, counters, dongle_stats=None):
Shad Ansari341a1c92022-03-02 09:14:40 -0800332 if not modem and dongle_stats:
333 if dongle_stats['RSRQ'] != '' and dongle_stats['RSRP'] != '':
334 rsrq = int((float(dongle_stats['RSRQ']) + 19.5) * 2)
335 rsrp = int(float(dongle_stats['RSRP']) + 140)
336 return {'rsrq': rsrq, 'rsrp': rsrp}
337 else:
Shad Ansari2a376d72022-03-10 14:01:32 -0800338 counters['dongle_rsrp_rsrq_error'] += 1
Shad Ansari341a1c92022-03-02 09:14:40 -0800339 return {'rsrq': 0, 'rsrp': 0}
340
341 # Fall back to modem cmds
Shad Ansarib234ff82022-02-17 22:14:35 -0800342
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700343 success, result = modem.write('AT+CESQ')
Don Newtonbd91ae22021-05-11 14:58:18 -0700344 logging.debug("get_signal_quality success %i result %s",success,result)
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700345 if not success or 'CESQ: ' not in result:
346 logging.error("Failed to get signal quality")
Shad Ansari2a376d72022-03-10 14:01:32 -0800347 counters['modem_cesq_error'] += 1
Don Newtonbd91ae22021-05-11 14:58:18 -0700348 return {'rsrq':0, 'rsrp':0}
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700349
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700350 tmp_rsrq = result.split('CESQ:')[1].split(',')[4]
351 tmp_rsrp = result.split('CESQ:')[1].split(',')[5]
352
353 rsrq = int(tmp_rsrq.strip())
354 rsrp = int(tmp_rsrp.strip().split(' ')[0])
355 result = {
356 'rsrq': 0 if rsrq is 255 else rsrq,
357 'rsrp': 0 if rsrp is 255 else rsrp
358 }
359
360 return result
361
Shad Ansarib234ff82022-02-17 22:14:35 -0800362
Shad Ansari2a376d72022-03-10 14:01:32 -0800363def get_dongle_stats(counters):
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800364 result = {'SuccessfulFetch' : False}
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800365 if "report_in_band" in CONF._fields:
366 result['inBandReporting'] = CONF.report_in_band
367 else:
368 result['inBandReporting'] = False
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800369 XMLkeys = ["MAC",
370 "PLMNStatus",
371 "UICCStatus",
372 "IMEI",
373 "IMSI",
374 "PLMNSelected",
375 "MCC",
376 "MNC",
377 "PhyCellID",
378 "CellGlobalID",
379 "Band",
380 "EARFCN",
381 "BandWidth",
Shad Ansarib234ff82022-02-17 22:14:35 -0800382 "RSRP",
383 "RSRQ",
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800384 "ServCellState",
385 "Connection",
386 "IPv4Addr"]
387 dongleStatsXML = None
388 try:
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800389 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 -0800390 except Exception as e:
391 logging.error("Failed to fetch dongle stats from URL: " + str(e))
Shad Ansari2a376d72022-03-10 14:01:32 -0800392 counters['dongle_read_error'] += 1
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800393 return result
394 try:
395 for key in XMLkeys:
396 try:
397 result[key] = dongleStatsXML.find(key).text
Shad Ansari2a376d72022-03-10 14:01:32 -0800398 except AttributeError:
399 logging.error("Failed to find " + key + " in XML")
400 counters['dongle_read_error'] += 1
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800401 result[key] = ""
402 result["SuccessfulFetch"] = True
403 except Exception as e:
404 logging.error("Failed to fetch dongle stats from XML: " + str(e))
Shad Ansari2a376d72022-03-10 14:01:32 -0800405 counters['dongle_read_error'] += 1
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800406 return result
407 return result
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700408
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800409
Shad Ansari2a376d72022-03-10 14:01:32 -0800410def 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 -0700411 report = {
412 'name': CONF.edge_name,
413 'status': {
414 'control_plane': "disconnected",
415 'user_plane': "disconnected"
416 },
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800417 'dongle_stats': {
418 'SuccessfulFetch' : False
419 },
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700420 'speedtest': {
421 'ping': {
422 'dns': {
Jeremy Ronquilloe0a8b422021-11-02 12:49:15 -0700423 'transmitted' : 0,
424 'received' : 0,
425 'median' : 0.0,
Jeremy Ronquillo6e352b72021-06-08 10:33:25 -0700426 'min': 0.0,
427 'avg': 0.0,
428 'max': 0.0,
429 'stddev': 0.0
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700430 }
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700431 },
432 'iperf': {
433 'cluster': {
434 'downlink': 0.0,
435 'uplink': 0.0
436 }
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700437 }
438 },
439 'signal_quality': {
440 'rsrq': 0,
441 'rsrp': 0
442 }
443 }
444
445 if cp_state is not None:
446 report['status']['control_plane'] = cp_state.name
447 if up_state is not None:
448 report['status']['user_plane'] = up_state.name
449 if speedtest_ping is not None:
450 report['speedtest']['ping'] = speedtest_ping
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700451 if speedtest_iperf is not None:
452 report['speedtest']['iperf'] = speedtest_iperf
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700453 report['signal_quality'] = signal_quality
Jeremy Ronquilloc45955a2021-11-09 12:04:57 -0800454 report['dongle_stats'] = dongle_stats
Shad Ansari2a376d72022-03-10 14:01:32 -0800455 report['counters'] = counters
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700456
457 logging.info("Sending report %s", report)
Don Newtonbd91ae22021-05-11 14:58:18 -0700458 global cycles
459 cycles += 1
460 logging.info("Number of cycles since modem restart %i",cycles)
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800461
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700462 try:
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800463 interface = None
464 report_via_modem = "report_in_band" in CONF._fields and CONF.report_in_band and \
465 "iface" in CONF.modem._fields and CONF.modem.iface
466 report_via_given_iface = "report_iface" in CONF._fields and CONF.report_iface
467
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800468 c = pycurl.Curl()
469 c.setopt(pycurl.URL, CONF.report_url)
470 c.setopt(pycurl.POST, True)
471 c.setopt(pycurl.HTTPHEADER, ['Content-Type: application/json'])
472 c.setopt(pycurl.TIMEOUT, 10)
473 c.setopt(pycurl.POSTFIELDS, json.dumps(report))
Jeremy Ronquillo56d23b12021-12-02 14:57:42 -0800474 c.setopt(pycurl.WRITEFUNCTION, lambda x: None) # don't output to console
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800475
476 if report_via_modem: # report in-band
477 interface = CONF.modem.iface
478 c.setopt(pycurl.INTERFACE, interface)
479 elif report_via_given_iface: # report over given interface
480 interface = CONF.report_iface
481 c.setopt(pycurl.INTERFACE, interface)
482 # else, reports over default interface
483
484 try:
485 c.perform()
486 logging.info("Report sent via " + interface + "!")
487 except Exception as e:
488 if report_via_modem and report_via_given_iface:
489 logging.warning("Sending report via modem failed. Attempting to send report via " + str(CONF.report_iface) + ".")
490 interface = CONF.report_iface
491 c.setopt(pycurl.INTERFACE, interface)
492 c.perform()
493 logging.info("Report sent via " + interface + "!")
494 else:
495 logging.error("Failed to send report: " + str(e))
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800496 c.close()
497 except Exception as e:
498 logging.error("Failed to send report: " + str(e))
Jeremy Ronquilloeff2e6d2021-12-06 10:39:37 -0800499 c.close()
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800500
Don Newtonbd91ae22021-05-11 14:58:18 -0700501def reset_usb():
Jeremy Ronquillo82a14612021-10-08 12:08:20 -0700502 try:
503 # Attempt to run uhubctl
504 if (int(subprocess.call("which uhubctl",shell=True)) == 0):
505 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
506 ret = subprocess.call(cmd,shell=True)
507 logging.info("Shutting down usb hub 2 results %s" , ret)
508 time.sleep(10)# let power down process settle out
509 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
510 ret = subprocess.call(cmd,shell=True)
511 logging.info("Starting up usb hub 2 results %s" , ret)
512 time.sleep(10) #allow dbus to finish
513 global cycles
514 cycles = 0
515 else:
516 reboot(120)
517 except Exception as e:
518 logging.error("Failed to run uhubctl: %s", e)
519 reboot(120)
520
521def reboot(delay):
522 logging.error("Failed to run uhubctl. Reboot system in " + str(delay) + " second(s).")
523 time.sleep(delay)
524 subprocess.check_output("sudo shutdown -r now", shell=True)
Don Newtonbd91ae22021-05-11 14:58:18 -0700525
Shad Ansari2a376d72022-03-10 14:01:32 -0800526def init_counters():
527 return {
528 'dongle_read_error': 0,
529 'dongle_connect_error': 0,
530 'dongle_rsrp_rsrq_error': 0,
531 'modem_cfun0_error': 0,
532 'modem_cfun1_error': 0,
533 'modem_cgatt_error': 0,
534 'modem_cesq_error': 0,
535 'dry_run_ping_error': 0,
536 'ping_error': 0,
537 'iperf_error': 0
538 }
539
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700540def main():
Don Newtonbd91ae22021-05-11 14:58:18 -0700541 global cycles
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800542 global hour_iperf_scheduled_time_last_ran
Don Newtonbd91ae22021-05-11 14:58:18 -0700543 cycles = 0
Jeremy Ronquilloef17e362021-11-08 10:56:42 -0800544 hour_iperf_scheduled_time_last_ran = -1
Jeremy Ronquillo8d108652021-11-22 17:34:58 -0800545
546 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
Shad Ansari2a376d72022-03-10 14:01:32 -0800578 counters = init_counters()
Shad Ansari341a1c92022-03-02 09:14:40 -0800579 while True:
580 dongle_retries = 0
Shad Ansari2a376d72022-03-10 14:01:32 -0800581 dongle_stats = get_dongle_stats(counters)
Shad Ansari341a1c92022-03-02 09:14:40 -0800582 while not dongle_stats['SuccessfulFetch']:
Shad Ansari341a1c92022-03-02 09:14:40 -0800583 dongle_retries += 1
584 if dongle_retries > 10:
Shad Ansari2a376d72022-03-10 14:01:32 -0800585 logging.warning("Rebooting Pi: dongle not readable")
Shad Ansari341a1c92022-03-02 09:14:40 -0800586 os.system("shutdown /r /t 0")
587 sys.exit(1)
Shad Ansari2a376d72022-03-10 14:01:32 -0800588 dongle_stats = get_dongle_stats(counters)
Shad Ansari341a1c92022-03-02 09:14:40 -0800589
Shad Ansari2a376d72022-03-10 14:01:32 -0800590 cp_state = get_control_plane_state(modem, counters, dongle_stats)
Shad Ansari341a1c92022-03-02 09:14:40 -0800591
592 if cp_state != State.connected:
593 logging.error("Dongle not connected")
594 connect_retries += 1
595 if connect_retries > 10:
Shad Ansari2a376d72022-03-10 14:01:32 -0800596 logging.warning("Rebooting Pi: dongle not connected")
Shad Ansari341a1c92022-03-02 09:14:40 -0800597 os.system("shutdown /r /t 0")
598 sys.exit(1)
599
Shad Ansari2a376d72022-03-10 14:01:32 -0800600 signal_quality = get_signal_quality(modem, counters, dongle_stats)
601
602 dry_run_ping_latency, dry_run_ping_result = dry_run_ping_test(counters)
603 if not dry_run_ping_result:
Shad Ansari341a1c92022-03-02 09:14:40 -0800604 logging.error("Dry run ping failed")
Shad Ansari2a376d72022-03-10 14:01:32 -0800605 counters['dry_run_ping_error'] += 1
Hyunsun Moon5cd1eec2021-04-02 22:33:42 -0700606
Shad Ansari2a376d72022-03-10 14:01:32 -0800607 ping_latency, ping_result = ping_test(modem, dry_run_ping_latency)
608 if not ping_result:
609 logging.error("Ping test failed")
610 counters['ping_error'] += 1
Shad Ansari341a1c92022-03-02 09:14:40 -0800611
Shad Ansari2a376d72022-03-10 14:01:32 -0800612 # If either of the ping tests pass, then declare user plane connected
613 if dry_run_ping_result or ping_result:
614 up_state = State.connected
Jeremy Ronquillo677c8832021-04-06 13:53:36 -0700615
Shad Ansari2a376d72022-03-10 14:01:32 -0800616 speedtest_iperf = iperf_test(counters)
617
618 report_status(counters, signal_quality, dongle_stats, cp_state, up_state, ping_latency, speedtest_iperf)
Shad Ansari341a1c92022-03-02 09:14:40 -0800619 time.sleep(CONF.report_interval)
Hyunsun Moon53097ea2020-09-04 17:20:29 -0700620
621 modem.close()
622
623
624if __name__ == "__main__":
625 main()