Python – Two Scripts to Get MAC Addresses, Lookup ARP to Get IP and Do an DNS Reverse Lookup

There are two scripts below. This is if you’re in an environment where you MAC Addresses and ARP table are on two different switches. The first scrtip is the MAC Address collector. The second script is the ARP lookup based off the MAC Addresses from the 1st script.

MAC COLLECTOR

# Author: Kerry Cordero
# Version: 1.0.0
# Description: This script will connect to the switch that has the MAC Addresses.  It will export the MAC Addresses and other data to an excel spreadsheet for the next part. Part 2 is to run, arp_lookup.py.

# mac_collector.py
from netmiko import ConnectHandler
import re
import pandas as pd
import getpass

def get_switch_credentials():
    """
    Prompt for switch connection details
    """
    print("\nEnter switch connection details:")
    switch_ip = input("Switch IP: ")
    username = input("Username: ")
    password = getpass.getpass("Password: ")
    enable_password = getpass.getpass("Enable Password: ")
    
    return switch_ip, username, password, enable_password

def get_interface_descriptions(switch_connection):
    """
    Get interface descriptions and return as a dictionary.
    """
    output = switch_connection.send_command("show interface description")
    descriptions = {}
    
    for line in output.splitlines():
        if re.match(r"^Interface", line) or re.match(r"^-+", line):
            continue
            
        parts = line.split()
        if len(parts) >= 4:
            interface = parts[0]
            description = ' '.join(parts[3:])
            descriptions[interface] = description
    
    return descriptions

def get_mac_table(switch_connection, interface_descriptions):
    """
    Get MAC address table for physical interfaces (Gi and Te ports only).
    """
    output = switch_connection.send_command("show mac address-table")
    output_te = switch_connection.send_command("show mac address-table | include Te")
    mac_table = []
    
    # Process regular output for Gi interfaces
    for line in output.splitlines():
        if re.search(r"^\s*\d+", line):
            columns = line.split()
            
            if len(columns) >= 6:
                vlan = columns[0]
                mac = columns[1].lower()
                port = columns[5]
                
                if re.match(r"^(Gi|GigabitEthernet)", port):
                    description = interface_descriptions.get(port, "No description")
                    description = re.sub(r'up up\s*', '', description).strip()
                    mac_table.append({
                        "Interface": port,
                        "Description": description,
                        "VLAN": vlan,
                        "MAC Address": mac
                    })
    
    # Process Te interfaces output
    for line in output_te.splitlines():
        if re.search(r"^\s*\d+", line):
            columns = line.split()
            
            if len(columns) >= 6:
                vlan = columns[0]
                mac = columns[1].lower()
                port = columns[5]
                
                if re.match(r"^(Te|TenGigabitEthernet)", port):
                    description = interface_descriptions.get(port, "No description")
                    description = re.sub(r'up up\s*', '', description).strip()
                    mac_table.append({
                        "Interface": port,
                        "Description": description,
                        "VLAN": vlan,
                        "MAC Address": mac
                    })
    
    return mac_table

def connect_to_switch(switch_ip, username, password, enable_password):
    """
    Establish connection to the switch with given credentials.
    """
    switch = {
        "device_type": "cisco_ios",
        "host": switch_ip,
        "username": username,
        "password": password,
        "secret": enable_password,
        "timeout": 60
    }
    return ConnectHandler(**switch)

def print_mac_table(mac_table):
    """
    Print MAC table in a formatted manner.
    """
    if not mac_table:
        print("\nNo MAC addresses found for Gi or Te interfaces.")
        return

    print("\nMAC Address Table (Gi and Te interfaces only)")
    print("-" * 90)
    print(f"{'Interface':<15} {'VLAN':<6} {'MAC Address':<17} {'Description':<50}")
    print("-" * 90)

    sorted_table = sorted(mac_table, key=lambda x: (
        '1' if x['Interface'].startswith('Te') else '2',
        x['Interface']
    ))
    
    for entry in sorted_table:
        print(f"{entry['Interface']:<15} {entry['VLAN']:<6} {entry['MAC Address']:<17} {entry['Description']:<50}")
    
    print("-" * 90)
    print(f"Total entries: {len(mac_table)}")

def export_to_excel(mac_table):
    """
    Export MAC table to Excel file
    """
    df = pd.DataFrame(mac_table)
    filename = "old-core-port-migration.xlsx"
    
    # Reorder columns for better readability
    df = df[['Interface', 'VLAN', 'MAC Address', 'Description']]
    
    # Export to Excel
    df.to_excel(filename, index=False, sheet_name='MAC Table')
    print(f"\nData exported to {filename}")

def main():
    try:
        # Get switch credentials
        switch_ip, username, password, enable_password = get_switch_credentials()
        
        print("\nConnecting to switch...")
        connection = connect_to_switch(
            switch_ip,
            username,
            password,
            enable_password
        )
        
        print("Getting interface descriptions...")
        connection.enable()
        interface_descriptions = get_interface_descriptions(connection)
        
        print("Getting MAC table...")
        mac_table = get_mac_table(connection, interface_descriptions)
        
        print("Printing results...")
        print_mac_table(mac_table)
        
        print("Exporting to Excel...")
        export_to_excel(mac_table)
        
        connection.disconnect()
        print("Disconnected from switch")

    except Exception as e:
        print(f"\nError occurred: {str(e)}")
        print(f"Error type: {type(e).__name__}")

