- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
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.
Solved! Go to solution.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
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.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
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.
And then search on "MR" to see all access points. You can then select all the switch ports and change them in one hit.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
Thanks. That's handy to know. 150 networks otherwise I would have just done the manual route.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
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.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
Thanks. I'll explore this route.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
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...
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
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.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
https://developer.cisco.com/meraki/api-v1/get-organization-switch-ports-statuses-by-switch/
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) )
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
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 -------
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
Thank you for sharing. I will give that a try.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
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.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
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}')
