From aa4c6b512566bc43990612f67386eb49a40db56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Chac=C3=B3n?= Date: Fri, 9 Sep 2022 10:43:49 -0600 Subject: [PATCH] Add --validate option. Do not refreshShapers if validation fails, unless first boot --- v1.2/LibreQoS.py | 639 ++++++++++++++++++++++++----------------------- 1 file changed, 328 insertions(+), 311 deletions(-) diff --git a/v1.2/LibreQoS.py b/v1.2/LibreQoS.py index f3454242..ad01d1d6 100644 --- a/v1.2/LibreQoS.py +++ b/v1.2/LibreQoS.py @@ -214,93 +214,130 @@ def refreshShapers(): networkJSONfile = 'network.json' # Check validation + safeToRunRefresh = False if (validateNetworkAndDevices() == True): shutil.copyfile('ShapedDevices.csv', 'lastGoodConfig.csv') shutil.copyfile('network.json', 'lastGoodConfig.json') print("Backed up good config as lastGoodConfig.csv and lastGoodConfig.json") + safeToRunRefresh = True else: - warnings.warn("Validation failed - pulling from last good conifg") - shapedDevicesFile = 'lastGoodConfig.csv' - networkJSONfile = 'lastGoodConfig.json' + if (isThisFirstRunSinceBoot == False): + warnings.warn("Validation failed. Because this is not the first run since boot - will exit.") + safeToRunRefresh = False + else: + warnings.warn("Validation failed. However - because this is the first run since boot - will load queues from last good config") + shapedDevicesFile = 'lastGoodConfig.csv' + networkJSONfile = 'lastGoodConfig.json' + safeToRunRefresh = True - # Load Subscriber Circuits & Devices - subscriberCircuits = [] - knownCircuitIDs = [] - with open(shapedDevicesFile) as csv_file: - csv_reader = csv.reader(csv_file, delimiter=',') - # Remove comments if any - commentsRemoved = [] - for row in csv_reader: - if not row[0].startswith('#'): - commentsRemoved.append(row) - # Remove header - commentsRemoved.pop(0) - for row in commentsRemoved: - circuitID, circuitName, deviceID, deviceName, ParentNode, mac, ipv4_input, ipv6_input, downloadMin, uploadMin, downloadMax, uploadMax, comment = row - ipv4_hosts = [] - # Each entry in ShapedDevices.csv can have multiple IPv4s or IPv6s seperated by commas. Split them up and parse each - if ipv4_input != "": - ipv4_input = ipv4_input.replace(' ','') - if "," in ipv4_input: - ipv4_list = ipv4_input.split(',') - else: - ipv4_list = [ipv4_input] - for ipEntry in ipv4_list: - if '/32' in ipEntry: - ipv4_hosts.append(ipEntry.replace('/32','')) - elif '/' in ipEntry: - theseHosts = ipaddress.ip_network(ipEntry).hosts() - for host in theseHosts: - host = str(host) - if '/32' in host: - host = host.replace('/32','') - ipv4_hosts.append(host) + if safeToRunRefresh == True: + # Load Subscriber Circuits & Devices + subscriberCircuits = [] + knownCircuitIDs = [] + with open(shapedDevicesFile) as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + # Remove comments if any + commentsRemoved = [] + for row in csv_reader: + if not row[0].startswith('#'): + commentsRemoved.append(row) + # Remove header + commentsRemoved.pop(0) + for row in commentsRemoved: + circuitID, circuitName, deviceID, deviceName, ParentNode, mac, ipv4_input, ipv6_input, downloadMin, uploadMin, downloadMax, uploadMax, comment = row + ipv4_hosts = [] + # Each entry in ShapedDevices.csv can have multiple IPv4s or IPv6s seperated by commas. Split them up and parse each + if ipv4_input != "": + ipv4_input = ipv4_input.replace(' ','') + if "," in ipv4_input: + ipv4_list = ipv4_input.split(',') else: - ipv4_hosts.append(ipEntry) - ipv6_hosts = [] - if ipv6_input != "": - ipv6_input = ipv6_input.replace(' ','') - if "," in ipv6_input: - ipv6_list = ipv6_input.split(',') - else: - ipv6_list = [ipv6_input] - for ipEntry in ipv6_list: - if '/128' in ipEntry: - ipv6_hosts.append(ipEntry) - elif '/' in ipEntry: - theseHosts = ipaddress.ip_network(ipEntry).hosts() - for host in theseHosts: - ipv6_hosts.append(str(host)) + ipv4_list = [ipv4_input] + for ipEntry in ipv4_list: + if '/32' in ipEntry: + ipv4_hosts.append(ipEntry.replace('/32','')) + elif '/' in ipEntry: + theseHosts = ipaddress.ip_network(ipEntry).hosts() + for host in theseHosts: + host = str(host) + if '/32' in host: + host = host.replace('/32','') + ipv4_hosts.append(host) + else: + ipv4_hosts.append(ipEntry) + ipv6_hosts = [] + if ipv6_input != "": + ipv6_input = ipv6_input.replace(' ','') + if "," in ipv6_input: + ipv6_list = ipv6_input.split(',') else: - ipv6_hosts.append(ipEntry) - # If there is something in the circuit ID field - if circuitID != "": - # Seen circuit before - if circuitID in knownCircuitIDs: - for circuit in subscriberCircuits: - if circuit['circuitID'] == circuitID: - if circuit['ParentNode'] != "none": - if circuit['ParentNode'] != ParentNode: - errorMessageString = "Device " + deviceName + " with deviceID " + deviceID + " had different Parent Node from other devices of circuit ID #" + circuitID - raise ValueError(errorMessageString) - if ((circuit['downloadMin'] != round(int(downloadMin)*tcpOverheadFactor)) - or (circuit['uploadMin'] != round(int(uploadMin)*tcpOverheadFactor)) - or (circuit['downloadMax'] != round(int(downloadMax)*tcpOverheadFactor)) - or (circuit['uploadMax'] != round(int(uploadMax)*tcpOverheadFactor))): - warnings.warn("Device " + deviceName + " with ID " + deviceID + " had different bandwidth parameters than other devices on this circuit. Will instead use the bandwidth parameters defined by the first device added to its circuit.") - devicesListForCircuit = circuit['devices'] - thisDevice = { - "deviceID": deviceID, - "deviceName": deviceName, - "mac": mac, - "ipv4s": ipv4_hosts, - "ipv6s": ipv6_hosts, - } - devicesListForCircuit.append(thisDevice) - circuit['devices'] = devicesListForCircuit - # Have not seen circuit before + ipv6_list = [ipv6_input] + for ipEntry in ipv6_list: + if '/128' in ipEntry: + ipv6_hosts.append(ipEntry) + elif '/' in ipEntry: + theseHosts = ipaddress.ip_network(ipEntry).hosts() + for host in theseHosts: + ipv6_hosts.append(str(host)) + else: + ipv6_hosts.append(ipEntry) + # If there is something in the circuit ID field + if circuitID != "": + # Seen circuit before + if circuitID in knownCircuitIDs: + for circuit in subscriberCircuits: + if circuit['circuitID'] == circuitID: + if circuit['ParentNode'] != "none": + if circuit['ParentNode'] != ParentNode: + errorMessageString = "Device " + deviceName + " with deviceID " + deviceID + " had different Parent Node from other devices of circuit ID #" + circuitID + raise ValueError(errorMessageString) + if ((circuit['downloadMin'] != round(int(downloadMin)*tcpOverheadFactor)) + or (circuit['uploadMin'] != round(int(uploadMin)*tcpOverheadFactor)) + or (circuit['downloadMax'] != round(int(downloadMax)*tcpOverheadFactor)) + or (circuit['uploadMax'] != round(int(uploadMax)*tcpOverheadFactor))): + warnings.warn("Device " + deviceName + " with ID " + deviceID + " had different bandwidth parameters than other devices on this circuit. Will instead use the bandwidth parameters defined by the first device added to its circuit.") + devicesListForCircuit = circuit['devices'] + thisDevice = { + "deviceID": deviceID, + "deviceName": deviceName, + "mac": mac, + "ipv4s": ipv4_hosts, + "ipv6s": ipv6_hosts, + } + devicesListForCircuit.append(thisDevice) + circuit['devices'] = devicesListForCircuit + # Have not seen circuit before + else: + knownCircuitIDs.append(circuitID) + if ParentNode == "": + ParentNode = "none" + ParentNode = ParentNode.strip() + deviceListForCircuit = [] + thisDevice = { + "deviceID": deviceID, + "deviceName": deviceName, + "mac": mac, + "ipv4s": ipv4_hosts, + "ipv6s": ipv6_hosts, + } + deviceListForCircuit.append(thisDevice) + thisCircuit = { + "circuitID": circuitID, + "circuitName": circuitName, + "ParentNode": ParentNode, + "devices": deviceListForCircuit, + "downloadMin": round(int(downloadMin)*tcpOverheadFactor), + "uploadMin": round(int(uploadMin)*tcpOverheadFactor), + "downloadMax": round(int(downloadMax)*tcpOverheadFactor), + "uploadMax": round(int(uploadMax)*tcpOverheadFactor), + "qdisc": '', + } + subscriberCircuits.append(thisCircuit) + # If there is nothing in the circuit ID field else: - knownCircuitIDs.append(circuitID) + # Copy deviceName to circuitName if none defined already + if circuitName == "": + circuitName = deviceName if ParentNode == "": ParentNode = "none" ParentNode = ParentNode.strip() @@ -325,238 +362,209 @@ def refreshShapers(): "qdisc": '', } subscriberCircuits.append(thisCircuit) - # If there is nothing in the circuit ID field - else: - # Copy deviceName to circuitName if none defined already - if circuitName == "": - circuitName = deviceName - if ParentNode == "": - ParentNode = "none" - ParentNode = ParentNode.strip() - deviceListForCircuit = [] - thisDevice = { - "deviceID": deviceID, - "deviceName": deviceName, - "mac": mac, - "ipv4s": ipv4_hosts, - "ipv6s": ipv6_hosts, - } - deviceListForCircuit.append(thisDevice) - thisCircuit = { - "circuitID": circuitID, - "circuitName": circuitName, - "ParentNode": ParentNode, - "devices": deviceListForCircuit, - "downloadMin": round(int(downloadMin)*tcpOverheadFactor), - "uploadMin": round(int(uploadMin)*tcpOverheadFactor), - "downloadMax": round(int(downloadMax)*tcpOverheadFactor), - "uploadMax": round(int(uploadMax)*tcpOverheadFactor), - "qdisc": '', - } - subscriberCircuits.append(thisCircuit) - # Load network heirarchy - with open(networkJSONfile, 'r') as j: - network = json.loads(j.read()) - - # Pull rx/tx queues / CPU cores avaialble - queuesAvailable = findQueuesAvailable() + # Load network heirarchy + with open(networkJSONfile, 'r') as j: + network = json.loads(j.read()) + + # Pull rx/tx queues / CPU cores avaialble + queuesAvailable = findQueuesAvailable() - # Generate Parent Nodes. Spread ShapedDevices.csv which lack defined ParentNode across these (balance across CPUs) - generatedPNs = [] - for x in range(queuesAvailable): - genPNname = "Generated_PN_" + str(x+1) - network[genPNname] = { - "downloadBandwidthMbps":generatedPNDownloadMbps, - "uploadBandwidthMbps":generatedPNUploadMbps - } - generatedPNs.append(genPNname) - genPNcounter = 0 - for circuit in subscriberCircuits: - if circuit['ParentNode'] == 'none': - circuit['ParentNode'] = generatedPNs[genPNcounter] - genPNcounter += 1 - if genPNcounter >= queuesAvailable: - genPNcounter = 0 - - # Find the bandwidth minimums for each node by combining mimimums of devices lower in that node's heirarchy - def findBandwidthMins(data, depth): - tabs = ' ' * depth - minDownload = 0 - minUpload = 0 - for elem in data: - for circuit in subscriberCircuits: - if elem == circuit['ParentNode']: - minDownload += circuit['downloadMin'] - minUpload += circuit['uploadMin'] - if 'children' in data[elem]: - minDL, minUL = findBandwidthMins(data[elem]['children'], depth+1) - minDownload += minDL - minUpload += minUL - data[elem]['downloadBandwidthMbpsMin'] = minDownload - data[elem]['uploadBandwidthMbpsMin'] = minUpload - return minDownload, minUpload - - minDownload, minUpload = findBandwidthMins(network, 0) - - # Parse network structure. For each tier, create corresponding HTB and leaf classes. Prepare for execution later - linuxTCcommands = [] - xdpCPUmapCommands = [] - devicesShaped = [] - parentNodes = [] - def traverseNetwork(data, depth, major, minor, queue, parentClassID, parentMaxDL, parentMaxUL): - tabs = ' ' * depth - for elem in data: - elemClassID = hex(major) + ':' + hex(minor) - # Cap based on this node's max bandwidth, or parent node's max bandwidth, whichever is lower - elemDownloadMax = min(data[elem]['downloadBandwidthMbps'],parentMaxDL) - elemUploadMax = min(data[elem]['uploadBandwidthMbps'],parentMaxUL) - # Calculations are done in findBandwidthMins(), determine optimal HTB rates (mins) and ceils (maxs) - # For some reason that doesn't always yield the expected result, so it's better to play with ceil more than rate - # Here we override the rate as 95% of ceil. - elemDownloadMin = round(elemDownloadMax*.95) - elemUploadMin = round(elemUploadMax*.95) - linuxTCcommands.append('class add dev ' + interfaceA + ' parent ' + parentClassID + ' classid ' + hex(minor) + ' htb rate '+ str(round(elemDownloadMin)) + 'mbit ceil '+ str(round(elemDownloadMax)) + 'mbit prio 3') - linuxTCcommands.append('class add dev ' + interfaceB + ' parent ' + parentClassID + ' classid ' + hex(minor) + ' htb rate '+ str(round(elemUploadMin)) + 'mbit ceil '+ str(round(elemUploadMax)) + 'mbit prio 3') - thisParentNode = { - "parentNodeName": elem, - "classID": elemClassID, - "downloadMax": elemDownloadMax, - "uploadMax": elemUploadMax, - } - parentNodes.append(thisParentNode) - minor += 1 - for circuit in subscriberCircuits: - #If a device from ShapedDevices.csv lists this elem as its Parent Node, attach it as a leaf to this elem HTB - if elem == circuit['ParentNode']: - maxDownload = min(circuit['downloadMax'],elemDownloadMax) - maxUpload = min(circuit['uploadMax'],elemUploadMax) - minDownload = min(circuit['downloadMin'],maxDownload) - minUpload = min(circuit['uploadMin'],maxUpload) - linuxTCcommands.append('class add dev ' + interfaceA + ' parent ' + elemClassID + ' classid ' + hex(minor) + ' htb rate '+ str(minDownload) + 'mbit ceil '+ str(maxDownload) + 'mbit prio 3') - linuxTCcommands.append('qdisc add dev ' + interfaceA + ' parent ' + hex(major) + ':' + hex(minor) + ' ' + fqOrCAKE) - linuxTCcommands.append('class add dev ' + interfaceB + ' parent ' + elemClassID + ' classid ' + hex(minor) + ' htb rate '+ str(minUpload) + 'mbit ceil '+ str(maxUpload) + 'mbit prio 3') - linuxTCcommands.append('qdisc add dev ' + interfaceB + ' parent ' + hex(major) + ':' + hex(minor) + ' ' + fqOrCAKE) - parentString = hex(major) + ':' - flowIDstring = hex(major) + ':' + hex(minor) - circuit['qdisc'] = flowIDstring - for device in circuit['devices']: - if device['ipv4s']: - for ipv4 in device['ipv4s']: - xdpCPUmapCommands.append('./xdp-cpumap-tc/src/xdp_iphash_to_cpu_cmdline --add --ip ' + str(ipv4) + ' --cpu ' + hex(queue-1) + ' --classid ' + flowIDstring) - if device['deviceName'] not in devicesShaped: - devicesShaped.append(device['deviceName']) - minor += 1 - # Recursive call this function for children nodes attached to this node - if 'children' in data[elem]: - # We need to keep tabs on the minor counter, because we can't have repeating class IDs. Here, we bring back the minor counter from the recursive function - minor = traverseNetwork(data[elem]['children'], depth+1, major, minor+1, queue, elemClassID, elemDownloadMax, elemUploadMax) - # If top level node, increment to next queue / cpu core - if depth == 0: - if queue >= queuesAvailable: - queue = 1 - major = queue - else: - queue += 1 - major += 1 - return minor + # Generate Parent Nodes. Spread ShapedDevices.csv which lack defined ParentNode across these (balance across CPUs) + generatedPNs = [] + for x in range(queuesAvailable): + genPNname = "Generated_PN_" + str(x+1) + network[genPNname] = { + "downloadBandwidthMbps":generatedPNDownloadMbps, + "uploadBandwidthMbps":generatedPNUploadMbps + } + generatedPNs.append(genPNname) + genPNcounter = 0 + for circuit in subscriberCircuits: + if circuit['ParentNode'] == 'none': + circuit['ParentNode'] = generatedPNs[genPNcounter] + genPNcounter += 1 + if genPNcounter >= queuesAvailable: + genPNcounter = 0 + + # Find the bandwidth minimums for each node by combining mimimums of devices lower in that node's heirarchy + def findBandwidthMins(data, depth): + tabs = ' ' * depth + minDownload = 0 + minUpload = 0 + for elem in data: + for circuit in subscriberCircuits: + if elem == circuit['ParentNode']: + minDownload += circuit['downloadMin'] + minUpload += circuit['uploadMin'] + if 'children' in data[elem]: + minDL, minUL = findBandwidthMins(data[elem]['children'], depth+1) + minDownload += minDL + minUpload += minUL + data[elem]['downloadBandwidthMbpsMin'] = minDownload + data[elem]['uploadBandwidthMbpsMin'] = minUpload + return minDownload, minUpload + + minDownload, minUpload = findBandwidthMins(network, 0) + + # Parse network structure. For each tier, create corresponding HTB and leaf classes. Prepare for execution later + linuxTCcommands = [] + xdpCPUmapCommands = [] + devicesShaped = [] + parentNodes = [] + def traverseNetwork(data, depth, major, minor, queue, parentClassID, parentMaxDL, parentMaxUL): + tabs = ' ' * depth + for elem in data: + elemClassID = hex(major) + ':' + hex(minor) + # Cap based on this node's max bandwidth, or parent node's max bandwidth, whichever is lower + elemDownloadMax = min(data[elem]['downloadBandwidthMbps'],parentMaxDL) + elemUploadMax = min(data[elem]['uploadBandwidthMbps'],parentMaxUL) + # Calculations are done in findBandwidthMins(), determine optimal HTB rates (mins) and ceils (maxs) + # For some reason that doesn't always yield the expected result, so it's better to play with ceil more than rate + # Here we override the rate as 95% of ceil. + elemDownloadMin = round(elemDownloadMax*.95) + elemUploadMin = round(elemUploadMax*.95) + linuxTCcommands.append('class add dev ' + interfaceA + ' parent ' + parentClassID + ' classid ' + hex(minor) + ' htb rate '+ str(round(elemDownloadMin)) + 'mbit ceil '+ str(round(elemDownloadMax)) + 'mbit prio 3') + linuxTCcommands.append('class add dev ' + interfaceB + ' parent ' + parentClassID + ' classid ' + hex(minor) + ' htb rate '+ str(round(elemUploadMin)) + 'mbit ceil '+ str(round(elemUploadMax)) + 'mbit prio 3') + thisParentNode = { + "parentNodeName": elem, + "classID": elemClassID, + "downloadMax": elemDownloadMax, + "uploadMax": elemUploadMax, + } + parentNodes.append(thisParentNode) + minor += 1 + for circuit in subscriberCircuits: + #If a device from ShapedDevices.csv lists this elem as its Parent Node, attach it as a leaf to this elem HTB + if elem == circuit['ParentNode']: + maxDownload = min(circuit['downloadMax'],elemDownloadMax) + maxUpload = min(circuit['uploadMax'],elemUploadMax) + minDownload = min(circuit['downloadMin'],maxDownload) + minUpload = min(circuit['uploadMin'],maxUpload) + linuxTCcommands.append('class add dev ' + interfaceA + ' parent ' + elemClassID + ' classid ' + hex(minor) + ' htb rate '+ str(minDownload) + 'mbit ceil '+ str(maxDownload) + 'mbit prio 3') + linuxTCcommands.append('qdisc add dev ' + interfaceA + ' parent ' + hex(major) + ':' + hex(minor) + ' ' + fqOrCAKE) + linuxTCcommands.append('class add dev ' + interfaceB + ' parent ' + elemClassID + ' classid ' + hex(minor) + ' htb rate '+ str(minUpload) + 'mbit ceil '+ str(maxUpload) + 'mbit prio 3') + linuxTCcommands.append('qdisc add dev ' + interfaceB + ' parent ' + hex(major) + ':' + hex(minor) + ' ' + fqOrCAKE) + parentString = hex(major) + ':' + flowIDstring = hex(major) + ':' + hex(minor) + circuit['qdisc'] = flowIDstring + for device in circuit['devices']: + if device['ipv4s']: + for ipv4 in device['ipv4s']: + xdpCPUmapCommands.append('./xdp-cpumap-tc/src/xdp_iphash_to_cpu_cmdline --add --ip ' + str(ipv4) + ' --cpu ' + hex(queue-1) + ' --classid ' + flowIDstring) + if device['deviceName'] not in devicesShaped: + devicesShaped.append(device['deviceName']) + minor += 1 + # Recursive call this function for children nodes attached to this node + if 'children' in data[elem]: + # We need to keep tabs on the minor counter, because we can't have repeating class IDs. Here, we bring back the minor counter from the recursive function + minor = traverseNetwork(data[elem]['children'], depth+1, major, minor+1, queue, elemClassID, elemDownloadMax, elemUploadMax) + # If top level node, increment to next queue / cpu core + if depth == 0: + if queue >= queuesAvailable: + queue = 1 + major = queue + else: + queue += 1 + major += 1 + return minor - # Print structure of network.json in debug or verbose mode - logging.info(json.dumps(network, indent=4)) + # Print structure of network.json in debug or verbose mode + logging.info(json.dumps(network, indent=4)) - # Here is the actual call to the recursive traverseNetwork() function. finalMinor is not used. - finalMinor = traverseNetwork(network, 0, major=1, minor=3, queue=1, parentClassID="1:1", parentMaxDL=upstreamBandwidthCapacityDownloadMbps, parentMaxUL=upstreamBandwidthCapacityUploadMbps) - - # Record start time of actual filter reload - reloadStartTime = datetime.now() - - # Clear Prior Settings - clearPriorSettings(interfaceA, interfaceB) - - # If this is the first time LibreQoS.py has run since system boot, load the XDP program and disable XPS - # Otherwise, just clear the existing IP filter rules for xdp - if isThisFirstRunSinceBoot: - # Set up XDP-CPUMAP-TC - shell('./xdp-cpumap-tc/bin/xps_setup.sh -d ' + interfaceA + ' --default --disable') - shell('./xdp-cpumap-tc/bin/xps_setup.sh -d ' + interfaceB + ' --default --disable') - shell('./xdp-cpumap-tc/src/xdp_iphash_to_cpu --dev ' + interfaceA + ' --lan') - shell('./xdp-cpumap-tc/src/xdp_iphash_to_cpu --dev ' + interfaceB + ' --wan') - if enableActualShellCommands: - # Here we use os.system for the command, because otherwise it sometimes gltiches out with Popen in shell() - result = os.system('./xdp-cpumap-tc/src/xdp_iphash_to_cpu_cmdline --clear') - shell('./xdp-cpumap-tc/src/tc_classify --dev-egress ' + interfaceA) - shell('./xdp-cpumap-tc/src/tc_classify --dev-egress ' + interfaceB) - else: - if enableActualShellCommands: - result = os.system('./xdp-cpumap-tc/src/xdp_iphash_to_cpu_cmdline --clear') + # Here is the actual call to the recursive traverseNetwork() function. finalMinor is not used. + finalMinor = traverseNetwork(network, 0, major=1, minor=3, queue=1, parentClassID="1:1", parentMaxDL=upstreamBandwidthCapacityDownloadMbps, parentMaxUL=upstreamBandwidthCapacityUploadMbps) + + # Record start time of actual filter reload + reloadStartTime = datetime.now() + + # Clear Prior Settings + clearPriorSettings(interfaceA, interfaceB) + + # If this is the first time LibreQoS.py has run since system boot, load the XDP program and disable XPS + # Otherwise, just clear the existing IP filter rules for xdp + if isThisFirstRunSinceBoot: + # Set up XDP-CPUMAP-TC + shell('./xdp-cpumap-tc/bin/xps_setup.sh -d ' + interfaceA + ' --default --disable') + shell('./xdp-cpumap-tc/bin/xps_setup.sh -d ' + interfaceB + ' --default --disable') + shell('./xdp-cpumap-tc/src/xdp_iphash_to_cpu --dev ' + interfaceA + ' --lan') + shell('./xdp-cpumap-tc/src/xdp_iphash_to_cpu --dev ' + interfaceB + ' --wan') + if enableActualShellCommands: + # Here we use os.system for the command, because otherwise it sometimes gltiches out with Popen in shell() + result = os.system('./xdp-cpumap-tc/src/xdp_iphash_to_cpu_cmdline --clear') + shell('./xdp-cpumap-tc/src/tc_classify --dev-egress ' + interfaceA) + shell('./xdp-cpumap-tc/src/tc_classify --dev-egress ' + interfaceB) + else: + if enableActualShellCommands: + result = os.system('./xdp-cpumap-tc/src/xdp_iphash_to_cpu_cmdline --clear') - # Create MQ qdisc for each interface - thisInterface = interfaceA - shell('tc qdisc replace dev ' + thisInterface + ' root handle 7FFF: mq') - for queue in range(queuesAvailable): - shell('tc qdisc add dev ' + thisInterface + ' parent 7FFF:' + hex(queue+1) + ' handle ' + hex(queue+1) + ': htb default 2') - shell('tc class add dev ' + thisInterface + ' parent ' + hex(queue+1) + ': classid ' + hex(queue+1) + ':1 htb rate '+ str(upstreamBandwidthCapacityDownloadMbps) + 'mbit ceil ' + str(upstreamBandwidthCapacityDownloadMbps) + 'mbit') - shell('tc qdisc add dev ' + thisInterface + ' parent ' + hex(queue+1) + ':1 ' + fqOrCAKE) - # Default class - traffic gets passed through this limiter with lower priority if not otherwise classified by the Shaper.csv - # Only 1/4 of defaultClassCapacity is guarenteed (to prevent hitting ceiling of upstream), for the most part it serves as an "up to" ceiling. - # Default class can use up to defaultClassCapacityDownloadMbps when that bandwidth isn't used by known hosts. - shell('tc class add dev ' + thisInterface + ' parent ' + hex(queue+1) + ':1 classid ' + hex(queue+1) + ':2 htb rate ' + str(defaultClassCapacityDownloadMbps/4) + 'mbit ceil ' + str(defaultClassCapacityDownloadMbps) + 'mbit prio 5') - shell('tc qdisc add dev ' + thisInterface + ' parent ' + hex(queue+1) + ':2 ' + fqOrCAKE) - - thisInterface = interfaceB - shell('tc qdisc replace dev ' + thisInterface + ' root handle 7FFF: mq') - for queue in range(queuesAvailable): - shell('tc qdisc add dev ' + thisInterface + ' parent 7FFF:' + hex(queue+1) + ' handle ' + hex(queue+1) + ': htb default 2') - shell('tc class add dev ' + thisInterface + ' parent ' + hex(queue+1) + ': classid ' + hex(queue+1) + ':1 htb rate '+ str(upstreamBandwidthCapacityUploadMbps) + 'mbit ceil ' + str(upstreamBandwidthCapacityUploadMbps) + 'mbit') - shell('tc qdisc add dev ' + thisInterface + ' parent ' + hex(queue+1) + ':1 ' + fqOrCAKE) - # Default class - traffic gets passed through this limiter with lower priority if not otherwise classified by the Shaper.csv. - # Only 1/4 of defaultClassCapacity is guarenteed (to prevent hitting ceiling of upstream), for the most part it serves as an "up to" ceiling. - # Default class can use up to defaultClassCapacityUploadMbps when that bandwidth isn't used by known hosts. - shell('tc class add dev ' + thisInterface + ' parent ' + hex(queue+1) + ':1 classid ' + hex(queue+1) + ':2 htb rate ' + str(defaultClassCapacityUploadMbps/4) + 'mbit ceil ' + str(defaultClassCapacityUploadMbps) + 'mbit prio 5') - shell('tc qdisc add dev ' + thisInterface + ' parent ' + hex(queue+1) + ':2 ' + fqOrCAKE) - - # Execute actual Linux TC and XDP-CPUMAP-TC filter commands - print("Executing linux TC class/qdisc commands") - with open('linux_tc.txt', 'w') as f: - for line in linuxTCcommands: - f.write(f"{line}\n") - logging.info(line) - shell("/sbin/tc -f -b linux_tc.txt") - print("Executed " + str(len(linuxTCcommands)) + " linux TC class/qdisc commands") - print("Executing XDP-CPUMAP-TC IP filter commands") - for command in xdpCPUmapCommands: - logging.info(command) - shell(command) - print("Executed " + str(len(xdpCPUmapCommands)) + " XDP-CPUMAP-TC IP filter commands") - reloadEndTime = datetime.now() - - # Recap - warn operator if devices were skipped - devicesSkipped = [] - for circuit in subscriberCircuits: - for device in circuit['devices']: - if device['deviceName'] not in devicesShaped: - devicesSkipped.append((device['deviceName'],device['deviceID'])) - if len(devicesSkipped) > 0: - warnings.warn('Some devices were not shaped. Please check to ensure they have a valid ParentNode listed in ShapedDevices.csv:', stacklevel=2) - print("Devices not shaped:") - for entry in devicesSkipped: - name, idNum = entry - print('DeviceID: ' + idNum + '\t DeviceName: ' + name) - - # Save for stats - with open('statsByCircuit.json', 'w') as infile: - json.dump(subscriberCircuits, infile) - with open('statsByParentNode.json', 'w') as infile: - json.dump(parentNodes, infile) - - # Record time this run completed at - # filename = os.path.join(_here, 'lastRun.txt') - with open("lastRun.txt", 'w') as file: - file.write(datetime.now().strftime("%d-%b-%Y (%H:%M:%S.%f)")) - - # Report reload time - reloadTimeSeconds = (reloadEndTime - reloadStartTime).seconds - print("Queue and IP filter reload completed in " + str(reloadTimeSeconds) + " seconds") + # Create MQ qdisc for each interface + thisInterface = interfaceA + shell('tc qdisc replace dev ' + thisInterface + ' root handle 7FFF: mq') + for queue in range(queuesAvailable): + shell('tc qdisc add dev ' + thisInterface + ' parent 7FFF:' + hex(queue+1) + ' handle ' + hex(queue+1) + ': htb default 2') + shell('tc class add dev ' + thisInterface + ' parent ' + hex(queue+1) + ': classid ' + hex(queue+1) + ':1 htb rate '+ str(upstreamBandwidthCapacityDownloadMbps) + 'mbit ceil ' + str(upstreamBandwidthCapacityDownloadMbps) + 'mbit') + shell('tc qdisc add dev ' + thisInterface + ' parent ' + hex(queue+1) + ':1 ' + fqOrCAKE) + # Default class - traffic gets passed through this limiter with lower priority if not otherwise classified by the Shaper.csv + # Only 1/4 of defaultClassCapacity is guarenteed (to prevent hitting ceiling of upstream), for the most part it serves as an "up to" ceiling. + # Default class can use up to defaultClassCapacityDownloadMbps when that bandwidth isn't used by known hosts. + shell('tc class add dev ' + thisInterface + ' parent ' + hex(queue+1) + ':1 classid ' + hex(queue+1) + ':2 htb rate ' + str(defaultClassCapacityDownloadMbps/4) + 'mbit ceil ' + str(defaultClassCapacityDownloadMbps) + 'mbit prio 5') + shell('tc qdisc add dev ' + thisInterface + ' parent ' + hex(queue+1) + ':2 ' + fqOrCAKE) + + thisInterface = interfaceB + shell('tc qdisc replace dev ' + thisInterface + ' root handle 7FFF: mq') + for queue in range(queuesAvailable): + shell('tc qdisc add dev ' + thisInterface + ' parent 7FFF:' + hex(queue+1) + ' handle ' + hex(queue+1) + ': htb default 2') + shell('tc class add dev ' + thisInterface + ' parent ' + hex(queue+1) + ': classid ' + hex(queue+1) + ':1 htb rate '+ str(upstreamBandwidthCapacityUploadMbps) + 'mbit ceil ' + str(upstreamBandwidthCapacityUploadMbps) + 'mbit') + shell('tc qdisc add dev ' + thisInterface + ' parent ' + hex(queue+1) + ':1 ' + fqOrCAKE) + # Default class - traffic gets passed through this limiter with lower priority if not otherwise classified by the Shaper.csv. + # Only 1/4 of defaultClassCapacity is guarenteed (to prevent hitting ceiling of upstream), for the most part it serves as an "up to" ceiling. + # Default class can use up to defaultClassCapacityUploadMbps when that bandwidth isn't used by known hosts. + shell('tc class add dev ' + thisInterface + ' parent ' + hex(queue+1) + ':1 classid ' + hex(queue+1) + ':2 htb rate ' + str(defaultClassCapacityUploadMbps/4) + 'mbit ceil ' + str(defaultClassCapacityUploadMbps) + 'mbit prio 5') + shell('tc qdisc add dev ' + thisInterface + ' parent ' + hex(queue+1) + ':2 ' + fqOrCAKE) + + # Execute actual Linux TC and XDP-CPUMAP-TC filter commands + print("Executing linux TC class/qdisc commands") + with open('linux_tc.txt', 'w') as f: + for line in linuxTCcommands: + f.write(f"{line}\n") + logging.info(line) + shell("/sbin/tc -f -b linux_tc.txt") + print("Executed " + str(len(linuxTCcommands)) + " linux TC class/qdisc commands") + print("Executing XDP-CPUMAP-TC IP filter commands") + for command in xdpCPUmapCommands: + logging.info(command) + shell(command) + print("Executed " + str(len(xdpCPUmapCommands)) + " XDP-CPUMAP-TC IP filter commands") + reloadEndTime = datetime.now() + + # Recap - warn operator if devices were skipped + devicesSkipped = [] + for circuit in subscriberCircuits: + for device in circuit['devices']: + if device['deviceName'] not in devicesShaped: + devicesSkipped.append((device['deviceName'],device['deviceID'])) + if len(devicesSkipped) > 0: + warnings.warn('Some devices were not shaped. Please check to ensure they have a valid ParentNode listed in ShapedDevices.csv:', stacklevel=2) + print("Devices not shaped:") + for entry in devicesSkipped: + name, idNum = entry + print('DeviceID: ' + idNum + '\t DeviceName: ' + name) + + # Save for stats + with open('statsByCircuit.json', 'w') as infile: + json.dump(subscriberCircuits, infile) + with open('statsByParentNode.json', 'w') as infile: + json.dump(parentNodes, infile) + + # Record time this run completed at + # filename = os.path.join(_here, 'lastRun.txt') + with open("lastRun.txt", 'w') as file: + file.write(datetime.now().strftime("%d-%b-%Y (%H:%M:%S.%f)")) + + # Report reload time + reloadTimeSeconds = (reloadEndTime - reloadStartTime).seconds + print("Queue and IP filter reload completed in " + str(reloadTimeSeconds) + " seconds") if __name__ == '__main__': parser = argparse.ArgumentParser() @@ -571,11 +579,20 @@ if __name__ == '__main__': help="Be verbose", action="store_const", dest="loglevel", const=logging.INFO, ) + parser.add_argument( + '--validate', + help="Just validate network.json and ShapedDevices.csv", + action=argparse.BooleanOptionalAction, + ) args = parser.parse_args() logging.basicConfig(level=args.loglevel) - # Starting - print("refreshShapers starting at " + datetime.now().strftime("%d/%m/%Y %H:%M:%S")) - # Refresh and/or set up queues - refreshShapers() - # Done - print("refreshShapers completed on " + datetime.now().strftime("%d/%m/%Y %H:%M:%S")) + + if args.validate: + status = validateNetworkAndDevices() + else: + # Starting + print("refreshShapers starting at " + datetime.now().strftime("%d/%m/%Y %H:%M:%S")) + # Refresh and/or set up queues + refreshShapers() + # Done + print("refreshShapers completed on " + datetime.now().strftime("%d/%m/%Y %H:%M:%S"))