if __name__ == "__main__":
    main()

ARP LOOKUP

# Author: Kerry Cordero
# Version: 1.0.0
# Description: This script will do an ARP lookup on the switch you log into using the MAC Address from the previous script.  It will then export the results to a spreadsheet.  

# arp_lookup.py
from netmiko import ConnectHandler
import pandas as pd
import re
import socket
import concurrent.futures
from concurrent.futures import ThreadPoolExecutor
import getpass

def get_switch_credentials():
    """
    Prompt for switch connection details
    """
    print("\nEnter Nexus switch connection details:")
    switch_ip = input("Switch IP: ")
    username = input("Username: ")
    password = getpass.getpass("Password: ")
    enable_password = getpass.getpass("Enable Password: ")
    
    return switch_ip, username, password, enable_password

def connect_to_switch(switch_ip, username, password, enable_password):
    """
    Establish connection to the Nexus switch with correct device type
    """
    switch = {
        "device_type": "cisco_nxos",
        "host": switch_ip,
        "username": username,
        "password": password,
        "secret": enable_password,
        "timeout": 60
    }
    return ConnectHandler(**switch)

def get_dns_name(ip):
    """
    Get DNS name for an IP address with timeout handling
    """
    try:
        hostname = socket.gethostbyaddr(ip)[0]
        return hostname
    except (socket.herror, socket.gaierror, socket.timeout):
        return "No DNS"
    except Exception as e:
        return f"DNS Error: {str(e)}"

def get_arp_table(switch_connection):
    """
    Get ARP table from Nexus switch and return as dictionary with MAC as key
    """
    output = switch_connection.send_command("show ip arp")
    arp_dict = {}
    
    for line in output.splitlines():
        if not line.strip() or 'Address' in line or '-' in line:
            continue
            
        parts = line.split()
        if len(parts) >= 4:
            ip = parts[0]
            mac = parts[2].lower()
            
            if re.match(r"([0-9a-f]{4}\.){2}[0-9a-f]{4}", mac):
                arp_dict[mac] = ip
    
    return arp_dict

def update_mac_table_with_arp():
    """
    Read MAC table from Excel, add IP addresses and DNS names, and save back to same file
    """
    input_file = "old-core-port-migration.xlsx"
    
    try:
        df = pd.read_excel(input_file)
        print(f"Successfully read {input_file}")
    except FileNotFoundError:
        print(f"Error: Could not find {input_file}")
        return
    
    try:
        # Get switch credentials
        switch_ip, username, password, enable_password = get_switch_credentials()
        
        print("Connecting to Nexus switch for ARP lookup...")
        connection = connect_to_switch(
            switch_ip,
            username,
            password,
            enable_password
        )
        
        print("Getting ARP table...")
        arp_dict = get_arp_table(connection)
        
        print("Adding IP addresses to MAC table...")
        df['IP Address'] = df['MAC Address'].map(arp_dict)
        
        print("\nPerforming DNS lookups (this may take a few minutes)...")
        ips_to_lookup = df['IP Address'].dropna().unique()
        
        dns_results = {}
        with ThreadPoolExecutor(max_workers=20) as executor:
            future_to_ip = {executor.submit(get_dns_name, ip): ip for ip in ips_to_lookup}
            
            total_lookups = len(future_to_ip)
            completed = 0
            
            for future in concurrent.futures.as_completed(future_to_ip):
                ip = future_to_ip[future]
                try:
                    dns_name = future.result()
                    dns_results[ip] = dns_name
                except Exception as e:
                    print(f"Error looking up {ip}: {str(e)}")
                    dns_results[ip] = "DNS Error"
                
                completed += 1
                if completed % 10 == 0:
                    print(f"Completed {completed} of {total_lookups} DNS lookups")
        
        df['DNS Name'] = df['IP Address'].map(dns_results)
        
        columns_order = ['Interface', 'VLAN', 'MAC Address', 'IP Address', 'DNS Name', 'Description']
        df = df[columns_order]
        
        df.to_excel(input_file, index=False, sheet_name='MAC-ARP Table')
        print(f"\nUpdated data saved to {input_file}")
        
        total_macs = len(df)
        macs_with_ip = df['IP Address'].notna().sum()
        macs_with_dns = df['DNS Name'].notna().sum()
        print(f"\nStatistics:")
        print(f"Total MAC addresses: {total_macs}")
        print(f"MAC addresses with IP: {macs_with_ip}")
        print(f"MAC addresses without IP: {total_macs - macs_with_ip}")
        print(f"IP addresses with DNS names: {macs_with_dns}")
        
        connection.disconnect()
        print("Disconnected from switch")
        
    except Exception as e:
        print(f"\nError occurred: {str(e)}")
        print(f"Error type: {type(e).__name__}")

def main():
    update_mac_table_with_arp()

if __name__ == "__main__":
    main()