From 728ec3c5dcc505f914ceb1b25ea3c5e4ba82d90c Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Tue, 24 Jun 2025 16:18:37 +0530 Subject: [PATCH 01/50] feat: migration to async_snmp_manager initialize --- debug_async_flow.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 debug_async_flow.py diff --git a/debug_async_flow.py b/debug_async_flow.py new file mode 100644 index 00000000..c6f95707 --- /dev/null +++ b/debug_async_flow.py @@ -0,0 +1,12 @@ +""" +Debug testing script to test the new async SNMP polling implementation of switchmap-ng +""" + +import sys +import os +import asyncio + + +sys.path.insert(0,'/Users/imexyyyyy/files/gsoc/switchmap-ng'); + +# once i make polling async then will update this according to that to check if devices are being polled asynchronously \ No newline at end of file From 91389bd8ee7ec1457f37e57bc8531974c22d405c Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Tue, 24 Jun 2025 17:14:53 +0530 Subject: [PATCH 02/50] feat: configure test script for local debugging --- debug_async_flow.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/debug_async_flow.py b/debug_async_flow.py index c6f95707..158d61b3 100644 --- a/debug_async_flow.py +++ b/debug_async_flow.py @@ -9,4 +9,24 @@ sys.path.insert(0,'/Users/imexyyyyy/files/gsoc/switchmap-ng'); -# once i make polling async then will update this according to that to check if devices are being polled asynchronously \ No newline at end of file +# once i make polling async then will update this according to that to check if devices are being polled asynchronously +#for now for testing using cli_device to poll single device for debugging + +from switchmap.poller import poll + +def main(): + print("Switchmap polling flow debugger") + + try: + + hostname= "162.249.37.218" + print(f"starting debug poll for hostname: {hostname}") + + poll.cli_device(hostname=hostname) + + except Exception as e: + print(f"Error duing polling: {e}") + +if __name__ == "__main__": + main() + From 1742c2031ceb9ee7a73082f184af857b17056ba4 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sat, 5 Jul 2025 16:15:08 +0530 Subject: [PATCH 03/50] feat: added SNMP value conversion and result formatting helpers --- switchmap/poller/snmp/async_snmp_manager.py | 118 ++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/switchmap/poller/snmp/async_snmp_manager.py b/switchmap/poller/snmp/async_snmp_manager.py index 7d2945c6..3a69b4ab 100644 --- a/switchmap/poller/snmp/async_snmp_manager.py +++ b/switchmap/poller/snmp/async_snmp_manager.py @@ -979,6 +979,124 @@ def _oid_valid_format(oid): # Otherwise valid return True +def _convert(value): + """Convert SNMP value from pysnmp object to Python type. + + Args: + result: pysnmp value object + + Returns: + converted: Value converted to appropriate Python type (bytes or int), + or None for null/empty values + """ + + # Handle pysnmp exception values + if isinstance(value, NoSuchObject): + return None + if isinstance(value, NoSuchInstance): + return None + if isinstance(value, EndOfMibView): + return None + + if hasattr(value, 'prettyPrint'): + value_str = value.prettyPrint() + + #Determine type based on pysnmp object type + value_type = type(value).__name__ + + # Handle string-like types - Convert to types for MIB compatibility + if any(t in value_type for t in ['OctetString','DisplayString','Opaque','Bits','IpAddress','ObjectIdentifier']): + # For objectID, convert to string first then to bytes + if 'ObjectIdentifier' in value_type: + return bytes(str(value_str), 'utf-8') + else: + return bytes(value_str,'utf-8') + + #Handle integer types + elif any(t in value_type for t in ['Integer','Counter','Gauge','TimeTicks','Unsigned']): + try: + return int(value_str) + except ValueError: + #! clear on this once again + #! approach 1 + # Direct int conversion of the obj if prettyPrint fails + if hasattr(value, '__int__'): + try: + return int(value) + except (ValueError, TypeError): + pass + + #! appraoch 2 + # Accessing .value attr directly + if hasattr(value, 'value'): + try: + return int(value.value) + except (ValueError, TypeError): + pass + + log_message = f"Failed to convert pysnmp integer valye: {value_type}, prettyPrint'{value_str}" + log.log2warning(1059, log_message) + return None + + # Handle direct access to value (for objects without prettyPrint) + if hasattr(value, 'value'): + try: + return int(value.value) + except (ValueError, TypeError): + return bytes(str(value.value), 'utf-8') + + #! will check this as well (if we need it ??) + # Default Fallback - convert to string then to bytes + try: + return bytes(str(value), 'utf-8') + except Exception: + return None + + +def _format_results(results,mock_filter, normalized = False): + """Normalized and format SNMP results + + Args: + results: List of (OID, value) tuples from pysnmp + mock_filter: The original OID to get. Facilitates unittesting by + filtering Mock values. + normalized: If True, then return results as a dict keyed by + only the last node of an OID, otherwise return results + keyed by the entire OID string. Normalization is useful + when trying to create multidimensional dicts where the + primary key is a universal value such as IF-MIB::ifIndex + or BRIDGE-MIB::dot1dBasePort + + Returns: + dict: Formatted results as OID-value pairs + + """ + + formatted = {} + + for oid_str,value in results: + + # Defensive: Double-check OID filtering for edge cases, testing and library quirks + # Our walk methods already filter, but this catches unusual scenarios + + if mock_filter and mock_filter not in oid_str: + continue + + #convert value using proper type conversion + converted_value = _convert(value=value) + + if normalized is True: + #use only the last node of the OID + key = oid_str.split('.')[-1] + else: + key = oid_str + + formatted[key] = converted_value + + return formatted + + + def _convert(value): """Convert SNMP value from pysnmp object to Python type. From 7b4376ec537e5c01cb9f81bc39dfed8c9684e8b3 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sun, 6 Jul 2025 18:01:44 +0530 Subject: [PATCH 04/50] chore: fixed few minor bugs && added get hostname method --- switchmap/poller/snmp/async_snmp_manager.py | 10 ++++++++-- switchmap/poller/snmp/test.py | 0 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 switchmap/poller/snmp/test.py diff --git a/switchmap/poller/snmp/async_snmp_manager.py b/switchmap/poller/snmp/async_snmp_manager.py index 3a69b4ab..c4b18c18 100644 --- a/switchmap/poller/snmp/async_snmp_manager.py +++ b/switchmap/poller/snmp/async_snmp_manager.py @@ -1079,8 +1079,14 @@ def _format_results(results,mock_filter, normalized = False): # Defensive: Double-check OID filtering for edge cases, testing and library quirks # Our walk methods already filter, but this catches unusual scenarios - if mock_filter and mock_filter not in oid_str: - continue + # FIX: Normalize both OIDs for comparison to handle leading dot mismatch + if mock_filter: + # Remove leading dots for comparison + filter_normalized = mock_filter.lstrip('.') + oid_normalized = oid_str.lstrip('.') + + if filter_normalized not in oid_normalized: + continue #convert value using proper type conversion converted_value = _convert(value=value) diff --git a/switchmap/poller/snmp/test.py b/switchmap/poller/snmp/test.py new file mode 100644 index 00000000..e69de29b From 7a18393b22a8e2644a8d7ea3a62453bc7545e9e5 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Wed, 16 Jul 2025 04:14:31 +0530 Subject: [PATCH 05/50] feat: added partial test for manager & added walk method for oid prefix --- switchmap/poller/snmp/async_snmp_manager.py | 9 +- test_async_manager.py | 119 ++++++++++++++++++++ 2 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 test_async_manager.py diff --git a/switchmap/poller/snmp/async_snmp_manager.py b/switchmap/poller/snmp/async_snmp_manager.py index c4b18c18..4ded23c5 100644 --- a/switchmap/poller/snmp/async_snmp_manager.py +++ b/switchmap/poller/snmp/async_snmp_manager.py @@ -1026,7 +1026,7 @@ def _convert(value): except (ValueError, TypeError): pass - #! appraoch 2 + #! approach 2 # Accessing .value attr directly if hasattr(value, 'value'): try: @@ -1045,7 +1045,7 @@ def _convert(value): except (ValueError, TypeError): return bytes(str(value.value), 'utf-8') - #! will check this as well (if we need it ??) + #! will check this as well (if we need it ??) ask peter or dominic sir # Default Fallback - convert to string then to bytes try: return bytes(str(value), 'utf-8') @@ -1076,10 +1076,7 @@ def _format_results(results,mock_filter, normalized = False): for oid_str,value in results: - # Defensive: Double-check OID filtering for edge cases, testing and library quirks - # Our walk methods already filter, but this catches unusual scenarios - - # FIX: Normalize both OIDs for comparison to handle leading dot mismatch + # Normalize both OIDs for comparison to handle leading dot mismatch if mock_filter: # Remove leading dots for comparison filter_normalized = mock_filter.lstrip('.') diff --git a/test_async_manager.py b/test_async_manager.py new file mode 100644 index 00000000..bdb97620 --- /dev/null +++ b/test_async_manager.py @@ -0,0 +1,119 @@ +""" +Simple script to test our async manager for misc data collections +also credentials validation && +checking for device connection +""" + +import sys +import time +import asyncio + + +sys.path.insert(0,"/Users/imexyyyyy/files/gsoc/switchmap-ng") + +from switchmap.poller.snmp import async_snmp_manager +from switchmap.poller.snmp import iana_enterprise +from switchmap.poller.configuration import ConfigPoller +from switchmap.poller import POLLING_OPTIONS, POLL + +async def test_misc_data_collection(): + print("Testing async misc data collection") + + + #testing for single device for now only + hostname = "162.249.37.218" + + try: + config = ConfigPoller() + + validate = async_snmp_manager.Validate( + POLLING_OPTIONS( + hostname=hostname, + authorizations=config.snmp_auth() + ) + ) + print(f"Testing async misc data for hostname {hostname}") + + + # getting the connection credentials + authorization = await validate.credentials() + + #!checking we no err in validation + if not authorization: + print(f"Failed to get valid snmp creds for hostname: {hostname}") + return False + + #checking creds + print(f"Group: {authorization.group}") + print(f"SNMP version: {authorization.version}") + + print(f"Secname: {authorization.secname}") + + print("Testing snmp connectivity with the device ") + + device = async_snmp_manager.Interact( + POLL( + hostname=hostname, + authorization=authorization + ) + ) + + is_contactable = await device.contactable() + + if not is_contactable: + print(f"device {hostname} is not contactable via SNMP") + return False + + print(f"Device {hostname} is contactable") + + #polling for systemID from our hostname + sysID = await device.sysobjectid() + + if not sysID: + print(f"Failed to get sysObjID from {hostname}") + + + # testing enterprise number + + enterprise_no = await device.enterprise_number() + + if enterprise_no is None: + print("Failed to get enterprise no") + return False + + print(f"Enterprise no: {enterprise_no}") + + #just for showing (simulating the misc data collection) + misc_data = { + "timestamp": int(time.time()), + "host": hostname, + "Enterprise_no": enterprise_no, + "sysObjID": sysID, + } + + print(f"Misc data collected: {misc_data}") + return True + + except Exception as e: + print(f"Exception err during async misc data test: {e}") + return False + + +async def main(): + print("starting async misc data tests") + + check_success = await test_misc_data_collection() + + if check_success: + print("Single device test passed") + else: + print("Single device test failed") + + +if __name__ == "__main__": + asyncio.run(main()) + + + + + From 7a148cdd83831a7a725c1a9078c448852c9228d4 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Fri, 18 Jul 2025 11:36:57 +0530 Subject: [PATCH 06/50] feat: initialize polling method for misc & system lv data --- switchmap/poller/snmp/test.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 switchmap/poller/snmp/test.py diff --git a/switchmap/poller/snmp/test.py b/switchmap/poller/snmp/test.py deleted file mode 100644 index e69de29b..00000000 From b6d53293bd0f8fc2cbd5b3f01028e94d92a8d63f Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sat, 19 Jul 2025 12:14:03 +0530 Subject: [PATCH 07/50] feat: added poll instance for each device & added methods to poll system queries MIBs, converted sys MIBs to async --- .gitignore | 4 ++++ switchmap/poller/snmp/mib/generic/mib_entity.py | 3 ++- switchmap/poller/snmp/mib/generic/mib_snmpv2.py | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b5583a7a..95aba4cf 100644 --- a/.gitignore +++ b/.gitignore @@ -243,3 +243,7 @@ dmypy.json cython_debug/ # End of https://www.toptal.com/developers/gitignore/api/python,jupyternotebooks + +# temp (remove after async implementation) +improvements.md + diff --git a/switchmap/poller/snmp/mib/generic/mib_entity.py b/switchmap/poller/snmp/mib/generic/mib_entity.py index f1401db8..05eb697a 100644 --- a/switchmap/poller/snmp/mib/generic/mib_entity.py +++ b/switchmap/poller/snmp/mib/generic/mib_entity.py @@ -88,7 +88,8 @@ async def system(self): # Initialize key variables data_dict = defaultdict(lambda: defaultdict(dict)) final = {} - + + #! here, also have to use asyncio gather to poll them asynchronously # Get data ( hw_rev, diff --git a/switchmap/poller/snmp/mib/generic/mib_snmpv2.py b/switchmap/poller/snmp/mib/generic/mib_snmpv2.py index b0252740..ba5c08df 100644 --- a/switchmap/poller/snmp/mib/generic/mib_snmpv2.py +++ b/switchmap/poller/snmp/mib/generic/mib_snmpv2.py @@ -93,6 +93,8 @@ async def system(self): key = 0 # Process + #! after checking every sys n layers working in async (work on major improvements) + #! use gather intead of going seqeuncial oidroot = ".1.3.6.1.2.1.1" for node in range(1, 7): oid = "{}.{}.0".format(oidroot, node) From 9b2bdab77ad39f6a908ff53c87e21594434baa14 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sat, 19 Jul 2025 14:39:49 +0530 Subject: [PATCH 08/50] Chore: added more validation on output result & fix walkv2 stuck while polling issue added a max polling timeout --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 95aba4cf..589c9a71 100644 --- a/.gitignore +++ b/.gitignore @@ -246,4 +246,4 @@ cython_debug/ # temp (remove after async implementation) improvements.md - +snmp_test.py From 54e2319e79daf8ba1bbb2cceebc65f5d16355387 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Wed, 30 Jul 2025 00:49:28 +0530 Subject: [PATCH 09/50] feat: polled layer lvl data from devices --- .gitignore | 3 + switchmap/poller/snmp/async_snmp_info.py | 131 +++++++++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/.gitignore b/.gitignore index 589c9a71..fac5671a 100644 --- a/.gitignore +++ b/.gitignore @@ -247,3 +247,6 @@ cython_debug/ # temp (remove after async implementation) improvements.md snmp_test.py +progress_work.md +switchmap_poller_new_mibs.md +switchmap_schema_new_metrics.md \ No newline at end of file diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index 122530a4..83957ccf 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -250,6 +250,137 @@ async def layer2(self): return data else: return None + + async def layer1(self): + """ + Get all layer 1 information from device. + + Args: + None + + Returns: + data: Aggregated layer1 data + """ + # Initialize key values + data = defaultdict(lambda: defaultdict(dict)) + processed = False + hostname = self.snmp_object.hostname() + + layer1_queries = get_queries("layer1") + + # Process MIB queries sequentially + for i, Query in enumerate(layer1_queries): + item = Query(self.snmp_object) + mib_name = item.__class__.__name__ + + # Check if supported + if await item.supported(): + processed = True + old_keys = list(data.keys()) + + data = await _add_layer1(item, data) + + new_keys = list(data.keys()) + added_keys = set(new_keys) - set(old_keys) + else: + print(f" MIB {mib_name} is NOT supported for {hostname}") + + # Return + if processed: + print(f"Layer1 data collected successfully for {hostname}") + else: + print(f"No layer1 MIBs supported for {hostname}") + + return data + + async def layer2(self): + """ + Args: + None + + Returns: + data: Aggregated layer2 data + + """ + # Initialize key variables + data = defaultdict(lambda: defaultdict(dict)) + processed = False + hostname = self.snmp_object.hostname() + + # Get layer2 information from MIB classes + layer2_queries = get_queries("leyer2") + #! chek layer2 queries how its functions to resolve the issue + for i,Query in enumerate(layer2_queries): + item = Query(self.snmp_object) + mib_name = item.__class__.__name__ + + # Check if supported + if await item.supported(): + processed = True + old_keys = list(data.keys()) + + data = await _add_layer2(item, data) + + new_keys = list(data.keys()) + added_keys = set(new_keys) - set(old_keys) + else: + print(f"MIB {mib_name} is not supported for {hostname}") + + # Return + if processed: + print(f"layer2 data collected successfully for {hostname}") + else: + print(f"No layer2 mibs supported for {hostname}") + + async def layer3(self): + """ + Get all layer3 information from device. + + Args: + None + + Returns: + data: Aggregated layer3 data + """ + + # Initialize key variables + data = defaultdict(lambda: defaultdict(dict)) + processed = False + hostname = self.snmp_object.hostname() + + # Get layer3 information from MIB classes + layer3_queries = get_queries("layer3") + + for i, Query in enumerate(layer3_queries): + item = Query(self.snmp_object) + mib_name = item.__class__.__name__ + + print(f"Testing MIB {i+1}/{len(layer3_queries)}: {mib_name} for {hostname}") + + # Check if supported + if await item.supported(): + print(f"MIB {mib_name} is supported for {hostname}") + processed = True + old_keys = list(data.keys()) + + data = await _add_layer3(item, data) + + new_keys = list(data.keys()) + added_keys = set(new_keys) - set(old_keys) + + print(f"MIB {mib_name} added: {list(added_keys)}") + else: + print(f"MIB {mib_name} is not supported for {hostname}") + + # Return + if processed: + print(f"Layer3 data collected successfully for {hostname}") + else: + print(f"No layer3 MIBs supported for {hostname}") + + + + async def layer3(self): """Get all layer3 information from device. From 42b6fcb79d1cadbda4c6f36ddda34360c8f00365 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Thu, 31 Jul 2025 01:04:54 +0530 Subject: [PATCH 10/50] Fix minor bugs --- switchmap/poller/snmp/async_snmp_info.py | 15 +++++++++++---- switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py | 4 ++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index 83957ccf..236c6020 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -268,16 +268,23 @@ async def layer1(self): layer1_queries = get_queries("layer1") + print(f"layer 1 level MIBs", layer1_queries) + # Process MIB queries sequentially for i, Query in enumerate(layer1_queries): item = Query(self.snmp_object) mib_name = item.__class__.__name__ + print(f"polling for MIB_name: {mib_name}") # Check if supported if await item.supported(): processed = True old_keys = list(data.keys()) + #! chck if the MIB are suppoerted + print(f"{mib_name} is supported") + + data = await _add_layer1(item, data) new_keys = list(data.keys()) @@ -308,7 +315,7 @@ async def layer2(self): hostname = self.snmp_object.hostname() # Get layer2 information from MIB classes - layer2_queries = get_queries("leyer2") + layer2_queries = get_queries("layer2") #! chek layer2 queries how its functions to resolve the issue for i,Query in enumerate(layer2_queries): item = Query(self.snmp_object) @@ -379,9 +386,6 @@ async def layer3(self): print(f"No layer3 MIBs supported for {hostname}") - - - async def layer3(self): """Get all layer3 information from device. @@ -517,7 +521,10 @@ async def _add_layer1(query, data): result = None if asyncio.iscoroutinefunction(query.layer1): + #! check if this pass + print(f"before polling") result = await query.layer1() + print(f"after polling c heck if mib2900 is passing") else: loop = asyncio.get_event_loop() result = await loop.run_in_executor(None, query.layer1) diff --git a/switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py b/switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py index 2e6daf6b..d22a59d2 100644 --- a/switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py +++ b/switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py @@ -90,6 +90,8 @@ async def layer1(self): for key, value in values.items(): final[key]["c2900PortLinkbeatStatus"] = value + print(f"ciscoc2900 data: ", final) + # Return return final @@ -116,6 +118,8 @@ async def c2900portlinkbeatstatus(self, oidonly=False): results = await self.snmp_object.swalk(oid, normalized=True) for key, value in results.items(): data_dict[int(key)] = value + + print(f"Debug: Processed data_dict: {data_dict}") # Return the interface descriptions return data_dict From 17ef72d0fbfe18bada1dfe6753ec18cc22965d7f Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sun, 3 Aug 2025 15:21:48 +0530 Subject: [PATCH 11/50] feat: migrated all layer1 mibs to async, with concurrent polling --- switchmap/poller/snmp/async_snmp_info.py | 2 - .../poller/snmp/mib/cisco/mib_ciscovtp.py | 6 +- .../poller/snmp/mib/generic/mib_qbridge.py | 2 +- test_async_manager.py | 119 ------------------ 4 files changed, 5 insertions(+), 124 deletions(-) delete mode 100644 test_async_manager.py diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index 236c6020..413a9265 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -297,7 +297,6 @@ async def layer1(self): print(f"Layer1 data collected successfully for {hostname}") else: print(f"No layer1 MIBs supported for {hostname}") - return data async def layer2(self): @@ -524,7 +523,6 @@ async def _add_layer1(query, data): #! check if this pass print(f"before polling") result = await query.layer1() - print(f"after polling c heck if mib2900 is passing") else: loop = asyncio.get_event_loop() result = await loop.run_in_executor(None, query.layer1) diff --git a/switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py b/switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py index 5db0d36e..4320a976 100644 --- a/switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py +++ b/switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py @@ -187,7 +187,8 @@ async def vlantrunkportencapsulationtype(self, oidonly=False): results = await self.snmp_object.swalk(oid, normalized=True) for key, value in results.items(): data_dict[int(key)] = value - + + print(f"result: {results}") # Return the interface descriptions return data_dict @@ -401,7 +402,8 @@ async def vlantrunkportvlansenabled(self, oidonly=False): for key, value in results.items(): # Get the ifindex value ifindex = int(key) - + + #! is this needed in pysnmp, have to check it throughly # Convert hex value to right justified 1024 character binary string vlans_hex = binascii.hexlify(value).decode("utf-8") binary_string = bin(int(vlans_hex, base))[2:].zfill(length_in_bits) diff --git a/switchmap/poller/snmp/mib/generic/mib_qbridge.py b/switchmap/poller/snmp/mib/generic/mib_qbridge.py index 95722732..3113943d 100644 --- a/switchmap/poller/snmp/mib/generic/mib_qbridge.py +++ b/switchmap/poller/snmp/mib/generic/mib_qbridge.py @@ -62,7 +62,7 @@ def __init__(self, snmp_object): """ # Define query object - self.snmp_object = snmp_object + self._snmp_object = snmp_object # Get one OID entry in MIB (dot1qPvid) test_oid = ".1.3.6.1.2.1.17.7.1.4.5.1.1" diff --git a/test_async_manager.py b/test_async_manager.py deleted file mode 100644 index bdb97620..00000000 --- a/test_async_manager.py +++ /dev/null @@ -1,119 +0,0 @@ -""" -Simple script to test our async manager for misc data collections -also credentials validation && -checking for device connection -""" - -import sys -import time -import asyncio - - -sys.path.insert(0,"/Users/imexyyyyy/files/gsoc/switchmap-ng") - -from switchmap.poller.snmp import async_snmp_manager -from switchmap.poller.snmp import iana_enterprise -from switchmap.poller.configuration import ConfigPoller -from switchmap.poller import POLLING_OPTIONS, POLL - -async def test_misc_data_collection(): - print("Testing async misc data collection") - - - #testing for single device for now only - hostname = "162.249.37.218" - - try: - config = ConfigPoller() - - validate = async_snmp_manager.Validate( - POLLING_OPTIONS( - hostname=hostname, - authorizations=config.snmp_auth() - ) - ) - print(f"Testing async misc data for hostname {hostname}") - - - # getting the connection credentials - authorization = await validate.credentials() - - #!checking we no err in validation - if not authorization: - print(f"Failed to get valid snmp creds for hostname: {hostname}") - return False - - #checking creds - print(f"Group: {authorization.group}") - print(f"SNMP version: {authorization.version}") - - print(f"Secname: {authorization.secname}") - - print("Testing snmp connectivity with the device ") - - device = async_snmp_manager.Interact( - POLL( - hostname=hostname, - authorization=authorization - ) - ) - - is_contactable = await device.contactable() - - if not is_contactable: - print(f"device {hostname} is not contactable via SNMP") - return False - - print(f"Device {hostname} is contactable") - - #polling for systemID from our hostname - sysID = await device.sysobjectid() - - if not sysID: - print(f"Failed to get sysObjID from {hostname}") - - - # testing enterprise number - - enterprise_no = await device.enterprise_number() - - if enterprise_no is None: - print("Failed to get enterprise no") - return False - - print(f"Enterprise no: {enterprise_no}") - - #just for showing (simulating the misc data collection) - misc_data = { - "timestamp": int(time.time()), - "host": hostname, - "Enterprise_no": enterprise_no, - "sysObjID": sysID, - } - - print(f"Misc data collected: {misc_data}") - return True - - except Exception as e: - print(f"Exception err during async misc data test: {e}") - return False - - -async def main(): - print("starting async misc data tests") - - check_success = await test_misc_data_collection() - - if check_success: - print("Single device test passed") - else: - print("Single device test failed") - - -if __name__ == "__main__": - asyncio.run(main()) - - - - - From 40ce2cc74da5f2bec4cc61425d40f2a9d3b47260 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sun, 3 Aug 2025 15:41:52 +0530 Subject: [PATCH 12/50] reformatted files with black --- switchmap/poller/snmp/async_poller.py | 1 + switchmap/poller/snmp/async_snmp_info.py | 73 +++++++------- switchmap/poller/snmp/async_snmp_manager.py | 94 +++++++++++-------- .../poller/snmp/mib/cisco/mib_ciscoc2900.py | 2 +- .../poller/snmp/mib/cisco/mib_ciscovtp.py | 4 +- .../poller/snmp/mib/generic/mib_entity.py | 2 +- 6 files changed, 96 insertions(+), 80 deletions(-) diff --git a/switchmap/poller/snmp/async_poller.py b/switchmap/poller/snmp/async_poller.py index debb8b32..6928da68 100644 --- a/switchmap/poller/snmp/async_poller.py +++ b/switchmap/poller/snmp/async_poller.py @@ -8,6 +8,7 @@ from switchmap.core import log + class Poll: """Asynchronous SNMP poller for switchmap-ng that gathers network data. diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index 413a9265..14e3f4b1 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -250,21 +250,21 @@ async def layer2(self): return data else: return None - + async def layer1(self): """ Get all layer 1 information from device. - Args: + Args: None - + Returns: data: Aggregated layer1 data """ - # Initialize key values + # Initialize key values data = defaultdict(lambda: defaultdict(dict)) - processed = False - hostname = self.snmp_object.hostname() + processed = False + hostname = self.snmp_object.hostname() layer1_queries = get_queries("layer1") @@ -273,54 +273,53 @@ async def layer1(self): # Process MIB queries sequentially for i, Query in enumerate(layer1_queries): item = Query(self.snmp_object) - mib_name = item.__class__.__name__ + mib_name = item.__class__.__name__ print(f"polling for MIB_name: {mib_name}") - # Check if supported + # Check if supported if await item.supported(): - processed = True + processed = True old_keys = list(data.keys()) #! chck if the MIB are suppoerted print(f"{mib_name} is supported") - data = await _add_layer1(item, data) new_keys = list(data.keys()) added_keys = set(new_keys) - set(old_keys) else: print(f" MIB {mib_name} is NOT supported for {hostname}") - - # Return + + # Return if processed: print(f"Layer1 data collected successfully for {hostname}") else: print(f"No layer1 MIBs supported for {hostname}") - return data - + return data + async def layer2(self): """ - Args: - None - - Returns: + Args: + None + + Returns: data: Aggregated layer2 data """ # Initialize key variables data = defaultdict(lambda: defaultdict(dict)) - processed = False - hostname = self.snmp_object.hostname() + processed = False + hostname = self.snmp_object.hostname() # Get layer2 information from MIB classes layer2_queries = get_queries("layer2") #! chek layer2 queries how its functions to resolve the issue - for i,Query in enumerate(layer2_queries): + for i, Query in enumerate(layer2_queries): item = Query(self.snmp_object) - mib_name = item.__class__.__name__ + mib_name = item.__class__.__name__ - # Check if supported + # Check if supported if await item.supported(): processed = True old_keys = list(data.keys()) @@ -331,13 +330,13 @@ async def layer2(self): added_keys = set(new_keys) - set(old_keys) else: print(f"MIB {mib_name} is not supported for {hostname}") - - # Return + + # Return if processed: print(f"layer2 data collected successfully for {hostname}") else: print(f"No layer2 mibs supported for {hostname}") - + async def layer3(self): """ Get all layer3 information from device. @@ -346,24 +345,26 @@ async def layer3(self): None Returns: - data: Aggregated layer3 data + data: Aggregated layer3 data """ # Initialize key variables data = defaultdict(lambda: defaultdict(dict)) - processed = False - hostname = self.snmp_object.hostname() + processed = False + hostname = self.snmp_object.hostname() # Get layer3 information from MIB classes layer3_queries = get_queries("layer3") for i, Query in enumerate(layer3_queries): item = Query(self.snmp_object) - mib_name = item.__class__.__name__ + mib_name = item.__class__.__name__ - print(f"Testing MIB {i+1}/{len(layer3_queries)}: {mib_name} for {hostname}") + print( + f"Testing MIB {i+1}/{len(layer3_queries)}: {mib_name} for {hostname}" + ) - # Check if supported + # Check if supported if await item.supported(): print(f"MIB {mib_name} is supported for {hostname}") processed = True @@ -377,13 +378,13 @@ async def layer3(self): print(f"MIB {mib_name} added: {list(added_keys)}") else: print(f"MIB {mib_name} is not supported for {hostname}") - - # Return + + # Return if processed: print(f"Layer3 data collected successfully for {hostname}") else: print(f"No layer3 MIBs supported for {hostname}") - + async def layer3(self): """Get all layer3 information from device. @@ -520,7 +521,7 @@ async def _add_layer1(query, data): result = None if asyncio.iscoroutinefunction(query.layer1): - #! check if this pass + #! check if this pass print(f"before polling") result = await query.layer1() else: diff --git a/switchmap/poller/snmp/async_snmp_manager.py b/switchmap/poller/snmp/async_snmp_manager.py index 4ded23c5..58c2b832 100644 --- a/switchmap/poller/snmp/async_snmp_manager.py +++ b/switchmap/poller/snmp/async_snmp_manager.py @@ -979,6 +979,7 @@ def _oid_valid_format(oid): # Otherwise valid return True + def _convert(value): """Convert SNMP value from pysnmp object to Python type. @@ -992,68 +993,81 @@ def _convert(value): # Handle pysnmp exception values if isinstance(value, NoSuchObject): - return None + return None if isinstance(value, NoSuchInstance): return None if isinstance(value, EndOfMibView): - return None - - if hasattr(value, 'prettyPrint'): + return None + + if hasattr(value, "prettyPrint"): value_str = value.prettyPrint() - #Determine type based on pysnmp object type - value_type = type(value).__name__ + # Determine type based on pysnmp object type + value_type = type(value).__name__ - # Handle string-like types - Convert to types for MIB compatibility - if any(t in value_type for t in ['OctetString','DisplayString','Opaque','Bits','IpAddress','ObjectIdentifier']): + # Handle string-like types - Convert to types for MIB compatibility + if any( + t in value_type + for t in [ + "OctetString", + "DisplayString", + "Opaque", + "Bits", + "IpAddress", + "ObjectIdentifier", + ] + ): # For objectID, convert to string first then to bytes - if 'ObjectIdentifier' in value_type: - return bytes(str(value_str), 'utf-8') + if "ObjectIdentifier" in value_type: + return bytes(str(value_str), "utf-8") else: - return bytes(value_str,'utf-8') - - #Handle integer types - elif any(t in value_type for t in ['Integer','Counter','Gauge','TimeTicks','Unsigned']): + return bytes(value_str, "utf-8") + + # Handle integer types + elif any( + t in value_type + for t in ["Integer", "Counter", "Gauge", "TimeTicks", "Unsigned"] + ): try: return int(value_str) except ValueError: #! clear on this once again #! approach 1 # Direct int conversion of the obj if prettyPrint fails - if hasattr(value, '__int__'): + if hasattr(value, "__int__"): try: return int(value) except (ValueError, TypeError): - pass + pass - #! approach 2 + #! approach 2 # Accessing .value attr directly - if hasattr(value, 'value'): + if hasattr(value, "value"): try: - return int(value.value) + return int(value.value) except (ValueError, TypeError): - pass - + pass + log_message = f"Failed to convert pysnmp integer valye: {value_type}, prettyPrint'{value_str}" log.log2warning(1059, log_message) return None - + # Handle direct access to value (for objects without prettyPrint) - if hasattr(value, 'value'): + if hasattr(value, "value"): try: return int(value.value) except (ValueError, TypeError): - return bytes(str(value.value), 'utf-8') - + return bytes(str(value.value), "utf-8") + #! will check this as well (if we need it ??) ask peter or dominic sir - # Default Fallback - convert to string then to bytes + # Default Fallback - convert to string then to bytes try: - return bytes(str(value), 'utf-8') + return bytes(str(value), "utf-8") except Exception: - return None - + return None + -def _format_results(results,mock_filter, normalized = False): +def _format_results(results, mock_filter, normalized=False): """Normalized and format SNMP results Args: @@ -1069,33 +1083,33 @@ def _format_results(results,mock_filter, normalized = False): Returns: dict: Formatted results as OID-value pairs - + """ formatted = {} - for oid_str,value in results: + for oid_str, value in results: # Normalize both OIDs for comparison to handle leading dot mismatch if mock_filter: # Remove leading dots for comparison - filter_normalized = mock_filter.lstrip('.') - oid_normalized = oid_str.lstrip('.') - + filter_normalized = mock_filter.lstrip(".") + oid_normalized = oid_str.lstrip(".") + if filter_normalized not in oid_normalized: - continue + continue - #convert value using proper type conversion + # convert value using proper type conversion converted_value = _convert(value=value) if normalized is True: - #use only the last node of the OID - key = oid_str.split('.')[-1] + # use only the last node of the OID + key = oid_str.split(".")[-1] else: key = oid_str formatted[key] = converted_value - + return formatted diff --git a/switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py b/switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py index d22a59d2..e431f5ea 100644 --- a/switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py +++ b/switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py @@ -118,7 +118,7 @@ async def c2900portlinkbeatstatus(self, oidonly=False): results = await self.snmp_object.swalk(oid, normalized=True) for key, value in results.items(): data_dict[int(key)] = value - + print(f"Debug: Processed data_dict: {data_dict}") # Return the interface descriptions diff --git a/switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py b/switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py index 4320a976..b440d1f1 100644 --- a/switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py +++ b/switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py @@ -187,7 +187,7 @@ async def vlantrunkportencapsulationtype(self, oidonly=False): results = await self.snmp_object.swalk(oid, normalized=True) for key, value in results.items(): data_dict[int(key)] = value - + print(f"result: {results}") # Return the interface descriptions return data_dict @@ -402,7 +402,7 @@ async def vlantrunkportvlansenabled(self, oidonly=False): for key, value in results.items(): # Get the ifindex value ifindex = int(key) - + #! is this needed in pysnmp, have to check it throughly # Convert hex value to right justified 1024 character binary string vlans_hex = binascii.hexlify(value).decode("utf-8") diff --git a/switchmap/poller/snmp/mib/generic/mib_entity.py b/switchmap/poller/snmp/mib/generic/mib_entity.py index 05eb697a..a16bced2 100644 --- a/switchmap/poller/snmp/mib/generic/mib_entity.py +++ b/switchmap/poller/snmp/mib/generic/mib_entity.py @@ -88,7 +88,7 @@ async def system(self): # Initialize key variables data_dict = defaultdict(lambda: defaultdict(dict)) final = {} - + #! here, also have to use asyncio gather to poll them asynchronously # Get data ( From c32c53b86e87610cc83a04b69ec4d4b46a75d2b8 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sun, 3 Aug 2025 16:58:47 +0530 Subject: [PATCH 13/50] chore: concurrent poll for mib_entity oids --- switchmap/poller/snmp/async_snmp_info.py | 4 ++-- switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py | 1 - switchmap/poller/snmp/mib/generic/mib_entity.py | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index 14e3f4b1..46d98382 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -280,8 +280,6 @@ async def layer1(self): if await item.supported(): processed = True old_keys = list(data.keys()) - - #! chck if the MIB are suppoerted print(f"{mib_name} is supported") data = await _add_layer1(item, data) @@ -336,6 +334,7 @@ async def layer2(self): print(f"layer2 data collected successfully for {hostname}") else: print(f"No layer2 mibs supported for {hostname}") + return data async def layer3(self): """ @@ -384,6 +383,7 @@ async def layer3(self): print(f"Layer3 data collected successfully for {hostname}") else: print(f"No layer3 MIBs supported for {hostname}") + return data async def layer3(self): diff --git a/switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py b/switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py index b440d1f1..de6ee4c3 100644 --- a/switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py +++ b/switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py @@ -188,7 +188,6 @@ async def vlantrunkportencapsulationtype(self, oidonly=False): for key, value in results.items(): data_dict[int(key)] = value - print(f"result: {results}") # Return the interface descriptions return data_dict diff --git a/switchmap/poller/snmp/mib/generic/mib_entity.py b/switchmap/poller/snmp/mib/generic/mib_entity.py index a16bced2..f1401db8 100644 --- a/switchmap/poller/snmp/mib/generic/mib_entity.py +++ b/switchmap/poller/snmp/mib/generic/mib_entity.py @@ -89,7 +89,6 @@ async def system(self): data_dict = defaultdict(lambda: defaultdict(dict)) final = {} - #! here, also have to use asyncio gather to poll them asynchronously # Get data ( hw_rev, From 765a666ee0f1e19455a58864305ccc88c7408dcd Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Tue, 5 Aug 2025 17:45:15 +0530 Subject: [PATCH 14/50] feat: layer2 & layer3 async migration completed --- switchmap/poller/snmp/async_snmp_info.py | 11 +++++++---- switchmap/poller/snmp/mib/generic/mib_ip.py | 2 +- switchmap/poller/snmp/mib/generic/mib_ipv6.py | 2 +- switchmap/poller/snmp/mib/generic/mib_qbridge.py | 2 +- switchmap/poller/snmp/mib/juniper/mib_junipervlan.py | 2 +- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index 46d98382..66360d03 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -270,7 +270,7 @@ async def layer1(self): print(f"layer 1 level MIBs", layer1_queries) - # Process MIB queries sequentially + #! Process MIB queries sequentially for i, Query in enumerate(layer1_queries): item = Query(self.snmp_object) mib_name = item.__class__.__name__ @@ -312,7 +312,8 @@ async def layer2(self): # Get layer2 information from MIB classes layer2_queries = get_queries("layer2") - #! chek layer2 queries how its functions to resolve the issue + + # !Process layer2 MIBs sequentially fn for i, Query in enumerate(layer2_queries): item = Query(self.snmp_object) mib_name = item.__class__.__name__ @@ -321,6 +322,7 @@ async def layer2(self): if await item.supported(): processed = True old_keys = list(data.keys()) + print(f"{mib_name} is supported") data = await _add_layer2(item, data) @@ -354,7 +356,9 @@ async def layer3(self): # Get layer3 information from MIB classes layer3_queries = get_queries("layer3") - + print(f"layer3 level MIBs", layer3_queries) + + #! currently polling mibs sequencially for i, Query in enumerate(layer3_queries): item = Query(self.snmp_object) mib_name = item.__class__.__name__ @@ -521,7 +525,6 @@ async def _add_layer1(query, data): result = None if asyncio.iscoroutinefunction(query.layer1): - #! check if this pass print(f"before polling") result = await query.layer1() else: diff --git a/switchmap/poller/snmp/mib/generic/mib_ip.py b/switchmap/poller/snmp/mib/generic/mib_ip.py index 2b716a1e..d4526f88 100644 --- a/switchmap/poller/snmp/mib/generic/mib_ip.py +++ b/switchmap/poller/snmp/mib/generic/mib_ip.py @@ -61,7 +61,7 @@ def __init__(self, snmp_object): """ # Define query object - self.snmp_object = snmp_object + self._snmp_object = snmp_object super().__init__(snmp_object, "", tags=["layer3"]) diff --git a/switchmap/poller/snmp/mib/generic/mib_ipv6.py b/switchmap/poller/snmp/mib/generic/mib_ipv6.py index 660e3b7d..fffa1ba8 100644 --- a/switchmap/poller/snmp/mib/generic/mib_ipv6.py +++ b/switchmap/poller/snmp/mib/generic/mib_ipv6.py @@ -61,7 +61,7 @@ def __init__(self, snmp_object): """ # Define query object - self.snmp_object = snmp_object + self._snmp_object = snmp_object # Get one OID entry in MIB (ipv6Forwarding) test_oid = ".1.3.6.1.2.1.55.1.1" diff --git a/switchmap/poller/snmp/mib/generic/mib_qbridge.py b/switchmap/poller/snmp/mib/generic/mib_qbridge.py index 3113943d..d3e8bdd7 100644 --- a/switchmap/poller/snmp/mib/generic/mib_qbridge.py +++ b/switchmap/poller/snmp/mib/generic/mib_qbridge.py @@ -30,7 +30,6 @@ def init_query(snmp_object): """ return QbridgeQuery(snmp_object) - class QbridgeQuery(Query): """Class interacts with Q-BRIDGE-MIB. @@ -74,6 +73,7 @@ def __init__(self, snmp_object): async def _get_bridge_data(self): """Load bridge data only when needed.""" + if self.baseportifindex is None: self.bridge_mib = BridgeQuery(self.snmp_object) diff --git a/switchmap/poller/snmp/mib/juniper/mib_junipervlan.py b/switchmap/poller/snmp/mib/juniper/mib_junipervlan.py index 24cbc249..9872dabd 100644 --- a/switchmap/poller/snmp/mib/juniper/mib_junipervlan.py +++ b/switchmap/poller/snmp/mib/juniper/mib_junipervlan.py @@ -64,7 +64,7 @@ def __init__(self, snmp_object): """ # Define query object - self.snmp_object = snmp_object + self._snmp_object = snmp_object # Get one OID entry in MIB (jnxExVlanTag) test_oid = ".1.3.6.1.4.1.2636.3.40.1.5.1.7.1.3" From 64284e8fcad52423d6cb7597cac5dfbf507d8b50 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sun, 10 Aug 2025 17:49:18 +0530 Subject: [PATCH 15/50] chore: replace current multiprocessing-based device polling --- debug_async_flow.py | 5 +- switchmap/poller/async_poll.py | 203 +++++++++++++++++++++++++++++++++ test_all_devices.py | 14 +++ 3 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 switchmap/poller/async_poll.py create mode 100644 test_all_devices.py diff --git a/debug_async_flow.py b/debug_async_flow.py index 158d61b3..2841ed24 100644 --- a/debug_async_flow.py +++ b/debug_async_flow.py @@ -12,7 +12,7 @@ # once i make polling async then will update this according to that to check if devices are being polled asynchronously #for now for testing using cli_device to poll single device for debugging -from switchmap.poller import poll +from switchmap.poller import async_poll def main(): print("Switchmap polling flow debugger") @@ -22,11 +22,10 @@ def main(): hostname= "162.249.37.218" print(f"starting debug poll for hostname: {hostname}") - poll.cli_device(hostname=hostname) + async_poll.run_cli_device(hostname=hostname) except Exception as e: print(f"Error duing polling: {e}") if __name__ == "__main__": main() - diff --git a/switchmap/poller/async_poll.py b/switchmap/poller/async_poll.py new file mode 100644 index 00000000..4a83b83e --- /dev/null +++ b/switchmap/poller/async_poll.py @@ -0,0 +1,203 @@ +"""Async Switchmap-NG poll module.""" + +import asyncio +from collections import namedtuple +from pprint import pprint +import os +import time + +# Import app libraries +from switchmap import API_POLLER_POST_URI +from switchmap.poller.snmp import async_poller +from switchmap.poller.update import device as udevice +from switchmap.poller.configuration import ConfigPoller +from switchmap.core import log,rest,files +from switchmap import AGENT_POLLER + +_META = namedtuple("_META", "zone hostname config") + + +async def devices(max_concurrent_devices=10): + """Poll all devices asynchronously. + + Args: + max_concurrent_devices: Maximum number of devices to poll concurrently + + Returns: + None + """ + + # Initialize key variables + arguments = [] + + # Get configuration + config = ConfigPoller() + + # Create a list of polling objects + zones = sorted(config.zones()) + + for zone in zones: + arguments.extend( + _META(zone=zone.name, hostname=_, config=config) for _ in zone.hostnames + ) + + if not arguments: + log_message = "No devices found in configuration" + log.log2info(1400, log_message) + return + + log_message = f"Starting async polling of {len(arguments)} devices with max concurrency: {max_concurrent_devices}" + log.log2info(1401, log_message) + + # Semaphore to limit concurrent devices + device_semaphore = asyncio.Semaphore(max_concurrent_devices) + + tasks = [ + device(argument, device_semaphore, post=True) for argument in arguments + ] + + # Execute all devices concurrently + start_time = time.time() + results = await asyncio.gather(*tasks, return_exceptions=True) + end_time = time.time() + + # Process results and log summary + success_count = sum(1 for r in results if r is True) + error_count = sum(1 for r in results if isinstance(r, Exception)) + failed_count = len(results) - success_count - error_count + + log_message = f"Polling completed in {end_time - start_time:.2f}s: {success_count} succeeded, {failed_count} failed, {error_count} errors" + log.log2info(1402, log_message) + + # Log specific errors + for i, result in enumerate(results): + if isinstance(result, Exception): + hostname = arguments[i].hostname + log_message = f"Device {hostname} polling error: {result}" + log.log2warning(1403, log_message) + + +async def device(poll_meta, device_semaphore, post=True): + """Poll each device asynchoronously. + + Args: + poll_meta: _META object containing zone, hostname, config + device_semaphore: Semaphore to limit concurrent devices + post: Post the data if True, else just print it + + Returns: + bool: True if successful, False otherwise + """ + + async with device_semaphore: + # Initialize key variables + hostname = poll_meta.hostname + zone = poll_meta.zone + config = poll_meta.config + + # Do nothing if the skip file exists + skip_file = files.skip_file(AGENT_POLLER, config) + if os.path.isfile(skip_file): + log_message = f"Skip file {skip_file} found. Aborting poll for {hostname} in zone '{zone}'" + log.log2debug(1404, log_message) + return False + + # Poll data for obviously valid hostname + if not hostname or not isinstance(hostname, str) or hostname.lower() == "none": + log_message = f"Invalid hostname: {hostname}" + log.log2debug(1405, log_message) + return False + + try: + poll = async_poller.Poll(hostname) + + # Initialize SNMP connection + if not await poll.initialize_snmp(): + log_message = f"Failed to initialize SNMP for {hostname}" + log.log2debug(1406, log_message) + return False + + # Query device data asynchronously + snmp_data = await poll.query() + + # Process if we get valid data + if bool(snmp_data) and isinstance(snmp_data, dict): + # Process device data + _device = udevice.Device(snmp_data) + data = _device.process() + data["misc"]["zone"] = zone + + #! do a little research on aiohttp + if post: + rest.post(API_POLLER_POST_URI, data, config) + log_message = f"Successfully polled and posted data for {hostname}" + log.log2debug(1407, log_message) + else: + pprint(data) + + return True + else: + log_message = f"Device {hostname} returns no data. Check connectivity/SNMP configuration" + log.log2debug(1408, log_message) + return False + + except Exception as e: + log_message = f"Unexpected error polling device {hostname}: {e}" + log.log2exception(1409, log_message) + return False + +async def cli_device(hostname): + """Poll single device for data - CLI interface. + + Args: + hostname: Host to poll + + Returns: + None + """ + # Initialize key variables + arguments = [] + + # Get configuration + config = ConfigPoller() + + # Create a list of polling objects + zones = sorted(config.zones()) + + # Create a list of arguments + for zone in zones: + for next_hostname in zone.hostnames: + if next_hostname == hostname: + arguments.append( + _META(zone=zone.name, hostname=hostname, config=config) + ) + + if arguments: + log_message = f"Found {hostname} in {len(arguments)} zone(s), starting async poll" + log.log2info(1410, log_message) + + # Poll each zone occurrence + tasks = [device(argument,asyncio.Semaphore(1) ,post=False) for argument in arguments] + results = await asyncio.gather(*tasks, return_exceptions=True) + + # Check results + success_count = sum(1 for r in results if r is True) + if success_count > 0: + log_message = f"Successfully polled {hostname} from {success_count}/{len(results)} zone(s)" + log.log2info(1411, log_message) + else: + log_message = f"Failed to poll {hostname} from any configured zone" + log.log2warning(1412, log_message) + + else: + log_message = f"No hostname {hostname} found in configuration" + log.log2see(1413, log_message) + +def run_devices(max_concurrent_devices=10): + """Run device polling - main entry point.""" + asyncio.run(devices(max_concurrent_devices)) + +def run_cli_device(hostname): + """Run CLI device polling - main entry point.""" + asyncio.run(cli_device(hostname)) + diff --git a/test_all_devices.py b/test_all_devices.py new file mode 100644 index 00000000..d1b6b831 --- /dev/null +++ b/test_all_devices.py @@ -0,0 +1,14 @@ +# Create test_all_devices.py +import sys +sys.path.insert(0,'/Users/imexyyyyy/files/gsoc/switchmap-ng') + +from switchmap.poller import async_poll + +def main(): + print("Testing ALL devices async polling...") + + # Test with lower concurrency first + async_poll.run_devices(max_concurrent_devices=3) + +if __name__ == "__main__": + main() \ No newline at end of file From 810260e3505ef96c85a7d32ec0c5a535db2d9829 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sun, 10 Aug 2025 17:49:39 +0530 Subject: [PATCH 16/50] lint fixed --- debug_async_flow.py | 10 +- switchmap/poller/async_poll.py | 116 ++++++++++-------- switchmap/poller/snmp/async_snmp_info.py | 2 +- .../poller/snmp/mib/generic/mib_qbridge.py | 3 +- test_all_devices.py | 9 +- 5 files changed, 80 insertions(+), 60 deletions(-) diff --git a/debug_async_flow.py b/debug_async_flow.py index 2841ed24..dd23719a 100644 --- a/debug_async_flow.py +++ b/debug_async_flow.py @@ -7,25 +7,27 @@ import asyncio -sys.path.insert(0,'/Users/imexyyyyy/files/gsoc/switchmap-ng'); +sys.path.insert(0, "/Users/imexyyyyy/files/gsoc/switchmap-ng") # once i make polling async then will update this according to that to check if devices are being polled asynchronously -#for now for testing using cli_device to poll single device for debugging +# for now for testing using cli_device to poll single device for debugging from switchmap.poller import async_poll + def main(): print("Switchmap polling flow debugger") try: - hostname= "162.249.37.218" + hostname = "162.249.37.218" print(f"starting debug poll for hostname: {hostname}") async_poll.run_cli_device(hostname=hostname) - + except Exception as e: print(f"Error duing polling: {e}") + if __name__ == "__main__": main() diff --git a/switchmap/poller/async_poll.py b/switchmap/poller/async_poll.py index 4a83b83e..2f590bbb 100644 --- a/switchmap/poller/async_poll.py +++ b/switchmap/poller/async_poll.py @@ -4,14 +4,14 @@ from collections import namedtuple from pprint import pprint import os -import time +import time -# Import app libraries +# Import app libraries from switchmap import API_POLLER_POST_URI from switchmap.poller.snmp import async_poller from switchmap.poller.update import device as udevice from switchmap.poller.configuration import ConfigPoller -from switchmap.core import log,rest,files +from switchmap.core import log, rest, files from switchmap import AGENT_POLLER _META = namedtuple("_META", "zone hostname config") @@ -27,36 +27,37 @@ async def devices(max_concurrent_devices=10): None """ - # Initialize key variables - arguments = [] + # Initialize key variables + arguments = [] - # Get configuration - config = ConfigPoller() + # Get configuration + config = ConfigPoller() - # Create a list of polling objects + # Create a list of polling objects zones = sorted(config.zones()) for zone in zones: arguments.extend( - _META(zone=zone.name, hostname=_, config=config) for _ in zone.hostnames + _META(zone=zone.name, hostname=_, config=config) + for _ in zone.hostnames ) - + if not arguments: log_message = "No devices found in configuration" log.log2info(1400, log_message) return - + log_message = f"Starting async polling of {len(arguments)} devices with max concurrency: {max_concurrent_devices}" log.log2info(1401, log_message) - # Semaphore to limit concurrent devices + # Semaphore to limit concurrent devices device_semaphore = asyncio.Semaphore(max_concurrent_devices) tasks = [ device(argument, device_semaphore, post=True) for argument in arguments ] - # Execute all devices concurrently + # Execute all devices concurrently start_time = time.time() results = await asyncio.gather(*tasks, return_exceptions=True) end_time = time.time() @@ -65,87 +66,94 @@ async def devices(max_concurrent_devices=10): success_count = sum(1 for r in results if r is True) error_count = sum(1 for r in results if isinstance(r, Exception)) failed_count = len(results) - success_count - error_count - + log_message = f"Polling completed in {end_time - start_time:.2f}s: {success_count} succeeded, {failed_count} failed, {error_count} errors" log.log2info(1402, log_message) - # Log specific errors + # Log specific errors for i, result in enumerate(results): if isinstance(result, Exception): hostname = arguments[i].hostname log_message = f"Device {hostname} polling error: {result}" log.log2warning(1403, log_message) - + async def device(poll_meta, device_semaphore, post=True): """Poll each device asynchoronously. Args: - poll_meta: _META object containing zone, hostname, config - device_semaphore: Semaphore to limit concurrent devices - post: Post the data if True, else just print it - - Returns: - bool: True if successful, False otherwise + poll_meta: _META object containing zone, hostname, config + device_semaphore: Semaphore to limit concurrent devices + post: Post the data if True, else just print it + + Returns: + bool: True if successful, False otherwise """ async with device_semaphore: - # Initialize key variables - hostname = poll_meta.hostname - zone = poll_meta.zone - config = poll_meta.config + # Initialize key variables + hostname = poll_meta.hostname + zone = poll_meta.zone + config = poll_meta.config - # Do nothing if the skip file exists + # Do nothing if the skip file exists skip_file = files.skip_file(AGENT_POLLER, config) if os.path.isfile(skip_file): log_message = f"Skip file {skip_file} found. Aborting poll for {hostname} in zone '{zone}'" log.log2debug(1404, log_message) return False - + # Poll data for obviously valid hostname - if not hostname or not isinstance(hostname, str) or hostname.lower() == "none": + if ( + not hostname + or not isinstance(hostname, str) + or hostname.lower() == "none" + ): log_message = f"Invalid hostname: {hostname}" log.log2debug(1405, log_message) return False - + try: poll = async_poller.Poll(hostname) - # Initialize SNMP connection + # Initialize SNMP connection if not await poll.initialize_snmp(): log_message = f"Failed to initialize SNMP for {hostname}" log.log2debug(1406, log_message) return False - + # Query device data asynchronously - snmp_data = await poll.query() + snmp_data = await poll.query() - # Process if we get valid data + # Process if we get valid data if bool(snmp_data) and isinstance(snmp_data, dict): - # Process device data + # Process device data _device = udevice.Device(snmp_data) data = _device.process() - data["misc"]["zone"] = zone - + data["misc"]["zone"] = zone + #! do a little research on aiohttp if post: rest.post(API_POLLER_POST_URI, data, config) - log_message = f"Successfully polled and posted data for {hostname}" + log_message = ( + f"Successfully polled and posted data for {hostname}" + ) log.log2debug(1407, log_message) else: pprint(data) - - return True + + return True else: log_message = f"Device {hostname} returns no data. Check connectivity/SNMP configuration" log.log2debug(1408, log_message) return False - + except Exception as e: log_message = f"Unexpected error polling device {hostname}: {e}" log.log2exception(1409, log_message) return False + async def cli_device(hostname): """Poll single device for data - CLI interface. @@ -157,13 +165,13 @@ async def cli_device(hostname): """ # Initialize key variables arguments = [] - + # Get configuration config = ConfigPoller() - + # Create a list of polling objects zones = sorted(config.zones()) - + # Create a list of arguments for zone in zones: for next_hostname in zone.hostnames: @@ -171,15 +179,20 @@ async def cli_device(hostname): arguments.append( _META(zone=zone.name, hostname=hostname, config=config) ) - + if arguments: - log_message = f"Found {hostname} in {len(arguments)} zone(s), starting async poll" + log_message = ( + f"Found {hostname} in {len(arguments)} zone(s), starting async poll" + ) log.log2info(1410, log_message) - + # Poll each zone occurrence - tasks = [device(argument,asyncio.Semaphore(1) ,post=False) for argument in arguments] + tasks = [ + device(argument, asyncio.Semaphore(1), post=False) + for argument in arguments + ] results = await asyncio.gather(*tasks, return_exceptions=True) - + # Check results success_count = sum(1 for r in results if r is True) if success_count > 0: @@ -188,16 +201,17 @@ async def cli_device(hostname): else: log_message = f"Failed to poll {hostname} from any configured zone" log.log2warning(1412, log_message) - + else: log_message = f"No hostname {hostname} found in configuration" log.log2see(1413, log_message) + def run_devices(max_concurrent_devices=10): """Run device polling - main entry point.""" asyncio.run(devices(max_concurrent_devices)) + def run_cli_device(hostname): """Run CLI device polling - main entry point.""" asyncio.run(cli_device(hostname)) - diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index 66360d03..bfd9a893 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -357,7 +357,7 @@ async def layer3(self): # Get layer3 information from MIB classes layer3_queries = get_queries("layer3") print(f"layer3 level MIBs", layer3_queries) - + #! currently polling mibs sequencially for i, Query in enumerate(layer3_queries): item = Query(self.snmp_object) diff --git a/switchmap/poller/snmp/mib/generic/mib_qbridge.py b/switchmap/poller/snmp/mib/generic/mib_qbridge.py index d3e8bdd7..75a3690b 100644 --- a/switchmap/poller/snmp/mib/generic/mib_qbridge.py +++ b/switchmap/poller/snmp/mib/generic/mib_qbridge.py @@ -30,6 +30,7 @@ def init_query(snmp_object): """ return QbridgeQuery(snmp_object) + class QbridgeQuery(Query): """Class interacts with Q-BRIDGE-MIB. @@ -73,7 +74,7 @@ def __init__(self, snmp_object): async def _get_bridge_data(self): """Load bridge data only when needed.""" - + if self.baseportifindex is None: self.bridge_mib = BridgeQuery(self.snmp_object) diff --git a/test_all_devices.py b/test_all_devices.py index d1b6b831..f3d34452 100644 --- a/test_all_devices.py +++ b/test_all_devices.py @@ -1,14 +1,17 @@ # Create test_all_devices.py import sys -sys.path.insert(0,'/Users/imexyyyyy/files/gsoc/switchmap-ng') + +sys.path.insert(0, "/Users/imexyyyyy/files/gsoc/switchmap-ng") from switchmap.poller import async_poll + def main(): print("Testing ALL devices async polling...") - + # Test with lower concurrency first async_poll.run_devices(max_concurrent_devices=3) + if __name__ == "__main__": - main() \ No newline at end of file + main() From 85ed8b7205fbcc51470a27ab6f1c479acf7e152d Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sun, 10 Aug 2025 04:00:12 +0530 Subject: [PATCH 17/50] chore: clean up, add logs & make polling concurrent on layer lv --- switchmap/poller/snmp/async_snmp_info.py | 112 +++++++++++------- switchmap/poller/snmp/async_snmp_manager.py | 4 - .../poller/snmp/mib/cisco/mib_ciscoc2900.py | 3 - 3 files changed, 70 insertions(+), 49 deletions(-) diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index bfd9a893..ebc25323 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -264,37 +264,50 @@ async def layer1(self): # Initialize key values data = defaultdict(lambda: defaultdict(dict)) processed = False - hostname = self.snmp_object.hostname() layer1_queries = get_queries("layer1") - print(f"layer 1 level MIBs", layer1_queries) + query_items = [ + (query_class(self.snmp_object), query_class.__name__) + for query_class in layer1_queries + ] - #! Process MIB queries sequentially - for i, Query in enumerate(layer1_queries): - item = Query(self.snmp_object) - mib_name = item.__class__.__name__ - print(f"polling for MIB_name: {mib_name}") + # Concurrent support check + support_results = await asyncio.gather( + *[item.supported() for item, _ in query_items] + ) - # Check if supported - if await item.supported(): - processed = True - old_keys = list(data.keys()) - print(f"{mib_name} is supported") + supported_items = [ + (item, name) + for (item, name), supported in zip(query_items, support_results) + if supported + ] + + if supported_items: + results = await asyncio.gather( + *[ + _add_layer1(item, defaultdict(lambda: defaultdict(dict))) + for item, _ in supported_items + ], + return_exceptions=True, + ) - data = await _add_layer1(item, data) + for i, result in enumerate(results): + if isinstance(result, Exception): + item_name = supported_items[i][1] + log.log2exception(1005, f"Error in {item_name}: {result}") + continue - new_keys = list(data.keys()) - added_keys = set(new_keys) - set(old_keys) - else: - print(f" MIB {mib_name} is NOT supported for {hostname}") + for key, value in result.items(): + data[key].update(value) + + processed = True # Return - if processed: - print(f"Layer1 data collected successfully for {hostname}") + if processed is True: + return data else: - print(f"No layer1 MIBs supported for {hostname}") - return data + return None async def layer2(self): """ @@ -308,35 +321,52 @@ async def layer2(self): # Initialize key variables data = defaultdict(lambda: defaultdict(dict)) processed = False - hostname = self.snmp_object.hostname() # Get layer2 information from MIB classes layer2_queries = get_queries("layer2") - # !Process layer2 MIBs sequentially fn - for i, Query in enumerate(layer2_queries): - item = Query(self.snmp_object) - mib_name = item.__class__.__name__ + query_items = [ + (query_class(self.snmp_object), query_class.__name__) + for query_class in layer2_queries + ] - # Check if supported - if await item.supported(): - processed = True - old_keys = list(data.keys()) - print(f"{mib_name} is supported") + support_results = await asyncio.gather( + *[item.supported() for item, _ in query_items] + ) - data = await _add_layer2(item, data) + # Filter supported MIBs + supported_items = [ + (item, name) + for (item, name), supported in zip(query_items, support_results) + if supported + ] - new_keys = list(data.keys()) - added_keys = set(new_keys) - set(old_keys) - else: - print(f"MIB {mib_name} is not supported for {hostname}") + if supported_items: + # Concurrent processing + results = await asyncio.gather( + *[ + _add_layer2(item, defaultdict(lambda: defaultdict(dict))) + for item, _ in supported_items + ], + return_exceptions=True, + ) + + for i, result in enumerate(results): + if isinstance(result, Exception): + continue + + # Merge this MIB's complete results + for key, value in result.items(): + data[key].update(value) + + processed = True # Return - if processed: - print(f"layer2 data collected successfully for {hostname}") + + if processed is True: + return data else: - print(f"No layer2 mibs supported for {hostname}") - return data + return None async def layer3(self): """ @@ -352,7 +382,6 @@ async def layer3(self): # Initialize key variables data = defaultdict(lambda: defaultdict(dict)) processed = False - hostname = self.snmp_object.hostname() # Get layer3 information from MIB classes layer3_queries = get_queries("layer3") @@ -525,7 +554,6 @@ async def _add_layer1(query, data): result = None if asyncio.iscoroutinefunction(query.layer1): - print(f"before polling") result = await query.layer1() else: loop = asyncio.get_event_loop() diff --git a/switchmap/poller/snmp/async_snmp_manager.py b/switchmap/poller/snmp/async_snmp_manager.py index 58c2b832..6663fbf1 100644 --- a/switchmap/poller/snmp/async_snmp_manager.py +++ b/switchmap/poller/snmp/async_snmp_manager.py @@ -1031,8 +1031,6 @@ def _convert(value): try: return int(value_str) except ValueError: - #! clear on this once again - #! approach 1 # Direct int conversion of the obj if prettyPrint fails if hasattr(value, "__int__"): try: @@ -1040,7 +1038,6 @@ def _convert(value): except (ValueError, TypeError): pass - #! approach 2 # Accessing .value attr directly if hasattr(value, "value"): try: @@ -1059,7 +1056,6 @@ def _convert(value): except (ValueError, TypeError): return bytes(str(value.value), "utf-8") - #! will check this as well (if we need it ??) ask peter or dominic sir # Default Fallback - convert to string then to bytes try: return bytes(str(value), "utf-8") diff --git a/switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py b/switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py index e431f5ea..11c017fb 100644 --- a/switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py +++ b/switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py @@ -90,8 +90,6 @@ async def layer1(self): for key, value in values.items(): final[key]["c2900PortLinkbeatStatus"] = value - print(f"ciscoc2900 data: ", final) - # Return return final @@ -119,7 +117,6 @@ async def c2900portlinkbeatstatus(self, oidonly=False): for key, value in results.items(): data_dict[int(key)] = value - print(f"Debug: Processed data_dict: {data_dict}") # Return the interface descriptions return data_dict From 8921862049c5c15fd1d176a33646936ed59d3ec8 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sun, 10 Aug 2025 04:05:47 +0530 Subject: [PATCH 18/50] chore: lint resolved & add test script for polling --- .gitignore | 7 ------- snmp_test.py | 9 +++++++++ switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py | 1 - 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index fac5671a..b5583a7a 100644 --- a/.gitignore +++ b/.gitignore @@ -243,10 +243,3 @@ dmypy.json cython_debug/ # End of https://www.toptal.com/developers/gitignore/api/python,jupyternotebooks - -# temp (remove after async implementation) -improvements.md -snmp_test.py -progress_work.md -switchmap_poller_new_mibs.md -switchmap_schema_new_metrics.md \ No newline at end of file diff --git a/snmp_test.py b/snmp_test.py index 2d8e2d10..0a3e891c 100644 --- a/snmp_test.py +++ b/snmp_test.py @@ -6,6 +6,11 @@ import traceback import time +<<<<<<< HEAD +======= +sys.path.insert(0, ".") + +>>>>>>> 1512d43 (chore: lint resolved & add test script for polling) from switchmap.poller.snmp.async_snmp_info import Query from switchmap.poller.snmp import async_snmp_manager from switchmap.poller.configuration import ConfigPoller @@ -48,6 +53,10 @@ async def test_everything(): print(f"device {hostname} is contactable!") +<<<<<<< HEAD +======= + # Get basic device info +>>>>>>> 1512d43 (chore: lint resolved & add test script for polling) sysobjectid = await snmp_object.sysobjectid() enterprise_no = await snmp_object.enterprise_number() print(f"Device info:") diff --git a/switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py b/switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py index 11c017fb..2e6daf6b 100644 --- a/switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py +++ b/switchmap/poller/snmp/mib/cisco/mib_ciscoc2900.py @@ -117,7 +117,6 @@ async def c2900portlinkbeatstatus(self, oidonly=False): for key, value in results.items(): data_dict[int(key)] = value - # Return the interface descriptions return data_dict From a033279cda41514c41da9b84defd0950147f87fb Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sun, 10 Aug 2025 20:23:56 +0530 Subject: [PATCH 19/50] chore: fixed minor bugs --- switchmap/poller/snmp/async_snmp_info.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index ebc25323..dea45367 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -351,7 +351,7 @@ async def layer2(self): return_exceptions=True, ) - for i, result in enumerate(results): + for result in enumerate(results): if isinstance(result, Exception): continue @@ -491,7 +491,7 @@ async def _add_data(source, target): target: Aggregated data """ # Process data - for primary in source.keys(): + for primary in source: for secondary, value in source[primary].items(): target[primary][secondary] = value From 689ac983ef93d5eda786b3bb89e0a45241871230 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Thu, 14 Aug 2025 00:24:02 +0530 Subject: [PATCH 20/50] chore: fix minor bugs && better err handling --- switchmap/poller/snmp/async_snmp_info.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index dea45367..20f2e95e 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -351,8 +351,12 @@ async def layer2(self): return_exceptions=True, ) - for result in enumerate(results): + for i, result in enumerate(results): if isinstance(result, Exception): + item_name = supported_items[i][1] + log.log2exception( + 1007, f"Layer2 error in {item_name}: {result}" + ) continue # Merge this MIB's complete results @@ -491,7 +495,7 @@ async def _add_data(source, target): target: Aggregated data """ # Process data - for primary in source: + for primary in source.keys(): for secondary, value in source[primary].items(): target[primary][secondary] = value From 97058e691f851d714e87842ec00def8cfba0a110 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Thu, 14 Aug 2025 00:32:59 +0530 Subject: [PATCH 21/50] review changes --- switchmap/poller/snmp/async_snmp_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/switchmap/poller/snmp/async_snmp_manager.py b/switchmap/poller/snmp/async_snmp_manager.py index 6663fbf1..c75fdfc6 100644 --- a/switchmap/poller/snmp/async_snmp_manager.py +++ b/switchmap/poller/snmp/async_snmp_manager.py @@ -1092,7 +1092,7 @@ def _format_results(results, mock_filter, normalized=False): filter_normalized = mock_filter.lstrip(".") oid_normalized = oid_str.lstrip(".") - if filter_normalized not in oid_normalized: + if not oid_normalized.startswith(filter_normalized): continue # convert value using proper type conversion From 34e851020b67c1492b03d9c22d453271c0db99ae Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Thu, 14 Aug 2025 01:38:52 +0530 Subject: [PATCH 22/50] chore: integrate poller daemon to use our async poller --- bin/systemd/switchmap_poller | 6 +++--- switchmap/poller/async_poll.py | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/bin/systemd/switchmap_poller b/bin/systemd/switchmap_poller index 929b499f..f527802c 100755 --- a/bin/systemd/switchmap_poller +++ b/bin/systemd/switchmap_poller @@ -32,7 +32,7 @@ from switchmap import AGENT_POLLER from switchmap.core.agent import Agent, AgentCLI from switchmap.core import general from switchmap.poller.configuration import ConfigPoller -from switchmap.poller import poll +from switchmap.poller import async_poll from switchmap.core import log # We have to create this named tuple outside the multiprocessing Pool @@ -74,7 +74,7 @@ class PollingAgent(Agent): """ # Initialize key variables delay = self._server_config.polling_interval() - multiprocessing = self._server_config.multiprocessing() + max_concurrent = self._server_config.agent_subprocesses() # Post data to the remote server while True: @@ -89,7 +89,7 @@ class PollingAgent(Agent): open(self.lockfile, "a").close() # Poll after sleeping - poll.devices(multiprocessing=multiprocessing) + async_poll.run_devices(max_concurrent_devices=max_concurrent) # Delete lockfile os.remove(self.lockfile) diff --git a/switchmap/poller/async_poll.py b/switchmap/poller/async_poll.py index 2f590bbb..74022d26 100644 --- a/switchmap/poller/async_poll.py +++ b/switchmap/poller/async_poll.py @@ -17,11 +17,12 @@ _META = namedtuple("_META", "zone hostname config") -async def devices(max_concurrent_devices=10): +async def devices(max_concurrent_devices=None): """Poll all devices asynchronously. Args: - max_concurrent_devices: Maximum number of devices to poll concurrently + max_concurrent_devices: Maximum number of devices to poll concurrently. + If None, uses config.agent_subprocesses() Returns: None @@ -32,6 +33,10 @@ async def devices(max_concurrent_devices=10): # Get configuration config = ConfigPoller() + + # Use config value if not provided + if max_concurrent_devices is None: + max_concurrent_devices = config.agent_subprocesses() # Create a list of polling objects zones = sorted(config.zones()) @@ -207,8 +212,13 @@ async def cli_device(hostname): log.log2see(1413, log_message) -def run_devices(max_concurrent_devices=10): +def run_devices(max_concurrent_devices=None): """Run device polling - main entry point.""" + # Use config if not specified + if max_concurrent_devices is None: + config = ConfigPoller() + max_concurrent_devices = config.agent_subprocesses() + asyncio.run(devices(max_concurrent_devices)) From 97514a21cde4497333f79503f36e30d44715c2bb Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Thu, 14 Aug 2025 22:05:09 +0530 Subject: [PATCH 23/50] chore: enhanced poller to reduce the load on device --- switchmap/poller/snmp/async_snmp_info.py | 6 ++---- switchmap/poller/snmp/async_snmp_manager.py | 5 ++--- switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py | 1 - switchmap/poller/snmp/mib/generic/mib_ip.py | 2 +- switchmap/poller/snmp/mib/generic/mib_ipv6.py | 2 +- switchmap/poller/snmp/mib/generic/mib_qbridge.py | 3 +-- switchmap/poller/snmp/mib/generic/mib_snmpv2.py | 2 -- switchmap/poller/snmp/mib/juniper/mib_junipervlan.py | 2 +- 8 files changed, 8 insertions(+), 15 deletions(-) diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index 20f2e95e..a8b4de14 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -252,8 +252,7 @@ async def layer2(self): return None async def layer1(self): - """ - Get all layer 1 information from device. + """Get all layer 1 information from device. Args: None @@ -354,7 +353,7 @@ async def layer2(self): for i, result in enumerate(results): if isinstance(result, Exception): item_name = supported_items[i][1] - log.log2exception( + log.log2warning( 1007, f"Layer2 error in {item_name}: {result}" ) continue @@ -382,7 +381,6 @@ async def layer3(self): Returns: data: Aggregated layer3 data """ - # Initialize key variables data = defaultdict(lambda: defaultdict(dict)) processed = False diff --git a/switchmap/poller/snmp/async_snmp_manager.py b/switchmap/poller/snmp/async_snmp_manager.py index c75fdfc6..28011892 100644 --- a/switchmap/poller/snmp/async_snmp_manager.py +++ b/switchmap/poller/snmp/async_snmp_manager.py @@ -990,7 +990,6 @@ def _convert(value): converted: Value converted to appropriate Python type (bytes or int), or None for null/empty values """ - # Handle pysnmp exception values if isinstance(value, NoSuchObject): return None @@ -1045,7 +1044,7 @@ def _convert(value): except (ValueError, TypeError): pass - log_message = f"Failed to convert pysnmp integer valye: {value_type}, prettyPrint'{value_str}" + log_message = f"Failed to convert pysnmp integer value: {value_type}, prettyPrint'{value_str}" log.log2warning(1059, log_message) return None @@ -1064,7 +1063,7 @@ def _convert(value): def _format_results(results, mock_filter, normalized=False): - """Normalized and format SNMP results + """Normalize and format SNMP results. Args: results: List of (OID, value) tuples from pysnmp diff --git a/switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py b/switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py index de6ee4c3..5db0d36e 100644 --- a/switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py +++ b/switchmap/poller/snmp/mib/cisco/mib_ciscovtp.py @@ -402,7 +402,6 @@ async def vlantrunkportvlansenabled(self, oidonly=False): # Get the ifindex value ifindex = int(key) - #! is this needed in pysnmp, have to check it throughly # Convert hex value to right justified 1024 character binary string vlans_hex = binascii.hexlify(value).decode("utf-8") binary_string = bin(int(vlans_hex, base))[2:].zfill(length_in_bits) diff --git a/switchmap/poller/snmp/mib/generic/mib_ip.py b/switchmap/poller/snmp/mib/generic/mib_ip.py index d4526f88..2b716a1e 100644 --- a/switchmap/poller/snmp/mib/generic/mib_ip.py +++ b/switchmap/poller/snmp/mib/generic/mib_ip.py @@ -61,7 +61,7 @@ def __init__(self, snmp_object): """ # Define query object - self._snmp_object = snmp_object + self.snmp_object = snmp_object super().__init__(snmp_object, "", tags=["layer3"]) diff --git a/switchmap/poller/snmp/mib/generic/mib_ipv6.py b/switchmap/poller/snmp/mib/generic/mib_ipv6.py index fffa1ba8..660e3b7d 100644 --- a/switchmap/poller/snmp/mib/generic/mib_ipv6.py +++ b/switchmap/poller/snmp/mib/generic/mib_ipv6.py @@ -61,7 +61,7 @@ def __init__(self, snmp_object): """ # Define query object - self._snmp_object = snmp_object + self.snmp_object = snmp_object # Get one OID entry in MIB (ipv6Forwarding) test_oid = ".1.3.6.1.2.1.55.1.1" diff --git a/switchmap/poller/snmp/mib/generic/mib_qbridge.py b/switchmap/poller/snmp/mib/generic/mib_qbridge.py index 75a3690b..95722732 100644 --- a/switchmap/poller/snmp/mib/generic/mib_qbridge.py +++ b/switchmap/poller/snmp/mib/generic/mib_qbridge.py @@ -62,7 +62,7 @@ def __init__(self, snmp_object): """ # Define query object - self._snmp_object = snmp_object + self.snmp_object = snmp_object # Get one OID entry in MIB (dot1qPvid) test_oid = ".1.3.6.1.2.1.17.7.1.4.5.1.1" @@ -74,7 +74,6 @@ def __init__(self, snmp_object): async def _get_bridge_data(self): """Load bridge data only when needed.""" - if self.baseportifindex is None: self.bridge_mib = BridgeQuery(self.snmp_object) diff --git a/switchmap/poller/snmp/mib/generic/mib_snmpv2.py b/switchmap/poller/snmp/mib/generic/mib_snmpv2.py index ba5c08df..b0252740 100644 --- a/switchmap/poller/snmp/mib/generic/mib_snmpv2.py +++ b/switchmap/poller/snmp/mib/generic/mib_snmpv2.py @@ -93,8 +93,6 @@ async def system(self): key = 0 # Process - #! after checking every sys n layers working in async (work on major improvements) - #! use gather intead of going seqeuncial oidroot = ".1.3.6.1.2.1.1" for node in range(1, 7): oid = "{}.{}.0".format(oidroot, node) diff --git a/switchmap/poller/snmp/mib/juniper/mib_junipervlan.py b/switchmap/poller/snmp/mib/juniper/mib_junipervlan.py index 9872dabd..24cbc249 100644 --- a/switchmap/poller/snmp/mib/juniper/mib_junipervlan.py +++ b/switchmap/poller/snmp/mib/juniper/mib_junipervlan.py @@ -64,7 +64,7 @@ def __init__(self, snmp_object): """ # Define query object - self._snmp_object = snmp_object + self.snmp_object = snmp_object # Get one OID entry in MIB (jnxExVlanTag) test_oid = ".1.3.6.1.4.1.2636.3.40.1.5.1.7.1.3" From 3ce8dfe57c114387ab1c9c47b1410235a95d6f6f Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Thu, 14 Aug 2025 22:22:49 +0530 Subject: [PATCH 24/50] fix formatting --- switchmap/poller/snmp/async_snmp_manager.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/switchmap/poller/snmp/async_snmp_manager.py b/switchmap/poller/snmp/async_snmp_manager.py index 28011892..8d5555ec 100644 --- a/switchmap/poller/snmp/async_snmp_manager.py +++ b/switchmap/poller/snmp/async_snmp_manager.py @@ -1044,7 +1044,10 @@ def _convert(value): except (ValueError, TypeError): pass - log_message = f"Failed to convert pysnmp integer value: {value_type}, prettyPrint'{value_str}" + log_message = ( + f"Failed to convert pysnmp integer value: " + f"{value_type}, prettyPrint'{value_str}" + ) log.log2warning(1059, log_message) return None From 213ed900d93a8f04eb11838686b2d8dba91696cd Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Thu, 14 Aug 2025 22:26:19 +0530 Subject: [PATCH 25/50] docstring fixed, add missing args --- switchmap/poller/snmp/async_snmp_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/switchmap/poller/snmp/async_snmp_manager.py b/switchmap/poller/snmp/async_snmp_manager.py index 8d5555ec..b770ace2 100644 --- a/switchmap/poller/snmp/async_snmp_manager.py +++ b/switchmap/poller/snmp/async_snmp_manager.py @@ -984,7 +984,7 @@ def _convert(value): """Convert SNMP value from pysnmp object to Python type. Args: - result: pysnmp value object + value: pysnmp value object Returns: converted: Value converted to appropriate Python type (bytes or int), From 3d48d06f1a4b38ce3159d7e7ff5742f39d3a3820 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sat, 16 Aug 2025 16:15:07 +0530 Subject: [PATCH 26/50] chore: sync files cleanup --- debug_async_flow.py | 33 - switchmap/poller/async_poll.py | 6 +- switchmap/poller/poll.py | 154 ---- switchmap/poller/snmp/poller.py | 114 --- switchmap/poller/snmp/snmp_info.py | 291 -------- switchmap/poller/snmp/snmp_manager.py | 979 -------------------------- 6 files changed, 3 insertions(+), 1574 deletions(-) delete mode 100644 debug_async_flow.py delete mode 100644 switchmap/poller/poll.py delete mode 100644 switchmap/poller/snmp/poller.py delete mode 100644 switchmap/poller/snmp/snmp_info.py delete mode 100644 switchmap/poller/snmp/snmp_manager.py diff --git a/debug_async_flow.py b/debug_async_flow.py deleted file mode 100644 index dd23719a..00000000 --- a/debug_async_flow.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -Debug testing script to test the new async SNMP polling implementation of switchmap-ng -""" - -import sys -import os -import asyncio - - -sys.path.insert(0, "/Users/imexyyyyy/files/gsoc/switchmap-ng") - -# once i make polling async then will update this according to that to check if devices are being polled asynchronously -# for now for testing using cli_device to poll single device for debugging - -from switchmap.poller import async_poll - - -def main(): - print("Switchmap polling flow debugger") - - try: - - hostname = "162.249.37.218" - print(f"starting debug poll for hostname: {hostname}") - - async_poll.run_cli_device(hostname=hostname) - - except Exception as e: - print(f"Error duing polling: {e}") - - -if __name__ == "__main__": - main() diff --git a/switchmap/poller/async_poll.py b/switchmap/poller/async_poll.py index 74022d26..65421c08 100644 --- a/switchmap/poller/async_poll.py +++ b/switchmap/poller/async_poll.py @@ -33,13 +33,13 @@ async def devices(max_concurrent_devices=None): # Get configuration config = ConfigPoller() - + # Use config value if not provided if max_concurrent_devices is None: max_concurrent_devices = config.agent_subprocesses() # Create a list of polling objects - zones = sorted(config.zones()) + zones = sorted(config.zones(), key=lambda z: z.name) for zone in zones: arguments.extend( @@ -218,7 +218,7 @@ def run_devices(max_concurrent_devices=None): if max_concurrent_devices is None: config = ConfigPoller() max_concurrent_devices = config.agent_subprocesses() - + asyncio.run(devices(max_concurrent_devices)) diff --git a/switchmap/poller/poll.py b/switchmap/poller/poll.py deleted file mode 100644 index 7cd32517..00000000 --- a/switchmap/poller/poll.py +++ /dev/null @@ -1,154 +0,0 @@ -"""Switchmap-NG poll modulre. - -Updates the database with device SNMP data. - -""" - -# Standard libraries -from multiprocessing import Pool -from collections import namedtuple -from pprint import pprint -import os - -# Import app libraries -from switchmap import API_POLLER_POST_URI -from switchmap.poller.snmp import poller -from switchmap.poller.update import device as udevice -from switchmap.poller.configuration import ConfigPoller -from switchmap.core import log -from switchmap.core import rest -from switchmap.core import files -from switchmap import AGENT_POLLER - -_META = namedtuple("_META", "zone hostname config") - - -def devices(multiprocessing=False): - """Poll all devices for data using subprocesses and create YAML files. - - Args: - multiprocessing: Run multiprocessing when True - - Returns: - None - - """ - # Initialize key variables - arguments = [] - - # Get configuration - config = ConfigPoller() - - # Get the number of threads to use in the pool - pool_size = config.agent_subprocesses() - - # Create a list of polling objects - zones = sorted(config.zones()) - - # Create a list of arguments - for zone in zones: - arguments.extend( - _META(zone=zone.name, hostname=_, config=config) - for _ in zone.hostnames - ) - - # Process the data - if bool(multiprocessing) is False: - for argument in arguments: - device(argument) - - else: - # Create a multiprocessing pool of sub process resources - with Pool(processes=pool_size) as pool: - # Create sub processes from the pool - pool.map(device, arguments) - - -def device(poll, post=True): - """Poll single device for data and create YAML files. - - Args: - poll: _META object - post: Post the data if True, else just print it. - - Returns: - None - - """ - # Initialize key variables - hostname = poll.hostname - zone = poll.zone - config = poll.config - - # Do nothing if the skip file exists - skip_file = files.skip_file(AGENT_POLLER, config) - if os.path.isfile(skip_file) is True: - log_message = """\ -Skip file {} found. Aborting poll for {} in zone "{}". A daemon \ -shutdown request was probably requested""".format( - skip_file, hostname, zone - ) - log.log2debug(1041, log_message) - return - - # Poll data for obviously valid hostnames (eg. "None" used in installation) - if bool(hostname) is True: - if isinstance(hostname, str) is True: - if hostname.lower() != "none": - poll = poller.Poll(hostname) - snmp_data = poll.query() - - # Process if we get valid data - if bool(snmp_data) and isinstance(snmp_data, dict): - # Process device data - _device = udevice.Device(snmp_data) - data = _device.process() - data["misc"]["zone"] = zone - - if bool(post) is True: - # Update the database tables with polled data - rest.post(API_POLLER_POST_URI, data, config) - else: - pprint(data) - else: - log_message = """\ -Device {} returns no data. Check your connectivity and/or SNMP configuration\ -""".format( - hostname - ) - log.log2debug(1025, log_message) - - -def cli_device(hostname): - """Poll single device for data and create YAML files. - - Args: - hostname: Host to poll - - Returns: - None - - """ - # Initialize key variables - arguments = [] - - # Get configuration - config = ConfigPoller() - - # Create a list of polling objects - zones = sorted(config.zones()) - - # Create a list of arguments - for zone in zones: - for next_hostname in zone.hostnames: - if next_hostname == hostname: - arguments.append( - _META(zone=zone.name, hostname=hostname, config=config) - ) - - if bool(arguments) is True: - for argument in arguments: - device(argument, post=False) - else: - log_message = "No hostname {} found in configuration".format(hostname) - log.log2see(1036, log_message) diff --git a/switchmap/poller/snmp/poller.py b/switchmap/poller/snmp/poller.py deleted file mode 100644 index 3be2f2b3..00000000 --- a/switchmap/poller/snmp/poller.py +++ /dev/null @@ -1,114 +0,0 @@ -"""SNMP Poller module.""" - -# Switchmap imports -from switchmap.poller.configuration import ConfigPoller -from switchmap.poller import POLLING_OPTIONS, SNMP, POLL -from . import snmp_info -from . import snmp_manager -from switchmap.core import log - - -class Poll: - """Asynchronous SNMP poller for switchmap-ng that gathers network data. - - This class manages SNMP credential validation and data querying for - network devices using asynchronous operations for improved performance - and scalability. - - Args: - hostname (str): The hostname or IP address of the device to poll - - Methods: - initialize_snmp(): Validates SNMP credentials and initializes SNMP - interaction - query(): Queries the device for topology data asynchronously - """ - - def __init__(self, hostname): - """Initialize the class. - - Args: - hostname: Hostname to poll - - Returns: - None - - """ - # Initialize key variables - self._server_config = ConfigPoller() - self._hostname = hostname - self._snmp_object = None - - # Get snmp configuration information from Switchmap-NG - validate = snmp_manager.Validate( - POLLING_OPTIONS( - hostname=hostname, - authorizations=self._server_config.snmp_auth(), - ) - ) - authorization = validate.credentials() - - # Create an SNMP object for querying - if _do_poll(authorization) is True: - self._snmp_object = snmp_manager.Interact( - POLL( - hostname=hostname, - authorization=authorization, - ) - ) - else: - log_message = ( - "Uncontactable or disabled host {}, or no valid SNMP " - "credentials found for it.".format(self._hostname) - ) - log.log2info(1081, log_message) - - def query(self): - """Query all remote hosts for data. - - Args: - None - - Returns: - None - - """ - # Initialize key variables - _data = None - - # Only query if wise - if bool(self._snmp_object) is False: - return _data - - # Get data - log_message = """\ -Querying topology data from host {}.""".format( - self._hostname - ) - log.log2info(1078, log_message) - - # Return the data polled from the device - status = snmp_info.Query(self._snmp_object) - _data = status.everything() - return _data - - -def _do_poll(authorization): - """Determine whether doing a poll is valid. - - Args: - authorization: SNMP object - - Returns: - poll: True if a poll should be done - - """ - # Initialize key variables - poll = False - - if bool(authorization) is True: - if isinstance(authorization, SNMP) is True: - poll = bool(authorization.enabled) - - # Return - return poll diff --git a/switchmap/poller/snmp/snmp_info.py b/switchmap/poller/snmp/snmp_info.py deleted file mode 100644 index cee2f140..00000000 --- a/switchmap/poller/snmp/snmp_info.py +++ /dev/null @@ -1,291 +0,0 @@ -"""Module to aggregate query results.""" - -import time -from collections import defaultdict - -from . import iana_enterprise -from . import get_queries - - -class Query: - """Class interacts with IfMIB devices. - - Args: - None - - Returns: - None - - """ - - def __init__(self, snmp_object): - """Instantiate the class. - - Args: - snmp_object: SNMP Interact class object from snmp_manager.py - - Returns: - None - - """ - # Define query object - self.snmp_object = snmp_object - - def everything(self): - """Get all information from device. - - Args: - None - - Returns: - data: Aggregated data - - """ - # Initialize key variables - data = {} - - # Append data - data["misc"] = self.misc() - data["layer1"] = self.layer1() - data["layer2"] = self.layer2() - data["layer3"] = self.layer3() - data["system"] = self.system() - - # Return - return data - - def misc(self): - """Provide miscellaneous information about device and the poll. - - Args: - None - - Returns: - data: Aggregated data - - """ - # Initialize data - data = defaultdict(lambda: defaultdict(dict)) - data["timestamp"] = int(time.time()) - data["host"] = self.snmp_object.hostname() - - # Get vendor information - sysobjectid = self.snmp_object.sysobjectid() - vendor = iana_enterprise.Query(sysobjectid=sysobjectid) - data["IANAEnterpriseNumber"] = vendor.enterprise() - - # Return - return data - - def system(self): - """Get all system information from device. - - Args: - None - - Returns: - data: Aggregated data - - """ - # Initialize data - data = defaultdict(lambda: defaultdict(dict)) - processed = False - - # Get system information from SNMPv2-MIB, ENTITY-MIB, IF-MIB - # Instantiate a query object for each system query - for item in [ - Query(self.snmp_object) for Query in get_queries("system") - ]: - if item.supported(): - processed = True - data = _add_system(item, data) - - # Return - if processed is True: - return data - else: - return None - - def layer1(self): - """Get all layer1 information from device. - - Args: - None - - Returns: - data: Aggregated data - - """ - # Initialize key values - data = defaultdict(lambda: defaultdict(dict)) - processed = False - - # Get information layer1 queries - - for item in [ - Query(self.snmp_object) for Query in get_queries("layer1") - ]: - if item.supported(): - processed = True - data = _add_layer1(item, data) - - # Return - if processed is True: - return data - else: - return None - - def layer2(self): - """Get all layer2 information from device. - - Args: - None - - Returns: - data: Aggregated data - - """ - # Initialize key variables - data = defaultdict(lambda: defaultdict(dict)) - processed = False - - for item in [ - Query(self.snmp_object) for Query in get_queries("layer2") - ]: - if item.supported(): - processed = True - data = _add_layer2(item, data) - - # Return - if processed is True: - return data - else: - return None - - def layer3(self): - """Get all layer3 information from device. - - Args: - None - - Returns: - data: Aggregated data - - """ - # Initialize key variables - data = defaultdict(lambda: defaultdict(dict)) - processed = False - - for item in [ - Query(self.snmp_object) for Query in get_queries("layer3") - ]: - if item.supported(): - processed = True - data = _add_layer3(item, data) - - # Return - if processed is True: - return data - else: - return None - - -def _add_data(source, target): - """Add data from source to target dict. Both dicts must have two keys. - - Args: - source: Source dict - target: Target dict - - Returns: - target: Aggregated data - - """ - # Process data - for primary in source.keys(): - for secondary, value in source[primary].items(): - target[primary][secondary] = value - - # Return - return target - - -def _add_layer1(query, original_data): - """Add data from successful layer1 MIB query to original data provided. - - Args: - query: MIB query object - original_data: Two keyed dict of data - - Returns: - new_data: Aggregated data - - """ - # Process query - result = query.layer1() - new_data = _add_data(result, original_data) - - # Return - return new_data - - -def _add_layer2(query, original_data): - """Add data from successful layer2 MIB query to original data provided. - - Args: - query: MIB query object - original_data: Two keyed dict of data - - Returns: - new_data: Aggregated data - - """ - # Process query - result = query.layer2() - new_data = _add_data(result, original_data) - - # Return - return new_data - - -def _add_layer3(query, original_data): - """Add data from successful layer3 MIB query to original data provided. - - Args: - query: MIB query object - original_data: Two keyed dict of data - - Returns: - new_data: Aggregated data - - """ - # Process query - result = query.layer3() - new_data = _add_data(result, original_data) - - # Return - return new_data - - -def _add_system(query, data): - """Add data from successful system MIB query to original data provided. - - Args: - query: MIB query object - data: Three keyed dict of data - - Returns: - data: Aggregated data - - """ - # Process query - result = query.system() - - # Add tag - for primary in result.keys(): - for secondary in result[primary].keys(): - for tertiary, value in result[primary][secondary].items(): - data[primary][secondary][tertiary] = value - - # Return - return data diff --git a/switchmap/poller/snmp/snmp_manager.py b/switchmap/poller/snmp/snmp_manager.py deleted file mode 100644 index 5ce5bf4d..00000000 --- a/switchmap/poller/snmp/snmp_manager.py +++ /dev/null @@ -1,979 +0,0 @@ -"""SNMP manager class.""" - -import os -import sys - -import easysnmp -from easysnmp import exceptions - -# Import project libraries -from switchmap.poller.configuration import ConfigPoller -from switchmap.poller import POLL -from switchmap.core import log -from switchmap.core import files -from . import iana_enterprise - - -class Validate: - """Class Verify SNMP data.""" - - def __init__(self, options): - """Initialize the Validate class. - - Args: - options: POLLING_OPTIONS object containing SNMP configuration - - Returns: - None - """ - # Initialize key variables - self._options = options - - def credentials(self): - """Determine valid SNMP credentials for a host. - - Args: - None - - Returns: - authentication: SNMP authorization object containing valid - credentials, or None if no valid credentials found - """ - # Initialize key variables - cache_exists = False - - # Create cache directory / file if not yet created - filename = files.snmp_file(self._options.hostname, ConfigPoller()) - if os.path.exists(filename) is True: - cache_exists = True - - # Create file if necessary - if cache_exists is False: - # Get credentials - authentication = self.validation() - - # Save credentials if successful - if bool(authentication): - _update_cache(filename, authentication.group) - - else: - # Read credentials from cache - if os.path.isfile(filename): - with open(filename) as f_handle: - group = f_handle.readline() - - # Get credentials - authentication = self.validation(group) - - # Try the rest if these credentials fail - if bool(authentication) is False: - authentication = self.validation() - - # Update cache if found - if bool(authentication): - _update_cache(filename, authentication.group) - - # Return - return authentication - - def validation(self, group=None): - """Determine valid SNMP authorization for a host. - - Args: - group: String containing SNMP group name to try, or None to try all - groups - - Returns: - result: SNMP authorization object if valid credentials found, - None otherwise - """ - # Initialize key variables - result = None - - # Probe device with all SNMP options - for authorization in self._options.authorizations: - # Only process enabled SNMP values - if bool(authorization.enabled) is False: - continue - - # Setup contact with the remote device - device = Interact( - POLL( - hostname=self._options.hostname, - authorization=authorization, - ) - ) - - # Try successive groups - if group is None: - # Verify connectivity - if device.contactable() is True: - result = authorization - break - else: - if authorization.group == group: - # Verify connectivity - if device.contactable() is True: - result = authorization - - # Return - return result - - -class Interact: - """Class Gets SNMP data.""" - - def __init__(self, _poll): - """Initialize the Interact class. - - Args: - _poll: POLL object containing SNMP configuration and target info - - Returns: - None - """ - # Initialize key variables - self._poll = _poll - - # Fail if there is no authentication - if bool(self._poll.authorization) is False: - log_message = ( - "SNMP parameters provided are blank. " "Non existent host?" - ) - log.log2die(1045, log_message) - - def enterprise_number(self): - """Get SNMP enterprise number for the device. - - Args: - None - - Returns: - int: SNMP enterprise number identifying the device vendor - """ - # Get the sysObjectID.0 value of the device - sysid = self.sysobjectid() - - # Get the vendor ID - enterprise_obj = iana_enterprise.Query(sysobjectid=sysid) - enterprise = enterprise_obj.enterprise() - - # Return - return enterprise - - def hostname(self): - """Get SNMP hostname for the interaction. - - Args: - None - - Returns: - str: Hostname of the target device - """ - # Initialize key variables - hostname = self._poll.hostname - - # Return - return hostname - - def contactable(self): - """Check if device is reachable via SNMP. - - Args: - None - - Returns: - bool: True if device responds to SNMP queries, False otherwise - """ - # Define key variables - contactable = False - result = None - - # Try to reach device - try: - # If we can poll the SNMP sysObjectID, - # then the device is contactable - result = self.sysobjectid(check_reachability=True) - if bool(result) is True: - contactable = True - - except Exception: - # Not contactable - contactable = False - - except: - # Log a message - log_message = "Unexpected SNMP error for device {}" "".format( - self._poll.hostname - ) - log.log2die(1008, log_message) - - # Return - return contactable - - def sysobjectid(self, check_reachability=False): - """Get the sysObjectID of the device. - - Args: - check_reachability: Boolean indicating whether to test connectivity. - Some session errors are ignored to return null result. - - Returns: - str: sysObjectID value as string, or None if not available - """ - # Initialize key variables - oid = ".1.3.6.1.2.1.1.2.0" - object_id = None - - # Get sysObjectID - results = self.get(oid, check_reachability=check_reachability) - if bool(results) is True: - object_id = results[oid].decode("utf-8") - - # Return - return object_id - - def oid_exists(self, oid_to_get, context_name=""): - """Determine if an OID exists on the device. - - Args: - oid_to_get: String containing OID to check - context_name: String containing SNMPv3 context name. - Default is empty string. - - Returns: - bool: True if OID exists, False otherwise - """ - # Initialize key variables - validity = False - - # Validate OID - if self._oid_exists_get(oid_to_get, context_name=context_name) is True: - validity = True - - if validity is False: - if ( - self._oid_exists_walk(oid_to_get, context_name=context_name) - is True - ): - validity = True - - # Return - return validity - - def _oid_exists_get(self, oid_to_get, context_name=""): - """Determine existence of OID on device. - - Args: - oid_to_get: OID to get - context_name: Set the contextName used for SNMPv3 messages. - The default contextName is the empty string "". Overrides the - defContext token in the snmp.conf file. - - Returns: - validity: True if exists - - """ - # Initialize key variables - validity = False - - # Process - (_, validity, result) = self.query( - oid_to_get, - get=True, - check_reachability=True, - context_name=context_name, - check_existence=True, - ) - - # If we get no result, then override validity - if bool(result) is False: - validity = False - elif isinstance(result, dict) is True: - if result[oid_to_get] is None: - validity = False - - # Return - return validity - - def _oid_exists_walk(self, oid_to_get, context_name=""): - """Determine existence of OID on device. - - Args: - oid_to_get: OID to get - context_name: Set the contextName used for SNMPv3 messages. - The default contextName is the empty string "". Overrides the - defContext token in the snmp.conf file. - - Returns: - validity: True if exists - - """ - # Initialize key variables - validity = False - - # Process - (_, validity, results) = self.query( - oid_to_get, - get=False, - check_reachability=True, - context_name=context_name, - check_existence=True, - ) - - # If we get no result, then override validity - if isinstance(results, dict) is True: - for _, value in results.items(): - if value is None: - validity = False - break - - # Return - return validity - - def swalk(self, oid_to_get, normalized=False, context_name=""): - """Perform a safe SNMPwalk that handles errors gracefully. - - Args: - oid_to_get: OID to get - normalized: If True, then return results as a dict keyed by - only the last node of an OID, otherwise return results - keyed by the entire OID string. Normalization is useful - when trying to create multidimensional dicts where the - primary key is a universal value such as IF-MIB::ifIndex - or BRIDGE-MIB::dot1dBasePort - context_name: Set the contextName used for SNMPv3 messages. - The default contextName is the empty string "". Overrides the - defContext token in the snmp.conf file. - - Returns: - dict: Results of SNMP walk as OID-value pairs - """ - # Process data - results = self.walk( - oid_to_get, - normalized=normalized, - check_reachability=True, - check_existence=True, - context_name=context_name, - safe=True, - ) - - # Return - return results - - def walk( - self, - oid_to_get, - normalized=False, - check_reachability=False, - check_existence=False, - context_name="", - safe=False, - ): - """Do an SNMPwalk. - - Args: - oid_to_get: OID to walk - normalized: If True, then return results as a dict keyed by - only the last node of an OID, otherwise return results - keyed by the entire OID string. Normalization is useful - when trying to create multidimensional dicts where the - primary key is a universal value such as IF-MIB::ifIndex - or BRIDGE-MIB::dot1dBasePort - check_reachability: - Set if testing for connectivity. Some session - errors are ignored so that a null result is returned - check_existence: - Set if checking for the existence of the OID - context_name: Set the contextName used for SNMPv3 messages. - The default contextName is the empty string "". Overrides the - defContext token in the snmp.conf file. - safe: Safe query if true. If there is an exception, then return \ - blank values. - - Returns: - result: Dictionary of tuples (OID, value) - - """ - (_, _, result) = self.query( - oid_to_get, - get=False, - check_reachability=check_reachability, - check_existence=check_existence, - normalized=normalized, - context_name=context_name, - safe=safe, - ) - return result - - def get( - self, - oid_to_get, - check_reachability=False, - check_existence=False, - normalized=False, - context_name="", - ): - """Do an SNMPget. - - Args: - oid_to_get: OID to get - check_reachability: Set if testing for connectivity. Some session - errors are ignored so that a null result is returned - check_existence: Set if checking for the existence of the OID - normalized: If True, then return results as a dict keyed by - only the last node of an OID, otherwise return results - keyed by the entire OID string. Normalization is useful - when trying to create multidimensional dicts where the - primary key is a universal value such as IF-MIB::ifIndex - or BRIDGE-MIB::dot1dBasePort - context_name: Set the contextName used for SNMPv3 messages. - The default contextName is the empty string "". Overrides the - defContext token in the snmp.conf file. - - Returns: - result: Dictionary of tuples (OID, value) - - """ - (_, _, result) = self.query( - oid_to_get, - get=True, - check_reachability=check_reachability, - check_existence=check_existence, - normalized=normalized, - context_name=context_name, - ) - return result - - def query( - self, - oid_to_get, - get=False, - check_reachability=False, - check_existence=False, - normalized=False, - context_name="", - safe=False, - ): - """Do an SNMP query. - - Args: - oid_to_get: OID to walk - get: Flag determining whether to do a GET or WALK - check_reachability: Set if testing for connectivity. Some session - errors are ignored so that a null result is returned - check_existence: Set if checking for the existence of the OID - normalized: If True, then return results as a dict keyed by - only the last node of an OID, otherwise return results - keyed by the entire OID string. Normalization is useful - when trying to create multidimensional dicts where the - primary key is a universal value such as IF-MIB::ifIndex - or BRIDGE-MIB::dot1dBasePort - context_name: Set the contextName used for SNMPv3 messages. - The default contextName is the empty string "". Overrides the - defContext token in the snmp.conf file. - safe: Safe query if true. If there is an exception, then return\ - blank values. - - Returns: - return_value: List of tuples (_contactable, exists, values) - - """ - # Initialize variables - _contactable = True - exists = True - results = [] - - # Check if OID is valid - if _oid_valid_format(oid_to_get) is False: - log_message = "OID {} has an invalid format".format(oid_to_get) - log.log2die(1057, log_message) - - # Create SNMP session - session = _Session(self._poll, context_name=context_name).session - - # Fill the results object by getting OID data - try: - # Get the data - if get is True: - results = [session.get(oid_to_get)] - - else: - if self._poll.authorization.version != 1: - # Bulkwalk for SNMPv2 and SNMPv3 - results = session.bulkwalk( - oid_to_get, non_repeaters=0, max_repetitions=25 - ) - else: - # Bulkwalk not supported in SNMPv1 - results = session.walk(oid_to_get) - - # Crash on error, return blank results if doing certain types of - # connectivity checks - except ( - exceptions.EasySNMPConnectionError, - exceptions.EasySNMPTimeoutError, - exceptions.EasySNMPUnknownObjectIDError, - exceptions.EasySNMPNoSuchNameError, - exceptions.EasySNMPNoSuchObjectError, - exceptions.EasySNMPNoSuchInstanceError, - exceptions.EasySNMPUndeterminedTypeError, - ) as exception_error: - # Update the error message - log_message = _exception_message( - self._poll.hostname, - oid_to_get, - context_name, - sys.exc_info(), - ) - - # Process easysnmp errors - (_contactable, exists) = _process_error( - log_message, - exception_error, - check_reachability, - check_existence, - ) - - except SystemError as exception_error: - log_message = _exception_message( - self._poll.hostname, - oid_to_get, - context_name, - sys.exc_info(), - ) - - # Process easysnmp errors - (_contactable, exists) = _process_error( - log_message, - exception_error, - check_reachability, - check_existence, - system_error=True, - ) - - except: - # Update the error message - log_message = _exception_message( - self._poll.hostname, - oid_to_get, - context_name, - sys.exc_info(), - ) - if bool(safe): - _contactable = None - exists = None - log.log2info(1209, log_message) - else: - log.log2die(1003, log_message) - - # Format results - values = _format_results(results, oid_to_get, normalized=normalized) - - # Return - return_value = (_contactable, exists, values) - return return_value - - -class _Session: - """Class to create an SNMP session with a device.""" - - def __init__(self, _poll, context_name=""): - """Initialize the _Session class. - - Args: - _poll: POLL object containing SNMP configuration - context_name: String containing SNMPv3 context name. - Default is empty string. - - Returns: - session: SNMP session - - """ - # Initialize key variables - self._context_name = context_name - - # Assign variables - self._poll = _poll - - # Fail if there is no authentication - if bool(self._poll.authorization) is False: - log_message = ( - "SNMP parameters provided are blank. " "Non existent host?" - ) - log.log2die(1046, log_message) - - # Create SNMP session - self.session = self._session() - - def _session(self): - """Create an SNMP session for queries. - - Args: - None - - Returns: - session: SNMP session - - """ - # Create session - if self._poll.authorization.version != 3: - session = easysnmp.Session( - community=self._poll.authorization.community, - hostname=self._poll.hostname, - version=self._poll.authorization.version, - remote_port=self._poll.authorization.port, - use_numeric=True, - context=self._context_name, - ) - else: - session = easysnmp.Session( - hostname=self._poll.hostname, - version=self._poll.authorization.version, - remote_port=self._poll.authorization.port, - use_numeric=True, - context=self._context_name, - security_level=self._security_level(), - security_username=self._poll.authorization.secname, - privacy_protocol=self._priv_protocol(), - privacy_password=self._poll.authorization.privpassword, - auth_protocol=self._auth_protocol(), - auth_password=self._poll.authorization.authpassword, - ) - - # Return - return session - - def _security_level(self): - """Determine SNMPv3 security level string. - - Args: - None - - Returns: - result: Security level - """ - # Determine the security level - if bool(self._poll.authorization.authprotocol) is True: - if bool(self._poll.authorization.privprotocol) is True: - result = "authPriv" - else: - result = "authNoPriv" - else: - result = "noAuthNoPriv" - - # Return - return result - - def _auth_protocol(self): - """Get SNMPv3 authentication protocol. - - Args: - None - - Returns: - str: Authentication protocol string ('MD5', 'SHA', or 'DEFAULT') - """ - # Initialize key variables - protocol = self._poll.authorization.authprotocol - - # Setup AuthProtocol (Default SHA) - if bool(protocol) is False: - result = "DEFAULT" - else: - if protocol.lower() == "md5": - result = "MD5" - else: - result = "SHA" - - # Return - return result - - def _priv_protocol(self): - """Get SNMPv3 privacy protocol. - - Args: - None - - Returns: - str: Privacy protocol string ('DES', 'AES', or 'DEFAULT') - """ - # Initialize key variables - protocol = self._poll.authorization.privprotocol - - # Setup privProtocol (Default AES256) - if bool(protocol) is False: - result = "DEFAULT" - else: - if protocol.lower() == "des": - result = "DES" - else: - result = "AES" - - # Return - return result - - -def _exception_message(hostname, oid, context, exc_info): - """Create standardized exception message for SNMP errors. - - Args: - hostname: Hostname - oid: OID being polled - context: SNMP context - exc_info: Exception information - - Returns: - str: Formatted error message - """ - # Create failure log message - try_log_message = ( - "Error occurred during SNMP query on host " - 'OID {} from {} for context "{}"' - "".format(oid, hostname, context) - ) - - # Add exception information - result = """\ -{}: [{}, {}, {}]""".format( - try_log_message, - exc_info[0], - exc_info[1], - exc_info[2], - ) - - # Return - return result - - -def _process_error( - log_message, - exception_error, - check_reachability, - check_existence, - system_error=False, -): - """Process the SNMP error. - - Args: - log_message: Log message - exception_error: Exception error object - check_reachability: Attempt to contact the device if True - check_existence: Check existence of the device if True - system_error: True if a System error - - Returns: - alive: True if contactable - - """ - # Initialize key varialbes - _contactable = True - exists = True - if system_error is False: - error_name = "EasySNMPError" - else: - error_name = "SystemError" - - # Check existence of OID - if check_existence is True: - if system_error is False: - if ( - isinstance( - exception_error, - easysnmp.exceptions.EasySNMPUnknownObjectIDError, - ) - is True - ): - exists = False - return (_contactable, exists) - elif ( - isinstance( - exception_error, - easysnmp.exceptions.EasySNMPNoSuchNameError, - ) - is True - ): - exists = False - return (_contactable, exists) - elif ( - isinstance( - exception_error, - easysnmp.exceptions.EasySNMPNoSuchObjectError, - ) - is True - ): - exists = False - return (_contactable, exists) - elif ( - isinstance( - exception_error, - easysnmp.exceptions.EasySNMPNoSuchInstanceError, - ) - is True - ): - exists = False - return (_contactable, exists) - else: - exists = False - return (_contactable, exists) - - # Checking if the device is reachable - if check_reachability is True: - _contactable = False - exists = False - return (_contactable, exists) - - # Die an agonizing death! - log_message = "{}: {}".format(error_name, log_message) - log.log2die(1023, log_message) - - -def _format_results(results, mock_filter, normalized=False): - """Normalize and format SNMP walk results. - - Args: - results: List of lists of results - mock_filter: The original OID to get. Facilitates unittesting by - filtering Mock values. - normalized: If True, then return results as a dict keyed by - only the last node of an OID, otherwise return results - keyed by the entire OID string. Normalization is useful - when trying to create multidimensional dicts where the - primary key is a universal value such as IF-MIB::ifIndex - or BRIDGE-MIB::dot1dBasePort - - Returns: - dict: Formatted results as OID-value pairs - """ - # Initialize key variables - return_results = {} - - for result in results: - # Recreate the OID - oid = "{}.{}".format(result.oid, result.oid_index) - - # Ignore unwanted OIDs - if mock_filter not in oid: - continue - - # Process the rest - if normalized is True: - return_results[result.oid_index] = _convert(result) - else: - return_results[oid] = _convert(result) - - # Return - return return_results - - -def _convert(result): - """Convert SNMP value from pysnmp object to Python type. - - Args: - result: Named tuple containing SNMP result - - Returns: - converted: Value converted to appropriate Python type (bytes or int), - or None for null/empty values - """ - # Initialieze key values - converted = None - value = result.value - snmp_type = result.snmp_type - - # Convert string type values to bytes - if snmp_type.upper() == "OCTETSTR": - converted = bytes(value, "utf-8") - elif snmp_type.upper() == "OPAQUE": - converted = bytes(value, "utf-8") - elif snmp_type.upper() == "BITS": - converted = bytes(value, "utf-8") - elif snmp_type.upper() == "IPADDR": - converted = bytes(value, "utf-8") - elif snmp_type.upper() == "NETADDR": - converted = bytes(value, "utf-8") - elif snmp_type.upper() == "OBJECTID": - # DO NOT CHANGE !!! - converted = bytes(str(value), "utf-8") - elif snmp_type.upper() == "NOSUCHOBJECT": - # Nothing if OID not found - converted = None - elif snmp_type.upper() == "NOSUCHINSTANCE": - # Nothing if OID not found - converted = None - elif snmp_type.upper() == "ENDOFMIBVIEW": - # Nothing - converted = None - elif snmp_type.upper() == "NULL": - # Nothing - converted = None - else: - # Convert everything else into integer values - # rfc1902.Integer - # rfc1902.Integer32 - # rfc1902.Counter32 - # rfc1902.Gauge32 - # rfc1902.Unsigned32 - # rfc1902.TimeTicks - # rfc1902.Counter64 - converted = int(value) - - # Return - return converted - - -def _oid_valid_format(oid): - """Validate OID string format. - - Args: - oid: String containing OID to validate - - Returns: - bool: True if OID format is valid, False otherwise - """ - # oid cannot be numeric - if isinstance(oid, str) is False: - return False - - # Make sure the oid is not blank - stripped_oid = oid.strip() - if not stripped_oid: - return False - - # Must start with a '.' - if oid[0] != ".": - return False - - # Must not end with a '.' - if oid[-1] == ".": - return False - - # Test each octet to be numeric - octets = oid.split(".") - - # Remove the first element of the list - octets.pop(0) - for value in octets: - try: - int(value) - except: - return False - - # Otherwise valid - return True - - -def _update_cache(filename, group): - """Update SNMP credentials cache file. - - Args: - filename: String containing path to cache file - group: String containing SNMP group name to cache - - Returns: - None - """ - # Do update - with open(filename, "w+") as env: - env.write(group) From eff3f8e90e24f7191af5c2976ac63d515c7a842b Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sun, 17 Aug 2025 19:21:34 +0530 Subject: [PATCH 27/50] chore: made all tha changes --- switchmap/poller/snmp/async_snmp_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index a8b4de14..057ab150 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -56,7 +56,7 @@ async def everything(self): keys = ["misc", "system", "layer1", "layer2", "layer3"] for key, result in zip(keys, results): if isinstance(result, Exception): - log.warning(f"{key} failed: {result}") + log.log2warning(f"{key} failed: {result}") elif result: data[key] = result From cb60d2ce99974555c570754eb4e09afdc469003c Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sat, 30 Aug 2025 01:14:45 +0530 Subject: [PATCH 28/50] feat: replaced sequential server posting to async --- requirements.txt | 1 + switchmap/poller/async_poll.py | 41 +++++++++++++++--------- switchmap/poller/snmp/async_snmp_info.py | 9 ++++-- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/requirements.txt b/requirements.txt index 244676e3..be4d146d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ gunicorn==20.0.4 # Posting requests +aiohttp # Polling easysnmp==0.2.5 diff --git a/switchmap/poller/async_poll.py b/switchmap/poller/async_poll.py index 65421c08..8893a62b 100644 --- a/switchmap/poller/async_poll.py +++ b/switchmap/poller/async_poll.py @@ -5,6 +5,7 @@ from pprint import pprint import os import time +import aiohttp # Import app libraries from switchmap import API_POLLER_POST_URI @@ -58,14 +59,14 @@ async def devices(max_concurrent_devices=None): # Semaphore to limit concurrent devices device_semaphore = asyncio.Semaphore(max_concurrent_devices) - tasks = [ - device(argument, device_semaphore, post=True) for argument in arguments - ] - - # Execute all devices concurrently - start_time = time.time() - results = await asyncio.gather(*tasks, return_exceptions=True) - end_time = time.time() + async with aiohttp.ClientSession() as session: + tasks = [ + device(argument, device_semaphore, session, post=True) for argument in arguments + ] + # Execute all devices concurrently + start_time = time.time() + results = await asyncio.gather(*tasks, return_exceptions=True) + end_time = time.time() # Process results and log summary success_count = sum(1 for r in results if r is True) @@ -83,7 +84,7 @@ async def devices(max_concurrent_devices=None): log.log2warning(1403, log_message) -async def device(poll_meta, device_semaphore, post=True): +async def device(poll_meta, device_semaphore, session,post=True): """Poll each device asynchoronously. Args: @@ -137,13 +138,20 @@ async def device(poll_meta, device_semaphore, post=True): data = _device.process() data["misc"]["zone"] = zone - #! do a little research on aiohttp if post: - rest.post(API_POLLER_POST_URI, data, config) - log_message = ( - f"Successfully polled and posted data for {hostname}" - ) - log.log2debug(1407, log_message) + try: + async with session.post(API_POLLER_POST_URI, json=data) as res: + if res.status == 200: + log_message = f"Successfully polled and posted data for {hostname}" + log.log2debug(1407, log_message) + else: + log_message = f"Failed to post data for {hostname}, status={res.status}" + log.log2warning(1414, log_message) + except aiohttp.ClientError as e: + log_message = f"HTTP error posting data for {hostname}: {e}" + log.log2exception(1415, log_message) + return False + else: pprint(data) @@ -192,8 +200,9 @@ async def cli_device(hostname): log.log2info(1410, log_message) # Poll each zone occurrence + semaphore = asyncio.Semaphore(1) tasks = [ - device(argument, asyncio.Semaphore(1), post=False) + device(argument, semaphore, post=False) for argument in arguments ] results = await asyncio.gather(*tasks, return_exceptions=True) diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index 057ab150..3531d080 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -72,9 +72,12 @@ async def misc(self): # Get vendor information sysobjectid = await self.snmp_object.sysobjectid() - vendor = iana_enterprise.Query(sysobjectid=sysobjectid) - data["IANAEnterpriseNumber"] = vendor.enterprise() - + if sysobjectid: + vendor = iana_enterprise.Query(sysobjectid=sysobjectid) + data["IANAEnterpriseNumber"] = vendor.enterprise() + else: + data["IANAEnterpriseNumber"] = None + return data async def system(self): From fbfd33c901f014dff4a9a82524a8d143611bbdfb Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sat, 30 Aug 2025 01:28:25 +0530 Subject: [PATCH 29/50] lint & format fix --- switchmap/poller/async_poll.py | 74 ++++++++++++++++-------- switchmap/poller/snmp/async_snmp_info.py | 4 +- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/switchmap/poller/async_poll.py b/switchmap/poller/async_poll.py index 8893a62b..15fd78a1 100644 --- a/switchmap/poller/async_poll.py +++ b/switchmap/poller/async_poll.py @@ -28,7 +28,6 @@ async def devices(max_concurrent_devices=None): Returns: None """ - # Initialize key variables arguments = [] @@ -53,29 +52,36 @@ async def devices(max_concurrent_devices=None): log.log2info(1400, log_message) return - log_message = f"Starting async polling of {len(arguments)} devices with max concurrency: {max_concurrent_devices}" + log_message = ( + f"Starting async polling of {len(arguments)} devices " + f"with max concurrency: {max_concurrent_devices}" + ) log.log2info(1401, log_message) # Semaphore to limit concurrent devices device_semaphore = asyncio.Semaphore(max_concurrent_devices) async with aiohttp.ClientSession() as session: - tasks = [ - device(argument, device_semaphore, session, post=True) for argument in arguments + tasks = [ + device(argument, device_semaphore, session, post=True) + for argument in arguments ] # Execute all devices concurrently - start_time = time.time() - results = await asyncio.gather(*tasks, return_exceptions=True) - end_time = time.time() + start_time = time.time() + results = await asyncio.gather(*tasks, return_exceptions=True) + end_time = time.time() # Process results and log summary success_count = sum(1 for r in results if r is True) error_count = sum(1 for r in results if isinstance(r, Exception)) failed_count = len(results) - success_count - error_count - log_message = f"Polling completed in {end_time - start_time:.2f}s: {success_count} succeeded, {failed_count} failed, {error_count} errors" + log_message = ( + f"Polling completed in {end_time - start_time:.2f}s: " + f"{success_count} succeeded, {failed_count} failed, " + f"{error_count} errors" + ) log.log2info(1402, log_message) - # Log specific errors for i, result in enumerate(results): if isinstance(result, Exception): @@ -84,18 +90,18 @@ async def devices(max_concurrent_devices=None): log.log2warning(1403, log_message) -async def device(poll_meta, device_semaphore, session,post=True): - """Poll each device asynchoronously. +async def device(poll_meta, device_semaphore, session, post=True): + """Poll each device asynchronously. Args: poll_meta: _META object containing zone, hostname, config device_semaphore: Semaphore to limit concurrent devices + session: aiohttp ClientSession for HTTP requests post: Post the data if True, else just print it Returns: bool: True if successful, False otherwise """ - async with device_semaphore: # Initialize key variables hostname = poll_meta.hostname @@ -105,7 +111,10 @@ async def device(poll_meta, device_semaphore, session,post=True): # Do nothing if the skip file exists skip_file = files.skip_file(AGENT_POLLER, config) if os.path.isfile(skip_file): - log_message = f"Skip file {skip_file} found. Aborting poll for {hostname} in zone '{zone}'" + log_message = ( + f"Skip file {skip_file} found. Aborting poll for " + f"{hostname} in zone '{zone}'" + ) log.log2debug(1404, log_message) return False @@ -140,15 +149,25 @@ async def device(poll_meta, device_semaphore, session,post=True): if post: try: - async with session.post(API_POLLER_POST_URI, json=data) as res: + async with session.post( + API_POLLER_POST_URI, json=data + ) as res: if res.status == 200: - log_message = f"Successfully polled and posted data for {hostname}" + log_message = ( + f"Successfully polled and posted data " + f"for {hostname}" + ) log.log2debug(1407, log_message) else: - log_message = f"Failed to post data for {hostname}, status={res.status}" + log_message = ( + f"Failed to post data for {hostname}, " + f"status={res.status}" + ) log.log2warning(1414, log_message) except aiohttp.ClientError as e: - log_message = f"HTTP error posting data for {hostname}: {e}" + log_message = ( + f"HTTP error posting data for {hostname}: {e}" + ) log.log2exception(1415, log_message) return False @@ -157,7 +176,10 @@ async def device(poll_meta, device_semaphore, session,post=True): return True else: - log_message = f"Device {hostname} returns no data. Check connectivity/SNMP configuration" + log_message = ( + f"Device {hostname} returns no data. Check " + f"connectivity/SNMP configuration" + ) log.log2debug(1408, log_message) return False @@ -201,16 +223,20 @@ async def cli_device(hostname): # Poll each zone occurrence semaphore = asyncio.Semaphore(1) - tasks = [ - device(argument, semaphore, post=False) - for argument in arguments - ] - results = await asyncio.gather(*tasks, return_exceptions=True) + async with aiohttp.ClientSession() as session: + tasks = [ + device(argument, semaphore, session, post=False) + for argument in arguments + ] + results = await asyncio.gather(*tasks, return_exceptions=True) # Check results success_count = sum(1 for r in results if r is True) if success_count > 0: - log_message = f"Successfully polled {hostname} from {success_count}/{len(results)} zone(s)" + log_message = ( + f"Successfully polled {hostname} from " + f"{success_count}/{len(results)} zone(s)" + ) log.log2info(1411, log_message) else: log_message = f"Failed to poll {hostname} from any configured zone" diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index 3531d080..fb8cc84d 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -76,8 +76,8 @@ async def misc(self): vendor = iana_enterprise.Query(sysobjectid=sysobjectid) data["IANAEnterpriseNumber"] = vendor.enterprise() else: - data["IANAEnterpriseNumber"] = None - + data["IANAEnterpriseNumber"] = None + return data async def system(self): From d8dbc7d57edf6a415e378bd7154153f72e0fc485 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sat, 30 Aug 2025 01:33:00 +0530 Subject: [PATCH 30/50] docstring ci fix --- switchmap/poller/async_poll.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/switchmap/poller/async_poll.py b/switchmap/poller/async_poll.py index 15fd78a1..9af2dcae 100644 --- a/switchmap/poller/async_poll.py +++ b/switchmap/poller/async_poll.py @@ -248,7 +248,15 @@ async def cli_device(hostname): def run_devices(max_concurrent_devices=None): - """Run device polling - main entry point.""" + """Run device polling - main entry point. + + Args: + max_concurrent_devices (int, optional): Maximum number of devices to + poll concurrently. If None, uses config.agent_subprocesses(). + + Returns: + None + """ # Use config if not specified if max_concurrent_devices is None: config = ConfigPoller() @@ -258,5 +266,12 @@ def run_devices(max_concurrent_devices=None): def run_cli_device(hostname): - """Run CLI device polling - main entry point.""" + """Run CLI device polling - main entry point. + + Args: + hostname (str): The hostname of the device to poll. + + Returns: + None + """ asyncio.run(cli_device(hostname)) From 0d31de7dfb70e5f0908455aae6824c93f288bd1e Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Sun, 31 Aug 2025 19:15:17 +0530 Subject: [PATCH 31/50] chore: cleanup & refactor logs --- switchmap/core/configuration.py | 23 ------------------ switchmap/poller/async_poll.py | 31 ++++++++++++++++++------ switchmap/poller/snmp/async_snmp_info.py | 2 +- 3 files changed, 24 insertions(+), 32 deletions(-) diff --git a/switchmap/core/configuration.py b/switchmap/core/configuration.py index 2c0bb63e..483b43e2 100644 --- a/switchmap/core/configuration.py +++ b/switchmap/core/configuration.py @@ -55,29 +55,6 @@ def __init__(self): ) log.log2die_safe(1006, log_message) - def agent_subprocesses(self): - """Get agent_subprocesses. - - Args: - None - - Returns: - result: result - - """ - # Get threads - threads = max(1, self._config_core.get("agent_subprocesses", 20)) - - # Get CPU cores - cores = multiprocessing.cpu_count() - desired_max_threads = max(1, cores - 1) - - # We don't want a value that's too big that the CPU cannot cope - result = min(threads, desired_max_threads) - - # Return - return result - def api_log_file(self, daemon): """Get api_log_file. diff --git a/switchmap/poller/async_poll.py b/switchmap/poller/async_poll.py index 9af2dcae..5257139c 100644 --- a/switchmap/poller/async_poll.py +++ b/switchmap/poller/async_poll.py @@ -8,7 +8,7 @@ import aiohttp # Import app libraries -from switchmap import API_POLLER_POST_URI +from switchmap import API_POLLER_POST_URI,API_PREFIX from switchmap.poller.snmp import async_poller from switchmap.poller.update import device as udevice from switchmap.poller.configuration import ConfigPoller @@ -35,13 +35,16 @@ async def devices(max_concurrent_devices=None): config = ConfigPoller() # Use config value if not provided - if max_concurrent_devices is None: - max_concurrent_devices = config.agent_subprocesses() + if not isinstance(max_concurrent_devices,int) or max_concurrent_devices < 1: + log.log2warning(1401, f"Invalid concurrency={max_concurrent_devices}; defaulting to 1") + max_concurrent_devices = 1 # Create a list of polling objects zones = sorted(config.zones(), key=lambda z: z.name) for zone in zones: + if not zone.hostnames: + continue arguments.extend( _META(zone=zone.name, hostname=_, config=config) for _ in zone.hostnames @@ -60,8 +63,9 @@ async def devices(max_concurrent_devices=None): # Semaphore to limit concurrent devices device_semaphore = asyncio.Semaphore(max_concurrent_devices) - - async with aiohttp.ClientSession() as session: + + timeout = aiohttp.ClientTimeout(total=30) + async with aiohttp.ClientSession(timeout=timeout) as session: tasks = [ device(argument, device_semaphore, session, post=True) for argument in arguments @@ -149,8 +153,13 @@ async def device(poll_meta, device_semaphore, session, post=True): if post: try: + # Construct full URL for posting + url = f"{config.server_url_root()}{API_PREFIX}{API_POLLER_POST_URI}" + log_message = f"Posting data for {hostname} to {url}" + log.log2debug(1416, log_message) + async with session.post( - API_POLLER_POST_URI, json=data + url, json=data ) as res: if res.status == 200: log_message = ( @@ -168,7 +177,7 @@ async def device(poll_meta, device_semaphore, session, post=True): log_message = ( f"HTTP error posting data for {hostname}: {e}" ) - log.log2exception(1415, log_message) + log.log2warning(1415, log_message) return False else: @@ -183,9 +192,13 @@ async def device(poll_meta, device_semaphore, session, post=True): log.log2debug(1408, log_message) return False + except (asyncio.TimeoutError, KeyError, ValueError) as e: + log_message = f"Recoverable error polling device {hostname}: {e}" + log.log2warning(1409, log_message) + return False except Exception as e: log_message = f"Unexpected error polling device {hostname}: {e}" - log.log2exception(1409, log_message) + log.log2warning(1409, log_message) return False @@ -209,6 +222,8 @@ async def cli_device(hostname): # Create a list of arguments for zone in zones: + if not zone.hostnames: + continue for next_hostname in zone.hostnames: if next_hostname == hostname: arguments.append( diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index fb8cc84d..09526e17 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -56,7 +56,7 @@ async def everything(self): keys = ["misc", "system", "layer1", "layer2", "layer3"] for key, result in zip(keys, results): if isinstance(result, Exception): - log.log2warning(f"{key} failed: {result}") + log.log2warning(1004, f"{key} failed: {result}") elif result: data[key] = result From 04fd3c58c234f32cefb8237446f984782c2928d2 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Wed, 17 Sep 2025 19:16:00 +0530 Subject: [PATCH 32/50] chore: fixed all issues happened during rebase --- snmp_test.py | 133 --------------- switchmap/core/configuration.py | 23 +++ switchmap/poller/async_poll.py | 22 +-- switchmap/poller/snmp/async_poller.py | 1 - switchmap/poller/snmp/async_snmp_info.py | 170 -------------------- switchmap/poller/snmp/async_snmp_manager.py | 48 ------ test_all_devices.py | 17 -- 7 files changed, 36 insertions(+), 378 deletions(-) delete mode 100644 snmp_test.py delete mode 100644 test_all_devices.py diff --git a/snmp_test.py b/snmp_test.py deleted file mode 100644 index 0a3e891c..00000000 --- a/snmp_test.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 -"""Test async_snmp_info.everything() using proper credential validation.""" - -import asyncio -import sys -import traceback -import time - -<<<<<<< HEAD -======= -sys.path.insert(0, ".") - ->>>>>>> 1512d43 (chore: lint resolved & add test script for polling) -from switchmap.poller.snmp.async_snmp_info import Query -from switchmap.poller.snmp import async_snmp_manager -from switchmap.poller.configuration import ConfigPoller -from switchmap.poller import POLLING_OPTIONS, POLL - - -async def test_everything(): - """Test everything() method with proper SNMP credential validation.""" - print("Testing async_snmp_info.everything()") - - hostname = "162.249.37.218" - - try: - # SNMP configuration - print(f"Getting SNMP configuration...") - config = ConfigPoller() - - print(f"Validating SNMP credentials for {hostname}...") - validate = async_snmp_manager.Validate( - POLLING_OPTIONS( - hostname=hostname, authorizations=config.snmp_auth() - ) - ) - - # Get valid authorization - authorization = await validate.credentials() - if not authorization: - print(f"Failed to get valid SNMP credentials for {hostname}") - return None - - snmp_object = async_snmp_manager.Interact( - POLL(hostname=hostname, authorization=authorization) - ) - - print(f"Testing device connectivity...") - is_contactable = await snmp_object.contactable() - if not is_contactable: - print(f"device {hostname} is not contactable via SNMP") - return None - - print(f"device {hostname} is contactable!") - -<<<<<<< HEAD -======= - # Get basic device info ->>>>>>> 1512d43 (chore: lint resolved & add test script for polling) - sysobjectid = await snmp_object.sysobjectid() - enterprise_no = await snmp_object.enterprise_number() - print(f"Device info:") - print(f"SysObjectID: {sysobjectid}") - print(f"Enterprise: {enterprise_no}") - - query_obj = Query(snmp_object) - - print(f"Calling everything() method...") - print(f"wait a little...") - - start_time = time.time() - everything_data = await query_obj.everything() - end_time = time.time() - - print(f"Completed in {end_time - start_time:.2f} seconds") - - # Display results - if everything_data: - print(f"\nSUCCESS! everything() returned data :)))))))") - print(f"Data str:") - for key, value in everything_data.items(): - if isinstance(value, dict) and value: - print(f" {key}: {len(value)} items") - # Show sample of nested data - sample_key = list(value.keys())[0] - sample_value = value[sample_key] - if isinstance(sample_value, dict): - print( - f"Sample {sample_key}: {len(sample_value)} sub-items" - ) - else: - print(f"Sample {sample_key}: {type(sample_value)}") - elif isinstance(value, dict): - print(f" {key}: empty dict") - else: - print(f" {key}: {type(value)} = {value}") - else: - print(f"everything() returned nonee result") - - return everything_data - - except Exception as e: - print(f"ERROR: :(((((({e}") - print(f"Full traceback:") - traceback.print_exc() - return None - - -async def main(): - """Main test function.""" - print("Async SNMP Info Test (Proper Credentials)") - print("=" * 50) - - result = await test_everything() - - if result is not None: - print(f"\nTest completed successfully!") - return True - else: - print(f"\nTest failed!") - return False - - -if __name__ == "__main__": - print(" Running Async SNMP Test with Proper Credentials...") - success = asyncio.run(main()) - - if success: - print("Test completed!") - sys.exit(0) - else: - print("Test failed!") - sys.exit(1) diff --git a/switchmap/core/configuration.py b/switchmap/core/configuration.py index 483b43e2..2c0bb63e 100644 --- a/switchmap/core/configuration.py +++ b/switchmap/core/configuration.py @@ -55,6 +55,29 @@ def __init__(self): ) log.log2die_safe(1006, log_message) + def agent_subprocesses(self): + """Get agent_subprocesses. + + Args: + None + + Returns: + result: result + + """ + # Get threads + threads = max(1, self._config_core.get("agent_subprocesses", 20)) + + # Get CPU cores + cores = multiprocessing.cpu_count() + desired_max_threads = max(1, cores - 1) + + # We don't want a value that's too big that the CPU cannot cope + result = min(threads, desired_max_threads) + + # Return + return result + def api_log_file(self, daemon): """Get api_log_file. diff --git a/switchmap/poller/async_poll.py b/switchmap/poller/async_poll.py index 5257139c..1162e14e 100644 --- a/switchmap/poller/async_poll.py +++ b/switchmap/poller/async_poll.py @@ -8,7 +8,7 @@ import aiohttp # Import app libraries -from switchmap import API_POLLER_POST_URI,API_PREFIX +from switchmap import API_POLLER_POST_URI, API_PREFIX from switchmap.poller.snmp import async_poller from switchmap.poller.update import device as udevice from switchmap.poller.configuration import ConfigPoller @@ -35,8 +35,14 @@ async def devices(max_concurrent_devices=None): config = ConfigPoller() # Use config value if not provided - if not isinstance(max_concurrent_devices,int) or max_concurrent_devices < 1: - log.log2warning(1401, f"Invalid concurrency={max_concurrent_devices}; defaulting to 1") + if ( + not isinstance(max_concurrent_devices, int) + or max_concurrent_devices < 1 + ): + log.log2warning( + 1401, + f"Invalid concurrency={max_concurrent_devices}; defaulting to 1", + ) max_concurrent_devices = 1 # Create a list of polling objects @@ -63,7 +69,7 @@ async def devices(max_concurrent_devices=None): # Semaphore to limit concurrent devices device_semaphore = asyncio.Semaphore(max_concurrent_devices) - + timeout = aiohttp.ClientTimeout(total=30) async with aiohttp.ClientSession(timeout=timeout) as session: tasks = [ @@ -153,14 +159,12 @@ async def device(poll_meta, device_semaphore, session, post=True): if post: try: - # Construct full URL for posting + # Construct full URL for posting url = f"{config.server_url_root()}{API_PREFIX}{API_POLLER_POST_URI}" log_message = f"Posting data for {hostname} to {url}" log.log2debug(1416, log_message) - - async with session.post( - url, json=data - ) as res: + + async with session.post(url, json=data) as res: if res.status == 200: log_message = ( f"Successfully polled and posted data " diff --git a/switchmap/poller/snmp/async_poller.py b/switchmap/poller/snmp/async_poller.py index 6928da68..debb8b32 100644 --- a/switchmap/poller/snmp/async_poller.py +++ b/switchmap/poller/snmp/async_poller.py @@ -8,7 +8,6 @@ from switchmap.core import log - class Poll: """Asynchronous SNMP poller for switchmap-ng that gathers network data. diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index 09526e17..6b2feed4 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -254,176 +254,6 @@ async def layer2(self): else: return None - async def layer1(self): - """Get all layer 1 information from device. - - Args: - None - - Returns: - data: Aggregated layer1 data - """ - # Initialize key values - data = defaultdict(lambda: defaultdict(dict)) - processed = False - - layer1_queries = get_queries("layer1") - - query_items = [ - (query_class(self.snmp_object), query_class.__name__) - for query_class in layer1_queries - ] - - # Concurrent support check - support_results = await asyncio.gather( - *[item.supported() for item, _ in query_items] - ) - - supported_items = [ - (item, name) - for (item, name), supported in zip(query_items, support_results) - if supported - ] - - if supported_items: - results = await asyncio.gather( - *[ - _add_layer1(item, defaultdict(lambda: defaultdict(dict))) - for item, _ in supported_items - ], - return_exceptions=True, - ) - - for i, result in enumerate(results): - if isinstance(result, Exception): - item_name = supported_items[i][1] - log.log2exception(1005, f"Error in {item_name}: {result}") - continue - - for key, value in result.items(): - data[key].update(value) - - processed = True - - # Return - if processed is True: - return data - else: - return None - - async def layer2(self): - """ - Args: - None - - Returns: - data: Aggregated layer2 data - - """ - # Initialize key variables - data = defaultdict(lambda: defaultdict(dict)) - processed = False - - # Get layer2 information from MIB classes - layer2_queries = get_queries("layer2") - - query_items = [ - (query_class(self.snmp_object), query_class.__name__) - for query_class in layer2_queries - ] - - support_results = await asyncio.gather( - *[item.supported() for item, _ in query_items] - ) - - # Filter supported MIBs - supported_items = [ - (item, name) - for (item, name), supported in zip(query_items, support_results) - if supported - ] - - if supported_items: - # Concurrent processing - results = await asyncio.gather( - *[ - _add_layer2(item, defaultdict(lambda: defaultdict(dict))) - for item, _ in supported_items - ], - return_exceptions=True, - ) - - for i, result in enumerate(results): - if isinstance(result, Exception): - item_name = supported_items[i][1] - log.log2warning( - 1007, f"Layer2 error in {item_name}: {result}" - ) - continue - - # Merge this MIB's complete results - for key, value in result.items(): - data[key].update(value) - - processed = True - - # Return - - if processed is True: - return data - else: - return None - - async def layer3(self): - """ - Get all layer3 information from device. - - Args: - None - - Returns: - data: Aggregated layer3 data - """ - # Initialize key variables - data = defaultdict(lambda: defaultdict(dict)) - processed = False - - # Get layer3 information from MIB classes - layer3_queries = get_queries("layer3") - print(f"layer3 level MIBs", layer3_queries) - - #! currently polling mibs sequencially - for i, Query in enumerate(layer3_queries): - item = Query(self.snmp_object) - mib_name = item.__class__.__name__ - - print( - f"Testing MIB {i+1}/{len(layer3_queries)}: {mib_name} for {hostname}" - ) - - # Check if supported - if await item.supported(): - print(f"MIB {mib_name} is supported for {hostname}") - processed = True - old_keys = list(data.keys()) - - data = await _add_layer3(item, data) - - new_keys = list(data.keys()) - added_keys = set(new_keys) - set(old_keys) - - print(f"MIB {mib_name} added: {list(added_keys)}") - else: - print(f"MIB {mib_name} is not supported for {hostname}") - - # Return - if processed: - print(f"Layer3 data collected successfully for {hostname}") - else: - print(f"No layer3 MIBs supported for {hostname}") - return data - - async def layer3(self): """Get all layer3 information from device. diff --git a/switchmap/poller/snmp/async_snmp_manager.py b/switchmap/poller/snmp/async_snmp_manager.py index b770ace2..352cb26b 100644 --- a/switchmap/poller/snmp/async_snmp_manager.py +++ b/switchmap/poller/snmp/async_snmp_manager.py @@ -1065,54 +1065,6 @@ def _convert(value): return None -def _format_results(results, mock_filter, normalized=False): - """Normalize and format SNMP results. - - Args: - results: List of (OID, value) tuples from pysnmp - mock_filter: The original OID to get. Facilitates unittesting by - filtering Mock values. - normalized: If True, then return results as a dict keyed by - only the last node of an OID, otherwise return results - keyed by the entire OID string. Normalization is useful - when trying to create multidimensional dicts where the - primary key is a universal value such as IF-MIB::ifIndex - or BRIDGE-MIB::dot1dBasePort - - Returns: - dict: Formatted results as OID-value pairs - - """ - - formatted = {} - - for oid_str, value in results: - - # Normalize both OIDs for comparison to handle leading dot mismatch - if mock_filter: - # Remove leading dots for comparison - filter_normalized = mock_filter.lstrip(".") - oid_normalized = oid_str.lstrip(".") - - if not oid_normalized.startswith(filter_normalized): - continue - - # convert value using proper type conversion - converted_value = _convert(value=value) - - if normalized is True: - # use only the last node of the OID - key = oid_str.split(".")[-1] - else: - key = oid_str - - formatted[key] = converted_value - - return formatted - - - - def _convert(value): """Convert SNMP value from pysnmp object to Python type. diff --git a/test_all_devices.py b/test_all_devices.py deleted file mode 100644 index f3d34452..00000000 --- a/test_all_devices.py +++ /dev/null @@ -1,17 +0,0 @@ -# Create test_all_devices.py -import sys - -sys.path.insert(0, "/Users/imexyyyyy/files/gsoc/switchmap-ng") - -from switchmap.poller import async_poll - - -def main(): - print("Testing ALL devices async polling...") - - # Test with lower concurrency first - async_poll.run_devices(max_concurrent_devices=3) - - -if __name__ == "__main__": - main() From cb2a304ad7c4861a0d6930aef6e69a672c2c3c49 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Wed, 17 Sep 2025 19:32:58 +0530 Subject: [PATCH 33/50] removed duplicated part cause during rebase --- switchmap/poller/snmp/async_snmp_manager.py | 86 --------------------- 1 file changed, 86 deletions(-) diff --git a/switchmap/poller/snmp/async_snmp_manager.py b/switchmap/poller/snmp/async_snmp_manager.py index 352cb26b..aea80b3a 100644 --- a/switchmap/poller/snmp/async_snmp_manager.py +++ b/switchmap/poller/snmp/async_snmp_manager.py @@ -979,92 +979,6 @@ def _oid_valid_format(oid): # Otherwise valid return True - -def _convert(value): - """Convert SNMP value from pysnmp object to Python type. - - Args: - value: pysnmp value object - - Returns: - converted: Value converted to appropriate Python type (bytes or int), - or None for null/empty values - """ - # Handle pysnmp exception values - if isinstance(value, NoSuchObject): - return None - if isinstance(value, NoSuchInstance): - return None - if isinstance(value, EndOfMibView): - return None - - if hasattr(value, "prettyPrint"): - value_str = value.prettyPrint() - - # Determine type based on pysnmp object type - value_type = type(value).__name__ - - # Handle string-like types - Convert to types for MIB compatibility - if any( - t in value_type - for t in [ - "OctetString", - "DisplayString", - "Opaque", - "Bits", - "IpAddress", - "ObjectIdentifier", - ] - ): - # For objectID, convert to string first then to bytes - if "ObjectIdentifier" in value_type: - return bytes(str(value_str), "utf-8") - else: - return bytes(value_str, "utf-8") - - # Handle integer types - elif any( - t in value_type - for t in ["Integer", "Counter", "Gauge", "TimeTicks", "Unsigned"] - ): - try: - return int(value_str) - except ValueError: - # Direct int conversion of the obj if prettyPrint fails - if hasattr(value, "__int__"): - try: - return int(value) - except (ValueError, TypeError): - pass - - # Accessing .value attr directly - if hasattr(value, "value"): - try: - return int(value.value) - except (ValueError, TypeError): - pass - - log_message = ( - f"Failed to convert pysnmp integer value: " - f"{value_type}, prettyPrint'{value_str}" - ) - log.log2warning(1059, log_message) - return None - - # Handle direct access to value (for objects without prettyPrint) - if hasattr(value, "value"): - try: - return int(value.value) - except (ValueError, TypeError): - return bytes(str(value.value), "utf-8") - - # Default Fallback - convert to string then to bytes - try: - return bytes(str(value), "utf-8") - except Exception: - return None - - def _convert(value): """Convert SNMP value from pysnmp object to Python type. From d37ec3dbc1ab6c85e55d33b5fbdb694cd1edaffb Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Wed, 24 Sep 2025 17:48:59 +0530 Subject: [PATCH 34/50] add decoder in ingester for parsing double encoded mac data --- switchmap/server/db/ingest/update/device.py | 36 +++++++++++++++++++-- switchmap/server/db/ingest/update/zone.py | 36 +++++++++++++++++++-- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/switchmap/server/db/ingest/update/device.py b/switchmap/server/db/ingest/update/device.py index 63f54352..77652a84 100644 --- a/switchmap/server/db/ingest/update/device.py +++ b/switchmap/server/db/ingest/update/device.py @@ -28,6 +28,35 @@ ) +def _decode_mac_address(encoded_mac): + """Decode double-encoded MAC addresses from async poller. + + Args: + encoded_mac: MAC address that may be double hex-encoded + + Returns: + str: Properly formatted MAC address or original if already valid + + """ + import binascii + + try: + # Try to decode hex-encoded string to ASCII + if isinstance(encoded_mac, str) and len(encoded_mac) > 12: + decoded = binascii.unhexlify(encoded_mac).decode('ascii') + # Check if it starts with '0x' (hex prefix) + if decoded.startswith('0x'): + # Return MAC without '0x' prefix + return decoded[2:] + + # If decoding fails or doesn't match pattern, return original + return encoded_mac + + except Exception: + # If any decoding fails, return original + return encoded_mac + + def process(data, idx_zone, dns=True): """Process data received from a device. @@ -505,13 +534,14 @@ def macport(self, test=False): {self._device.hostname} based on SNMP MIB-BRIDGE entries""" log.log2debug(1065, log_message) - # Iterate over the MACs found for next_mac in sorted(_macs): - # Initialize loop variables + # Initialize variables valid_mac = None # Create lowercase version of MAC address - mactest = general.mac(next_mac) + # Handle double-encoded MAC addresses from async poller + decoded_mac = _decode_mac_address(next_mac) + mactest = general.mac(decoded_mac) if bool(mactest.valid) is False: continue else: diff --git a/switchmap/server/db/ingest/update/zone.py b/switchmap/server/db/ingest/update/zone.py index 623d7ad4..6d8b65a1 100644 --- a/switchmap/server/db/ingest/update/zone.py +++ b/switchmap/server/db/ingest/update/zone.py @@ -16,6 +16,34 @@ ) +def _decode_mac_address(encoded_mac): + """Decode double-encoded MAC addresses from async poller. + + Args: + encoded_mac: MAC address that may be double hex-encoded + + Returns: + str: Properly formatted MAC address or original if already valid + + """ + import binascii + + try: + # Try to decode hex-encoded string to ASCII + if isinstance(encoded_mac, str) and len(encoded_mac) > 12: + decoded = binascii.unhexlify(encoded_mac).decode('ascii') + # Check if it starts with '0x' (hex prefix) + if decoded.startswith('0x'): + return decoded[2:] + + # If decoding fails or doesn't match pattern, return original + return encoded_mac + + except Exception: + # If any decoding fails, return original + return encoded_mac + + def process(data, idx_zone, dns=True): """Process data received from a device. @@ -400,7 +428,9 @@ def _process_pairmacips(idx_zone, table): continue # Create lowercase version of mac address. Skip if invalid - mactest = general.mac(next_mac) + # Handle double-encoded MAC addresses from async poller + decoded_mac = _decode_mac_address(next_mac) + mactest = general.mac(decoded_mac) if bool(mactest.valid) is False: continue else: @@ -442,7 +472,9 @@ def _arp_table(idx_zone, data): continue # Create lowercase version of mac address. Skip if invalid. - mactest = general.mac(next_mac) + # Handle double-encoded MAC addresses from async poller + decoded_mac = _decode_mac_address(next_mac) + mactest = general.mac(decoded_mac) if bool(mactest.valid) is False: continue else: From 1c4ba55aa7f74f661a69b9d1522a514fbe6c9ad3 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Wed, 24 Sep 2025 22:50:58 +0530 Subject: [PATCH 35/50] linted --- switchmap/poller/snmp/async_snmp_manager.py | 1 + switchmap/server/db/ingest/update/device.py | 16 ++++++++-------- switchmap/server/db/ingest/update/zone.py | 18 +++++++++--------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/switchmap/poller/snmp/async_snmp_manager.py b/switchmap/poller/snmp/async_snmp_manager.py index aea80b3a..7d2945c6 100644 --- a/switchmap/poller/snmp/async_snmp_manager.py +++ b/switchmap/poller/snmp/async_snmp_manager.py @@ -979,6 +979,7 @@ def _oid_valid_format(oid): # Otherwise valid return True + def _convert(value): """Convert SNMP value from pysnmp object to Python type. diff --git a/switchmap/server/db/ingest/update/device.py b/switchmap/server/db/ingest/update/device.py index 77652a84..314d3ba2 100644 --- a/switchmap/server/db/ingest/update/device.py +++ b/switchmap/server/db/ingest/update/device.py @@ -30,28 +30,28 @@ def _decode_mac_address(encoded_mac): """Decode double-encoded MAC addresses from async poller. - + Args: encoded_mac: MAC address that may be double hex-encoded - + Returns: str: Properly formatted MAC address or original if already valid - + """ import binascii - + try: # Try to decode hex-encoded string to ASCII if isinstance(encoded_mac, str) and len(encoded_mac) > 12: - decoded = binascii.unhexlify(encoded_mac).decode('ascii') + decoded = binascii.unhexlify(encoded_mac).decode("ascii") # Check if it starts with '0x' (hex prefix) - if decoded.startswith('0x'): + if decoded.startswith("0x"): # Return MAC without '0x' prefix return decoded[2:] - + # If decoding fails or doesn't match pattern, return original return encoded_mac - + except Exception: # If any decoding fails, return original return encoded_mac diff --git a/switchmap/server/db/ingest/update/zone.py b/switchmap/server/db/ingest/update/zone.py index 6d8b65a1..6a19791e 100644 --- a/switchmap/server/db/ingest/update/zone.py +++ b/switchmap/server/db/ingest/update/zone.py @@ -18,27 +18,27 @@ def _decode_mac_address(encoded_mac): """Decode double-encoded MAC addresses from async poller. - + Args: encoded_mac: MAC address that may be double hex-encoded - + Returns: str: Properly formatted MAC address or original if already valid - + """ import binascii - + try: # Try to decode hex-encoded string to ASCII if isinstance(encoded_mac, str) and len(encoded_mac) > 12: - decoded = binascii.unhexlify(encoded_mac).decode('ascii') + decoded = binascii.unhexlify(encoded_mac).decode("ascii") # Check if it starts with '0x' (hex prefix) - if decoded.startswith('0x'): - return decoded[2:] - + if decoded.startswith("0x"): + return decoded[2:] + # If decoding fails or doesn't match pattern, return original return encoded_mac - + except Exception: # If any decoding fails, return original return encoded_mac From ccde00f64e8e2167f5ea5cc3a67b30b456d25e88 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Wed, 24 Sep 2025 22:56:31 +0530 Subject: [PATCH 36/50] fixed docstring --- switchmap/poller/async_poll.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/switchmap/poller/async_poll.py b/switchmap/poller/async_poll.py index 1162e14e..968765d5 100644 --- a/switchmap/poller/async_poll.py +++ b/switchmap/poller/async_poll.py @@ -160,7 +160,8 @@ async def device(poll_meta, device_semaphore, session, post=True): if post: try: # Construct full URL for posting - url = f"{config.server_url_root()}{API_PREFIX}{API_POLLER_POST_URI}" + url = (f"{config.server_url_root()}{API_PREFIX}" + f"{API_POLLER_POST_URI}") log_message = f"Posting data for {hostname} to {url}" log.log2debug(1416, log_message) From 9a685ce2fa2209f9afa80bc5be9a841e66609f02 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Wed, 24 Sep 2025 22:57:40 +0530 Subject: [PATCH 37/50] fix black lint --- switchmap/poller/async_poll.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/switchmap/poller/async_poll.py b/switchmap/poller/async_poll.py index 968765d5..01f3b41c 100644 --- a/switchmap/poller/async_poll.py +++ b/switchmap/poller/async_poll.py @@ -160,8 +160,10 @@ async def device(poll_meta, device_semaphore, session, post=True): if post: try: # Construct full URL for posting - url = (f"{config.server_url_root()}{API_PREFIX}" - f"{API_POLLER_POST_URI}") + url = ( + f"{config.server_url_root()}{API_PREFIX}" + f"{API_POLLER_POST_URI}" + ) log_message = f"Posting data for {hostname} to {url}" log.log2debug(1416, log_message) From 51cfca6f156f95d37960545f1069860f12000542 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Thu, 25 Sep 2025 16:52:17 +0530 Subject: [PATCH 38/50] fixed oui empty mac ingestion bug --- bin/tools/process_oiu_file.py | 3 +- switchmap/core/mac_utils.py | 40 +++++++++++++++++++++ switchmap/poller/async_poll.py | 10 +++--- switchmap/server/db/ingest/update/device.py | 32 ++--------------- switchmap/server/db/ingest/update/zone.py | 33 ++--------------- 5 files changed, 51 insertions(+), 67 deletions(-) create mode 100644 switchmap/core/mac_utils.py diff --git a/bin/tools/process_oiu_file.py b/bin/tools/process_oiu_file.py index 0d2407c3..629ae6ab 100755 --- a/bin/tools/process_oiu_file.py +++ b/bin/tools/process_oiu_file.py @@ -38,7 +38,8 @@ def main(): """ # Read Oui file args = _cli() - _oui.update_db_oui(args.filename, new=args.new_installation) + # _oui.update_db_oui(args.filename, new=args.new_installation) + _oui.update_db_oui(args.filename) def _cli(): diff --git a/switchmap/core/mac_utils.py b/switchmap/core/mac_utils.py new file mode 100644 index 00000000..b09a5d6a --- /dev/null +++ b/switchmap/core/mac_utils.py @@ -0,0 +1,40 @@ +"""MAC address utility functions.""" + +import binascii + + +def decode_mac_address(encoded_mac): + """Decode double-encoded MAC addresses from async poller. + + This function handles MAC addresses that may be double hex-encoded + and returns them in a standard format. + + Args: + encoded_mac: MAC address that may be double hex-encoded + + Returns: + str: Properly formatted MAC address or original if already valid + + """ + # Fast-path non-strings + if not isinstance(encoded_mac, str): + return encoded_mac + + s = encoded_mac.strip() + + # Handle plain '0x' prefix + if s.lower().startswith("0x"): + return s[2:] + + # Attempt to unhexlify only when likely hex and long enough + hexchars = "0123456789abcdefABCDEF" + if len(s) > 12 and len(s) % 2 == 0 and all(c in hexchars for c in s): + try: + decoded = binascii.unhexlify(s).decode("ascii") + except (binascii.Error, UnicodeDecodeError, ValueError): + return encoded_mac + if decoded.lower().startswith("0x"): + return decoded[2:] + return decoded + + return s diff --git a/switchmap/poller/async_poll.py b/switchmap/poller/async_poll.py index 01f3b41c..8f9f6117 100644 --- a/switchmap/poller/async_poll.py +++ b/switchmap/poller/async_poll.py @@ -35,7 +35,9 @@ async def devices(max_concurrent_devices=None): config = ConfigPoller() # Use config value if not provided - if ( + if max_concurrent_devices is None: + max_concurrent_devices = config.agent_subprocesses() + elif ( not isinstance(max_concurrent_devices, int) or max_concurrent_devices < 1 ): @@ -203,10 +205,6 @@ async def device(poll_meta, device_semaphore, session, post=True): log_message = f"Recoverable error polling device {hostname}: {e}" log.log2warning(1409, log_message) return False - except Exception as e: - log_message = f"Unexpected error polling device {hostname}: {e}" - log.log2warning(1409, log_message) - return False async def cli_device(hostname): @@ -225,7 +223,7 @@ async def cli_device(hostname): config = ConfigPoller() # Create a list of polling objects - zones = sorted(config.zones()) + zones = sorted(config.zones(), key=lambda z: z.name) # Create a list of arguments for zone in zones: diff --git a/switchmap/server/db/ingest/update/device.py b/switchmap/server/db/ingest/update/device.py index 314d3ba2..8a49ff81 100644 --- a/switchmap/server/db/ingest/update/device.py +++ b/switchmap/server/db/ingest/update/device.py @@ -8,6 +8,7 @@ # Application imports from switchmap.core import log from switchmap.core import general +from switchmap.core.mac_utils import decode_mac_address from switchmap.server.db.ingest.query import device as _misc_device from switchmap.server.db.misc import interface as _historical from switchmap.server.db.table import device as _device @@ -28,35 +29,6 @@ ) -def _decode_mac_address(encoded_mac): - """Decode double-encoded MAC addresses from async poller. - - Args: - encoded_mac: MAC address that may be double hex-encoded - - Returns: - str: Properly formatted MAC address or original if already valid - - """ - import binascii - - try: - # Try to decode hex-encoded string to ASCII - if isinstance(encoded_mac, str) and len(encoded_mac) > 12: - decoded = binascii.unhexlify(encoded_mac).decode("ascii") - # Check if it starts with '0x' (hex prefix) - if decoded.startswith("0x"): - # Return MAC without '0x' prefix - return decoded[2:] - - # If decoding fails or doesn't match pattern, return original - return encoded_mac - - except Exception: - # If any decoding fails, return original - return encoded_mac - - def process(data, idx_zone, dns=True): """Process data received from a device. @@ -540,7 +512,7 @@ def macport(self, test=False): # Create lowercase version of MAC address # Handle double-encoded MAC addresses from async poller - decoded_mac = _decode_mac_address(next_mac) + decoded_mac = decode_mac_address(next_mac) mactest = general.mac(decoded_mac) if bool(mactest.valid) is False: continue diff --git a/switchmap/server/db/ingest/update/zone.py b/switchmap/server/db/ingest/update/zone.py index 6a19791e..88e6b8b9 100644 --- a/switchmap/server/db/ingest/update/zone.py +++ b/switchmap/server/db/ingest/update/zone.py @@ -7,6 +7,7 @@ # Application imports from switchmap.core import log from switchmap.core import general +from switchmap.core.mac_utils import decode_mac_address from switchmap.server.db.table import oui as _oui from switchmap.server import ZoneObjects from switchmap.server import PairMacIp @@ -16,34 +17,6 @@ ) -def _decode_mac_address(encoded_mac): - """Decode double-encoded MAC addresses from async poller. - - Args: - encoded_mac: MAC address that may be double hex-encoded - - Returns: - str: Properly formatted MAC address or original if already valid - - """ - import binascii - - try: - # Try to decode hex-encoded string to ASCII - if isinstance(encoded_mac, str) and len(encoded_mac) > 12: - decoded = binascii.unhexlify(encoded_mac).decode("ascii") - # Check if it starts with '0x' (hex prefix) - if decoded.startswith("0x"): - return decoded[2:] - - # If decoding fails or doesn't match pattern, return original - return encoded_mac - - except Exception: - # If any decoding fails, return original - return encoded_mac - - def process(data, idx_zone, dns=True): """Process data received from a device. @@ -429,7 +402,7 @@ def _process_pairmacips(idx_zone, table): # Create lowercase version of mac address. Skip if invalid # Handle double-encoded MAC addresses from async poller - decoded_mac = _decode_mac_address(next_mac) + decoded_mac = decode_mac_address(next_mac) mactest = general.mac(decoded_mac) if bool(mactest.valid) is False: continue @@ -473,7 +446,7 @@ def _arp_table(idx_zone, data): # Create lowercase version of mac address. Skip if invalid. # Handle double-encoded MAC addresses from async poller - decoded_mac = _decode_mac_address(next_mac) + decoded_mac = decode_mac_address(next_mac) mactest = general.mac(decoded_mac) if bool(mactest.valid) is False: continue From c6acb86593bea982c303f9f1be5db644fb950002 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Thu, 25 Sep 2025 17:22:26 +0530 Subject: [PATCH 39/50] fix: handled the loophole for diff error --- switchmap/poller/async_poll.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/switchmap/poller/async_poll.py b/switchmap/poller/async_poll.py index 8f9f6117..b9a5dcdb 100644 --- a/switchmap/poller/async_poll.py +++ b/switchmap/poller/async_poll.py @@ -170,7 +170,7 @@ async def device(poll_meta, device_semaphore, session, post=True): log.log2debug(1416, log_message) async with session.post(url, json=data) as res: - if res.status == 200: + if 200 <= res.status < 300: log_message = ( f"Successfully polled and posted data " f"for {hostname}" @@ -182,6 +182,7 @@ async def device(poll_meta, device_semaphore, session, post=True): f"status={res.status}" ) log.log2warning(1414, log_message) + return False except aiohttp.ClientError as e: log_message = ( f"HTTP error posting data for {hostname}: {e}" From ae8ae157a15307251ec0fa8a5e667f516a445058 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Thu, 25 Sep 2025 18:40:12 +0530 Subject: [PATCH 40/50] minor fixes --- bin/tools/process_oiu_file.py | 3 +-- switchmap/poller/snmp/async_snmp_info.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/bin/tools/process_oiu_file.py b/bin/tools/process_oiu_file.py index 629ae6ab..0d2407c3 100755 --- a/bin/tools/process_oiu_file.py +++ b/bin/tools/process_oiu_file.py @@ -38,8 +38,7 @@ def main(): """ # Read Oui file args = _cli() - # _oui.update_db_oui(args.filename, new=args.new_installation) - _oui.update_db_oui(args.filename) + _oui.update_db_oui(args.filename, new=args.new_installation) def _cli(): diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/async_snmp_info.py index 6b2feed4..d664b446 100644 --- a/switchmap/poller/snmp/async_snmp_info.py +++ b/switchmap/poller/snmp/async_snmp_info.py @@ -56,7 +56,7 @@ async def everything(self): keys = ["misc", "system", "layer1", "layer2", "layer3"] for key, result in zip(keys, results): if isinstance(result, Exception): - log.log2warning(1004, f"{key} failed: {result}") + log.log2warning(1417, f"{key} failed: {result}") elif result: data[key] = result From aec55c710888b82e110d1d1a9ef7d5dc2cd1a6dc Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Thu, 25 Sep 2025 22:25:28 +0530 Subject: [PATCH 41/50] fixed all duplicate codes & added support for new parameter in the update_db_oui() --- switchmap/core/files.py | 2 +- switchmap/poller/snmp/async_snmp_manager.py | 8 ++++---- switchmap/poller/snmp/mib/generic/mib_if.py | 2 +- switchmap/server/db/misc/oui.py | 20 +++++++++++++------- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/switchmap/core/files.py b/switchmap/core/files.py index b59a2487..dd450ee5 100644 --- a/switchmap/core/files.py +++ b/switchmap/core/files.py @@ -253,7 +253,7 @@ def read_yaml_file(filepath, as_string=False, die=True): "".format(filepath) ) if bool(die) is True: - log.log2die_safe(1001, log_message) + log.log2die_safe(1008, log_message) else: log.log2debug(1002, log_message) return {} diff --git a/switchmap/poller/snmp/async_snmp_manager.py b/switchmap/poller/snmp/async_snmp_manager.py index 7d2945c6..cfe24a46 100644 --- a/switchmap/poller/snmp/async_snmp_manager.py +++ b/switchmap/poller/snmp/async_snmp_manager.py @@ -574,13 +574,13 @@ async def query( f"Unexpected async SNMP error for " f"{self._poll.hostname}: {exception_error}" ) - log.log2info(1210, log_message) + log.log2info(1041, log_message) else: log_message = ( f"Unexpected async SNMP error for " f"{self._poll.hostname}: {exception_error}" ) - log.log2die(1003, log_message) + log.log2die(1023, log_message) # Ensure formatted_result is set for exception cases formatted_result = {} @@ -1048,7 +1048,7 @@ def _convert(value): f"Failed to convert pysnmp integer value: " f"{value_type}, prettyPrint'{value_str}" ) - log.log2warning(1059, log_message) + log.log2warning(1036, log_message) return None # Handle direct access to value (for objects without prettyPrint) @@ -1125,4 +1125,4 @@ def _update_cache(filename, group): f_handle.write(group) except Exception as e: log_message = f"Failed to update cache file {filename}: {e}" - log.log2warning(1049, log_message) + log.log2warning(1025, log_message) diff --git a/switchmap/poller/snmp/mib/generic/mib_if.py b/switchmap/poller/snmp/mib/generic/mib_if.py index f979c36d..9d405359 100644 --- a/switchmap/poller/snmp/mib/generic/mib_if.py +++ b/switchmap/poller/snmp/mib/generic/mib_if.py @@ -109,7 +109,7 @@ async def limited_query(method, name): try: return name, await method() except Exception as e: - log.log2warning(1301, f"Error in {name}: {e}") + log.log2warning(1092, f"Error in {name}: {e}") return name, {} queries = [ diff --git a/switchmap/server/db/misc/oui.py b/switchmap/server/db/misc/oui.py index 44f32a24..e4797e30 100644 --- a/switchmap/server/db/misc/oui.py +++ b/switchmap/server/db/misc/oui.py @@ -11,11 +11,12 @@ from sqlalchemy.exc import IntegrityError -def update_db_oui(filepath): +def update_db_oui(filepath, new=False): """Update the database with Oui data. Args: filepath: File to process + new: If True, skip existing entry checks for new installations Returns: None @@ -46,11 +47,16 @@ def update_db_oui(filepath): organization = row["organization"].strip() rows.append(IOui(oui=oui, organization=organization, enabled=1)) - for row in rows: - existing_entry = _oui.exists(row.oui) - if not existing_entry: - _oui.insert_row([row]) - elif existing_entry.organization != row.organization: - _oui.update_row(existing_entry.idx_oui, row) + if new: + # For new installations, do bulk insert without checking existing entries + _oui.insert_row(rows) + else: + # For updates, check existing entries + for row in rows: + existing_entry = _oui.exists(row.oui) + if not existing_entry: + _oui.insert_row([row]) + elif existing_entry.organization != row.organization: + _oui.update_row(existing_entry.idx_oui, row) SCOPED_SESSION.commit() From 1dc7092cb543a68c03ee0d22cae6bd40179dcda3 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Thu, 25 Sep 2025 22:27:29 +0530 Subject: [PATCH 42/50] fixed ci --- switchmap/server/db/misc/oui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/switchmap/server/db/misc/oui.py b/switchmap/server/db/misc/oui.py index e4797e30..e4fcb79d 100644 --- a/switchmap/server/db/misc/oui.py +++ b/switchmap/server/db/misc/oui.py @@ -48,7 +48,7 @@ def update_db_oui(filepath, new=False): rows.append(IOui(oui=oui, organization=organization, enabled=1)) if new: - # For new installations, do bulk insert without checking existing entries + # Bulk insert on fresh install, skip checks _oui.insert_row(rows) else: # For updates, check existing entries From d19331ea0dd7522dc6235cc4887fae5138d21a3b Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Fri, 26 Sep 2025 00:48:35 +0530 Subject: [PATCH 43/50] fix all the failing tests & renamed poller files --- requirements.txt | 1 + switchmap/poller/{async_poll.py => poll.py} | 4 +- .../snmp/{async_poller.py => poller.py} | 10 +- .../snmp/{async_snmp_info.py => snmp_info.py} | 0 ...{async_snmp_manager.py => snmp_manager.py} | 0 tests/switchmap_/poller/test_async_poll.py | 229 ++++++++++++++++++ tests/switchmap_/poller/test_poll.py | 203 ---------------- 7 files changed, 237 insertions(+), 210 deletions(-) rename switchmap/poller/{async_poll.py => poll.py} (99%) rename switchmap/poller/snmp/{async_poller.py => poller.py} (92%) rename switchmap/poller/snmp/{async_snmp_info.py => snmp_info.py} (100%) rename switchmap/poller/snmp/{async_snmp_manager.py => snmp_manager.py} (100%) create mode 100644 tests/switchmap_/poller/test_async_poll.py delete mode 100644 tests/switchmap_/poller/test_poll.py diff --git a/requirements.txt b/requirements.txt index be4d146d..b8788380 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,6 +32,7 @@ graphql_server==3.0.0b5 # Testing mock pytest +pytest-asyncio # Other more-itertools diff --git a/switchmap/poller/async_poll.py b/switchmap/poller/poll.py similarity index 99% rename from switchmap/poller/async_poll.py rename to switchmap/poller/poll.py index b9a5dcdb..c4937cab 100644 --- a/switchmap/poller/async_poll.py +++ b/switchmap/poller/poll.py @@ -9,7 +9,7 @@ # Import app libraries from switchmap import API_POLLER_POST_URI, API_PREFIX -from switchmap.poller.snmp import async_poller +from switchmap.poller.snmp import poller from switchmap.poller.update import device as udevice from switchmap.poller.configuration import ConfigPoller from switchmap.core import log, rest, files @@ -141,7 +141,7 @@ async def device(poll_meta, device_semaphore, session, post=True): return False try: - poll = async_poller.Poll(hostname) + poll = poller.Poll(hostname) # Initialize SNMP connection if not await poll.initialize_snmp(): diff --git a/switchmap/poller/snmp/async_poller.py b/switchmap/poller/snmp/poller.py similarity index 92% rename from switchmap/poller/snmp/async_poller.py rename to switchmap/poller/snmp/poller.py index debb8b32..5077f94d 100644 --- a/switchmap/poller/snmp/async_poller.py +++ b/switchmap/poller/snmp/poller.py @@ -3,8 +3,8 @@ # Switchmap imports from switchmap.poller.configuration import ConfigPoller from switchmap.poller import POLLING_OPTIONS, SNMP, POLL -from . import async_snmp_manager -from . import async_snmp_info +from . import snmp_manager +from . import snmp_info from switchmap.core import log @@ -45,7 +45,7 @@ async def initialize_snmp(self): bool: True if successful, False otherwise """ # Get snmp config information from Switchmap-NG - validate = async_snmp_manager.Validate( + validate = snmp_manager.Validate( POLLING_OPTIONS( hostname=self._hostname, authorizations=self._server_config.snmp_auth(), @@ -57,7 +57,7 @@ async def initialize_snmp(self): # Create an SNMP object for querying if _do_poll(authorization) is True: - self.snmp_object = async_snmp_manager.Interact( + self.snmp_object = snmp_manager.Interact( POLL(hostname=self._hostname, authorization=authorization) ) return True @@ -94,7 +94,7 @@ async def query(self): log.log2info(1078, log_message) - status = async_snmp_info.Query(snmp_object=self.snmp_object) + status = snmp_info.Query(snmp_object=self.snmp_object) _data = await status.everything() diff --git a/switchmap/poller/snmp/async_snmp_info.py b/switchmap/poller/snmp/snmp_info.py similarity index 100% rename from switchmap/poller/snmp/async_snmp_info.py rename to switchmap/poller/snmp/snmp_info.py diff --git a/switchmap/poller/snmp/async_snmp_manager.py b/switchmap/poller/snmp/snmp_manager.py similarity index 100% rename from switchmap/poller/snmp/async_snmp_manager.py rename to switchmap/poller/snmp/snmp_manager.py diff --git a/tests/switchmap_/poller/test_async_poll.py b/tests/switchmap_/poller/test_async_poll.py new file mode 100644 index 00000000..1f78bdc4 --- /dev/null +++ b/tests/switchmap_/poller/test_async_poll.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +"""Test the async poller poll module.""" + +import os +import sys +import pytest +from unittest.mock import patch, MagicMock, AsyncMock +from switchmap.poller.poll import devices, device, cli_device, _META + +# Try to create a working PYTHONPATH +EXEC_DIR = os.path.dirname(os.path.realpath(__file__)) +ROOT_DIR = os.path.abspath( + os.path.join( + os.path.abspath( + os.path.join(EXEC_DIR, os.pardir) # Move up to 'poller' + ), + os.pardir, # Move up to 'switchmap_' + ) +) +_EXPECTED = "{0}switchmap-ng{0}tests{0}switchmap_{0}poller".format(os.sep) +if EXEC_DIR.endswith(_EXPECTED) is True: + # We need to prepend the path in case the repo has been installed + # elsewhere on the system using PIP. This could corrupt expected results + sys.path.insert(0, ROOT_DIR) +else: + print( + f'This script is not installed in the "{_EXPECTED}" directory. ' + "Please fix." + ) + sys.exit(2) + + +@pytest.fixture +def mock_config_setup(): + """Set up mock configuration for tests.""" + mock_config_instance = MagicMock() + mock_zone = MagicMock() + mock_zone.name = "zone1" + mock_zone.hostnames = ["device1", "device2"] + mock_config_instance.zones.return_value = [mock_zone] + mock_config_instance.agent_subprocesses.return_value = 2 + return mock_config_instance + + +@pytest.fixture +def mock_poll_meta(): + """Create a mock poll meta object.""" + return _META(zone="zone1", hostname="device1", config=MagicMock()) + + +class TestAsyncPoll: + """Test cases for the async poll module functionality.""" + + @pytest.mark.asyncio + async def test_devices_basic_functionality(self, mock_config_setup): + """Test basic device polling functionality.""" + with patch("switchmap.poller.poll.ConfigPoller") as mock_config: + mock_config.return_value = mock_config_setup + with patch( + "switchmap.poller.poll.device", new_callable=AsyncMock + ) as mock_device: + mock_device.return_value = True + + await devices() + + # Verify device was called for each hostname + assert mock_device.call_count == 2 + + # Check that the calls were made with correct hostnames + call_args_list = mock_device.call_args_list + hostnames_called = [ + call[0][0].hostname for call in call_args_list + ] + assert "device1" in hostnames_called + assert "device2" in hostnames_called + + @pytest.mark.asyncio + async def test_devices_with_custom_concurrency(self, mock_config_setup): + """Test device polling with custom concurrency limit.""" + with patch("switchmap.poller.poll.ConfigPoller") as mock_config: + mock_config.return_value = mock_config_setup + with patch( + "switchmap.poller.poll.device", new_callable=AsyncMock + ) as mock_device: + mock_device.return_value = True + + await devices(max_concurrent_devices=1) + + # Should still call device for both hostnames + assert mock_device.call_count == 2 + + @pytest.mark.asyncio + async def test_cli_device_found(self, mock_config_setup): + """Test CLI device polling when hostname is found.""" + with patch("switchmap.poller.poll.ConfigPoller") as mock_config: + mock_config.return_value = mock_config_setup + with patch( + "switchmap.poller.poll.device", new_callable=AsyncMock + ) as mock_device: + mock_device.return_value = True + + await cli_device("device1") + + # Should call device once for the found hostname + assert mock_device.call_count == 1 + assert mock_device.call_args[0][0].hostname == "device1" + + @pytest.mark.asyncio + async def test_cli_device_not_found(self, mock_config_setup): + """Test CLI device polling when hostname is not found.""" + with patch("switchmap.poller.poll.ConfigPoller") as mock_config: + mock_config.return_value = mock_config_setup + with patch( + "switchmap.poller.poll.device", new_callable=AsyncMock + ) as mock_device: + + await cli_device("nonexistent_device") + + # Should not call device for non-existent hostname + assert mock_device.call_count == 0 + + @pytest.mark.asyncio + async def test_device_with_skip_file(self, mock_poll_meta): + """Test device processing when skip file exists.""" + with patch("switchmap.poller.poll.files.skip_file") as mock_skip_file: + mock_skip_file.return_value = "/path/to/skip/file" + with patch( + "switchmap.poller.poll.os.path.isfile" + ) as mock_isfile: + mock_isfile.return_value = True + with patch("switchmap.poller.poll.log.log2debug") as mock_log: + # Create mock semaphore and session + mock_semaphore = AsyncMock() + mock_session = MagicMock() + + result = await device( + mock_poll_meta, mock_semaphore, mock_session + ) + + # Should return False when skip file exists + assert result is False + mock_log.assert_called() + + @pytest.mark.asyncio + async def test_device_invalid_hostname(self): + """Test device processing with invalid hostname.""" + mock_semaphore = AsyncMock() + mock_session = MagicMock() + + # Test with None hostname + poll_meta = _META(zone="zone1", hostname=None, config=MagicMock()) + result = await device(poll_meta, mock_semaphore, mock_session) + assert result is False + + # Test with "none" hostname + poll_meta = _META(zone="zone1", hostname="none", config=MagicMock()) + result = await device(poll_meta, mock_semaphore, mock_session) + assert result is False + + @pytest.mark.asyncio + async def test_device_snmp_failure(self, mock_poll_meta): + """Test device processing when SNMP initialization fails.""" + mock_skip_file_path = "/path/to/skip/file" + with patch("switchmap.poller.poll.files.skip_file") as mock_skip_file: + mock_skip_file.return_value = mock_skip_file_path + with patch( + "switchmap.poller.poll.os.path.isfile" + ) as mock_isfile: + mock_isfile.return_value = False + with patch( + "switchmap.poller.poll.poller.Poll" + ) as mock_poll_cls: + mock_poll_instance = AsyncMock() + mock_poll_instance.initialize_snmp.return_value = False + mock_poll_cls.return_value = mock_poll_instance + + mock_semaphore = AsyncMock() + mock_session = MagicMock() + + result = await device( + mock_poll_meta, mock_semaphore, mock_session + ) + + # Should return False when SNMP initialization fails + assert result is False + + @pytest.mark.asyncio + async def test_device_successful_poll_no_post(self, mock_poll_meta): + """Test successful device polling without posting data.""" + mock_skip_file_path = "/path/to/skip/file" + with patch("switchmap.poller.poll.files.skip_file") as mock_skip_file: + mock_skip_file.return_value = mock_skip_file_path + with patch( + "switchmap.poller.poll.os.path.isfile" + ) as mock_isfile: + mock_isfile.return_value = False + with patch( + "switchmap.poller.poll.poller.Poll" + ) as mock_poll_cls: + mock_poll_instance = AsyncMock() + mock_poll_instance.initialize_snmp.return_value = True + mock_poll_instance.query.return_value = {"test": "data"} + mock_poll_cls.return_value = mock_poll_instance + + mock_semaphore = AsyncMock() + mock_session = MagicMock() + + with patch( + "switchmap.poller.poll.udevice.Device" + ) as mock_device_class: + mock_device_instance = MagicMock() + mock_device_instance.process.return_value = { + "misc": {}, + "test": "data" + } + mock_device_class.return_value = mock_device_instance + + result = await device( + mock_poll_meta, + mock_semaphore, + mock_session, + post=False + ) + + # Should return True for successful poll + assert result is True + # Should create Device instance and call process + mock_device_class.assert_called_once() + mock_device_instance.process.assert_called_once() diff --git a/tests/switchmap_/poller/test_poll.py b/tests/switchmap_/poller/test_poll.py deleted file mode 100644 index 83f109ac..00000000 --- a/tests/switchmap_/poller/test_poll.py +++ /dev/null @@ -1,203 +0,0 @@ -#!/usr/bin/env python3 -"""Test the poller poll module.""" - -import os -import sys -import unittest -from unittest.mock import patch, MagicMock, call -from multiprocessing import ProcessError, TimeoutError -from switchmap.poller.poll import devices, device, cli_device, _META - -# Try to create a working PYTHONPATH -EXEC_DIR = os.path.dirname(os.path.realpath(__file__)) -ROOT_DIR = os.path.abspath( - os.path.join( - os.path.abspath( - os.path.join(EXEC_DIR, os.pardir) # Move up to 'poller' - ), - os.pardir, # Move up to 'switchmap_' - ) -) -_EXPECTED = "{0}tests{0}switchmap_{0}poller".format(os.sep) - -if EXEC_DIR.endswith(_EXPECTED): - # Prepend the root directory to the Python path - sys.path.insert(0, ROOT_DIR) -else: - print( - f'This script is not installed in the "{_EXPECTED}" directory. ' - "Please fix." - ) - sys.exit(2) - - -class TestPollModule(unittest.TestCase): - """Test cases for the poll module functionality.""" - - def setUp(self): - """Set up the test environment.""" - self.mock_config_instance = MagicMock() - self.mock_zone = MagicMock() - self.mock_zone.name = "zone1" - self.mock_zone.hostnames = ["device1", "device2"] - self.mock_config_instance.zones.return_value = [self.mock_zone] - self.mock_config_instance.agent_subprocesses.return_value = 2 - - def test_devices_without_multiprocessing(self): - """Test device processing without multiprocessing.""" - with patch("switchmap.poller.poll.ConfigPoller") as mock_config: - mock_config.return_value = self.mock_config_instance - with patch("switchmap.poller.poll.device") as mock_device: - devices(multiprocessing=False) - expected_calls = [ - call( - _META( - zone="zone1", - hostname="device1", - config=self.mock_config_instance, - ) - ), - call( - _META( - zone="zone1", - hostname="device2", - config=self.mock_config_instance, - ) - ), - ] - mock_device.assert_has_calls(expected_calls) - - def test_devices_with_multiprocessing(self): - """Test device processing with multiprocessing.""" - with patch("switchmap.poller.poll.ConfigPoller") as mock_config: - mock_config.return_value = self.mock_config_instance - with patch("switchmap.poller.poll.Pool") as mock_pool: - mock_pool_instance = MagicMock() - mock_pool.return_value.__enter__.return_value = ( - mock_pool_instance - ) - - devices(multiprocessing=True) - - mock_pool.assert_called_once_with(processes=2) - expected_args = [ - _META( - zone="zone1", - hostname="device1", - config=self.mock_config_instance, - ), - _META( - zone="zone1", - hostname="device2", - config=self.mock_config_instance, - ), - ] - mock_pool_instance.map.assert_called_once_with( - device, expected_args - ) - - def test_devices_with_multiprocessing_pool_error(self): - """Test error handling when pool creation fails.""" - with patch("switchmap.poller.poll.ConfigPoller") as mock_config: - mock_config.return_value = self.mock_config_instance - with patch("switchmap.poller.poll.Pool") as mock_pool: - mock_pool.side_effect = OSError("Failed to create pool") - with self.assertRaises(OSError): - devices(multiprocessing=True) - - def test_devices_with_multiprocessing_worker_error(self): - """Test error handling when a worker process fails.""" - with patch("switchmap.poller.poll.ConfigPoller") as mock_config: - mock_config.return_value = self.mock_config_instance - with patch("switchmap.poller.poll.Pool") as mock_pool: - mock_pool_instance = MagicMock() - mock_pool.return_value.__enter__.return_value = ( - mock_pool_instance - ) - mock_pool_instance.map.side_effect = RuntimeError( - "Worker process failed" - ) - - with self.assertRaises((RuntimeError, ProcessError)): - devices(multiprocessing=True) - - def test_devices_with_multiprocessing_timeout(self): - """Test handling of worker process timeout in multiprocessing mode.""" - with patch("switchmap.poller.poll.ConfigPoller") as mock_config: - mock_config.return_value = self.mock_config_instance - with patch("switchmap.poller.poll.Pool") as mock_pool: - mock_pool_instance = MagicMock() - mock_pool.return_value.__enter__.return_value = ( - mock_pool_instance - ) - mock_pool_instance.map.side_effect = TimeoutError( - "Worker timeout" - ) - - with self.assertRaises(TimeoutError): - devices(multiprocessing=True) - - @patch("switchmap.poller.poll.files.skip_file") - @patch("switchmap.poller.poll.os.path.isfile") - @patch("switchmap.poller.poll.poller.Poll") - @patch("switchmap.poller.poll.rest.post") - def test_device_with_skip_file( - self, mock_rest_post, mock_poll, mock_isfile, mock_skip_file - ): - """Test device processing when skip file exists.""" - mock_skip_file.return_value = "/path/to/skip/file" - mock_isfile.return_value = True - - with patch("switchmap.poller.poll.log.log2debug") as mock_log: - poll_meta = _META(zone="zone1", hostname="device1", config=None) - device(poll_meta) - mock_log.assert_called_once_with(1041, unittest.mock.ANY) - mock_poll.assert_not_called() - mock_rest_post.assert_not_called() - - @patch("switchmap.poller.poll.files.skip_file") - @patch("switchmap.poller.poll.os.path.isfile") - @patch("switchmap.poller.poll.poller.Poll") - @patch("switchmap.poller.poll.rest.post") - def test_device_with_invalid_snmp_data( - self, mock_rest_post, mock_poll, mock_isfile, mock_skip_file - ): - """Test device processing with invalid SNMP data.""" - mock_skip_file.return_value = "/path/to/skip/file" - mock_isfile.return_value = False - mock_poll_instance = MagicMock() - mock_poll_instance.query.return_value = None - mock_poll.return_value = mock_poll_instance - - with patch("switchmap.poller.poll.log.log2debug") as mock_log: - poll_meta = _META(zone="zone1", hostname="device1", config=None) - device(poll_meta) - mock_log.assert_called_once_with(1025, unittest.mock.ANY) - mock_rest_post.assert_not_called() - - def test_cli_device_not_found(self): - """Test CLI device handling when device not found.""" - with patch("switchmap.poller.poll.ConfigPoller") as mock_config: - mock_config.return_value = self.mock_config_instance - with patch("switchmap.poller.poll.log.log2see") as mock_log: - cli_device("unknown-device") - mock_log.assert_called_once_with( - 1036, "No hostname unknown-device found in configuration" - ) - - def test_cli_device_found(self): - """Test CLI device handling when device is found.""" - with patch("switchmap.poller.poll.ConfigPoller") as mock_config: - mock_config.return_value = self.mock_config_instance - with patch("switchmap.poller.poll.device") as mock_device: - cli_device("device1") - expected_meta = _META( - zone="zone1", - hostname="device1", - config=self.mock_config_instance, - ) - mock_device.assert_called_once_with(expected_meta, post=False) - - -if __name__ == "__main__": - unittest.main() From 22dc83e7fa819d04db79ba1372c93b6fc2ff13ad Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Fri, 26 Sep 2025 00:53:27 +0530 Subject: [PATCH 44/50] minor fix --- switchmap/server/db/misc/oui.py | 8 ++++++-- tests/switchmap_/poller/test_async_poll.py | 18 ++++++------------ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/switchmap/server/db/misc/oui.py b/switchmap/server/db/misc/oui.py index e4fcb79d..3fe8b6c2 100644 --- a/switchmap/server/db/misc/oui.py +++ b/switchmap/server/db/misc/oui.py @@ -49,8 +49,12 @@ def update_db_oui(filepath, new=False): if new: # Bulk insert on fresh install, skip checks - _oui.insert_row(rows) - else: + try: + _oui.insert_row(rows) + except IntegrityError: + SCOPED_SESSION.rollback() + new = False + if not new: # For updates, check existing entries for row in rows: existing_entry = _oui.exists(row.oui) diff --git a/tests/switchmap_/poller/test_async_poll.py b/tests/switchmap_/poller/test_async_poll.py index 1f78bdc4..adc621e2 100644 --- a/tests/switchmap_/poller/test_async_poll.py +++ b/tests/switchmap_/poller/test_async_poll.py @@ -7,7 +7,7 @@ from unittest.mock import patch, MagicMock, AsyncMock from switchmap.poller.poll import devices, device, cli_device, _META -# Try to create a working PYTHONPATH +# Create a working PYTHONPATH EXEC_DIR = os.path.dirname(os.path.realpath(__file__)) ROOT_DIR = os.path.abspath( os.path.join( @@ -124,9 +124,7 @@ async def test_device_with_skip_file(self, mock_poll_meta): """Test device processing when skip file exists.""" with patch("switchmap.poller.poll.files.skip_file") as mock_skip_file: mock_skip_file.return_value = "/path/to/skip/file" - with patch( - "switchmap.poller.poll.os.path.isfile" - ) as mock_isfile: + with patch("switchmap.poller.poll.os.path.isfile") as mock_isfile: mock_isfile.return_value = True with patch("switchmap.poller.poll.log.log2debug") as mock_log: # Create mock semaphore and session @@ -163,9 +161,7 @@ async def test_device_snmp_failure(self, mock_poll_meta): mock_skip_file_path = "/path/to/skip/file" with patch("switchmap.poller.poll.files.skip_file") as mock_skip_file: mock_skip_file.return_value = mock_skip_file_path - with patch( - "switchmap.poller.poll.os.path.isfile" - ) as mock_isfile: + with patch("switchmap.poller.poll.os.path.isfile") as mock_isfile: mock_isfile.return_value = False with patch( "switchmap.poller.poll.poller.Poll" @@ -190,9 +186,7 @@ async def test_device_successful_poll_no_post(self, mock_poll_meta): mock_skip_file_path = "/path/to/skip/file" with patch("switchmap.poller.poll.files.skip_file") as mock_skip_file: mock_skip_file.return_value = mock_skip_file_path - with patch( - "switchmap.poller.poll.os.path.isfile" - ) as mock_isfile: + with patch("switchmap.poller.poll.os.path.isfile") as mock_isfile: mock_isfile.return_value = False with patch( "switchmap.poller.poll.poller.Poll" @@ -211,7 +205,7 @@ async def test_device_successful_poll_no_post(self, mock_poll_meta): mock_device_instance = MagicMock() mock_device_instance.process.return_value = { "misc": {}, - "test": "data" + "test": "data", } mock_device_class.return_value = mock_device_instance @@ -219,7 +213,7 @@ async def test_device_successful_poll_no_post(self, mock_poll_meta): mock_poll_meta, mock_semaphore, mock_session, - post=False + post=False, ) # Should return True for successful poll From 628004dd4742c843b0d5a2b6513456c1c04093f4 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Fri, 26 Sep 2025 01:00:57 +0530 Subject: [PATCH 45/50] fix docstring violations --- tests/switchmap_/poller/test_async_poll.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/switchmap_/poller/test_async_poll.py b/tests/switchmap_/poller/test_async_poll.py index adc621e2..7286dbad 100644 --- a/tests/switchmap_/poller/test_async_poll.py +++ b/tests/switchmap_/poller/test_async_poll.py @@ -32,7 +32,14 @@ @pytest.fixture def mock_config_setup(): - """Set up mock configuration for tests.""" + """Set up mock configuration for tests. + + Args: + None + + Returns: + MagicMock: Mock configuration instance with zones and subprocesses. + """ mock_config_instance = MagicMock() mock_zone = MagicMock() mock_zone.name = "zone1" @@ -44,7 +51,14 @@ def mock_config_setup(): @pytest.fixture def mock_poll_meta(): - """Create a mock poll meta object.""" + """Create a mock poll meta object. + + Args: + None + + Returns: + _META: Mock poll meta object with zone, hostname, and config. + """ return _META(zone="zone1", hostname="device1", config=MagicMock()) From facd32ef54fb0f4cb4e9cb62a6bf3afe7bb2eb54 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Fri, 26 Sep 2025 18:00:46 +0530 Subject: [PATCH 46/50] transformed all sync failing test compatible with our async poller --- .../snmp/mib/cisco/test_mib_ciscoc2900.py | 33 ++--- .../snmp/mib/cisco/test_mib_ciscocdp.py | 52 ++++--- .../snmp/mib/cisco/test_mib_ciscoietfip.py | 4 +- .../snmp/mib/cisco/test_mib_ciscostack.py | 25 ++-- .../test_mib_ciscovlaniftablerelationship.py | 41 +++--- .../mib/cisco/test_mib_ciscovlanmembership.py | 44 +++--- .../snmp/mib/cisco/test_mib_ciscovtp.py | 80 +++++------ .../snmp/mib/generic/test_mib_bridge.py | 4 +- .../snmp/mib/generic/test_mib_entity.py | 4 +- .../snmp/mib/generic/test_mib_essswitch.py | 34 ++--- .../snmp/mib/generic/test_mib_etherlike.py | 33 ++--- .../poller/snmp/mib/generic/test_mib_if.py | 128 ++++++++---------- .../poller/snmp/mib/generic/test_mib_ip.py | 29 ++-- .../snmp/mib/juniper/test_mib_junipervlan.py | 4 +- 14 files changed, 229 insertions(+), 286 deletions(-) diff --git a/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscoc2900.py b/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscoc2900.py index 266201a6..545034e9 100755 --- a/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscoc2900.py +++ b/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscoc2900.py @@ -4,7 +4,7 @@ import os import sys import unittest -from mock import Mock +from unittest.mock import Mock, AsyncMock # Try to create a working PYTHONPATH EXEC_DIR = os.path.dirname(os.path.realpath(__file__)) @@ -153,7 +153,7 @@ def test_init_query(self): pass -class TestMibCiscoc2900(unittest.TestCase): +class TestMibCiscoc2900(unittest.IsolatedAsyncioTestCase): """Checks all functions and methods.""" ######################################################################### @@ -167,11 +167,9 @@ class TestMibCiscoc2900(unittest.TestCase): # Set the stage for SNMPwalk for integer results snmpobj_integer = Mock(spec=Query) - mock_spec_integer = { - "swalk.return_value": nwalk_results_integer, - "walk.return_value": nwalk_results_integer, - } - snmpobj_integer.configure_mock(**mock_spec_integer) + # Configure async methods + snmpobj_integer.swalk = AsyncMock(return_value=nwalk_results_integer) + snmpobj_integer.walk = AsyncMock(return_value=nwalk_results_integer) # Initializing key variables expected_dict = { @@ -214,7 +212,7 @@ def test___init__(self): """Testing function __init__.""" pass - def test_layer1(self): + async def test_layer1(self): """Testing function layer1.""" # Initializing key variables expected_dict = { @@ -230,12 +228,11 @@ def test_layer1(self): # Set the stage for SNMPwalk snmpobj = Mock(spec=Query) - mock_spec = {"swalk.return_value": self.nwalk_results_integer} - snmpobj.configure_mock(**mock_spec) + snmpobj.swalk = AsyncMock(return_value=self.nwalk_results_integer) # Get results testobj = testimport.init_query(snmpobj) - results = testobj.layer1() + results = await testobj.layer1() # Basic testing of results for primary in results.keys(): @@ -245,7 +242,7 @@ def test_layer1(self): expected_dict[primary][secondary], ) - def test_c2900portlinkbeatstatus(self): + async def test_c2900portlinkbeatstatus(self): """Testing function c2900portlinkbeatstatus.""" # Initialize key variables oid_key = "c2900PortLinkbeatStatus" @@ -253,7 +250,7 @@ def test_c2900portlinkbeatstatus(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.c2900portlinkbeatstatus() + results = await testobj.c2900portlinkbeatstatus() # Basic testing of results for key, value in results.items(): @@ -261,18 +258,18 @@ def test_c2900portlinkbeatstatus(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.c2900portlinkbeatstatus(oidonly=True) + results = await testobj.c2900portlinkbeatstatus(oidonly=True) self.assertEqual(results, oid) - def test_c2900portduplexstatus(self): + async def test_c2900portduplexstatus(self): """Testing function c2900portduplexstatus.""" # Initialize key variables - oid_key = "c2900PortLinkbeatStatus" + oid_key = "c2900PortDuplexStatus" oid = ".1.3.6.1.4.1.9.9.87.1.4.1.1.32" # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.c2900portduplexstatus() + results = await testobj.c2900portduplexstatus() # Basic testing of results for key, value in results.items(): @@ -280,7 +277,7 @@ def test_c2900portduplexstatus(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.c2900portduplexstatus(oidonly=True) + results = await testobj.c2900portduplexstatus(oidonly=True) self.assertEqual(results, oid) diff --git a/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscocdp.py b/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscocdp.py index d5deec65..359efee4 100755 --- a/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscocdp.py +++ b/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscocdp.py @@ -4,7 +4,7 @@ import os import sys import unittest -from mock import Mock +from unittest.mock import Mock, AsyncMock # Try to create a working PYTHONPATH EXEC_DIR = os.path.dirname(os.path.realpath(__file__)) @@ -106,7 +106,7 @@ def swalk(self): pass -class TestCiscoCDPFunctions(unittest.TestCase): +class TestCiscoCDPFunctions(unittest.IsolatedAsyncioTestCase): """Checks all methods.""" ######################################################################### @@ -142,7 +142,7 @@ def test_init_query(self): pass -class TestCiscoCDP(unittest.TestCase): +class TestCiscoCDP(unittest.IsolatedAsyncioTestCase): """Checks all functions and methods.""" ######################################################################### @@ -174,26 +174,24 @@ def tearDownClass(cls): # Cleanup the CONFIG.cleanup() - def test_supported(self): + async def test_supported(self): """Testing method / function supported.""" # Set the stage for oid_exists returning True snmpobj = Mock(spec=Query) - mock_spec = {"oid_exists.return_value": True} - snmpobj.configure_mock(**mock_spec) + snmpobj.oid_exists = AsyncMock(return_value=True) # Test supported testobj = testimport.init_query(snmpobj) - self.assertEqual(testobj.supported(), True) + self.assertEqual(await testobj.supported(), True) # Set the stage for oid_exists returning False - mock_spec = {"oid_exists.return_value": False} - snmpobj.configure_mock(**mock_spec) + snmpobj.oid_exists = AsyncMock(return_value=False) # Test unsupported testobj = testimport.init_query(snmpobj) - self.assertEqual(testobj.supported(), False) + self.assertEqual(await testobj.supported(), False) - def test_layer1(self): + async def test_layer1(self): """Testing method / function layer1.""" # Initializing key variables expected_dict = { @@ -211,12 +209,11 @@ def test_layer1(self): # Set the stage for SNMPwalk snmpobj = Mock(spec=Query) - mock_spec = {"swalk.return_value": self.walk_results_string} - snmpobj.configure_mock(**mock_spec) + snmpobj.swalk = AsyncMock(return_value=self.walk_results_string) # Get results testobj = testimport.init_query(snmpobj) - results = testobj.layer1() + results = await testobj.layer1() # Basic testing of results for primary in results.keys(): @@ -226,61 +223,58 @@ def test_layer1(self): expected_dict[primary][secondary], ) - def test_cdpcachedeviceid(self): + async def test_cdpcachedeviceid(self): """Testing method / function cdpcachedeviceid.""" # Set the stage for SNMPwalk snmpobj = Mock(spec=Query) - mock_spec = {"swalk.return_value": self.walk_results_string} - snmpobj.configure_mock(**mock_spec) + snmpobj.swalk = AsyncMock(return_value=self.walk_results_string) # Get results testobj = testimport.init_query(snmpobj) - results = testobj.cdpcachedeviceid() + results = await testobj.cdpcachedeviceid() # Basic testing of results for key in results.keys(): self.assertEqual(isinstance(key, int), True) # Test that we are getting the correct OID - results = testobj.cdpcachedeviceid(oidonly=True) + results = await testobj.cdpcachedeviceid(oidonly=True) self.assertEqual(results, ".1.3.6.1.4.1.9.9.23.1.2.1.1.6") - def test_cdpcacheplatform(self): + async def test_cdpcacheplatform(self): """Testing method / function cdpcacheplatform.""" # Set the stage for SNMPwalk snmpobj = Mock(spec=Query) - mock_spec = {"swalk.return_value": self.walk_results_string} - snmpobj.configure_mock(**mock_spec) + snmpobj.swalk = AsyncMock(return_value=self.walk_results_string) # Get results testobj = testimport.init_query(snmpobj) - results = testobj.cdpcacheplatform() + results = await testobj.cdpcacheplatform() # Basic testing of results for key in results.keys(): self.assertEqual(isinstance(key, int), True) # Test that we are getting the correct OID - results = testobj.cdpcacheplatform(oidonly=True) + results = await testobj.cdpcacheplatform(oidonly=True) self.assertEqual(results, ".1.3.6.1.4.1.9.9.23.1.2.1.1.8") - def test_cdpcachedeviceport(self): + async def test_cdpcachedeviceport(self): """Testing method / function cdpcachedeviceport.""" # Set the stage for SNMPwalk snmpobj = Mock(spec=Query) - mock_spec = {"swalk.return_value": self.walk_results_string} - snmpobj.configure_mock(**mock_spec) + snmpobj.swalk = AsyncMock(return_value=self.walk_results_string) # Get results testobj = testimport.init_query(snmpobj) - results = testobj.cdpcachedeviceport() + results = await testobj.cdpcachedeviceport() # Basic testing of results for key in results.keys(): self.assertEqual(isinstance(key, int), True) # Test that we are getting the correct OID - results = testobj.cdpcachedeviceport(oidonly=True) + results = await testobj.cdpcachedeviceport(oidonly=True) self.assertEqual(results, ".1.3.6.1.4.1.9.9.23.1.2.1.1.7") def test__ifindex(self): diff --git a/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscoietfip.py b/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscoietfip.py index 8839e7f8..9f33d705 100755 --- a/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscoietfip.py +++ b/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscoietfip.py @@ -104,7 +104,7 @@ def swalk(self): pass -class TestCiscoIetfIpQueryFunctions(unittest.TestCase): +class TestCiscoIetfIpQueryFunctions(unittest.IsolatedAsyncioTestCase): """Checks all methods.""" ######################################################################### @@ -140,7 +140,7 @@ def test_init_query(self): pass -class TestCiscoIetfIpQuery(unittest.TestCase): +class TestCiscoIetfIpQuery(unittest.IsolatedAsyncioTestCase): """Checks all functions and methods.""" ######################################################################### diff --git a/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscostack.py b/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscostack.py index 234736da..0a1f8341 100755 --- a/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscostack.py +++ b/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscostack.py @@ -4,7 +4,7 @@ import os import sys import unittest -from mock import Mock +from unittest.mock import Mock, AsyncMock # Try to create a working PYTHONPATH EXEC_DIR = os.path.dirname(os.path.realpath(__file__)) @@ -117,7 +117,7 @@ def walk(self): pass -class TestMibCiscoStackFunctions(unittest.TestCase): +class TestMibCiscoStackFunctions(unittest.IsolatedAsyncioTestCase): """Checks all methods.""" ######################################################################### @@ -153,7 +153,7 @@ def test_init_query(self): pass -class TestMibCiscoStack(unittest.TestCase): +class TestMibCiscoStack(unittest.IsolatedAsyncioTestCase): """Checks all functions and methods.""" ######################################################################### @@ -167,11 +167,8 @@ class TestMibCiscoStack(unittest.TestCase): # Set the stage for SNMPwalk for integer results snmpobj_integer = Mock(spec=Query) - mock_spec_integer = { - "swalk.return_value": nwalk_results_integer, - "walk.return_value": nwalk_results_integer, - } - snmpobj_integer.configure_mock(**mock_spec_integer) + snmpobj_integer.swalk = AsyncMock(return_value=nwalk_results_integer) + snmpobj_integer.walk = AsyncMock(return_value=nwalk_results_integer) # Initializing key variables expected_dict = {100: {"portDuplex": 100}, 200: {"portDuplex": 100}} @@ -209,7 +206,7 @@ def test_layer1(self): """Testing function layer1.""" pass - def test_portduplex(self): + async def test_portduplex(self): """Testing function portduplex.""" # Initialize key variables oid_key = "portDuplex" @@ -217,7 +214,7 @@ def test_portduplex(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.portduplex() + results = await testobj.portduplex() # Basic testing of results for key, value in results.items(): @@ -225,10 +222,10 @@ def test_portduplex(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.portduplex(oidonly=True) + results = await testobj.portduplex(oidonly=True) self.assertEqual(results, oid) - def test__portifindex(self): + async def test__portifindex(self): """Testing function _portifindex.""" # Initialize key variables oid_key = "portDuplex" @@ -236,7 +233,7 @@ def test__portifindex(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj._portifindex() + results = await testobj._portifindex() # Basic testing of results for key, value in results.items(): @@ -244,7 +241,7 @@ def test__portifindex(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj._portifindex(oidonly=True) + results = await testobj._portifindex(oidonly=True) self.assertEqual(results, oid) diff --git a/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscovlaniftablerelationship.py b/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscovlaniftablerelationship.py index 16477229..0cfa9fb7 100755 --- a/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscovlaniftablerelationship.py +++ b/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscovlaniftablerelationship.py @@ -4,7 +4,9 @@ import os import sys import unittest -from mock import Mock +from unittest.mock import Mock, AsyncMock + +from sqlalchemy import true # Try to create a working PYTHONPATH EXEC_DIR = os.path.dirname(os.path.realpath(__file__)) @@ -55,6 +57,7 @@ sys.exit(2) # Create the necessary configuration to load the module +from switchmap.poller import snmp from tests.testlib_ import setup CONFIG = setup.config() @@ -119,7 +122,7 @@ def walk(self): pass -class TestMibCiscoVlanIfTableFunctions(unittest.TestCase): +class TestMibCiscoVlanIfTableFunctions(unittest.IsolatedAsyncioTestCase): """Checks all methods.""" ######################################################################### @@ -155,7 +158,7 @@ def test_init_query(self): pass -class TestMibCiscoVlanIfTable(unittest.TestCase): +class TestMibCiscoVlanIfTable(unittest.IsolatedAsyncioTestCase): """Checks all functions and methods.""" ######################################################################### @@ -174,11 +177,8 @@ class TestMibCiscoVlanIfTable(unittest.TestCase): # Set the stage for SNMPwalk for integer results snmpobj_integer = Mock(spec=Query) - mock_spec_integer = { - "swalk.return_value": nwalk_results_integer, - "walk.return_value": nwalk_results_integer, - } - snmpobj_integer.configure_mock(**mock_spec_integer) + snmpobj_integer.swalk = AsyncMock(return_value=nwalk_results_integer) + snmpobj_integer.walk = AsyncMock(return_value=nwalk_results_integer) # Initializing key variables expected_dict = { @@ -217,26 +217,24 @@ def test___init__(self): """Testing function __init__.""" pass - def test_supported(self): + async def test_supported(self): """Testing method / function supported.""" # Set the stage for oid_exists returning True snmpobj = Mock(spec=Query) - mock_spec = {"oid_exists.return_value": True} - snmpobj.configure_mock(**mock_spec) + snmpobj.oid_exists = AsyncMock(return_value=True) # Test supported testobj = testimport.init_query(snmpobj) - self.assertEqual(testobj.supported(), True) + self.assertEqual(await testobj.supported(), True) # Set the stage for oid_exists returning False - mock_spec = {"oid_exists.return_value": False} - snmpobj.configure_mock(**mock_spec) + snmpobj.oid_exists = AsyncMock(return_value=False) # Test unsupported testobj = testimport.init_query(snmpobj) - self.assertEqual(testobj.supported(), False) + self.assertEqual(await testobj.supported(), False) - def test_layer1(self): + async def test_layer1(self): """Testing function layer1.""" # Initializing key variables expected_dict = { @@ -248,12 +246,11 @@ def test_layer1(self): # Set the stage for SNMPwalk snmpobj = Mock(spec=Query) - mock_spec = {"swalk.return_value": self.nwalk_results_integer} - snmpobj.configure_mock(**mock_spec) + snmpobj.swalk = AsyncMock(return_value=self.nwalk_results_integer) # Get results testobj = testimport.init_query(snmpobj) - results = testobj.layer1() + results = await testobj.layer1() # Basic testing of results for primary in results.keys(): @@ -263,14 +260,14 @@ def test_layer1(self): expected_dict[primary][secondary], ) - def test_cviroutedvlanifindex(self): + async def test_cviroutedvlanifindex(self): """Testing function cviroutedvlanifindex.""" oid_key = "CiscoVlanIftableRelationship" oid = ".1.3.6.1.4.1.9.9.128.1.1.1.1.3" # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.cviroutedvlanifindex() + results = await testobj.cviroutedvlanifindex() # Basic testing of results for key, value in results.items(): @@ -279,7 +276,7 @@ def test_cviroutedvlanifindex(self): self.assertEqual(value, expected_value) # Test that we are getting the correct OID - results = testobj.cviroutedvlanifindex(oidonly=True) + results = await testobj.cviroutedvlanifindex(oidonly=True) self.assertEqual(results, oid) diff --git a/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscovlanmembership.py b/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscovlanmembership.py index 0c697f5f..b7bd27dc 100755 --- a/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscovlanmembership.py +++ b/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscovlanmembership.py @@ -4,7 +4,7 @@ import os import sys import unittest -from mock import Mock +from unittest.mock import Mock, AsyncMock # Try to create a working PYTHONPATH EXEC_DIR = os.path.dirname(os.path.realpath(__file__)) @@ -119,7 +119,7 @@ def walk(self): pass -class TestMibCiscoVlanMemberFunctions(unittest.TestCase): +class TestMibCiscoVlanMemberFunctions(unittest.IsolatedAsyncioTestCase): """Checks all methods.""" ######################################################################### @@ -155,7 +155,7 @@ def test_init_query(self): pass -class TestMibCiscoVlanMember(unittest.TestCase): +class TestMibCiscoVlanMember(unittest.IsolatedAsyncioTestCase): """Checks all functions and methods.""" ######################################################################### @@ -169,11 +169,8 @@ class TestMibCiscoVlanMember(unittest.TestCase): # Set the stage for SNMPwalk for integer results snmpobj_integer = Mock(spec=Query) - mock_spec_integer = { - "swalk.return_value": nwalk_results_integer, - "walk.return_value": nwalk_results_integer, - } - snmpobj_integer.configure_mock(**mock_spec_integer) + snmpobj_integer.swalk = AsyncMock(return_value=nwalk_results_integer) + snmpobj_integer.walk = AsyncMock(return_value=nwalk_results_integer) # Initializing key variables expected_dict = { @@ -210,26 +207,24 @@ def test___init__(self): """Testing function __init__.""" pass - def test_supported(self): + async def test_supported(self): """Testing method / function supported.""" # Set the stage for oid_exists returning True snmpobj = Mock(spec=Query) - mock_spec = {"oid_exists.return_value": True} - snmpobj.configure_mock(**mock_spec) + snmpobj.oid_exists = AsyncMock(return_value=True) # Test supported testobj = testimport.init_query(snmpobj) - self.assertEqual(testobj.supported(), True) + self.assertEqual(await testobj.supported(), True) # Set the stage for oid_exists returning False - mock_spec = {"oid_exists.return_value": False} - snmpobj.configure_mock(**mock_spec) + snmpobj.oid_exists = AsyncMock(return_value=False) # Test unsupported testobj = testimport.init_query(snmpobj) - self.assertEqual(testobj.supported(), False) + self.assertEqual(await testobj.supported(), False) - def test_layer1(self): + async def test_layer1(self): """Testing function layer1.""" # Initializing key variables expected_dict = { @@ -239,12 +234,11 @@ def test_layer1(self): # Set the stage for SNMPwalk snmpobj = Mock(spec=Query) - mock_spec = {"swalk.return_value": self.nwalk_results_integer} - snmpobj.configure_mock(**mock_spec) + snmpobj.swalk = AsyncMock(return_value=self.nwalk_results_integer) # Get results testobj = testimport.init_query(snmpobj) - results = testobj.layer1() + results = await testobj.layer1() # Basic testing of results for primary in results.keys(): @@ -254,14 +248,14 @@ def test_layer1(self): expected_dict[primary][secondary], ) - def test_vmvlan(self): + async def test_vmvlan(self): """Testing function vmvlan.""" oid_key = "vmVlan" oid = ".1.3.6.1.4.1.9.9.68.1.2.2.1.2" # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.vmvlan() + results = await testobj.vmvlan() # Basic testing of results for key, value in results.items(): @@ -269,10 +263,10 @@ def test_vmvlan(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.vmvlan(oidonly=True) + results = await testobj.vmvlan(oidonly=True) self.assertEqual(results, oid) - def test_vmportstatus(self): + async def test_vmportstatus(self): """Testing function vmportstatus.""" # Initialize key variables oid_key = "vmPortStatus" @@ -280,7 +274,7 @@ def test_vmportstatus(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.vmportstatus() + results = await testobj.vmportstatus() # Basic testing of results for key, value in results.items(): @@ -288,7 +282,7 @@ def test_vmportstatus(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.vmportstatus(oidonly=True) + results = await testobj.vmportstatus(oidonly=True) self.assertEqual(results, oid) diff --git a/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscovtp.py b/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscovtp.py index 64d9007e..f974950b 100755 --- a/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscovtp.py +++ b/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscovtp.py @@ -5,7 +5,7 @@ import sys import binascii import unittest -from mock import Mock +from unittest.mock import Mock, AsyncMock # Try to create a working PYTHONPATH EXEC_DIR = os.path.dirname(os.path.realpath(__file__)) @@ -118,7 +118,7 @@ def walk(self): pass -class TestMibCiscoVTPFunctions(unittest.TestCase): +class TestMibCiscoVTPFunctions(unittest.IsolatedAsyncioTestCase): """Checks all methods.""" ######################################################################### @@ -154,7 +154,7 @@ def test_init_query(self): pass -class TestMibCiscoVTP(unittest.TestCase): +class TestMibCiscoVTP(unittest.IsolatedAsyncioTestCase): """Checks all functions and methods.""" ######################################################################### @@ -168,33 +168,24 @@ class TestMibCiscoVTP(unittest.TestCase): # Set the stage for SNMPwalk for integer results snmpobj_integer = Mock(spec=Query) - mock_spec_integer = { - "swalk.return_value": nwalk_results_integer, - "walk.return_value": nwalk_results_integer, - } - snmpobj_integer.configure_mock(**mock_spec_integer) + snmpobj_integer.swalk = AsyncMock(return_value=nwalk_results_integer) + snmpobj_integer.walk = AsyncMock(return_value=nwalk_results_integer) # Normalized walk returning integers for the ifIndex nwalk_results_ifindex = {100: 100, 200: 200} # Set the stage for SNMPwalk for integer results for the ifIndex snmpobj_ifindex = Mock(spec=Query) - mock_spec_ifindex = { - "swalk.return_value": nwalk_results_ifindex, - "walk.return_value": nwalk_results_ifindex, - } - snmpobj_ifindex.configure_mock(**mock_spec_ifindex) + snmpobj_ifindex.swalk = AsyncMock(return_value=nwalk_results_ifindex) + snmpobj_ifindex.walk = AsyncMock(return_value=nwalk_results_ifindex) # Normalized walk returning strings nwalk_results_bytes = {100: b"1234", 200: b"5678"} # Set the stage for SNMPwalk for string results snmpobj_bytes = Mock(spec=Query) - mock_spec_bytes = { - "swalk.return_value": nwalk_results_bytes, - "walk.return_value": nwalk_results_bytes, - } - snmpobj_bytes.configure_mock(**mock_spec_bytes) + snmpobj_bytes.swalk = AsyncMock(return_value=nwalk_results_bytes) + snmpobj_bytes.walk = AsyncMock(return_value=nwalk_results_bytes) # Normalized walk returning binary data nwalk_results_binary = { @@ -204,11 +195,8 @@ class TestMibCiscoVTP(unittest.TestCase): # Set the stage for SNMPwalk for binary results snmpobj_binary = Mock(spec=Query) - mock_spec_binary = { - "swalk.return_value": nwalk_results_binary, - "walk.return_value": nwalk_results_binary, - } - snmpobj_binary.configure_mock(**mock_spec_binary) + snmpobj_binary.swalk = AsyncMock(return_value=nwalk_results_binary) + snmpobj_binary.walk = AsyncMock(return_value=nwalk_results_binary) # Initializing key variables expected_dict = { @@ -218,7 +206,7 @@ class TestMibCiscoVTP(unittest.TestCase): "vlanTrunkPortNativeVlan": 1234, "vlanTrunkPortEncapsulationType": 1234, "vlanTrunkPortVlansEnabled": 1234, - "vtpVlanType": 1234, + "vtpVlanType": "1234", "vtpVlanName": "1234", "vtpVlanState": 1234, }, @@ -228,7 +216,7 @@ class TestMibCiscoVTP(unittest.TestCase): "vlanTrunkPortNativeVlan": 5678, "vlanTrunkPortEncapsulationType": 5678, "vlanTrunkPortVlansEnabled": 5678, - "vtpVlanType": 5678, + "vtpVlanType": "5678", "vtpVlanName": "5678", "vtpVlanState": 5678, }, @@ -275,7 +263,7 @@ def test_layer1(self): # the same type of results (eg. int, string, hex) pass - def test_vlantrunkportencapsulationtype(self): + async def test_vlantrunkportencapsulationtype(self): """Testing function vlantrunkportencapsulationtype.""" # Initialize key variables oid_key = "vlanTrunkPortEncapsulationType" @@ -283,7 +271,7 @@ def test_vlantrunkportencapsulationtype(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.vlantrunkportencapsulationtype() + results = await testobj.vlantrunkportencapsulationtype() # Basic testing of results for key, value in results.items(): @@ -291,10 +279,10 @@ def test_vlantrunkportencapsulationtype(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.vlantrunkportencapsulationtype(oidonly=True) + results = await testobj.vlantrunkportencapsulationtype(oidonly=True) self.assertEqual(results, oid) - def test_vlantrunkportnativevlan(self): + async def test_vlantrunkportnativevlan(self): """Testing function vlantrunkportnativevlan.""" # Initialize key variables oid_key = "vlanTrunkPortNativeVlan" @@ -302,7 +290,7 @@ def test_vlantrunkportnativevlan(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.vlantrunkportnativevlan() + results = await testobj.vlantrunkportnativevlan() # Basic testing of results for key, value in results.items(): @@ -310,10 +298,10 @@ def test_vlantrunkportnativevlan(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.vlantrunkportnativevlan(oidonly=True) + results = await testobj.vlantrunkportnativevlan(oidonly=True) self.assertEqual(results, oid) - def test_vlantrunkportdynamicstatus(self): + async def test_vlantrunkportdynamicstatus(self): """Testing function vlantrunkportdynamicstatus.""" # Initialize key variables oid_key = "vlanTrunkPortDynamicStatus" @@ -321,7 +309,7 @@ def test_vlantrunkportdynamicstatus(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.vlantrunkportdynamicstatus() + results = await testobj.vlantrunkportdynamicstatus() # Basic testing of results for key, value in results.items(): @@ -329,10 +317,10 @@ def test_vlantrunkportdynamicstatus(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.vlantrunkportdynamicstatus(oidonly=True) + results = await testobj.vlantrunkportdynamicstatus(oidonly=True) self.assertEqual(results, oid) - def test_vlantrunkportdynamicstate(self): + async def test_vlantrunkportdynamicstate(self): """Testing function vlantrunkportdynamicstate.""" # Initialize key variables oid_key = "vlanTrunkPortDynamicState" @@ -340,7 +328,7 @@ def test_vlantrunkportdynamicstate(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.vlantrunkportdynamicstate() + results = await testobj.vlantrunkportdynamicstate() # Basic testing of results for key, value in results.items(): @@ -348,10 +336,10 @@ def test_vlantrunkportdynamicstate(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.vlantrunkportdynamicstate(oidonly=True) + results = await testobj.vlantrunkportdynamicstate(oidonly=True) self.assertEqual(results, oid) - def test_vtpvlanname(self): + async def test_vtpvlanname(self): """Testing function vtpvlanname.""" # Initialize key variables oid_key = "vtpVlanName" @@ -359,7 +347,7 @@ def test_vtpvlanname(self): # Get results testobj = testimport.init_query(self.snmpobj_bytes) - results = testobj.vtpvlanname() + results = await testobj.vtpvlanname() # Basic testing of results for key, value in results.items(): @@ -367,10 +355,10 @@ def test_vtpvlanname(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.vtpvlanname(oidonly=True) + results = await testobj.vtpvlanname(oidonly=True) self.assertEqual(results, oid) - def test_vtpvlantype(self): + async def test_vtpvlantype(self): """Testing function vtpvlantype.""" # Initialize key variables oid_key = "vtpVlanType" @@ -378,7 +366,7 @@ def test_vtpvlantype(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.vtpvlantype() + results = await testobj.vtpvlantype() # Basic testing of results for key, value in results.items(): @@ -386,10 +374,10 @@ def test_vtpvlantype(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.vtpvlantype(oidonly=True) + results = await testobj.vtpvlantype(oidonly=True) self.assertEqual(results, oid) - def test_vtpvlanstate(self): + async def test_vtpvlanstate(self): """Testing function vtpvlanstate.""" # Initialize key variables oid_key = "vtpVlanState" @@ -397,7 +385,7 @@ def test_vtpvlanstate(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.vtpvlanstate() + results = await testobj.vtpvlanstate() # Basic testing of results for key, value in results.items(): @@ -405,7 +393,7 @@ def test_vtpvlanstate(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.vtpvlanstate(oidonly=True) + results = await testobj.vtpvlanstate(oidonly=True) self.assertEqual(results, oid) def test_vlantrunkportvlansenabled(self): diff --git a/tests/switchmap_/poller/snmp/mib/generic/test_mib_bridge.py b/tests/switchmap_/poller/snmp/mib/generic/test_mib_bridge.py index 935bbbaa..814b077f 100755 --- a/tests/switchmap_/poller/snmp/mib/generic/test_mib_bridge.py +++ b/tests/switchmap_/poller/snmp/mib/generic/test_mib_bridge.py @@ -115,7 +115,7 @@ def walk(self): pass -class TestMibBridgeFunctions(unittest.TestCase): +class TestMibBridgeFunctions(unittest.IsolatedAsyncioTestCase): """Checks all methods.""" ######################################################################### @@ -151,7 +151,7 @@ def test_init_query(self): pass -class TestMibBridge(unittest.TestCase): +class TestMibBridge(unittest.IsolatedAsyncioTestCase): """Checks all methods.""" ######################################################################### diff --git a/tests/switchmap_/poller/snmp/mib/generic/test_mib_entity.py b/tests/switchmap_/poller/snmp/mib/generic/test_mib_entity.py index 8444addf..ccf0a90f 100755 --- a/tests/switchmap_/poller/snmp/mib/generic/test_mib_entity.py +++ b/tests/switchmap_/poller/snmp/mib/generic/test_mib_entity.py @@ -115,7 +115,7 @@ def walk(self): pass -class TestMibEntityFunctions(unittest.TestCase): +class TestMibEntityFunctions(unittest.IsolatedAsyncioTestCase): """Checks all methods.""" ######################################################################### @@ -151,7 +151,7 @@ def test_init_query(self): pass -class TestMibEntity(unittest.TestCase): +class TestMibEntity(unittest.IsolatedAsyncioTestCase): """Checks all methods.""" ######################################################################### diff --git a/tests/switchmap_/poller/snmp/mib/generic/test_mib_essswitch.py b/tests/switchmap_/poller/snmp/mib/generic/test_mib_essswitch.py index f75f3df7..aa5381c0 100755 --- a/tests/switchmap_/poller/snmp/mib/generic/test_mib_essswitch.py +++ b/tests/switchmap_/poller/snmp/mib/generic/test_mib_essswitch.py @@ -4,7 +4,7 @@ import os import sys import unittest -from mock import Mock +from unittest.mock import Mock, AsyncMock # Try to create a working PYTHONPATH EXEC_DIR = os.path.dirname(os.path.realpath(__file__)) @@ -106,7 +106,7 @@ def swalk(self): pass -class TestMibESSSwitchFunctions(unittest.TestCase): +class TestMibESSSwitchFunctions(unittest.IsolatedAsyncioTestCase): """Checks all methods.""" ######################################################################### @@ -142,7 +142,7 @@ def test_init_query(self): pass -class TestMibESSSwitch(unittest.TestCase): +class TestMibESSSwitch(unittest.IsolatedAsyncioTestCase): """Checks all functions and methods.""" ######################################################################### @@ -171,26 +171,24 @@ def tearDownClass(cls): # Cleanup the CONFIG.cleanup() - def test_supported(self): + async def test_supported(self): """Testing method / function supported.""" # Set the stage for oid_exists returning True snmpobj = Mock(spec=Query) - mock_spec = {"oid_exists.return_value": True} - snmpobj.configure_mock(**mock_spec) + snmpobj.oid_exists = AsyncMock(return_value=True) # Test supported testobj = testimport.init_query(snmpobj) - self.assertEqual(testobj.supported(), True) + self.assertEqual(await testobj.supported(), True) # Set the stage for oid_exists returning False - mock_spec = {"oid_exists.return_value": False} - snmpobj.configure_mock(**mock_spec) + snmpobj.oid_exists = AsyncMock(return_value=False) # Test unsupported testobj = testimport.init_query(snmpobj) - self.assertEqual(testobj.supported(), False) + self.assertEqual(await testobj.supported(), False) - def test_layer1(self): + async def test_layer1(self): """Testing method / function layer1.""" # Initializing key variables expected_dict = { @@ -200,12 +198,11 @@ def test_layer1(self): # Set the stage for SNMPwalk snmpobj = Mock(spec=Query) - mock_spec = {"swalk.return_value": self.nwalk_results_integer} - snmpobj.configure_mock(**mock_spec) + snmpobj.swalk = AsyncMock(return_value=self.nwalk_results_integer) # Get results testobj = testimport.init_query(snmpobj) - results = testobj.layer1() + results = await testobj.layer1() # Basic testing of results for primary in results.keys(): @@ -215,23 +212,22 @@ def test_layer1(self): expected_dict[primary][secondary], ) - def test_swportduplexstatus(self): + async def test_swportduplexstatus(self): """Testing method / function swportduplexstatus.""" # Set the stage for SNMPwalk snmpobj = Mock(spec=Query) - mock_spec = {"swalk.return_value": self.nwalk_results_integer} - snmpobj.configure_mock(**mock_spec) + snmpobj.swalk = AsyncMock(return_value=self.nwalk_results_integer) # Get results testobj = testimport.init_query(snmpobj) - results = testobj.swportduplexstatus() + results = await testobj.swportduplexstatus() # Basic testing of results for key in results.keys(): self.assertEqual(isinstance(key, int), True) # Test that we are getting the correct OID - results = testobj.swportduplexstatus(oidonly=True) + results = await testobj.swportduplexstatus(oidonly=True) self.assertEqual(results, ".1.3.6.1.4.1.437.1.1.3.3.1.1.30") diff --git a/tests/switchmap_/poller/snmp/mib/generic/test_mib_etherlike.py b/tests/switchmap_/poller/snmp/mib/generic/test_mib_etherlike.py index 5c141bc3..747d10e3 100755 --- a/tests/switchmap_/poller/snmp/mib/generic/test_mib_etherlike.py +++ b/tests/switchmap_/poller/snmp/mib/generic/test_mib_etherlike.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 """Test the mib_etherlike module.""" +from token import ASYNC import unittest -from mock import Mock +from unittest.mock import Mock, AsyncMock import os import sys @@ -153,7 +154,7 @@ def test_init_query(self): pass -class TestMibEtherlike(unittest.TestCase): +class TestMibEtherlike(unittest.IsolatedAsyncioTestCase): """Checks all functions and methods.""" ######################################################################### @@ -182,26 +183,24 @@ def tearDownClass(cls): # Cleanup the CONFIG.cleanup() - def test_supported(self): + async def test_supported(self): """Testing method / function supported.""" # Set the stage for oid_exists returning True snmpobj = Mock(spec=Query) - mock_spec = {"oid_exists.return_value": True} - snmpobj.configure_mock(**mock_spec) + snmpobj.oid_exists = AsyncMock(return_value=True) # Test supported testobj = testimport.init_query(snmpobj) - self.assertEqual(testobj.supported(), True) + self.assertEqual(await testobj.supported(), True) # Set the stage for oid_exists returning False - mock_spec = {"oid_exists.return_value": False} - snmpobj.configure_mock(**mock_spec) + snmpobj.oid_exists = AsyncMock(return_value=False) # Test unsupported testobj = testimport.init_query(snmpobj) - self.assertEqual(testobj.supported(), False) + self.assertEqual(await testobj.supported(), False) - def test_layer1(self): + async def test_layer1(self): """Testing method / function layer1.""" # Initializing key variables expected_dict = { @@ -211,12 +210,11 @@ def test_layer1(self): # Set the stage for SNMPwalk snmpobj = Mock(spec=Query) - mock_spec = {"swalk.return_value": self.nwalk_results_integer} - snmpobj.configure_mock(**mock_spec) + snmpobj.swalk = AsyncMock(return_value=self.nwalk_results_integer) # Get results testobj = testimport.init_query(snmpobj) - results = testobj.layer1() + results = await testobj.layer1() # Basic testing of results for primary in results.keys(): @@ -226,23 +224,22 @@ def test_layer1(self): expected_dict[primary][secondary], ) - def test_dot3statsduplexstatus(self): + async def test_dot3statsduplexstatus(self): """Testing method / function dot3statsduplexstatus.""" # Set the stage for SNMPwalk snmpobj = Mock(spec=Query) - mock_spec = {"swalk.return_value": self.nwalk_results_integer} - snmpobj.configure_mock(**mock_spec) + snmpobj.swalk = AsyncMock(return_value=self.nwalk_results_integer) # Get results testobj = testimport.init_query(snmpobj) - results = testobj.dot3statsduplexstatus() + results = await testobj.dot3statsduplexstatus() # Basic testing of results for key in results.keys(): self.assertEqual(isinstance(key, int), True) # Test that we are getting the correct OID - results = testobj.dot3statsduplexstatus(oidonly=True) + results = await testobj.dot3statsduplexstatus(oidonly=True) self.assertEqual(results, ".1.3.6.1.2.1.10.7.2.1.19") diff --git a/tests/switchmap_/poller/snmp/mib/generic/test_mib_if.py b/tests/switchmap_/poller/snmp/mib/generic/test_mib_if.py index 4c1ab15d..cded2fa7 100755 --- a/tests/switchmap_/poller/snmp/mib/generic/test_mib_if.py +++ b/tests/switchmap_/poller/snmp/mib/generic/test_mib_if.py @@ -5,7 +5,7 @@ import sys import binascii import unittest -from mock import Mock +from unittest.mock import Mock, AsyncMock # Try to create a working PYTHONPATH EXEC_DIR = os.path.dirname(os.path.realpath(__file__)) @@ -154,7 +154,7 @@ def test_init_query(self): pass -class TestMibIf(unittest.TestCase): +class TestMibIf(unittest.IsolatedAsyncioTestCase): """Checks all functions and methods.""" ######################################################################### @@ -168,33 +168,24 @@ class TestMibIf(unittest.TestCase): # Set the stage for SNMPwalk for integer results snmpobj_integer = Mock(spec=Query) - mock_spec_integer = { - "swalk.return_value": nwalk_results_integer, - "walk.return_value": nwalk_results_integer, - } - snmpobj_integer.configure_mock(**mock_spec_integer) + snmpobj_integer.swalk = AsyncMock(return_value=nwalk_results_integer) + snmpobj_integer.walk = AsyncMock(return_value=nwalk_results_integer) # Normalized walk returning integers for the ifIndex nwalk_results_ifindex = {100: 100, 200: 200} # Set the stage for SNMPwalk for integer results for the ifIndex snmpobj_ifindex = Mock(spec=Query) - mock_spec_ifindex = { - "swalk.return_value": nwalk_results_ifindex, - "walk.return_value": nwalk_results_ifindex, - } - snmpobj_ifindex.configure_mock(**mock_spec_ifindex) + snmpobj_ifindex.swalk = AsyncMock(return_value=nwalk_results_ifindex) + snmpobj_ifindex.walk = AsyncMock(return_value=nwalk_results_ifindex) # Normalized walk returning strings nwalk_results_bytes = {100: b"1234", 200: b"5678"} # Set the stage for SNMPwalk for string results snmpobj_bytes = Mock(spec=Query) - mock_spec_bytes = { - "swalk.return_value": nwalk_results_bytes, - "walk.return_value": nwalk_results_bytes, - } - snmpobj_bytes.configure_mock(**mock_spec_bytes) + snmpobj_bytes.swalk = AsyncMock(return_value=nwalk_results_bytes) + snmpobj_bytes.walk = AsyncMock(return_value=nwalk_results_bytes) # Normalized walk returning binary data nwalk_results_binary = { @@ -204,11 +195,8 @@ class TestMibIf(unittest.TestCase): # Set the stage for SNMPwalk for binary results snmpobj_binary = Mock(spec=Query) - mock_spec_binary = { - "swalk.return_value": nwalk_results_binary, - "walk.return_value": nwalk_results_binary, - } - snmpobj_binary.configure_mock(**mock_spec_binary) + snmpobj_binary.swalk = AsyncMock(return_value=nwalk_results_binary) + snmpobj_binary.walk = AsyncMock(return_value=nwalk_results_binary) # Initializing key variables expected_dict = { @@ -289,7 +277,7 @@ def test_layer1(self): # the same type of results (eg. int, string, hex) pass - def test_iflastchange(self): + async def test_iflastchange(self): """Testing function iflastchange.""" # Initialize key variables oid_key = "ifLastChange" @@ -297,7 +285,7 @@ def test_iflastchange(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.iflastchange() + results = await testobj.iflastchange() # Basic testing of results for key, value in results.items(): @@ -305,10 +293,10 @@ def test_iflastchange(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.iflastchange(oidonly=True) + results = await testobj.iflastchange(oidonly=True) self.assertEqual(results, oid) - def test_ifinoctets(self): + async def test_ifinoctets(self): """Testing function ifinoctets.""" # Initialize key variables oid_key = "ifInOctets" @@ -316,7 +304,7 @@ def test_ifinoctets(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.ifinoctets() + results = await testobj.ifinoctets() # Basic testing of results for key, value in results.items(): @@ -324,10 +312,10 @@ def test_ifinoctets(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.ifinoctets(oidonly=True) + results = await testobj.ifinoctets(oidonly=True) self.assertEqual(results, oid) - def test_ifoutoctets(self): + async def test_ifoutoctets(self): """Testing function ifoutoctets.""" # Initialize key variables oid_key = "ifOutOctets" @@ -335,7 +323,7 @@ def test_ifoutoctets(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.ifoutoctets() + results = await testobj.ifoutoctets() # Basic testing of results for key, value in results.items(): @@ -343,10 +331,10 @@ def test_ifoutoctets(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.ifoutoctets(oidonly=True) + results = await testobj.ifoutoctets(oidonly=True) self.assertEqual(results, oid) - def test_ifdescr(self): + async def test_ifdescr(self): """Testing function ifdescr.""" # Initialize key variables oid_key = "ifDescr" @@ -354,7 +342,7 @@ def test_ifdescr(self): # Get results testobj = testimport.init_query(self.snmpobj_bytes) - results = testobj.ifdescr() + results = await testobj.ifdescr() # Basic testing of results for key, value in results.items(): @@ -362,10 +350,10 @@ def test_ifdescr(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.ifdescr(oidonly=True) + results = await testobj.ifdescr(oidonly=True) self.assertEqual(results, oid) - def test_iftype(self): + async def test_iftype(self): """Testing function iftype.""" # Initialize key variables oid_key = "ifType" @@ -373,7 +361,7 @@ def test_iftype(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.iftype() + results = await testobj.iftype() # Basic testing of results for key, value in results.items(): @@ -381,10 +369,10 @@ def test_iftype(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.iftype(oidonly=True) + results = await testobj.iftype(oidonly=True) self.assertEqual(results, oid) - def test_ifspeed(self): + async def test_ifspeed(self): """Testing function ifspeed.""" # Initialize key variables oid_key = "ifSpeed" @@ -392,7 +380,7 @@ def test_ifspeed(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.ifspeed() + results = await testobj.ifspeed() # Basic testing of results for key, value in results.items(): @@ -400,10 +388,10 @@ def test_ifspeed(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.ifspeed(oidonly=True) + results = await testobj.ifspeed(oidonly=True) self.assertEqual(results, oid) - def test_ifadminstatus(self): + async def test_ifadminstatus(self): """Testing function ifadminstatus.""" # Initialize key variables oid_key = "ifAdminStatus" @@ -411,7 +399,7 @@ def test_ifadminstatus(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.ifadminstatus() + results = await testobj.ifadminstatus() # Basic testing of results for key, value in results.items(): @@ -419,10 +407,10 @@ def test_ifadminstatus(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.ifadminstatus(oidonly=True) + results = await testobj.ifadminstatus(oidonly=True) self.assertEqual(results, oid) - def test_ifoperstatus(self): + async def test_ifoperstatus(self): """Testing function ifoperstatus.""" # Initialize key variables oid_key = "ifOperStatus" @@ -430,7 +418,7 @@ def test_ifoperstatus(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.ifoperstatus() + results = await testobj.ifoperstatus() # Basic testing of results for key, value in results.items(): @@ -438,10 +426,10 @@ def test_ifoperstatus(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.ifoperstatus(oidonly=True) + results = await testobj.ifoperstatus(oidonly=True) self.assertEqual(results, oid) - def test_ifalias(self): + async def test_ifalias(self): """Testing function ifalias.""" # Initialize key variables oid_key = "ifAlias" @@ -449,7 +437,7 @@ def test_ifalias(self): # Get results testobj = testimport.init_query(self.snmpobj_bytes) - results = testobj.ifalias() + results = await testobj.ifalias() # Basic testing of results for key, value in results.items(): @@ -457,10 +445,10 @@ def test_ifalias(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.ifalias(oidonly=True) + results = await testobj.ifalias(oidonly=True) self.assertEqual(results, oid) - def test_ifname(self): + async def test_ifname(self): """Testing function ifname.""" # Initialize key variables oid_key = "ifName" @@ -468,7 +456,7 @@ def test_ifname(self): # Get results testobj = testimport.init_query(self.snmpobj_bytes) - results = testobj.ifname() + results = await testobj.ifname() # Basic testing of results for key, value in results.items(): @@ -476,10 +464,10 @@ def test_ifname(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.ifname(oidonly=True) + results = await testobj.ifname(oidonly=True) self.assertEqual(results, oid) - def test_ifindex(self): + async def test_ifindex(self): """Testing function ifindex.""" # Initialize key variables oid_key = "ifIndex" @@ -487,7 +475,7 @@ def test_ifindex(self): # Get results testobj = testimport.init_query(self.snmpobj_ifindex) - results = testobj.ifindex() + results = await testobj.ifindex() # Basic testing of results for key, value in results.items(): @@ -499,10 +487,10 @@ def test_ifindex(self): self.assertEqual(key, value) # Test that we are getting the correct OID - results = testobj.ifindex(oidonly=True) + results = await testobj.ifindex(oidonly=True) self.assertEqual(results, oid) - def test_ifphysaddress(self): + async def test_ifphysaddress(self): """Testing function ifphysaddress.""" # Initialize key variables oid_key = "ifPhysAddress" @@ -510,7 +498,7 @@ def test_ifphysaddress(self): # Get results testobj = testimport.init_query(self.snmpobj_binary) - results = testobj.ifphysaddress() + results = await testobj.ifphysaddress() # Basic testing of results for key, value in results.items(): @@ -518,10 +506,10 @@ def test_ifphysaddress(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.ifphysaddress(oidonly=True) + results = await testobj.ifphysaddress(oidonly=True) self.assertEqual(results, oid) - def test_ifinmulticastpkts(self): + async def test_ifinmulticastpkts(self): """Testing function ifinmulticastpkts.""" # Initialize key variables oid_key = "ifInMulticastPkts" @@ -529,7 +517,7 @@ def test_ifinmulticastpkts(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.ifinmulticastpkts() + results = await testobj.ifinmulticastpkts() # Basic testing of results for key, value in results.items(): @@ -537,10 +525,10 @@ def test_ifinmulticastpkts(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.ifinmulticastpkts(oidonly=True) + results = await testobj.ifinmulticastpkts(oidonly=True) self.assertEqual(results, oid) - def test_ifoutmulticastpkts(self): + async def test_ifoutmulticastpkts(self): """Testing function ifoutmulticastpkts.""" # Initialize key variables oid_key = "ifOutMulticastPkts" @@ -548,7 +536,7 @@ def test_ifoutmulticastpkts(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.ifoutmulticastpkts() + results = await testobj.ifoutmulticastpkts() # Basic testing of results for key, value in results.items(): @@ -556,10 +544,10 @@ def test_ifoutmulticastpkts(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.ifoutmulticastpkts(oidonly=True) + results = await testobj.ifoutmulticastpkts(oidonly=True) self.assertEqual(results, oid) - def test_ifinbroadcastpkts(self): + async def test_ifinbroadcastpkts(self): """Testing function ifinbroadcastpkts.""" # Initialize key variables oid_key = "ifInBroadcastPkts" @@ -567,7 +555,7 @@ def test_ifinbroadcastpkts(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.ifinbroadcastpkts() + results = await testobj.ifinbroadcastpkts() # Basic testing of results for key, value in results.items(): @@ -575,10 +563,10 @@ def test_ifinbroadcastpkts(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.ifinbroadcastpkts(oidonly=True) + results = await testobj.ifinbroadcastpkts(oidonly=True) self.assertEqual(results, oid) - def test_ifoutbroadcastpkts(self): + async def test_ifoutbroadcastpkts(self): """Testing function ifoutbroadcastpkts.""" # Initialize key variables oid_key = "ifOutBroadcastPkts" @@ -586,7 +574,7 @@ def test_ifoutbroadcastpkts(self): # Get results testobj = testimport.init_query(self.snmpobj_integer) - results = testobj.ifoutbroadcastpkts() + results = await testobj.ifoutbroadcastpkts() # Basic testing of results for key, value in results.items(): @@ -594,7 +582,7 @@ def test_ifoutbroadcastpkts(self): self.assertEqual(value, self.expected_dict[key][oid_key]) # Test that we are getting the correct OID - results = testobj.ifoutbroadcastpkts(oidonly=True) + results = await testobj.ifoutbroadcastpkts(oidonly=True) self.assertEqual(results, oid) def test_ifstackstatus(self): diff --git a/tests/switchmap_/poller/snmp/mib/generic/test_mib_ip.py b/tests/switchmap_/poller/snmp/mib/generic/test_mib_ip.py index 69767046..9a44747e 100755 --- a/tests/switchmap_/poller/snmp/mib/generic/test_mib_ip.py +++ b/tests/switchmap_/poller/snmp/mib/generic/test_mib_ip.py @@ -4,6 +4,7 @@ import os import sys import unittest +from unittest.mock import AsyncMock from mock import Mock # Try to create a working PYTHONPATH @@ -153,7 +154,7 @@ def test_init_query(self): pass -class TestMibIp(unittest.TestCase): +class TestMibIp(unittest.IsolatedAsyncioTestCase): """Checks all functions and methods.""" ######################################################################### @@ -181,18 +182,12 @@ class TestMibIp(unittest.TestCase): # Set the stage for SNMPwalk for binary results snmpobj_ipv4_binary = Mock(spec=Query) - mock_spec_ipv4_binary = { - "swalk.return_value": walk_results_ipv4_binary, - "walk.return_value": walk_results_ipv4_binary, - } - snmpobj_ipv4_binary.configure_mock(**mock_spec_ipv4_binary) + snmpobj_ipv4_binary.swalk = AsyncMock(return_value=walk_results_ipv4_binary) + snmpobj_ipv4_binary.walk = AsyncMock(return_value=walk_results_ipv4_binary) snmpobj_ipv6_binary = Mock(spec=Query) - mock_spec_ipv6_binary = { - "swalk.return_value": walk_results_ipv6_binary, - "walk.return_value": walk_results_ipv6_binary, - } - snmpobj_ipv6_binary.configure_mock(**mock_spec_ipv6_binary) + snmpobj_ipv6_binary.swalk = AsyncMock(return_value=walk_results_ipv6_binary) + snmpobj_ipv6_binary.walk = AsyncMock(return_value=walk_results_ipv6_binary) # Initialize expected results ipv4_expected_dict = { @@ -242,14 +237,14 @@ def test_layer3(self): # Initializing key variables pass - def test_ipnettomediatable(self): + async def test_ipnettomediatable(self): """Testing method / function ipnettomediatable.""" # Initialize key variables oid = ".1.3.6.1.2.1.4.22.1.2" # Get results testobj = testimport.init_query(self.snmpobj_ipv4_binary) - results = testobj.ipnettomediatable() + results = await testobj.ipnettomediatable() # Basic testing of results for key, value in results.items(): @@ -257,17 +252,17 @@ def test_ipnettomediatable(self): self.assertEqual(value, self.ipv4_expected_dict[key]) # Test that we are getting the correct OID - results = testobj.ipnettomediatable(oidonly=True) + results = await testobj.ipnettomediatable(oidonly=True) self.assertEqual(results, oid) - def test_ipnettophysicalphysaddress(self): + async def test_ipnettophysicalphysaddress(self): """Testing method / function ipnettophysicalphysaddress.""" # Initialize key variables oid = ".1.3.6.1.2.1.4.35.1.4" # Get results testobj = testimport.init_query(self.snmpobj_ipv6_binary) - results = testobj.ipnettophysicalphysaddress() + results = await testobj.ipnettophysicalphysaddress() # Basic testing of results for key, value in results.items(): @@ -275,7 +270,7 @@ def test_ipnettophysicalphysaddress(self): self.assertEqual(value, self.ipv6_expected_dict[key]) # Test that we are getting the correct OID - results = testobj.ipnettophysicalphysaddress(oidonly=True) + results = await testobj.ipnettophysicalphysaddress(oidonly=True) self.assertEqual(results, oid) diff --git a/tests/switchmap_/poller/snmp/mib/juniper/test_mib_junipervlan.py b/tests/switchmap_/poller/snmp/mib/juniper/test_mib_junipervlan.py index 12c78ce0..4e7545ab 100755 --- a/tests/switchmap_/poller/snmp/mib/juniper/test_mib_junipervlan.py +++ b/tests/switchmap_/poller/snmp/mib/juniper/test_mib_junipervlan.py @@ -116,7 +116,7 @@ def walk(self): pass -class TestJuniperVlanFunctions(unittest.TestCase): +class TestJuniperVlanFunctions(unittest.IsolatedAsyncioTestCase): """Checks all methods.""" ######################################################################### @@ -152,7 +152,7 @@ def test_init_query(self): pass -class TestJuniperVlan(unittest.TestCase): +class TestJuniperVlan(unittest.IsolatedAsyncioTestCase): """Checks all methods.""" ######################################################################### From 6af38b67485fc535270b287a55d02a4825bad690 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Fri, 26 Sep 2025 19:20:34 +0530 Subject: [PATCH 47/50] minor fixes --- .../snmp/mib/generic/test_mib_etherlike.py | 1 - .../poller/snmp/mib/generic/test_mib_ip.py | 3 +- .../poller/snmp/mib/generic/test_mib_lldp.py | 1 - tests/switchmap_/poller/test_async_poll.py | 34 +++++++++++++++++++ 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/tests/switchmap_/poller/snmp/mib/generic/test_mib_etherlike.py b/tests/switchmap_/poller/snmp/mib/generic/test_mib_etherlike.py index 747d10e3..1631f092 100755 --- a/tests/switchmap_/poller/snmp/mib/generic/test_mib_etherlike.py +++ b/tests/switchmap_/poller/snmp/mib/generic/test_mib_etherlike.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 """Test the mib_etherlike module.""" -from token import ASYNC import unittest from unittest.mock import Mock, AsyncMock import os diff --git a/tests/switchmap_/poller/snmp/mib/generic/test_mib_ip.py b/tests/switchmap_/poller/snmp/mib/generic/test_mib_ip.py index 9a44747e..e781d29a 100755 --- a/tests/switchmap_/poller/snmp/mib/generic/test_mib_ip.py +++ b/tests/switchmap_/poller/snmp/mib/generic/test_mib_ip.py @@ -4,8 +4,7 @@ import os import sys import unittest -from unittest.mock import AsyncMock -from mock import Mock +from unittest.mock import Mock, AsyncMock # Try to create a working PYTHONPATH EXEC_DIR = os.path.dirname(os.path.realpath(__file__)) diff --git a/tests/switchmap_/poller/snmp/mib/generic/test_mib_lldp.py b/tests/switchmap_/poller/snmp/mib/generic/test_mib_lldp.py index f84e2af4..d5c98ef9 100755 --- a/tests/switchmap_/poller/snmp/mib/generic/test_mib_lldp.py +++ b/tests/switchmap_/poller/snmp/mib/generic/test_mib_lldp.py @@ -4,7 +4,6 @@ import unittest import os import sys -from mock import Mock # Try to create a working PYTHONPATH diff --git a/tests/switchmap_/poller/test_async_poll.py b/tests/switchmap_/poller/test_async_poll.py index 7286dbad..1d60ea41 100644 --- a/tests/switchmap_/poller/test_async_poll.py +++ b/tests/switchmap_/poller/test_async_poll.py @@ -88,6 +88,40 @@ async def test_devices_basic_functionality(self, mock_config_setup): assert "device1" in hostnames_called assert "device2" in hostnames_called + @pytest.mark.asyncio + async def test_devices_invalid_concurrency(self, mock_config_setup): + """Test devices() with invalid concurrency values.""" + with patch("switchmap.poller.poll.ConfigPoller") as mock_config: + mock_config.return_value = mock_config_setup + with patch( + "switchmap.poller.poll.device", new_callable=AsyncMock + ) as mock_device: + mock_device.return_value = True + + # Test negative concurrency + await devices(max_concurrent_devices=-1) + # Should still call device functions with default concurrency + assert mock_device.call_count == 2 + + @pytest.mark.asyncio + async def test_devices_empty_zones(self): + """Test devices() with zones containing no hostnames.""" + mock_config_instance = MagicMock() + mock_zone = MagicMock() + mock_zone.name = "empty_zone" + mock_zone.hostnames = [] # Empty hostnames + mock_config_instance.zones.return_value = [mock_zone] + mock_config_instance.agent_subprocesses.return_value = 2 + + with patch("switchmap.poller.poll.ConfigPoller") as mock_config: + mock_config.return_value = mock_config_instance + with patch( + "switchmap.poller.poll.device", new_callable=AsyncMock + ) as mock_device: + await devices() + # Should not call device when no hostnames + assert mock_device.call_count == 0 + @pytest.mark.asyncio async def test_devices_with_custom_concurrency(self, mock_config_setup): """Test device polling with custom concurrency limit.""" From ac396c5f28046b6d0c1f44ea8f0ad764d092974b Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Fri, 26 Sep 2025 19:25:37 +0530 Subject: [PATCH 48/50] removed unused vars --- .../snmp/mib/cisco/test_mib_ciscovlaniftablerelationship.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscovlaniftablerelationship.py b/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscovlaniftablerelationship.py index 0cfa9fb7..d76e1cf1 100755 --- a/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscovlaniftablerelationship.py +++ b/tests/switchmap_/poller/snmp/mib/cisco/test_mib_ciscovlaniftablerelationship.py @@ -6,8 +6,6 @@ import unittest from unittest.mock import Mock, AsyncMock -from sqlalchemy import true - # Try to create a working PYTHONPATH EXEC_DIR = os.path.dirname(os.path.realpath(__file__)) ROOT_DIR = os.path.abspath( From 16f4ff9a4a2624095eac7bfe3b568419d336808a Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Fri, 26 Sep 2025 22:53:09 +0530 Subject: [PATCH 49/50] fixes bugs --- bin/systemd/switchmap_poller | 4 ++-- switchmap/poller/snmp/poller.py | 4 ++-- switchmap/poller/snmp/snmp_manager.py | 18 +++++++++++++----- tests/switchmap_/poller/test_async_poll.py | 9 +++++---- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/bin/systemd/switchmap_poller b/bin/systemd/switchmap_poller index f527802c..66260ec5 100755 --- a/bin/systemd/switchmap_poller +++ b/bin/systemd/switchmap_poller @@ -32,7 +32,7 @@ from switchmap import AGENT_POLLER from switchmap.core.agent import Agent, AgentCLI from switchmap.core import general from switchmap.poller.configuration import ConfigPoller -from switchmap.poller import async_poll +from switchmap.poller import poll from switchmap.core import log # We have to create this named tuple outside the multiprocessing Pool @@ -89,7 +89,7 @@ class PollingAgent(Agent): open(self.lockfile, "a").close() # Poll after sleeping - async_poll.run_devices(max_concurrent_devices=max_concurrent) + poll.run_devices(max_concurrent_devices=max_concurrent) # Delete lockfile os.remove(self.lockfile) diff --git a/switchmap/poller/snmp/poller.py b/switchmap/poller/snmp/poller.py index 5077f94d..f03dea23 100644 --- a/switchmap/poller/snmp/poller.py +++ b/switchmap/poller/snmp/poller.py @@ -41,7 +41,7 @@ def __init__(self, hostname): async def initialize_snmp(self): """Initialize SNMP connection asynchronously. - Returns; + Returns: bool: True if successful, False otherwise """ # Get snmp config information from Switchmap-NG @@ -83,7 +83,7 @@ async def query(self): # Only query if the device is contactable if bool(self.snmp_object) is False: - log.log2die(1001, f"No valid SNMP object for {self._hostname} ") + log.log2warning(1001, f"No valid SNMP object for {self._hostname} ") return _data # Get data diff --git a/switchmap/poller/snmp/snmp_manager.py b/switchmap/poller/snmp/snmp_manager.py index cfe24a46..83e76dbb 100644 --- a/switchmap/poller/snmp/snmp_manager.py +++ b/switchmap/poller/snmp/snmp_manager.py @@ -313,8 +313,10 @@ async def _oid_exists_get(self, oid_to_get, context_name=""): if exists and bool(result): # Make sure the OID key exists in result + exact_key = oid_to_get + alt_key = oid_to_get.lstrip(".") if isinstance(result, dict) and oid_to_get in result: - if result[oid_to_get] is not None: + if result.get(exact_key) is not None or result.get(alt_key) is not None: validity = True elif isinstance(result, dict) and result: # If result has data but not exact OID, still consider valid @@ -662,8 +664,11 @@ async def _session(self, walk_operation=False): elif auth_proto == "sha512": auth_protocol = usmHMAC384SHA512AuthProtocol else: - # Default to SHA-256 for better security - auth_protocol = usmHMAC192SHA256AuthProtocol + log.log2warning( + 1218, + f"Unknown auth protocol '{auth.authprotocol}', leaving unset", + ) + auth_protocol = None # Set privacy protocol only if privprotocol is specified # Also if we have authentication (privacy requires authentication) @@ -678,8 +683,11 @@ async def _session(self, walk_operation=False): elif priv_proto == "aes256": priv_protocol = usmAesCfb256Protocol else: - # Default to AES-256 for best security - priv_protocol = usmAesCfb256Protocol + log.log2warning( + 1218, + f"Unknown auth protocol '{auth.privprotocol}', leaving unset", + ) + priv_protocol = None auth_data = UsmUserData( userName=auth.secname, diff --git a/tests/switchmap_/poller/test_async_poll.py b/tests/switchmap_/poller/test_async_poll.py index 1d60ea41..8d715a25 100644 --- a/tests/switchmap_/poller/test_async_poll.py +++ b/tests/switchmap_/poller/test_async_poll.py @@ -4,6 +4,7 @@ import os import sys import pytest +import asyncio from unittest.mock import patch, MagicMock, AsyncMock from switchmap.poller.poll import devices, device, cli_device, _META @@ -176,7 +177,7 @@ async def test_device_with_skip_file(self, mock_poll_meta): mock_isfile.return_value = True with patch("switchmap.poller.poll.log.log2debug") as mock_log: # Create mock semaphore and session - mock_semaphore = AsyncMock() + mock_semaphore = asyncio.Semaphore(1) mock_session = MagicMock() result = await device( @@ -190,7 +191,7 @@ async def test_device_with_skip_file(self, mock_poll_meta): @pytest.mark.asyncio async def test_device_invalid_hostname(self): """Test device processing with invalid hostname.""" - mock_semaphore = AsyncMock() + mock_semaphore = asyncio.Semaphore(1) mock_session = MagicMock() # Test with None hostname @@ -218,7 +219,7 @@ async def test_device_snmp_failure(self, mock_poll_meta): mock_poll_instance.initialize_snmp.return_value = False mock_poll_cls.return_value = mock_poll_instance - mock_semaphore = AsyncMock() + mock_semaphore = asyncio.Semaphore(1) mock_session = MagicMock() result = await device( @@ -244,7 +245,7 @@ async def test_device_successful_poll_no_post(self, mock_poll_meta): mock_poll_instance.query.return_value = {"test": "data"} mock_poll_cls.return_value = mock_poll_instance - mock_semaphore = AsyncMock() + mock_semaphore = asyncio.Semaphore(1) mock_session = MagicMock() with patch( From 2cb1b0539b4bedb8c1bf85b3ec053034fe9e83d5 Mon Sep 17 00:00:00 2001 From: Abhishek Raj Date: Fri, 26 Sep 2025 22:56:56 +0530 Subject: [PATCH 50/50] lint fixed --- switchmap/poller/snmp/snmp_manager.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/switchmap/poller/snmp/snmp_manager.py b/switchmap/poller/snmp/snmp_manager.py index 83e76dbb..9ab1f73e 100644 --- a/switchmap/poller/snmp/snmp_manager.py +++ b/switchmap/poller/snmp/snmp_manager.py @@ -316,7 +316,10 @@ async def _oid_exists_get(self, oid_to_get, context_name=""): exact_key = oid_to_get alt_key = oid_to_get.lstrip(".") if isinstance(result, dict) and oid_to_get in result: - if result.get(exact_key) is not None or result.get(alt_key) is not None: + if ( + result.get(exact_key) is not None + or result.get(alt_key) is not None + ): validity = True elif isinstance(result, dict) and result: # If result has data but not exact OID, still consider valid @@ -666,9 +669,10 @@ async def _session(self, walk_operation=False): else: log.log2warning( 1218, - f"Unknown auth protocol '{auth.authprotocol}', leaving unset", + f"Unknown auth protocol '{auth.authprotocol}'," + f"leaving unset", ) - auth_protocol = None + auth_protocol = None # Set privacy protocol only if privprotocol is specified # Also if we have authentication (privacy requires authentication) @@ -685,7 +689,8 @@ async def _session(self, walk_operation=False): else: log.log2warning( 1218, - f"Unknown auth protocol '{auth.privprotocol}', leaving unset", + f"Unknown auth protocol '{auth.privprotocol}'," + f"leaving unset", ) priv_protocol = None