Audit of Switch ports all APs are conected to

Solved
amabt
Building a reputation

Audit of Switch ports all APs are conected to

Hi Everyone,

 

We recent discovered that unfortunately when some of the AP were commissioned at our site the tech did not set the Switch port the AP is on to a Trunk port. This is causing issues. We need to do an audit of all the AP in our Org to make sure the switch port the are on is set to Trunk.

 

What is the most efficient API end point we can use for this?

 

I'm think of below route:

Get Organization Devices -> Filter to wireless only.

 

Loop through each device then work out which switch and which port its connects to? <-- this step I am not sure how to get the info?

 

Then use Update Device Switch Port to update the port.

 

Thanks.

1 Accepted Solution
John_on_API
Meraki Employee
Meraki Employee

I'm surprised nobody recommended https://developer.cisco.com/meraki/api-v1/get-network-topology-link-layer/ -- which is purpose built for this kind of thing! And way easier than polling per-device discovery info.

View solution in original post

14 Replies 14
PhilipDAth
Kind of a big deal
Kind of a big deal

Depending on how many networks you have, you could go into each network and go Switching/Switchports.

 

Use the spanner in the top right hand corner, and enabled the CDL/LLDP column.

PhilipDAth_0-1725581477375.png

 

And then search on "MR" to see all access points.  You can then select all the switch ports and change them in one hit.

amabt
Building a reputation

Thanks. That's handy to know. 150 networks otherwise I would have just done the manual route.

PhilipDAth
Kind of a big deal
Kind of a big deal

API wise;

 

You could use this to get a list of all MS switches (set productTypes filter to "switch"):

https://developer.cisco.com/meraki/api-v1/get-organization-inventory-devices/

 

And then for each switch, use this to get the LLDP neighbours.

https://developer.cisco.com/meraki/api-v1/get-device-lldp-cdp/

 

And then for each switch port above that shows something with a systemName containing "Meraki MR", update the port.

 

amabt
Building a reputation

Thanks. I'll explore this route.

PhilipDAth
Kind of a big deal
Kind of a big deal

I bet @sungod will have a better approach.  🙂

sungod
Kind of a big deal

I think the way you suggest is straightforward 😀

 

Tbh with all the new/updated API calls, I need to spend a while reading through the details on each one - used to do this back in 80s/90s on new UNIX releases, I think it was actually faster riffling through the paper manuals than it is to do on the developer website...

PhilipDAth
Kind of a big deal
Kind of a big deal

Going sideways; depending on what model switches you have; you could enable SecurePort which can automatically recognise and configure a switch port for an MR.

https://documentation.meraki.com/MS/Access_Control/SecurePort_(formerly_known_as_SecureConnect)

 

You could also consider create a standardised port profile for APs, and rather than apply specific port settings, apply the profile for an AP.

PhilipDAth_0-1725582064563.png

 

 

RaphaelL
Kind of a big deal
Kind of a big deal

https://developer.cisco.com/meraki/api-v1/get-organization-switch-ports-statuses-by-switch/

https://developer.cisco.com/meraki/api-v1/api-reference-early-access-api-products-switch-configure-p...

 

 

This will return ALL switchports statuses from your Orgs ( contains CDP LLDP info )

The second call will return the config. 

 

If the port has cdp lldp corresponding to a AP , then loop through the ports and find the config. Voila.

 

