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}')