class SIMSpeedTest(object): MIN_DOWNLOAD_SPD = 0.0 # Mbps MIN_UPLOAD_SPD = 0.0 # Mbps SCHEDULE = 0 # Run Boot2 every {SCHEDULE} minutes. 0 = Only run on boot. NUM_ACTIVE_SIMS = 0 # Number of fastest (download) SIMs to keep active. 0 = all; do not disable SIMs ONLY_RUN_ONCE = False # True means do not run if Boot2 has been run on this device before. STATUS_DEVS_PATH = '/status/wan/devices' CFG_RULES2_PATH = '/config/wan/rules2' CTRL_WAN_DEVS_PATH = '/control/wan/devices' API_URL = 'https://www.cradlepointecm.com/api/v2' CONNECTION_STATE_TIMEOUT = 7 * 60 # 7 Min NETPERF_TIMEOUT = 5 * 60 # 5 Min sims = {} def __init__(self): self.client = EventingCSClient('Boot2') def check_if_run_before(self): if self.ONLY_RUN_ONCE: if self.client.get('/config/wan/rules2/0/_id_' ) == '00000000-1234-1234-1234-1234567890ab': self.client.log( 'ERROR - Boot2 has been run before! /config/wan/rules2/0/_id = 00000000-1234-1234-1234-1234567890ab' ) raise RunBefore( 'ERROR - Boot2 has been run before! /config/wan/rules2/0/_id = 00000000-1234-1234-1234-1234567890ab' ) return False def wait_for_ncm_sync(self): # WAN connection_state if self.client.get('status/wan/connection_state') != 'connected': self.client.log('Waiting until WAN is connected...') timeout_count = 500 while self.client.get('/status/wan/connection_state') != 'connected': timeout_count -= 1 if not timeout_count: raise Timeout('WAN not connecting') time.sleep(2) # ECM State if self.client.get('status/ecm/state') != 'connected': self.client.log('Waiting until NCM is connected...') self.client.put('/control/ecm', {'start': True}) timeout_count = 500 while self.client.get('/status/ecm/state') != 'connected': timeout_count -= 1 if not timeout_count: raise Timeout('NCM not connecting') time.sleep(2) # ECM Sync if self.client.get('status/ecm/sync') != 'ready': self.client.log('Waiting until NCM is synced...') self.client.put('/control/ecm', {'start': True}) timeout_count = 500 while self.client.get('/status/ecm/sync') != 'ready': self.client.put('/control/ecm', {'start': True}) timeout_count -= 1 if not timeout_count: raise Timeout('NCM not connecting') time.sleep(2) return def NCM_suspend(self): self.client.log('Stopping NCM') timeout_count = 500 while not 'ready' == self.client.get('/status/ecm/sync'): timeout_count -= 1 if not timeout_count: raise Timeout('NCM sync not ready') time.sleep(2) self.client.put('/control/ecm', {'stop': True}) timeout_count = 500 while not 'stopped' == self.client.get('/status/ecm/state'): timeout_count -= 1 if not timeout_count: raise Timeout('NCM not stopping') time.sleep(2) def find_sims(self): while True: sims = {} wan_devs = self.client.get(self.STATUS_DEVS_PATH) or {} for uid, status in wan_devs.items(): if uid.startswith('mdm-'): error_text = status.get('status', {}).get('error_text', '') if error_text: if 'NOSIM' in error_text: continue sims[uid] = status num_sims = len(sims) if not num_sims: self.client.log('No SIMs found at all yet') time.sleep(10) continue if num_sims < 2: self.client.log('Only 1 SIM found!') raise OneModem('Only 1 SIM found!') else: break self.client.log(f'Found SIMs: {sims.keys()}') self.sims = sims return True def modem_state(self, sim, state): # Blocking call that will wait until a given state is shown as the modem's status timeout_counter = 0 sleep_seconds = 0 conn_path = '%s/%s/status/connection_state' % (self.STATUS_DEVS_PATH, sim) self.client.log(f'Connecting {self.port_sim(sim)}') while True: sleep_seconds += 5 conn_state = self.client.get(conn_path) self.client.log( f'Waiting for {self.port_sim(sim)} to connect. Current State={conn_state}' ) if conn_state == state: break if timeout_counter > self.CONNECTION_STATE_TIMEOUT: self.client.log(f'Timeout waiting on {self.port_sim(sim)}') raise Timeout(conn_path) time.sleep(min(sleep_seconds, 45)) timeout_counter += sleep_seconds self.client.log(f'{self.port_sim(sim)} connected.') return True def iface(self, sim): iface = self.client.get('%s/%s/info/iface' % (self.STATUS_DEVS_PATH, sim)) return iface def port_sim(self, sim): return f'{self.sims[sim]["info"]["port"]} {self.sims[sim]["info"]["sim"]}' def run_speedtest(self, speedtest): self.client.put('/state/system/netperf', {"run_count": 0}) res = self.client.put("/control/netperf", speedtest) self.client.log(f'Starting Speedtest... {res}') timeout_counter = 0 # wait for results delay = speedtest['input']['options']['limit']['time'] + 8 status_path = "/control/netperf/output/status" while True: time.sleep(delay) status = self.client.get(status_path) if status == 'complete': break if timeout_counter > self.NETPERF_TIMEOUT: self.client.log( f"Timeout waiting on speedtest for {speedtest['input']['options']['ifc_wan']}" ) raise Timeout(status_path) timeout_counter += delay if status != 'complete': self.client.log(f"ERROR: status=%s expected 'complete' {status}") return None # now get the result results_path = self.client.get("/control/netperf/output/results_path") results = None while not results: results = self.client.get(results_path) time.sleep(2) self.client.log('Speedtest Complete.') return results def do_speedtest(self, sim): default_speedtest['input']['options']['ifc_wan'] = self.iface(sim) default_speedtest['input']['options']['send'] = False default_speedtest['input']['options']['recv'] = True tcp_down = self.run_speedtest(default_speedtest).get('tcp_down') default_speedtest['input']['options']['send'] = True default_speedtest['input']['options']['recv'] = False tcp_up = self.run_speedtest(default_speedtest).get('tcp_up') if not tcp_up: self.client.log('do_speedtest tcp_up results missing!') default_speedtest['input']['options']['send'] = True default_speedtest['input']['options']['recv'] = False results = self.run_speedtest(default_speedtest) tcp_up = results.get('tcp_up') or None if not tcp_down: self.client.log('do_speedtest tcp_down results missing!') default_speedtest['input']['options']['send'] = False default_speedtest['input']['options']['recv'] = True results = self.run_speedtest(default_speedtest) tcp_down = results.get('tcp_down') or None down = float(tcp_down.get('THROUGHPUT', 0.0)) if tcp_down else 0.0 up = float(tcp_up.get('THROUGHPUT', 0.0)) if tcp_up else 0.0 return down, up def test_sim(self, device): try: if self.modem_state(device, 'connected'): # Get diagnostics and log it diagnostics = self.client.get( f'{self.STATUS_DEVS_PATH}/{device}/diagnostics') self.sims[device]['diagnostics'] = diagnostics self.client.log( f'Modem Diagnostics: {self.port_sim(device)} RSRP:{diagnostics.get("RSRP")}' ) # Do speedtest and log results self.sims[device]['download'], self.sims[device][ 'upload'] = self.do_speedtest(self.sims[device]) self.client.log( f'Speedtest Results: {self.port_sim(device)} TCP Download: ' f'{self.sims[device]["download"]}Mbps TCP Upload: {self.sims[device]["upload"]}Mbps' ) # Verify minimum speeds if self.sims[device].get( 'download', 0.0) > self.MIN_DOWNLOAD_SPD and self.sims[device].get( 'upload', 0.0) > self.MIN_UPLOAD_SPD: return True else: # Did not meet minimums self.client.log( f'{self.port_sim(device)} Failed to meet minimums! MIN_DOWNLOAD_SPD: {self.MIN_DOWNLOAD_SPD} MIN_UPLOAD_SPD: {self.MIN_UPLOAD_SPD}' ) return False except Timeout: message = f'Timed out running speedtest on {self.port_sim(device)}' self.client.log(message) self.client.alert(message) self.sims[device]['download'] = self.sims[device]['upload'] = 0.0 return False def create_message(self, uid, *args): message = '' for arg in args: if arg == 'download': message = "DL:{:.2f}Mbps".format( self.sims[uid]['download']) if not message else ' '.join([ message, "DL:{:.2f}Mbps".format( self.sims[uid]['download']) ]) elif arg == 'upload': message = "UL:{:.2f}Mbps".format( self.sims[uid]['upload']) if not message else ' '.join([ message, "UL:{:.2f}Mbps".format( self.sims[uid]['upload']) ]) elif arg in ['PRD', 'HOMECARRID', 'RFBAND']: # Do not include labels for these fields message = "{}".format( self.sims[uid]['diagnostics'][arg] ) if not message else ' '.join( [message, "{}".format(self.sims[uid]['diagnostics'][arg])]) else: # Include field labels (e.g. "RSRP:-82") message = "{}:{}".format( arg, self.sims[uid]['diagnostics'] [arg]) if not message else ' '.join([ message, "{}:{}".format( arg, self.sims[uid]['diagnostics'][arg]) ]) return message def lock_sim(self, sim): rules = [{ "_id_": "00000000-1234-1234-1234-123456789000", "priority": 0, "trigger_name": f"{self.sims[sim]['info']['port']} {self.sims[sim]['info']['sim']}", "trigger_string": f"type|is|mdm%sim|is|{self.sims[sim]['info']['sim']}%port|is|{self.sims[sim]['info']['port']}" }] for i, uid in enumerate(self.sims): if uid != sim: rule = { "_id_": f"0000000{i+1}-1234-1234-1234-123456789000", "priority": -9 + i, "trigger_name": f"{self.sims[sim]['info']['port']} {self.sims[sim]['info']['sim']}", "trigger_string": f"type|is|mdm%sim|is|{self.sims[sim]['info']['sim']}%port|is|{self.sims[sim]['info']['port']}", "disabled": True } rules.append(rule) self.client.put('config/wan/rules2', rules) time.sleep(2) def create_rules(self, sim_list): wan_rules = [{ "_id_": "00000000-1234-1234-1234-1234567890ab", "priority": -10, "trigger_name": "Ethernet", "trigger_string": "type|is|ethernet" }] for i in range(0, len(sim_list)): rule = { "_id_": f"0000000{i+1}-1234-1234-1234-123456789000", "priority": -9 + i, "trigger_name": f"{self.sims[sim_list[i]]['info']['port']} {self.sims[sim_list[i]]['info']['sim']}", "trigger_string": f"type|is|mdm%sim|is|{self.sims[sim_list[i]]['info']['sim']}%port|is|{self.sims[sim_list[i]]['info']['port']}" } if self.NUM_ACTIVE_SIMS and i >= self.NUM_ACTIVE_SIMS: rule['disabled'] = True wan_rules.append(rule) req = self.client.put('config/wan/rules2/', wan_rules) time.sleep(2) if self.client.get('config/wan/rules2/0/_id_' ) == '00000000-1234-1234-1234-1234567890ab': self.client.log(f'Updated WAN rules') else: self.client.log(f'WAN Rules not updated! : {req}') return def run(self): # *** Main Application Starts Here *** self.client.log( f'Boot2 Starting... MIN_DOWNLOAD_SPD:{self.MIN_DOWNLOAD_SPD} MIN_UPLOAD_SPD:{self.MIN_UPLOAD_SPD} ' f'SCHEDULE:{self.SCHEDULE} NUM_ACTIVE_SIMS:{self.NUM_ACTIVE_SIMS} ONLY_RUN_ONCE:{self.ONLY_RUN_ONCE}' ) self.check_if_run_before() self.wait_for_ncm_sync() # Get info from router product_name = self.client.get("/status/product_info/product_name") system_id = self.client.get("/config/system/system_id") router_id = self.client.get('status/ecm/client_id') self.find_sims() # Find active SIM slots # Send startup alert message = f'Boot2 Starting! {system_id} - {product_name} - Router ID: {router_id}' self.client.log(f'Sending alert to NCM: {message}') self.client.alert(message) # Pause for 3 seconds to allow NCM Alert to be sent before suspending NCM time.sleep(3) self.NCM_suspend() success = False # Boot2 Success Status - Becomes True when a SIM meets minimum speeds # Test the connected SIM first primary_device = self.client.get('status/wan/primary_device') if 'mdm-' in primary_device: # make sure its a modem if self.test_sim(primary_device): success = True # test remaining SIMs for sim in self.sims: if not self.sims[sim].get('download'): self.lock_sim(sim) if self.test_sim(sim): success = True # Prioritizes SIMs based on download speed sorted_results = sorted(self.sims, key=lambda x: self.sims[x]['download'], reverse=True) # Create WAN rules self.create_rules(sorted_results) time.sleep(3) # Build text for custom1 field results_text = datetime.datetime.now().strftime( '%m/%d/%y %H:%M:%S') # Start with a timestamp if not success: results_text += f' FAILED TO MEET MINIMUMS! MIN_DOWNLOAD_SPD:{self.MIN_DOWNLOAD_SPD} MIN_UPLOAD_SPD:{self.MIN_UPLOAD_SPD}' for uid in sorted_results: # Add the results of each SIM with the fields specified: results_text = ' | '.join([ results_text, self.create_message(uid, 'PRD', 'HOMECARRID', 'RFBAND', 'RSRP', 'download', 'upload') ]) # put messages to NCM custom fields self.wait_for_ncm_sync() if apikeys.get('X-ECM-API-ID') != 'YOUR': self.client.log( f'X-ECM-API-ID: {apikeys["X-ECM-API-ID"]} X-CP-API-ID: {apikeys["X-CP-API-ID"]}' ) req = requests.put(f'{self.API_URL}/routers/{router_id}/', headers=apikeys, json={'custom1': results_text[:255]}) self.client.log(f'NCM PUT Custom1 Result: {req.status_code}') else: self.client.log( 'No NCM API Keys configured, skipping PUT to custom1') # Complete! Send results. message = f"Boot2 Complete! {system_id} Results: {results_text}" self.client.log(message) self.client.alert(message)
class DataUsageCheck(object): """ Establish global variables. Set rate shaping values (in Kbps) for 70, 80, 90 & 100% rate tiers. e.g. minbwup_70 & minbwdown_70 refers to upload & download at 70% Each of the rate tiers have a default throttling limit set below: 70% - 6000Kbps Tx/Rx 80% - 3000Kbps Tx/Rx 90% - 1500Kbps Tx/Rx 100% - 600Kbps Tx/Rx """ minbwup_70 = 6000 minbwdown_70 = 6000 minbwup_80 = 3000 minbwdown_80 = 3000 minbwup_90 = 1500 minbwdown_90 = 1500 minbwup_100 = 600 minbwdown_100 = 600 STATUS_DEVS_PATH = '/status/wan/devices' STATUS_DATACAP_PATH = '/status/wan/datacap' CFG_RULES2_PATH = '/config/wan/rules2' def __init__(self): self.cp = EventingCSClient(app_name) def find_modems(self): while True: devs = self.cp.get(self.STATUS_DEVS_PATH) modems_list = [x for x in devs if x.startswith('mdm-')] self.cp.log(f'modems_list: {modems_list}') num_modems = len(modems_list) if not num_modems: self.cp.log('No Modems found at all yet') time.sleep(10) continue else: return modems_list def find_modem_profiles(self): wan_ifcs = self.cp.get(self.CFG_RULES2_PATH) modem_profiles_list = [ x['_id_'] for x in wan_ifcs if x['trigger_string'].startswith('type|is|mdm') ] self.cp.log(f'modem_profiles_list: {modem_profiles_list}') return modem_profiles_list def reset_throttle(self, modem_profiles_list, monthlyreset): for mdm in modem_profiles_list: if monthlyreset: self.cp.delete(self.CFG_RULES2_PATH + '/' + mdm + '/bandwidth_egress') self.cp.delete(self.CFG_RULES2_PATH + '/' + mdm + '/bandwidth_ingress') else: if 'bandwidth_egress' in self.cp.get(self.CFG_RULES2_PATH + '/' + mdm): self.cp.delete(self.CFG_RULES2_PATH + '/' + mdm + '/bandwidth_egress') if 'bandwidth_ingress' in self.cp.get(self.CFG_RULES2_PATH + '/' + mdm): self.cp.delete(self.CFG_RULES2_PATH + '/' + mdm + '/bandwidth_ingress') self.cp.put('config/qos/enabled', False) if monthlyreset: self.cp.log( 'Monthly data usage reset - disabling reduced LTE data rate') message = ( f'Monthly data usage reset - disabling reduced LTE data rate ' f'for {self.system_id} - {self.product_name} - Router ID: ' f'{self.router_id}') self.cp.alert(message) def set_throttle(self, modem_profiles_list, minbwup, minbwdown, tierset): for mdm in modem_profiles_list: self.cp.put(self.CFG_RULES2_PATH + '/' + mdm + '/bandwidth_egress', minbwup) self.cp.put( self.CFG_RULES2_PATH + '/' + mdm + '/bandwidth_ingress', minbwdown) self.cp.put('config/qos/enabled', True) self.cp.log('Exceeded monthly data usage threshold - ' + str(tierset) + '% tier - reducing LTE data rate') message = ( f'Exceeded monthly data usage threshold - reducing LTE data rate ' f'for {self.system_id} - {self.product_name} - Router ID: ' f'{self.router_id}') self.cp.alert(message) def run(self): # Get info from router to populate description field in NCM # alert message self.product_name = self.cp.get('/status/product_info/product_name') self.system_id = self.cp.get('/config/system/system_id') self.router_id = self.cp.get('status/ecm/client_id') # Retrieve list of modems and their profiles modems_list = [str(x.split('-')[1]) for x in self.find_modems()] modem_profiles_list = self.find_modem_profiles() # Reset any throttling to account for router reboots. If a # data cap alert is still active during the monthly cycle, the # appropriate rate shaping will be re-applied monthlyreset = False self.reset_throttle(modem_profiles_list, monthlyreset) time.sleep(5) currtierset = 0 while True: if self.cp.get(self.STATUS_DATACAP_PATH + '/completed_alerts/'): alerts = self.cp.get(self.STATUS_DATACAP_PATH + '/completed_alerts/') limitreached = 0 tierset = 0 for indalert in alerts: for modem in modems_list: if (indalert['alerts'] and indalert['rule_id'] == modem + '-monthly'): if 'email_alert' in indalert['alerts']: limitreached += 1 tierset = 100 minbwup = self.minbwup_100 minbwdown = self.minbwdown_100 continue elif 'early_email-90.0' in indalert['alerts']: limitreached += 1 tierset = 90 minbwup = self.minbwup_90 minbwdown = self.minbwdown_90 continue elif 'early_email-80.0' in indalert['alerts']: limitreached += 1 tierset = 80 minbwup = self.minbwup_80 minbwdown = self.minbwdown_80 continue elif 'early_email-70.0' in indalert['alerts']: limitreached += 1 tierset = 70 minbwup = self.minbwup_70 minbwdown = self.minbwdown_70 continue if limitreached > 0 and currtierset != tierset: currtierset = tierset self.set_throttle(modem_profiles_list, minbwup, minbwdown, currtierset) elif limitreached == 0 and currtierset > 0: currtierset = 0 monthlyreset = True self.reset_throttle(modem_profiles_list, monthlyreset) elif currtierset > 0: currtierset = 0 monthlyreset = True self.reset_throttle(modem_profiles_list, monthlyreset) time.sleep(10)
class DataUsageCheck(object): """ Establish global variables. Set rate shaping values (in Kbps) """ # Modem Defaults (as of 7.0.40) - Not used when QoS is Disabled maxbwup = 25000 maxbwdown = 25000 minbwup = 512 minbwdown = 512 capreached = 0 STATUS_DEVS_PATH = '/status/wan/devices' STATUS_DATACAP_PATH = '/status/wan/datacap' CFG_RULES2_PATH = '/config/wan/rules2' def __init__(self): self.cp = EventingCSClient(app_name) def find_modems(self): while True: devs = self.cp.get(self.STATUS_DEVS_PATH) modems_list = [x for x in devs if x.startswith('mdm-')] self.cp.log(f'modems_list: {modems_list}') num_modems = len(modems_list) if not num_modems: self.cp.log('No Modems found at all yet') time.sleep(10) continue else: return modems_list def find_modem_profiles(self): wan_ifcs = self.cp.get(self.CFG_RULES2_PATH) modem_profiles_list = [ x['_id_'] for x in wan_ifcs if x['trigger_string'].startswith('type|is|mdm') ] self.cp.log(f'modem_profiles_list: {modem_profiles_list}') return modem_profiles_list def reset_throttle(self, modem_profiles_list, monthlyreset): for mdm in modem_profiles_list: if monthlyreset: self.cp.delete(self.CFG_RULES2_PATH + '/' + mdm + '/bandwidth_egress') self.cp.delete(self.CFG_RULES2_PATH + '/' + mdm + '/bandwidth_ingress') else: if 'bandwidth_egress' in self.cp.get(self.CFG_RULES2_PATH + '/' + mdm): self.cp.delete(self.CFG_RULES2_PATH + '/' + mdm + '/bandwidth_egress') if 'bandwidth_ingress' in self.cp.get(self.CFG_RULES2_PATH + '/' + mdm): self.cp.delete(self.CFG_RULES2_PATH + '/' + mdm + '/bandwidth_ingress') self.cp.put('config/qos/enabled', False) if monthlyreset: self.cp.log( 'Monthly data usage reset - disabling reduced LTE data rate') message = ( f'Monthly data usage reset - disabling reduced LTE data rate ' f'for {self.system_id} - {self.product_name} - Router ID: ' f'{self.router_id}') self.cp.alert(message) self.capreached = 0 def set_throttle(self, modem_profiles_list): for mdm in modem_profiles_list: self.cp.put(self.CFG_RULES2_PATH + '/' + mdm + '/bandwidth_egress', self.minbwup) self.cp.put( self.CFG_RULES2_PATH + '/' + mdm + '/bandwidth_ingress', self.minbwdown) self.cp.put('config/qos/enabled', True) self.cp.log( 'Exceeded monthly data usage threshold - reducing LTE data rate') message = ( f'Exceeded monthly data usage threshold - reducing LTE data rate ' f'for {self.system_id} - {self.product_name} - Router ID: ' f'{self.router_id}') self.cp.alert(message) self.capreached = 1 def run(self): # Get info from router to populate description field in NCM # alert message self.product_name = self.cp.get('/status/product_info/product_name') self.system_id = self.cp.get('/config/system/system_id') self.router_id = self.cp.get('status/ecm/client_id') # Retrieve list of modems and their profiles modems_list = [str(x.split('-')[1]) for x in self.find_modems()] modem_profiles_list = self.find_modem_profiles() # Reset any throttling to account for router reboots. If a # data cap alert is still active during the monthly cycle, the # appropriate rate shaping will be re-applied monthlyreset = False self.reset_throttle(modem_profiles_list, monthlyreset) time.sleep(5) while True: if self.cp.get(self.STATUS_DATACAP_PATH + '/completed_alerts/'): alerts = self.cp.get(self.STATUS_DATACAP_PATH + '/completed_alerts/') limitreached = 0 for modem in modems_list: if [ x['rule_id'] for x in alerts if x['rule_id'] == modem + '-monthly' if 'email_alert' in x['alerts'] ]: limitreached += 1 if limitreached > 0 and self.capreached == 0: self.set_throttle(modem_profiles_list) elif limitreached == 0 and self.capreached == 1: monthlyreset = True self.reset_throttle(modem_profiles_list, monthlyreset) elif self.capreached == 1: monthlyreset = True self.reset_throttle(modem_profiles_list, monthlyreset) time.sleep(10)