As suggested by Phil , if you have MS and MR , I would suggest to configure SecurePort ( https://documentation.meraki.com/MS/Access_Control/SecurePort_(formerly_known_as_SecureConnect) )

amabt
Building a reputation

Thanks @RaphaelL  I'll give that a try.

JGill
Building a reputation

I have a python script that pulls all the switch / port / client data for our org.  You can see AP's by name in the lldp field, and the switch port settings  (access, trunk, and associated vlans,.  It's a down and dirty script, but will output a csv file and you can just filter on the APs.     You can use a similar API to go correct them once you have a list.   Not sure how this will come across but shoot me a note if you have a question.

# Start of python file -------

import meraki
import json
import csv
import os
from datetime import datetime

API_KEY = os.environ.get('MERAKI_API')
organization_id = os.environ.get('MERAKI_ORG')

dashboard = meraki.DashboardAPI(API_KEY, output_log=False, print_console=False)

 

now = datetime.now()

dt_string = now.strftime("%m/%d/%Y %H:%M:%S %Z")
dtFilename = now.strftime("%m-%d-%Y_")
print(dt_string)
print(dtFilename)


clientDatafile = dtFilename+'ClientData.csv'

try:
    datafile = open(clientDatafile, 'w', newline='')
    csv_writer = csv.writer(datafile)
except Exception as error:
    print("Error: ", error )
    exit(error)


count = 0

networks = dashboard.organizations.getOrganizationNetworks(
    organization_id, total_pages='all'
)

print('Client Listing : ', clientDatafile )
for network in networks:

    print('Network ID: ' + network['id'] + ' Name: ' + network['name'])


    try:
        clients = dashboard.networks.getNetworkClients(
         network['id'], timespan=2592000, total_pages='all', perPage='1000'
        )
    except:
        print('opps')
       
    for client in clients:
        if count == 0:
           header = client.keys()
           csv_writer.writerow(header)
           count += 1
        csv_writer.writerow(client.values())    
 
#  End of python file.       

 

amabt
Building a reputation

Thank you for sharing. I will give that a try.

John_on_API
Meraki Employee
Meraki Employee

I'm surprised nobody recommended https://developer.cisco.com/meraki/api-v1/get-network-topology-link-layer/ -- which is purpose built for this kind of thing! And way easier than polling per-device discovery info.

amabt
Building a reputation

Thanks @John_on_API  I'll add that to the list to try as well.

amabt
Building a reputation

Thanks to @John_on_API  this did the trick for me. Below is the final code. Hope its useful to others to adapt as they see fit.

 

 

# Script to retrieve Meraki AP info from all networks from all org the API key has access to
#All Switches and AP devcies name are assumed to start with AP* or SW*

from datetime import datetime
import os
import meraki
import pandas as pd

# Meraki API Key name that is retrieve from the OS environment variable.
MERAKI_DASHBOARD_API_KEY_NAME = 'MERAKI_API_KEY'  # Readonly key

#Set CSV / XLXS folder path to the output folder under the script
output_path = f"{os.path.dirname(__file__)}/output"

def extract_ap_details(linkLayerDevices_response, site_name, org_name):
    """
    Extract access point details from the 'links' node of the given response.
    Strips "Port " from portId to get only the port number, renames field to 'port_number',
    and includes additional parameters 'site_name' and 'org_name'.

    Parameters:
        linkLayerDevices_response (dict): The response dictionary containing 'links' node.
        site_name (str): The name of the site where the devices are located.
        org_name (str): The name of the organisation.

    Returns:
        list: A list of dictionaries containing details of switches, access points, and metadata.
    """
    ap_details = []

    # Safely get the 'links' list from the response
    links = linkLayerDevices_response.get("links", [])
    if not isinstance(links, list):
        return ap_details  # Return empty list if 'links' is not a list

    # Iterate through each link in the list
    for link in links:
        ends = link.get("ends", [])
        if not (isinstance(ends, list) and len(ends) == 2):
            continue  # Skip if 'ends' is not a list of exactly 2 dictionaries

        # Extract potential switch and access point nodes
        switch = ends[0]
        access_point = ends[1]

        # Validate the "SW" and "AP" naming conventions
        sw_device = switch.get("device", {})
        ap_device = access_point.get("device", {})
        sw_name = sw_device.get("name", "")
        ap_name = ap_device.get("name", "")

        if not sw_name.startswith("SW"):
            continue  # Skip if the device name does not start with "SW"

        if not ap_name.startswith("AP"):
            continue  # Skip if the second device name does not start with "AP"

        # Extract additional details with safe fallbacks
        sw_discovered = switch.get("discovered", {})
        sw_lldp = sw_discovered.get("lldp") or {}  # Default to empty dict if None
        sw_cdp = sw_discovered.get("cdp") or {}  # Default to empty dict if None

        # Extract and clean port number
        port_id = sw_lldp.get("portId", "").replace("Port ", "").strip()

        # Append the validated data into the result list
        ap_details.append({
            "ap_name": ap_name,
            "ap_serial": ap_device.get("serial", ""),  # Default to empty string if missing
            "sw_name": sw_name,
            "sw_serial": sw_device.get("serial", ""),  # Default to empty string if missing
            "sw_port_number": port_id,  # Cleaned port number
            "sw_port_native_vlan": str(sw_cdp.get("nativeVlan", "")),  # Default to empty string if missing
            "site_name": site_name,  # Added site_name
            "org_name": org_name   # Added org_name
        })

    return ap_details

def main():
    if MERAKI_DASHBOARD_API_KEY_NAME in os.environ:
        print(f"API Key Environment variable [{MERAKI_DASHBOARD_API_KEY_NAME}] found.")

    else:
        print(f"API Key Environment variable [{MERAKI_DASHBOARD_API_KEY_NAME}] Does not exists!")
        exit(1)

    MERAKI_DASHBOARD_API_KEY = os.getenv(MERAKI_DASHBOARD_API_KEY_NAME)  # Get the actual Meraki API Key

    # Start a Meraki dashboard API session
    try:
        print(f"\nTrying to connect to Meraki API Using [{MERAKI_DASHBOARD_API_KEY_NAME}]")

        dashboard = meraki.DashboardAPI(
            api_key=MERAKI_DASHBOARD_API_KEY,
            base_url='https://api.meraki.com/api/v1/',
            print_console=False, output_log=False, suppress_logging=True,
            wait_on_rate_limit=True,
            maximum_retries=100
        )

    except meraki.APIError as e:
        print("Unable to connect to Meraki API.")
        print(f'Meraki API error: {e}')
        print(f'status code = {e.status}')
        print(f'reason = {e.reason}')
        print(f'error = {e.message}')

    else:

        # Get list of organizations to which the supplied API key has access to.
        organizations = dashboard.organizations.getOrganizations()
        organizations = sorted(organizations, key=lambda x: x['name'])  # Sort Org by name

        # Print name of the Orgs found
        print(f"Found: [{len(organizations)}] organisations.")
        for org in organizations:
            print(f"{org['name']}")

        # Iterate through list of Orgs and gather details we need

        all_org_ap_info = []  # Store all the networks in all orgs so we can write to a file later.

        for org in organizations:

            org_id = org['id']  # Save ID name for use later
            org_name = org['name']  # Save Org name for use later

            print(f"\nAnalysing organization {org_name}:")

            # Get list of networks in current organization
            try:
                org_networks = dashboard.organizations.getOrganizationNetworks(org_id)

            except meraki.APIError as e:
                print(f'Meraki API error: {e}')
                print(f'status code = {e.status}')
                print(f'reason = {e.reason}')
                print(f'error = {e.message}')
                continue

            except Exception as e:
                print(f'some other error: {e}')
                continue  # Skip error in current Meraki network

            #We are only interested in neworks where there is a Switch AND wireless
            print(f"\n[Org:{org_name}] Filtering Meraki network type to those that has a Switch & Wireless")
            # Use list comprehension to filter the list
            networks = [
                network for network in networks
                if "switch" in network["productTypes"] and "wireless" in network["productTypes"]
            ]

            # Sort List of network by name
            print(f"\n[Org:{org_name}] Sorting network list by alphabetical order")
            networks = sorted(networks, key=lambda x: x['name'])

            # Iterate through networks in current organisation
            total = len(networks)  # count number of Meraki networks
            counter = 1  # Keep count of number of network as we iterate through them.

            print(f'Iterating through {total} networks in organization in {org["name"]} to get list of APs information')

            # Loop through the list of networks and retrieve the needed info
            for network in networks:
                # Create a new list of dictionaries to store AP info
                network_ap_info = []

                network_name = network["name"]

                #get this Network Link Layer devices info which contains the SW LLDP & AP info we need
                linkLayerDevices_response = response = dashboard.networks.getNetworkTopologyLinkLayer(network["id"])

                #retrieve the APs infor for this network
                network_ap_info = extract_ap_details(linkLayerDevices_response, network["name"], org_name)

                # Sort the list by the "ap_name" key using list comprehension
                network_ap_info = sorted(network_ap_info, key=lambda x: x["ap_name"])

                #Combine the curent org network info into the all_org dict for later use
                all_org_ap_info = network_ap_info + all_org_ap_info

                print(f"Gathering AP info for Org:[{org['name']}] Network:[{network_name}] completed.")

                print(f"Gathering info for Org:[{org['name']}] completed.")

        df_all_org_ap_info_info = pd.DataFrame(all_org_ap_info)
        #
        # write the DataFrame to a CSV file
        current_datetime = datetime.now().strftime("%Y-%m-%d %H-%M-%S")
        filename = f"{output_path}/all_orgs_ap-_info--{current_datetime}.xlsx"

        df_all_org_ap_info_info.to_excel(filename)
        print(f"Exported filename: {filename}")

        print(f"NOTE: APs without a Switch port number are offline or dormant.")

if __name__ == "__main__":
    # Call Main to do all the actual work.
    start_time = datetime.now()
    main()
    end_time = datetime.now()
    print(f'\nScript complete, total runtime {end_time - start_time}')

 

 

Get notified when there are additional replies to this discussion.