├── .gitignore ├── HEADER ├── LICENSE ├── README.md ├── async_run.py ├── example.xlsx ├── img ├── api_access.png ├── health_check.png ├── meraki_health_check.png ├── my_profile.png ├── network-firmware-sample.png ├── org_settings.png ├── rf_profile.png ├── rf_utilization.png ├── summary-sample.png ├── summary.png └── switch_port_counters.png └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | __pycache__/run.cpython-38.pyc 3 | output/* 4 | sevt.py 5 | -------------------------------------------------------------------------------- /HEADER: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Cisco and/or its affiliates. 2 | 3 | This software is licensed to you under the terms of the Cisco Sample 4 | Code License, Version 1.1 (the "License"). You may obtain a copy of the 5 | License at 6 | 7 | https://developer.cisco.com/docs/licenses 8 | 9 | All use of the material herein must be in accordance with the terms of 10 | the License. All rights not expressly granted by the License are 11 | reserved. Unless required by applicable law or agreed to separately in 12 | writing, software distributed under the License is distributed on an "AS 13 | IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 14 | or implied. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CISCO SAMPLE CODE LICENSE 2 | Version 1.1 3 | Copyright (c) 2021 Cisco and/or its affiliates 4 | 5 | These terms govern this Cisco Systems, Inc. ("Cisco"), example or demo 6 | source code and its associated documentation (together, the "Sample 7 | Code"). By downloading, copying, modifying, compiling, or redistributing 8 | the Sample Code, you accept and agree to be bound by the following terms 9 | and conditions (the "License"). If you are accepting the License on 10 | behalf of an entity, you represent that you have the authority to do so 11 | (either you or the entity, "you"). Sample Code is not supported by Cisco 12 | TAC and is not tested for quality or performance. This is your only 13 | license to the Sample Code and all rights not expressly granted are 14 | reserved. 15 | 16 | 1. LICENSE GRANT: Subject to the terms and conditions of this License, 17 | Cisco hereby grants to you a perpetual, worldwide, non-exclusive, non- 18 | transferable, non-sublicensable, royalty-free license to copy and 19 | modify the Sample Code in source code form, and compile and 20 | redistribute the Sample Code in binary/object code or other executable 21 | forms, in whole or in part, solely for use with Cisco products and 22 | services. For interpreted languages like Java and Python, the 23 | executable form of the software may include source code and 24 | compilation is not required. 25 | 26 | 2. CONDITIONS: You shall not use the Sample Code independent of, or to 27 | replicate or compete with, a Cisco product or service. Cisco products 28 | and services are licensed under their own separate terms and you shall 29 | not use the Sample Code in any way that violates or is inconsistent 30 | with those terms (for more information, please visit: 31 | www.cisco.com/go/terms). 32 | 33 | 3. OWNERSHIP: Cisco retains sole and exclusive ownership of the Sample 34 | Code, including all intellectual property rights therein, except with 35 | respect to any third-party material that may be used in or by the 36 | Sample Code. Any such third-party material is licensed under its own 37 | separate terms (such as an open source license) and all use must be in 38 | full accordance with the applicable license. This License does not 39 | grant you permission to use any trade names, trademarks, service 40 | marks, or product names of Cisco. If you provide any feedback to Cisco 41 | regarding the Sample Code, you agree that Cisco, its partners, and its 42 | customers shall be free to use and incorporate such feedback into the 43 | Sample Code, and Cisco products and services, for any purpose, and 44 | without restriction, payment, or additional consideration of any kind. 45 | If you initiate or participate in any litigation against Cisco, its 46 | partners, or its customers (including cross-claims and counter-claims) 47 | alleging that the Sample Code and/or its use infringe any patent, 48 | copyright, or other intellectual property right, then all rights 49 | granted to you under this License shall terminate immediately without 50 | notice. 51 | 52 | 4. LIMITATION OF LIABILITY: CISCO SHALL HAVE NO LIABILITY IN CONNECTION 53 | WITH OR RELATING TO THIS LICENSE OR USE OF THE SAMPLE CODE, FOR 54 | DAMAGES OF ANY KIND, INCLUDING BUT NOT LIMITED TO DIRECT, INCIDENTAL, 55 | AND CONSEQUENTIAL DAMAGES, OR FOR ANY LOSS OF USE, DATA, INFORMATION, 56 | PROFITS, BUSINESS, OR GOODWILL, HOWEVER CAUSED, EVEN IF ADVISED OF THE 57 | POSSIBILITY OF SUCH DAMAGES. 58 | 59 | 5. DISCLAIMER OF WARRANTY: SAMPLE CODE IS INTENDED FOR EXAMPLE PURPOSES 60 | ONLY AND IS PROVIDED BY CISCO "AS IS" WITH ALL FAULTS AND WITHOUT 61 | WARRANTY OR SUPPORT OF ANY KIND. TO THE MAXIMUM EXTENT PERMITTED BY 62 | LAW, ALL EXPRESS AND IMPLIED CONDITIONS, REPRESENTATIONS, AND 63 | WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OR 64 | CONDITION OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON- 65 | INFRINGEMENT, SATISFACTORY QUALITY, NON-INTERFERENCE, AND ACCURACY, 66 | ARE HEREBY EXCLUDED AND EXPRESSLY DISCLAIMED BY CISCO. CISCO DOES NOT 67 | WARRANT THAT THE SAMPLE CODE IS SUITABLE FOR PRODUCTION OR COMMERCIAL 68 | USE, WILL OPERATE PROPERLY, IS ACCURATE OR COMPLETE, OR IS WITHOUT 69 | ERROR OR DEFECT. 70 | 71 | 6. GENERAL: This License shall be governed by and interpreted in 72 | accordance with the laws of the State of California, excluding its 73 | conflict of laws provisions. You agree to comply with all applicable 74 | United States export laws, rules, and regulations. If any provision of 75 | this License is judged illegal, invalid, or otherwise unenforceable, 76 | that provision shall be severed and the rest of the License shall 77 | remain in full force and effect. No failure by Cisco to enforce any of 78 | its rights related to the Sample Code or to a breach of this License 79 | in a particular situation will act as a waiver of such rights. In the 80 | event of any inconsistencies with any other terms, this License shall 81 | take precedence. 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | [![Run in Cisco Cloud IDE](https://static.production.devnetcloud.com/codeexchange/assets/images/devnet-runable-icon.svg)](https://developer.cisco.com/devenv/?id=devenv-vscode-base&GITHUB_SOURCE_REPO=https://github.com/obrigg/meraki-health-check) [![published](https://static.production.devnetcloud.com/codeexchange/assets/images/devnet-published.svg)](https://developer.cisco.com/codeexchange/github/repo/obrigg/Vanilla-ISE) 5 | 6 | ### The Challenge 7 | 8 | Cisco Meraki is an amazing cloud-managed IT solution, simplying network, security, security cameras and IoT infrastructure. 9 | However, even the most intelligent AI/ML driven solution is still volunerable to users misconfiguring various options (usually without reading the documentation). Misconfiguration can result in an outage, or poor user experience (if you will limit user's traffic to 1Mbps - things will work slowly.. AI won't help there as it's the admin's "intent"). 10 | 11 | ### The Solution 12 | 13 | This script will leverage the Meraki API to compare an organization's settings and status against a set of best practices and thresholds - uncovering configurations that should be changed. 14 | 15 | #### Example output 16 | Summary tab: 17 |

18 | Channel utilization tab: 19 |

20 | RF Profile tab: 21 |

22 | Switchport counters tab: 23 |

24 | 25 | --- 26 | ### List of checks 27 | #### General checks 28 | 1. Network heath alerts: gathering all health alerts from all of the organization's networks. 29 | 2. Multiple adminstrator users: verifying the organization has more than one admin with full control (per best practices). 30 | 3. Admin 2FA: checks which admin users have 2FA enabled, and which admins do not (per best practices). 31 | 4. API calls: present whether Dashboard API is being used, and by which admin usesr. 32 | 5. API v0 usage: The Dashboard API v0 is being deprecated, and integrations should be updated to API v1. 33 | 6. Firmware checks: compares the firmware versions of each network and device types to the latest stable release. 34 | #### Wireless checks 35 | 1. Channel utilization (for 5GHz only, 2.4GHz is beyond saving...) 36 | 2. RF Profile check: 37 | * Configured Minimum Tx power (usually mistaken with EIRP, resulting to too high Tx power). 38 | * Configured minimum Bitrate (see [best practices](https://documentation.meraki.com/MR/WiFi_Basics_and_Best_Practices/Multi-SSID_Deployment_Considerations)). 39 | * Configured channel Width. 40 | * Manually configured RX-SOP (most won't configure it right, and it's better left at "auto"). 41 | * Number of enabled SSIDs (see [best practices](https://documentation.meraki.com/MR/WiFi_Basics_and_Best_Practices/Multi-SSID_Deployment_Considerations)). 42 | 43 | #### Switching checks 44 | 1. Are jumbo-frames enabled, by checking the MTU (see [best practices](https://documentation.meraki.com/Architectures_and_Best_Practices/Cisco_Meraki_Best_Practice_Design/Best_Practice_Design_-_MS_Switching/General_MS_Best_Practices)). 45 | 2. Is RSTP enabled? (best of luck handling loops without it.. see [best practices](https://documentation.meraki.com/Architectures_and_Best_Practices/Cisco_Meraki_Best_Practice_Design/Best_Practice_Design_-_MS_Switching/General_MS_Best_Practices)) 46 | 3. Port counters: 47 | * CRC errors. 48 | * Collisions. 49 | * Broadcasts exceeding threshold. 50 | * Multicasts exceeding threshold. 51 | * Topology changes (TCNs) exceeding threshold. 52 | --- 53 | Convinced the health-check is worth 5 minutes of your time? let's do this! 54 | ### How to run the script: 55 | 56 | #### Generate your Meraki API Key 57 | 58 | 1. Access the [Meraki dashboard](dashboard.meraki.com). 59 | 2. For access to the API, first enable the API for your organization under Organization > Settings > Dashboard API access. 60 |

61 | 3. After enabling the API, go to "my profile" on the upper right side of the dashboard to generate an API key. This API key will be associated with the Dashboard Administrator account which generates it, and will inherit the same permissions as that account. You can generate, revoke, and regenerate your API key on your profile. 62 |

63 |

64 | 65 | ALWAYS keep your API key safe as it provides authentication to all of your organizations with the API enabled. 66 | 67 | If your API key has been compromised - **revoke it immediately** through the dashboard, and generate a new API key. 68 | 69 | #### Installing the Meraki Python SDK 70 | `pip install -r requirements.txt` 71 | #### Storing the Meraki API Key as an environment variable 72 | You don't have to store the API key, as the script will ask you to enter it. However, it would be more covenient to store it instead of typing each every time. 73 | 74 | Linux: 75 | `export MERAKI_DASHBOARD_API_KEY = ` 76 | 77 | Windows: 78 | `set MERAKI_DASHBOARD_API_KEY = ` 79 | 80 | #### And... you're ready. Good luck! 81 | 82 | `python async_run.py` 83 | 84 | Feedback is a gift 85 | 86 | * The script helps? I'd love to hear. 87 | * You think the script sucks? Let's make it better! 88 | * Have suggestions to additional **common** problems that should be included? Open an issue, I'd love to hear that too. 89 | 90 | ### Run code in Cisco Code Exchange Cloud IDE 91 | [![Run in Cisco Cloud IDE](https://static.production.devnetcloud.com/codeexchange/assets/images/devnet-runable-icon.svg)](https://developer.cisco.com/devenv/?id=devenv-vscode-base&GITHUB_SOURCE_REPO=https://github.com/obrigg/meraki-health-check) 92 | 93 | Installing the Meraki Python SDK 94 | 95 | `pip install -r requirements.txt` 96 | 97 | Set as an environment variable with API KEY for testing 98 | 99 | `export MERAKI_DASHBOARD_API_KEY=d03190ff333a3c7feaed89fec5b3b2529f59e8ec` 100 | 101 | Run the following command 102 | 103 | ```bash 104 | python async_run.py 105 | ``` 106 | 107 | Expected output 108 | 109 | ```bash 110 | Fetching organizations... 111 | Meraki Organizations 112 | ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ 113 | ┃ Organization # ┃ Org Name ┃ 114 | ┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ 115 | │ 0 │ DeLab │ 116 | │ 1 │ DevNet Test Org │ 117 | │ 2 │ DevNet Test Org │ 118 | │ 3 │ DevNetAssoc │ 119 | │ 4 │ DevRelations │ 120 | │ 5 │ DevRelx23 │ 121 | │ 6 │ Forest City - Other │ 122 | │ 7 │ GGTEST_MyOrg1 │ 123 | │ 8 │ Hi Cory │ 124 | │ 9 │ Hi Cory │ 125 | │ 10 │ Jacks_test_net │ 126 | │ 11 │ MARYDALKO_HOME │ 127 | │ 12 │ MARYDALKO_HOME │ 128 | │ 13 │ MARYDALKO_HOME │ 129 | │ 14 │ My Org │ 130 | │ 15 │ My organization │ 131 | │ 16 │ My organization │ 132 | │ 17 │ My organization │ 133 | │ 18 │ My organization - clone │ 134 | │ 19 │ New Meraki Org │ 135 | │ 20 │ PM_Test │ 136 | │ 21 │ Personal.Lekhnath │ 137 | │ 22 │ SVR │ 138 | │ 23 │ Sample Org │ 139 | │ 24 │ TNF - The Network Factory │ 140 | │ 25 │ Wild Willys Org │ 141 | │ 26 │ Wils Test Creation │ 142 | │ 27 │ Wotan │ 143 | │ 28 │ Your Organization │ 144 | │ 29 │ abcdefg │ 145 | │ 30 │ changetest │ 146 | │ 31 │ gk │ 147 | │ 32 │ helloworld │ 148 | │ 33 │ organization with name changed │ 149 | │ 34 │ sample_network │ 150 | │ 35 │ thienbao │ 151 | └────────────────┴────────────────────────────────┘ 152 | Kindly select the organization ID you would like to query: 153 | ``` 154 | Type in `1` and press `Enter` 155 | As a result of the running script, a related report file was created. In the current case file `DevNet Test Org.xlsx` was created. 156 | Sample Screenshots from the report 157 | 158 | ![](img/summary-sample.png) 159 | ![](img/network-firmware-sample.png) 160 | --- 161 | 162 | ### Known limitations / caveats 163 | 1. The script intentionally ignores the 2.4GHz spectrum, as it is beyond salavion. It can be altered, if needed, in the `check_wifi_channel_utilization` function. 164 | 2. The SSID amount check counts every **enabled** SSIDs, even if the SSID is limited to certain APs or to a certain band. You may have three ssids on 2.4GHz and three different SSIDs on 5GHz, but the check will fail as it counts six SSIDs. 165 | 3. The API usage is checking the last 5,000 API calls. It can be changed in the code, more API calls being examines = longer run time for the script (The async version checks up to 10,000 API calls per admin user). 166 | ---- 167 | ### Licensing info 168 | Copyright (c) 2022 Cisco and/or its affiliates. 169 | 170 | This software is licensed to you under the terms of the Cisco Sample 171 | Code License, Version 1.1 (the "License"). You may obtain a copy of the 172 | License at 173 | 174 | https://developer.cisco.com/docs/licenses 175 | 176 | All use of the material herein must be in accordance with the terms of 177 | the License. All rights not expressly granted by the License are 178 | reserved. Unless required by applicable law or agreed to separately in 179 | writing, software distributed under the License is distributed on an "AS 180 | IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 181 | or implied. 182 | -------------------------------------------------------------------------------- /async_run.py: -------------------------------------------------------------------------------- 1 | __version__ = "23.06.19.01" 2 | __author__ = "Oren Brigg" 3 | __author_email__ = "obrigg@cisco.com" 4 | __license__ = "Cisco Sample Code License, Version 1.1 - https://developer.cisco.com/site/license/cisco-sample-code-license/" 5 | 6 | 7 | import os 8 | import sys 9 | import time 10 | import meraki 11 | import asyncio 12 | import requests 13 | import meraki.aio 14 | from getpass import getpass 15 | from rich import print as pp 16 | from rich.table import Table 17 | from openpyxl import Workbook 18 | from rich.console import Console 19 | from openpyxl.styles import Font, Color 20 | 21 | 22 | def select_org(): 23 | # Fetch and select the organization 24 | print("\n\nFetching organizations...\n") 25 | try: 26 | organizations = dashboard.organizations.getOrganizations() 27 | except Exception as e: 28 | pp(f"[red]An error has occured: \n\n{e}[/red]\n\nExiting...") 29 | pp("A newly generated API key will require up to 15 minutes to synchronize with Meraki API gateways. \ 30 | \nIf you're using a new key - kindly try again in a few minutes.") 31 | sys.exit(1) 32 | organizations.sort(key=lambda x: x["name"]) 33 | ids = [] 34 | table = Table(title="Meraki Organizations") 35 | table.add_column("Organization #", justify="left", style="cyan", no_wrap=True) 36 | table.add_column("Org Name", justify="left", style="cyan", no_wrap=True) 37 | counter = 0 38 | for organization in organizations: 39 | ids.append(organization["id"]) 40 | table.add_row(str(counter), organization["name"]) 41 | counter += 1 42 | console = Console() 43 | console.print(table) 44 | isOrgDone = False 45 | while isOrgDone == False: 46 | selected = input( 47 | "\nKindly select the organization ID you would like to query: " 48 | ) 49 | try: 50 | if int(selected) in range(0, counter): 51 | isOrgDone = True 52 | else: 53 | print("\t[red]Invalid Organization Number\n") 54 | except: 55 | print("\t[red]Invalid Organization Number\n") 56 | return (organizations[int(selected)]["id"], organizations[int(selected)]["name"]) 57 | 58 | 59 | async def async_check_network_health_alerts( 60 | aiomeraki: meraki.aio.AsyncDashboardAPI, network: dict 61 | ): 62 | """ 63 | This fuction checks the network health alerts for a given network. 64 | """ 65 | # print(f"\t\tChecking network health alerts for network: {network['name']}") 66 | try: 67 | alerts = await aiomeraki.networks.getNetworkHealthAlerts(network["id"]) 68 | except meraki.exceptions.AsyncAPIError as e: 69 | pp( 70 | f'[bold magenta]Meraki AIO API Error (OrgID "{ org_id }", OrgName "{ org_name }"): \n { e }' 71 | ) 72 | except Exception as e: 73 | pp(f"[bold magenta]Some other ERROR: {e}") 74 | if len(alerts) == 0: 75 | pp(f"[green]No network health alerts for network {network['name']}") 76 | results[network["name"]]["network_health_alerts"] = {"is_ok": True} 77 | else: 78 | result = {"is_ok": False, "alert_list": []} 79 | pp(f"[red]Network alerts detected for network {network['name']}") 80 | for alert in alerts: 81 | try: 82 | del alert["scope"]["devices"][0]["url"] 83 | del alert["scope"]["devices"][0]["mac"] 84 | except: 85 | pass 86 | result["alert_list"].append( 87 | { 88 | "severity": alert["severity"], 89 | "category": alert["category"], 90 | "type": alert["type"], 91 | "details": alert["scope"], 92 | } 93 | ) 94 | pp( 95 | f"[red]Severity: {alert['severity']}\tCategory: {alert['category']}\tType: {alert['type']}" 96 | ) 97 | results[network["name"]]["network_health_alerts"] = result 98 | 99 | 100 | async def async_check_wifi_channel_utilization( 101 | aiomeraki: meraki.aio.AsyncDashboardAPI, network: dict 102 | ): 103 | """ 104 | This fuction checks the wifi channel utilization for a given network. 105 | if the channel utilization is above the threshold, the check will fail. 106 | 107 | it will populate "results" with a dictionary with the result for each AP. 108 | e.g. { 109 | 'is_ok': False, 110 | 'Q2KD-XXXX-XXXX': {'is_ok': False, 'name': 'AP1', 'utilization': 51.66, 'occurances': 3}, 111 | 'Q2KD-XXXX-XXXX': {'is_ok': False, 'name': 'AP2', 'utilization': 56.69, 'occurances': 17}, 112 | 'Q2KD-XXXX-XXXX': {'is_ok': True, 'name': 'AP3', 'utilization': 16.93, 'occurances': 8}, 113 | 'Q2KD-XXXX-XXXX': {'is_ok': False, 'name': 'AP4', 'utilization': 59.48, 'occurances': 1} 114 | } 115 | """ 116 | # print(f"\t\tChecking wifi channel utilization for network: {network['name']}") 117 | result = {"is_ok": True} 118 | try: 119 | channel_utilization = ( 120 | await aiomeraki.networks.getNetworkNetworkHealthChannelUtilization( 121 | network["id"], perPage=100 122 | ) 123 | ) 124 | # TODO: pagination 125 | for ap in channel_utilization: 126 | utilization_list = [ 127 | ap["wifi1"][util]["utilization"] for util in range(len(ap["wifi1"])) 128 | ] 129 | exceeded_utilization_list = [ 130 | utilization 131 | for utilization in utilization_list 132 | if utilization > thresholds["5G Channel Utilization"] 133 | ] 134 | if len(utilization_list) == 0: 135 | pp(f"[yellow]AP {ap['serial']} does not have 5GHz enabled. Skipping...") 136 | elif len(exceeded_utilization_list) > 0: 137 | pp( 138 | f"[red]5GHz Channel Utilization exceeded {thresholds['5G Channel Utilization']}% {len(exceeded_utilization_list)} times, with a peak of {max(utilization_list)}% for AP {ap['serial']}" 139 | ) 140 | result[ap["serial"]] = { 141 | "is_ok": False, 142 | "utilization": max(utilization_list), 143 | "occurances": len(exceeded_utilization_list), 144 | } 145 | result["is_ok"] = False 146 | else: 147 | pp( 148 | f"[green]5GHz Channel did not exceed {thresholds['5G Channel Utilization']}% for AP {ap['serial']}, max utilization was {max(utilization_list)}" 149 | ) 150 | result[ap["serial"]] = { 151 | "is_ok": True, 152 | "utilization": max(utilization_list), 153 | "occurances": 0, 154 | } 155 | # Adding AP names 156 | try: 157 | network_devices = await aiomeraki.networks.getNetworkDevices(network["id"]) 158 | except meraki.exceptions.AsyncAPIError as e: 159 | pp( 160 | f'[bold magenta]Meraki AIO API Error (Network Name "{network["name"]}": \n { e }' 161 | ) 162 | except Exception as e: 163 | pp(f"[bold magenta]Some other ERROR: {e}") 164 | for device in network_devices: 165 | if device["serial"] in result: 166 | result[device["serial"]]["name"] = device.get("name", device["serial"]) 167 | # 168 | results[network["name"]]["channel_utilization_check"] = result 169 | # 170 | except meraki.exceptions.AsyncAPIError as e: 171 | pp( 172 | f'[bold magenta]Meraki AIO API Error (OrgID "{ org_id }", OrgName "{ org_name }"): \n { e }' 173 | ) 174 | results[network["name"]]["channel_utilization_check"] = {"is_ok": False} 175 | except Exception as e: 176 | pp(f"[bold magenta]Some other ERROR: {e}") 177 | results[network["name"]]["channel_utilization_check"] = {"is_ok": False} 178 | 179 | 180 | async def async_check_wifi_rf_profiles( 181 | aiomeraki: meraki.aio.AsyncDashboardAPI, network: dict 182 | ): 183 | """ 184 | This fuction checks the RF profiles for a given network. 185 | 186 | it will populate "results" with a dictionary with the result for each AP. 187 | e.g. 188 | {'is_ok': False, 189 | 'RF Profile 1': { 190 | 'is_ok': False, 191 | 'tests': { 192 | 'min_power': {'is_ok': True, 'value': 8}, 193 | 'min_bitrate': {'is_ok': True, 'value': 54}, 194 | 'channel_width': {'is_ok': True, 'value': '40'}, 195 | 'rxsop': {'is_ok': False, 'value': -75} 196 | }}, 197 | 'RF Profile 2': { 198 | 'is_ok': True, 199 | 'tests': { 200 | 'min_power': {'is_ok': True, 'value': 5}, 201 | 'min_bitrate': {'is_ok': True, 'value': 54}, 202 | 'channel_width': {'is_ok': False, 'value': 'auto'}, 203 | 'rxsop': {'is_ok': True, 'value': None} 204 | }}} 205 | 206 | """ 207 | # print(f"\t\tChecking WiFi RF Profiles for network: {network['name']}") 208 | result = {"is_ok": True} 209 | try: 210 | rf_profiles = await aiomeraki.wireless.getNetworkWirelessRfProfiles( 211 | network["id"] 212 | ) 213 | except: 214 | pp(f"[red]Could not fetch RF profiles for network {network['name']}") 215 | return { 216 | "is_ok": False, 217 | "ERROR": { 218 | "is_ok": False, 219 | "tests": { 220 | "min_power": {"is_ok": False, "value": ""}, 221 | "min_bitrate": {"is_ok": True, "value": ""}, 222 | "channel_width": {"is_ok": True, "value": ""}, 223 | "rxsop": {"is_ok": False, "value": ""}, 224 | }, 225 | }, 226 | } 227 | for rf_profile in rf_profiles: 228 | result[rf_profile["name"]] = { 229 | "is_ok": True, 230 | "tests": { 231 | "min_power": {"is_ok": True}, 232 | "min_bitrate": {"is_ok": True}, 233 | "channel_width": {"is_ok": True}, 234 | "rxsop": {"is_ok": True}, 235 | }, 236 | } 237 | # Check min TX power 238 | if rf_profile["fiveGhzSettings"]["minPower"] > thresholds["5G Min TX Power"]: 239 | pp( 240 | f"[red]The min TX power is too high at {rf_profile['fiveGhzSettings']['minPower']}dBm (not including antenna gain) for RF profile {rf_profile['name']}" 241 | ) 242 | result[rf_profile["name"]]["tests"]["min_power"] = { 243 | "is_ok": False, 244 | "value": rf_profile["fiveGhzSettings"]["minPower"], 245 | } 246 | result[rf_profile["name"]]["is_ok"] = False 247 | result["is_ok"] = False 248 | else: 249 | pp( 250 | f"[green]The min TX power is {rf_profile['fiveGhzSettings']['minPower']}dBm for RF profile {rf_profile['name']}" 251 | ) 252 | result[rf_profile["name"]]["tests"]["min_power"] = { 253 | "is_ok": True, 254 | "value": rf_profile["fiveGhzSettings"]["minPower"], 255 | } 256 | 257 | # Check min bitrate 258 | if rf_profile["fiveGhzSettings"]["minBitrate"] < thresholds["5G Min Bitrate"]: 259 | pp( 260 | f"[red]The min bitrate is {rf_profile['fiveGhzSettings']['minBitrate']}Mbps for RF profile {rf_profile['name']}" 261 | ) 262 | result[rf_profile["name"]]["tests"]["min_bitrate"] = { 263 | "is_ok": False, 264 | "value": rf_profile["fiveGhzSettings"]["minBitrate"], 265 | } 266 | result[rf_profile["name"]]["is_ok"] = False 267 | result["is_ok"] = False 268 | else: 269 | pp( 270 | f"[green]The min bitrate is {rf_profile['fiveGhzSettings']['minBitrate']}Mbps for RF profile {rf_profile['name']}" 271 | ) 272 | result[rf_profile["name"]]["tests"]["min_bitrate"] = { 273 | "is_ok": True, 274 | "value": rf_profile["fiveGhzSettings"]["minBitrate"], 275 | } 276 | 277 | # Check channel width 278 | if rf_profile["fiveGhzSettings"]["channelWidth"] == "auto": 279 | pp( 280 | f"[red]The channel width is {rf_profile['fiveGhzSettings']['channelWidth']} for RF profile {rf_profile['name']}" 281 | ) 282 | result[rf_profile["name"]]["tests"]["channel_width"] = { 283 | "is_ok": False, 284 | "value": rf_profile["fiveGhzSettings"]["channelWidth"], 285 | } 286 | result[rf_profile["name"]]["is_ok"] = False 287 | result["is_ok"] = False 288 | elif ( 289 | int(rf_profile["fiveGhzSettings"]["channelWidth"]) 290 | > thresholds["5G Max Channel Width"] 291 | ): 292 | pp( 293 | f"[red]The channel width is {rf_profile['fiveGhzSettings']['channelWidth']}MHz for RF profile {rf_profile['name']}" 294 | ) 295 | result[rf_profile["name"]]["tests"]["channel_width"] = { 296 | "is_ok": False, 297 | "value": rf_profile["fiveGhzSettings"]["channelWidth"], 298 | } 299 | result[rf_profile["name"]]["is_ok"] = False 300 | result["is_ok"] = False 301 | else: 302 | pp( 303 | f"[green]The channel width is {rf_profile['fiveGhzSettings']['channelWidth']}MHz for RF profile {rf_profile['name']}" 304 | ) 305 | result[rf_profile["name"]]["tests"]["channel_width"] = { 306 | "is_ok": True, 307 | "value": rf_profile["fiveGhzSettings"]["channelWidth"], 308 | } 309 | 310 | # Check if rx-sop is confiugred 311 | if rf_profile["fiveGhzSettings"]["rxsop"] != None: 312 | pp(f"[red]RX-SOP is configured for RF profile {rf_profile['name']}") 313 | result[rf_profile["name"]]["tests"]["rxsop"] = { 314 | "is_ok": False, 315 | "value": rf_profile["fiveGhzSettings"]["rxsop"], 316 | } 317 | result[rf_profile["name"]]["is_ok"] = False 318 | result["is_ok"] = False 319 | else: 320 | pp(f"[green]RX-SOP is not configured for RF profile {rf_profile['name']}") 321 | result[rf_profile["name"]]["tests"]["rxsop"] = { 322 | "is_ok": True, 323 | "value": rf_profile["fiveGhzSettings"]["rxsop"], 324 | } 325 | results[network["name"]]["rf_profiles_check"] = result 326 | 327 | 328 | async def async_check_wifi_ssid_amount( 329 | aiomeraki: meraki.aio.AsyncDashboardAPI, network: dict 330 | ): 331 | """ 332 | This fuction checks the amount of SSIDs for a given network. 333 | 334 | e.g. { 335 | 'is_ok': False, 336 | 'amount': 5 337 | } 338 | """ 339 | # print("\n\t\tChecking WiFi SSID Amount...\n") 340 | result = {"is_ok": True} 341 | try: 342 | ssid_list = await aiomeraki.wireless.getNetworkWirelessSsids(network["id"]) 343 | except meraki.exceptions.AsyncAPIError as e: 344 | pp( 345 | f'[bold magenta]Meraki AIO API Error (OrgID "{ org_id }", OrgName "{ org_name }"): \n { e }' 346 | ) 347 | except Exception as e: 348 | pp(f"[bold magenta]Some other ERROR: {e}") 349 | enabled_ssid_counter = 0 350 | for ssid in ssid_list: 351 | if ssid["enabled"]: 352 | enabled_ssid_counter += 1 353 | result["ssid_amount"] = enabled_ssid_counter 354 | if enabled_ssid_counter <= thresholds["ssid_amount"]: 355 | pp( 356 | f"[green]There are {enabled_ssid_counter} SSIDs enabled for network {network['name']}" 357 | ) 358 | else: 359 | pp( 360 | f"[red]There are {enabled_ssid_counter} SSIDs enabled for network {network['name']}" 361 | ) 362 | result["is_ok"] = False 363 | results[network["name"]]["ssid_amount_check"] = result 364 | 365 | 366 | async def async_check_switches_port_counters( 367 | aiomeraki: meraki.aio.AsyncDashboardAPI, network: dict 368 | ): 369 | """ 370 | This fuction checks the port counters for all switches in a given network. 371 | 372 | it will return a dictionary with the result for each switch. 373 | e.g. { 374 | 'is_ok': False, 375 | 'Switch 1': {'is_ok': False, 'crc': ['3'], 'collision': [], 'broadcast': ['17', '18', '19', '20', '27'], 'multicast': [], 'topology_changes': []}, 376 | 'Switch 2': {'is_ok': False, 'crc': [], 'collision': ['5'], 'broadcast': ['1', '14', '49'], 'multicast': [], 'topology_changes': []}, 377 | 'Switch 3': {'is_ok': True, 'crc': [], 'collision': [], 'broadcast': [], 'multicast': [], 'topology_changes': []}, 378 | } 379 | """ 380 | # print(f"\t\tChecking Switch Port Counters for network {network['name']}...") 381 | try: 382 | device_list = await aiomeraki.networks.getNetworkDevices(network["id"]) 383 | except meraki.exceptions.AsyncAPIError as e: 384 | pp( 385 | f'[bold magenta]Meraki AIO API Error (OrgID "{ org_id }", OrgName "{ org_name }"): \n { e }' 386 | ) 387 | except Exception as e: 388 | pp(f"[bold magenta]Some other ERROR: {e}") 389 | results[network["name"]]["port_counters_check"] = {"is_ok": True} 390 | port_check_task = [] 391 | for device in device_list: 392 | if "MS" in device["model"] or "C9" in device["model"]: 393 | if "name" not in device.keys(): 394 | device["name"] = device["serial"] 395 | port_check_task.append( 396 | async_check_switch_port_counters(aiomeraki, network, device) 397 | ) 398 | results[network["name"]]["port_counters_check"][device["name"]] = { 399 | "is_ok": True 400 | } 401 | # 402 | for task in asyncio.as_completed(port_check_task): 403 | await task 404 | 405 | 406 | async def async_check_switch_port_counters( 407 | aiomeraki: meraki.aio.AsyncDashboardAPI, network: dict, device: dict 408 | ): 409 | # print(f"\t\tChecking Switch Port Counters for network {network['name']}...") 410 | result = { 411 | "is_ok": True, 412 | "crc": [], 413 | "collision": [], 414 | "broadcast": [], 415 | "multicast": [], 416 | "topology_changes": [], 417 | } 418 | try: 419 | switch_counters = await aiomeraki.switch.getDeviceSwitchPortsStatusesPackets( 420 | device["serial"] 421 | ) 422 | except meraki.exceptions.AsyncAPIError as e: 423 | pp( 424 | f'[bold magenta]Meraki AIO API Error (OrgID "{ org_id }", OrgName "{ org_name }"): \n { e }' 425 | ) 426 | except Exception as e: 427 | pp(f"[bold magenta]Some other ERROR: {e}") 428 | for port in switch_counters: 429 | for port_counter in port["packets"]: 430 | # CRC and collision errors 431 | if port_counter["desc"] == "CRC align errors" and port_counter["total"] > 0: 432 | pp( 433 | f"[red]{port_counter['total']} CRC errors on switch {device['name']} - port {port['portId']}" 434 | ) 435 | result["crc"].append(port["portId"]) 436 | result["is_ok"] = False 437 | if port_counter["desc"] == "Collisions" and port_counter["total"] > 0: 438 | pp( 439 | f"[red]{port_counter['total']} collisions on switch {device['name']} - port {port['portId']}" 440 | ) 441 | result["collision"].append(port["portId"]) 442 | result["is_ok"] = False 443 | # Broadcast and Multicast rates 444 | if ( 445 | port_counter["desc"] == "Broadcast" 446 | and port_counter["ratePerSec"]["total"] > thresholds["broadcast_rate"] 447 | ): 448 | pp( 449 | f"[red]{port_counter['ratePerSec']['total']} broadcast/s on switch {device['name']} - port {port['portId']}" 450 | ) 451 | result["broadcast"].append(port["portId"]) 452 | result["is_ok"] = False 453 | if ( 454 | port_counter["desc"] == "Multicast" 455 | and port_counter["ratePerSec"]["total"] > thresholds["multicast_rate"] 456 | ): 457 | pp( 458 | f"[red]{port_counter['ratePerSec']['total']} multicast/s on switch {device['name']} - port {port['portId']}" 459 | ) 460 | result["multicast"].append(port["portId"]) 461 | result["is_ok"] = False 462 | # Topology changes 463 | if ( 464 | port_counter["desc"] == "Topology changes" 465 | and port_counter["total"] > thresholds["topology_changes"] 466 | ): 467 | pp( 468 | f"[red]{port_counter['total']} topology changes on switch {device['name']} - port {port['portId']}" 469 | ) 470 | result["topology_changes"].append(port["portId"]) 471 | result["is_ok"] = False 472 | if results[network["name"]]["port_counters_check"][device["name"]]["is_ok"]: 473 | pp(f"[green]No port errors on switch {device['name']}") 474 | results[network["name"]]["port_counters_check"][device["name"]] = result 475 | 476 | 477 | async def async_check_switch_stp( 478 | aiomeraki: meraki.aio.AsyncDashboardAPI, network: dict 479 | ): 480 | """ 481 | This fuction checks the STP status for a given network. 482 | """ 483 | # print("\n\t\tChecking Switch STP Status...\n") 484 | try: 485 | stp_status = await aiomeraki.switch.getNetworkSwitchStp(network["id"]) 486 | except meraki.exceptions.AsyncAPIError as e: 487 | pp( 488 | f'[bold magenta]Meraki AIO API Error (OrgID "{ org_id }", OrgName "{ org_name }"): \n { e }' 489 | ) 490 | except Exception as e: 491 | pp(f"[bold magenta]Some other ERROR: {e}") 492 | if stp_status["rstpEnabled"]: 493 | pp(f"[green]STP is enabled for network {network['name']}") 494 | results[network["name"]]["stp_check"] = {"is_ok": True} 495 | else: 496 | pp(f"[red]STP is disabled for network {network['name']}") 497 | results[network["name"]]["stp_check"] = {"is_ok": False} 498 | 499 | 500 | async def async_check_switch_mtu( 501 | aiomeraki: meraki.aio.AsyncDashboardAPI, network: dict 502 | ): 503 | """ 504 | This fuction checks the MTU of a given network. 505 | """ 506 | # print(f"\t\tChecking Switch MTU for network {network['name']}...") 507 | try: 508 | mtu = await aiomeraki.switch.getNetworkSwitchMtu(network["id"]) 509 | except meraki.exceptions.AsyncAPIError as e: 510 | pp( 511 | f'[bold magenta]Meraki AIO API Error (OrgID "{ org_id }", OrgName "{ org_name }"): \n { e }' 512 | ) 513 | except Exception as e: 514 | pp(f"[bold magenta]Some other ERROR: {e}") 515 | if mtu["defaultMtuSize"] == 9578 and mtu["overrides"] == []: 516 | pp( 517 | f"[green]Jumbo Frames enabled for network {network['name']} (MTU: {mtu['defaultMtuSize']})" 518 | ) 519 | results[network["name"]]["mtu_check"] = {"is_ok": True} 520 | else: 521 | pp( 522 | f"[red]Jumbo Frames disabled for network {network['name']} (MTU: {mtu['defaultMtuSize']}).\ 523 | \nIt's recommended to keep at default of 9578 unless intermediate devices don’t support jumbo frames" 524 | ) 525 | results[network["name"]]["mtu_check"] = {"is_ok": False} 526 | 527 | 528 | async def async_check_switch_storm_control( 529 | aiomeraki: meraki.aio.AsyncDashboardAPI, network: dict 530 | ): 531 | """ 532 | This fuction checks the storm control settings of a given network. 533 | """ 534 | # print(f"\t\tChecking Switch Storm Control for network: {network['name']}...") 535 | try: 536 | storm_control = await aiomeraki.switch.getNetworkSwitchStormControl( 537 | network["id"] 538 | ) 539 | except meraki.exceptions.AsyncAPIError as e: 540 | pp( 541 | f'[bold magenta]Meraki AIO API Error (OrgID "{ org_id }", OrgName "{ org_name }"): \n { e }' 542 | ) 543 | except Exception as e: 544 | pp(f"[bold magenta]Some other ERROR: {e}") 545 | try: 546 | if ( 547 | storm_control["broadcastThreshold"] < 100 548 | and storm_control["multicastThreshold"] < 100 549 | and storm_control["unknownUnicastThreshold"] < 100 550 | ): 551 | pp(f"[green]Storm-control is enabled for network {network['name']}.") 552 | results[network["name"]]["storm_control"] = {"is_ok": True} 553 | else: 554 | pp( 555 | f"[yellow]Storm-control is disabled for network {network['name']}. Best practices suggest a limit should be configured." 556 | ) 557 | results[network["name"]]["storm_control"] = {"is_ok": False} 558 | except: 559 | pp(f"[yellow]Storm-control is not supported for network {network['name']}.") 560 | results[network["name"]]["storm_control"] = {"is_ok": False} 561 | 562 | 563 | async def async_check_network_firmware( 564 | aiomeraki: meraki.aio.AsyncDashboardAPI, network: dict 565 | ): 566 | """ 567 | This fuction checks the firmware versions of a given network. 568 | e.g. { 569 | 'appliance': {'current_version_name': 'MX 16.15', 'latest_stable_version': 'MX 16.15'}, 570 | 'wireless': {'current_version_name': 'MR 27.7.1', 'latest_stable_version': 'MR 28.5'} 571 | } 572 | """ 573 | result = {"is_ok": True} 574 | # print(f"\t\tChecking Firmware Version for network {network['name']}...") 575 | try: 576 | response = await aiomeraki.networks.getNetworkFirmwareUpgrades(network["id"]) 577 | firmware = response["products"] 578 | for product in firmware: 579 | current_version = firmware[product]["currentVersion"]["shortName"] 580 | # Looking for the latest stable version 581 | for version in firmware[product]["availableVersions"]: 582 | if version["releaseType"] == "stable": 583 | latest_stable_version = version["shortName"] 584 | if current_version == latest_stable_version: 585 | pp( 586 | f"[green]{network['name']}: {product.upper()} is running the current stable version ({current_version})." 587 | ) 588 | elif firmware[product]["nextUpgrade"]["time"] != "": 589 | pp( 590 | f"[green]{network['name']}: {product.upper()} is not running the current stable version ({current_version}), but an upgrade is scheduled for {firmware[product]['nextUpgrade']['time']}" 591 | ) 592 | else: 593 | pp( 594 | f"[red]{network['name']}: {product.upper()} is not running the current stable version (current: {current_version}, current stable version: {latest_stable_version})" 595 | ) 596 | result["is_ok"] = False 597 | # 598 | result[product] = { 599 | "current_version": current_version, 600 | "latest_stable_version": latest_stable_version, 601 | "scheduled_upgrade": firmware[product]["nextUpgrade"]["time"], 602 | } 603 | results[network["name"]]["network_firmware_check"] = result 604 | except meraki.exceptions.AsyncAPIError as e: 605 | pp( 606 | f'[bold magenta]Meraki AIO API Error (OrgID "{ org_id }", OrgName "{ org_name }"): \n { e }' 607 | ) 608 | 609 | except Exception as e: 610 | pp(f"[bold magenta]Some other ERROR: {e}") 611 | 612 | 613 | async def async_check_org_admins(aiomeraki: meraki.aio.AsyncDashboardAPI): 614 | """ 615 | This fuction checks the administration settings of the organization. 616 | 617 | it will return a dictionary with the results for the admin checks. 618 | e.g. { 619 | 'is_ok': False, 620 | 'more_than_one_admin': True, 621 | 'users': { 622 | '123456': {'email': 'user1@org.com', 'name': 'user1', '2fa': True, 'api_calls': 50}, 623 | '654321': {'email': 'user2@org.com', 'name': 'user2', '2fa': False, 'api_calls': 50}}, 624 | 'missing_2fa': True, 625 | 'api_calls': 127 626 | } 627 | 628 | """ 629 | # print(f"\t\tAnalyzing organization admins...") 630 | results["org_settings"] = { 631 | "is_ok": False, 632 | "more_than_one_admin": False, 633 | "users": {}, 634 | "missing_2fa": True, 635 | "api_calls": 0, 636 | "using_v0": False, 637 | } 638 | try: 639 | org_admins = await aiomeraki.organizations.getOrganizationAdmins(org_id) 640 | except meraki.exceptions.AsyncAPIError as e: 641 | pp( 642 | f'[bold magenta]Meraki AIO API Error (OrgID "{ org_id }", OrgName "{ org_name }"): \n { e }' 643 | ) 644 | except Exception as e: 645 | pp(f"[bold magenta]Some other ERROR: {e}") 646 | for admin in org_admins: 647 | results["org_settings"]["users"][admin["id"]] = { 648 | "email": admin["email"], 649 | "name": admin["name"], 650 | "2fa": admin["twoFactorAuthEnabled"], 651 | "api_calls": 0, 652 | "using_v0": False, 653 | } 654 | if admin["twoFactorAuthEnabled"] == False: 655 | pp( 656 | f"[yellow]Missing 2FA for admin {admin['name']} (email: {admin['email']})" 657 | ) 658 | else: 659 | pp( 660 | f"[green]Admin {admin['name']} (email: {admin['email']}) has 2FA enabled" 661 | ) 662 | # Filter full right admins (not just read-only or network specific admins) 663 | full_right_admins = [admin for admin in org_admins if admin["orgAccess"] == "full"] 664 | if len(full_right_admins) > 1: 665 | results["org_settings"]["more_than_one_admin"] = True 666 | pp(f"[green]More than one admin has full rights. This is recommended.") 667 | else: 668 | pp( 669 | f"[red]Only one admin has full rights. It's recommended to have at least one admin with full rights." 670 | ) 671 | if ( 672 | results["org_settings"]["more_than_one_admin"] == True 673 | and results["org_settings"]["missing_2fa"] == [] 674 | ): 675 | results["org_settings"]["is_ok"] = True 676 | else: 677 | results["org_settings"]["is_ok"] = False 678 | # Check API access 679 | check_api_calls_tasks = [ 680 | async_check_api_calls(aiomeraki, admin["id"]) for admin in org_admins 681 | ] 682 | for task in asyncio.as_completed(check_api_calls_tasks): 683 | await task 684 | api_call_count = 0 685 | for admin in results["org_settings"]["users"]: 686 | api_call_count += results["org_settings"]["users"][admin]["api_calls"] 687 | results["org_settings"]["api_calls"] = api_call_count 688 | pp( 689 | f"API access usage: {results['org_settings']['api_calls']} API calls during the last week." 690 | ) 691 | 692 | 693 | async def async_check_api_calls( 694 | aiomeraki: meraki.aio.AsyncDashboardAPI, org_admin: str 695 | ): 696 | try: 697 | api_requests = await aiomeraki.organizations.getOrganizationApiRequests( 698 | org_id, adminId=org_admin, timespan=7 * 86400, perPage=1000, total_pages=10 699 | ) 700 | except meraki.exceptions.AsyncAPIError as e: 701 | pp( 702 | f'[bold magenta]Meraki AIO API Error (OrgID "{ org_id }", OrgName "{ org_name }"): \n { e }' 703 | ) 704 | except Exception as e: 705 | pp(f"[bold magenta]Some other ERROR: {e}") 706 | for request in api_requests: 707 | results["org_settings"]["users"][request["adminId"]]["api_calls"] += 1 708 | if "/v0/" in request["path"]: 709 | results["org_settings"]["using_v0"] = True 710 | if not results["org_settings"]["users"][request["adminId"]]["using_v0"]: 711 | pp( 712 | f"[red]Admin {results['org_settings']['users'][request['adminId']]['name']} (email: {results['org_settings']['users'][request['adminId']]['email']}) is using the v0 API" 713 | ) 714 | results["org_settings"]["users"][request["adminId"]]["using_v0"] = True 715 | results["org_settings"]["is_ok"] = False 716 | 717 | 718 | def check_wireless_ports(networks: list): 719 | # Fetch all APs' ethernet port statuses 720 | aggregated_ap_uplinks = [] 721 | startingAfter = '' 722 | isDone = False 723 | while not isDone: 724 | pp(f"Fetching ethernet ports, received {len(aggregated_ap_uplinks)} ethernet ports so far..") 725 | try: 726 | ap_uplinks = dashboard.wireless.getOrganizationWirelessDevicesEthernetStatuses(org_id, perPage=1000, startingAfter=startingAfter) 727 | aggregated_ap_uplinks += ap_uplinks 728 | if len(ap_uplinks) < 1000: 729 | isDone = True 730 | else: 731 | startingAfter = ap_uplinks[999]['serial'] 732 | except Exception as e: 733 | pp(f"[bold magenta]Some other ERROR: {e}") 734 | isDone = True 735 | # 736 | for ap in aggregated_ap_uplinks: 737 | # Translating network ID to network name 738 | network_id = ap['network']['id'] 739 | network_name = "N/A" 740 | for network in networks: 741 | if network['id'] == network_id: 742 | network_name = network['name'] 743 | break 744 | # Checking if the AP has 5GHz history, otherwise it won't appear in the results 745 | if ap["serial"] in results[network_name]["channel_utilization_check"].keys(): 746 | # Checking the APs uplink 747 | # TODO: Adjust the code for APs with more than one port 748 | ap_port = ap['ports'][0] 749 | results[network_name]["channel_utilization_check"][ap["serial"]]["speed"] = ap_port['linkNegotiation']['speed'] 750 | results[network_name]["channel_utilization_check"][ap["serial"]]["duplex"] = ap_port['linkNegotiation']['duplex'] 751 | results[network_name]["channel_utilization_check"][ap["serial"]]["power"] = ap['power']['mode'] 752 | # 753 | if ap_port['linkNegotiation']['speed'] == None or ap_port['linkNegotiation']['speed'] < 1000: 754 | results[network_name]["channel_utilization_check"][ap["serial"]]["is_ok"] = False 755 | if ap_port['linkNegotiation']['duplex'] == 'half' or ap_port['linkNegotiation']['duplex'] == None: 756 | results[network_name]["channel_utilization_check"][ap["serial"]]["is_ok"] = False 757 | if ap['power']['mode'] != 'full': 758 | results[network_name]["channel_utilization_check"][ap["serial"]]["is_ok"] = False 759 | 760 | 761 | def generate_excel_report(results: dict) -> None: 762 | print("\n\t\tGenerating an Excel Report...\n") 763 | ABC = [None, "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"] 764 | workbook = Workbook() 765 | sheet = workbook.active 766 | # 767 | # Intro tab 768 | sheet.title = "Introduction" 769 | sheet["B3"] = "Introduction to the Meraki health check report" 770 | sheet[ 771 | "B5" 772 | ] = "Cisco Meraki is an amazing cloud-managed IT solution, simplifying network, security, security cameras, and IoT infrastructure." 773 | sheet[ 774 | "B6" 775 | ] = "However, even the most intelligent AI/ML-driven solution is still vulnerable to users misconfiguring various options (usually without reading the documentation)." 776 | sheet[ 777 | "B7" 778 | ] = "Misconfiguration can result in an outage, or poor user experience (if you will limit user's traffic to 1Mbps - things will work slowly.. AI won't help there as it's the admin's 'intent')." 779 | sheet[ 780 | "B9" 781 | ] = "This report is presenting the alignment between your Meraki networks' state and configuration with Meraki best practices, " 782 | sheet[ 783 | "B10" 784 | ] = "and a set of thresholds I have selected based on my personal experience." 785 | sheet["B12"] = "In the report you will find the following tabs:" 786 | sheet[ 787 | "B13" 788 | ] = "1. Summary - This tab presents a summary of the results of the health check." 789 | sheet[ 790 | "B14" 791 | ] = "2. Network Health Alerts - This tab presents the dashboard alerts from all networks in a single view." 792 | sheet[ 793 | "B15" 794 | ] = f"3. Network Health - This tab presents the Channel Utilization for every wireless AP. We will examine only the 5GHz spectrum; If you are using the 2.4GHz spectrum - it's beyond saving..." 795 | sheet[ 796 | "C16" 797 | ] = f"The threshold is set to {thresholds['5G Channel Utilization']}%. APs with utilization above this threshold for many occurrences (10+) may be experiencing RF issues." 798 | sheet[ 799 | "B17" 800 | ] = "4. Firmware Upgrade - This tab presents the firmware status for every network. Highlighting networks that require a firmware upgrade." 801 | sheet[ 802 | "B18" 803 | ] = "5. RF profiles - This tab presents the RF profiles for every network." 804 | sheet[ 805 | "C19" 806 | ] = f"Minimum Tx power: Setting the minimum Tx power too high, might result in wireless APs interfering with each other, as they are not allowed to decrease their power. The threshold is set to {thresholds['5G Min TX Power']} dBm." 807 | sheet[ 808 | "C20" 809 | ] = f"Minimum bitrate: Broadcasts and Multicasts will be sent over the wireless at this speed. The lower the speed - the more airtime is wasted. The threshold is set to {thresholds['5G Min Bitrate']} Mbps." 810 | sheet[ 811 | "C21" 812 | ] = f"Channel Width: Depending on local regulation and wireless AP density, there is a limited number of channels that can be used. In most deployments, channel width of more than {thresholds['5G Max Channel Width']}MHz might cause interference between the wireless APs." 813 | sheet[ 814 | "C22" 815 | ] = f"RX-SOP: This is a fine-tuning network design tool that should be used only after consulting an independent wireless expert or Meraki Support. If it's configured - there should be a good reason for it. More details at: https://documentation.meraki.com/MR/Radio_Settings/Receive_Start_of_Packet_(RX-SOP)" 816 | sheet[ 817 | "B23" 818 | ] = f"6. Switch port counters: This tab presents every switch in every network." 819 | sheet[ 820 | "C24" 821 | ] = f"Ports with CRC errors: We do not expect to see any CRC errors on our network, ports with more than 0 CRC errors will appear here." 822 | sheet[ 823 | "C25" 824 | ] = f"Ports with collisions: It's 2022.. we shouldn't be seeing hubs or collisions on our network. Ports with more than 0 collisions will appear here." 825 | sheet[ 826 | "C26" 827 | ] = f"Multicasts exceeding threshold: Multicast traffic may be legitimate, we're highlighting ports with more than {thresholds['multicast_rate']} multicasts per second for visibility (and making sure they are legitimate)." 828 | sheet[ 829 | "C27" 830 | ] = f"Broadcasts exceeding threshold: Broadcasts above a certain threshold should be looked at, we're highlighting ports with more than {thresholds['broadcast_rate']} broadcasts per second for visibility (and making sure they are legitimate)." 831 | sheet[ 832 | "C28" 833 | ] = f"Topology changes exceeding threshold: TCN means something has changed in the STP topology. We're highlighting ports with more than {thresholds['topology_changes']} topology changes for visibility (and making sure they are legitimate)." 834 | sheet[ 835 | "B29" 836 | ] = f"7. Organization Settings - This tab presents the organization settings." 837 | sheet[ 838 | "C30" 839 | ] = f"Multiple admins: We're looking for a single admin with full rights. If you see more than one admin with full rights - it's recommended to have at least one admin with full rights." 840 | sheet[ 841 | "C31" 842 | ] = f"2FA: Two Factor Authentication is an important security mechanism, highly recommended for securing your admin accounts." 843 | sheet[ 844 | "C32" 845 | ] = f"API access: presenting which admin users are using the Dashboard API and whether they are using the v0 API which is being deprecated." 846 | # 847 | # Summary tab 848 | workbook.create_sheet("Summary") 849 | sheet = workbook["Summary"] 850 | sheet["A1"] = "Organization Name" 851 | sheet["B1"] = "Network Name" 852 | sheet["C1"] = "Test Name" 853 | sheet["D1"] = "Test Result" 854 | # 855 | sheet["A2"] = org_name 856 | sheet["B2"] = "N/A" 857 | sheet["C2"] = "Organization Settings" 858 | if results["org_settings"]["is_ok"] == True: 859 | sheet["D2"] = "OK" 860 | else: 861 | sheet["D2"] = "Fail" 862 | sheet["D2"].font = Font(bold=True, color="00FF0000") 863 | # 864 | line = 3 865 | for network in results: 866 | if network == "org_settings": 867 | continue 868 | for test_name in results[network]: 869 | sheet[f"A{line}"] = org_name 870 | sheet[f"B{line}"] = network 871 | sheet[f"C{line}"] = test_name 872 | if results[network][test_name]["is_ok"]: 873 | sheet[f"D{line}"] = "Pass" 874 | else: 875 | sheet[f"D{line}"] = "Fail" 876 | sheet[f"D{line}"].font = Font(bold=True, color="00FF0000") 877 | line += 1 878 | # 879 | # Adding filters 880 | sheet.auto_filter.ref = f"A1:{ABC[sheet.max_column]}{line}" 881 | sheet.auto_filter.add_filter_column(0, ["Test Result"]) 882 | # 883 | # Organization Admin tab 884 | workbook.create_sheet("Organization Admin") 885 | sheet = workbook["Organization Admin"] 886 | sheet["A1"] = "Organization Name" 887 | sheet["A2"] = org_name 888 | sheet["B1"] = "2+ admins" 889 | if results["org_settings"]["more_than_one_admin"]: 890 | sheet["B2"] = "Yes" 891 | else: 892 | sheet["B2"] = "No" 893 | sheet["B2"].font = Font(bold=True, color="00FF0000") 894 | sheet["C1"] = "Admins missing 2FA" 895 | sheet["C2"] = ( 896 | str(results["org_settings"]["missing_2fa"]) 897 | if results["org_settings"]["missing_2fa"] != [] 898 | else "" 899 | ) 900 | sheet["C2"].font = Font(bold=True, color="00FF0000") 901 | sheet["D1"] = "API Calls (last 7 days)" 902 | sheet["D2"] = results["org_settings"]["api_calls"] 903 | sheet["E1"] = "Using API v0 ?" 904 | if results["org_settings"]["using_v0"]: 905 | sheet["E2"] = "Yes" 906 | sheet["E2"].font = Font(bold=True, color="00FF0000") 907 | else: 908 | sheet["E2"] = "No" 909 | # 910 | sheet["A5"] = "Organization Name" 911 | sheet["B5"] = "Admin Name" 912 | sheet["C5"] = "Admin Email" 913 | sheet["D5"] = "2FA enablement" 914 | sheet["E5"] = "API Calls (last 7 days)" 915 | sheet["F5"] = "Using API v0" 916 | line = 6 917 | for admin in results["org_settings"]["users"]: 918 | sheet[f"A{line}"] = org_name 919 | sheet[f"B{line}"] = results["org_settings"]["users"][admin]["name"] 920 | sheet[f"C{line}"] = results["org_settings"]["users"][admin]["email"] 921 | if results["org_settings"]["users"][admin]["2fa"]: 922 | sheet[f"D{line}"] = "Yes" 923 | else: 924 | sheet[f"D{line}"] = "No" 925 | sheet[f"D{line}"].font = Font(bold=True, color="00FF0000") 926 | sheet[f"E{line}"] = results["org_settings"]["users"][admin]["api_calls"] 927 | if results["org_settings"]["users"][admin]["using_v0"]: 928 | sheet[f"F{line}"] = "Yes" 929 | sheet[f"F{line}"].font = Font(bold=True, color="00FF0000") 930 | else: 931 | sheet[f"F{line}"] = "No" 932 | line += 1 933 | # 934 | # Adding filters 935 | sheet.auto_filter.ref = f"A5:{ABC[sheet.max_column]}{line}" 936 | sheet.auto_filter.add_filter_column(0, ["Admin Name"]) 937 | # 938 | # Network Health Alerts tab 939 | workbook.create_sheet("Network Health Alerts") 940 | sheet = workbook["Network Health Alerts"] 941 | sheet["A1"] = "Organization Name" 942 | sheet["B1"] = "Network Name" 943 | sheet["C1"] = "Severity" 944 | sheet["D1"] = "Category" 945 | sheet["E1"] = "Type" 946 | sheet["F1"] = "Details" 947 | line = 2 948 | # 949 | for network in results: 950 | if network == "org_settings": 951 | continue 952 | if results[network]["network_health_alerts"]["is_ok"] == True: 953 | pass 954 | else: 955 | for alert in results[network]["network_health_alerts"]["alert_list"]: 956 | sheet[f"A{line}"] = org_name 957 | sheet[f"B{line}"] = network 958 | sheet[f"C{line}"] = alert["severity"] 959 | sheet[f"D{line}"] = alert["category"] 960 | sheet[f"E{line}"] = alert["type"] 961 | sheet[f"F{line}"] = str(alert["details"]) 962 | if alert["severity"] == "critical": 963 | for cell in sheet[line:line]: 964 | cell.font = Font(bold=True, color="00FF0000") 965 | elif alert["severity"] == "warning": 966 | for cell in sheet[line:line]: 967 | cell.font = Font(bold=True, color="00FF9900") 968 | line += 1 969 | # 970 | # Adding filters 971 | sheet.auto_filter.ref = f"A1:{ABC[sheet.max_column]}{line}" 972 | sheet.auto_filter.add_filter_column(0, ["Network Name"]) 973 | # 974 | # Network Firmware tab 975 | workbook.create_sheet("Network Firmware") 976 | sheet = workbook["Network Firmware"] 977 | sheet["A1"] = "Organization Name" 978 | sheet["B1"] = "Network Name" 979 | sheet["C1"] = "Product Catagory" 980 | sheet["D1"] = "Current Version" 981 | sheet["E1"] = "Latest Stable Version" 982 | sheet["F1"] = "Scheduled Update" 983 | line = 2 984 | # 985 | for network in results: 986 | if "network_firmware_check" in results[network].keys(): 987 | for product in results[network]["network_firmware_check"]: 988 | if product == "is_ok": # skipping the is_ok key 989 | continue 990 | sheet[f"A{line}"] = org_name 991 | sheet[f"B{line}"] = network 992 | sheet[f"C{line}"] = product 993 | sheet[f"D{line}"] = results[network]["network_firmware_check"][product][ 994 | "current_version" 995 | ] 996 | sheet[f"E{line}"] = results[network]["network_firmware_check"][product][ 997 | "latest_stable_version" 998 | ] 999 | sheet[f"F{line}"] = results[network]["network_firmware_check"][product][ 1000 | "scheduled_upgrade" 1001 | ] 1002 | if ( 1003 | results[network]["network_firmware_check"][product][ 1004 | "current_version" 1005 | ] 1006 | != results[network]["network_firmware_check"][product][ 1007 | "latest_stable_version" 1008 | ] 1009 | ): 1010 | for cell in sheet[line:line]: 1011 | cell.font = Font(bold=True, color="00FF9900") 1012 | line += 1 1013 | # 1014 | # Adding filters 1015 | sheet.auto_filter.ref = f"A1:{ABC[sheet.max_column]}{line}" 1016 | sheet.auto_filter.add_filter_column(0, ["Network Name"]) 1017 | # 1018 | # Channel Utilization tab 1019 | workbook.create_sheet("Channel Utilization") 1020 | sheet = workbook["Channel Utilization"] 1021 | sheet["A1"] = "Organization Name" 1022 | sheet["B1"] = "Network Name" 1023 | sheet["C1"] = "AP Name" 1024 | sheet["D1"] = "Result" 1025 | sheet["E1"] = "Max Utilization" 1026 | sheet["F1"] = "Occurances" 1027 | line = 2 1028 | # 1029 | for network in results: 1030 | if "channel_utilization_check" in results[network].keys(): 1031 | for ap in results[network]["channel_utilization_check"]: 1032 | if ap == "is_ok": # skipping the is_ok key 1033 | continue 1034 | sheet[f"A{line}"] = org_name 1035 | sheet[f"B{line}"] = network 1036 | sheet[f"C{line}"] = results[network]["channel_utilization_check"][ap][ 1037 | "name" 1038 | ] 1039 | if ( 1040 | results[network]["channel_utilization_check"][ap]["occurances"] 1041 | < thresholds["5G Occurances Warning"] 1042 | ): 1043 | sheet[f"D{line}"] = "Pass" 1044 | elif ( 1045 | results[network]["channel_utilization_check"][ap]["occurances"] 1046 | < thresholds["5G Occurances Alarm"] 1047 | ): 1048 | sheet[f"D{line}"] = "Fail" 1049 | sheet[f"D{line}"].font = Font(bold=True, color="00FF6600") 1050 | sheet[f"E{line}"].font = Font(bold=True, color="00FF6600") 1051 | else: 1052 | sheet[f"D{line}"] = "Fail" 1053 | sheet[f"D{line}"].font = Font(bold=True, color="00FF0000") 1054 | sheet[f"E{line}"].font = Font(bold=True, color="00FF0000") 1055 | sheet[f"E{line}"] = results[network]["channel_utilization_check"][ap][ 1056 | "utilization" 1057 | ] 1058 | sheet[f"F{line}"] = results[network]["channel_utilization_check"][ap][ 1059 | "occurances" 1060 | ] 1061 | sheet["G1"] = "Speed" 1062 | sheet["H1"] = "Duplex" 1063 | sheet["I1"] = "Power" 1064 | speed = results[network]["channel_utilization_check"][ap]["speed"] 1065 | sheet[f"G{line}"] = speed 1066 | if speed == None: 1067 | sheet[f"G{line}"].font = Font(bold=True, color="00FF0000") 1068 | elif speed < 1000: 1069 | sheet[f"G{line}"].font = Font(bold=True, color="00FF0000") 1070 | duplex = results[network]["channel_utilization_check"][ap]["duplex"] 1071 | sheet[f"H{line}"] = duplex 1072 | if duplex != "full": 1073 | sheet[f"H{line}"].font = Font(bold=True, color="00FF0000") 1074 | power = results[network]["channel_utilization_check"][ap]["power"] 1075 | sheet[f"I{line}"] = power 1076 | if power != 'full': 1077 | sheet[f"I{line}"].font = Font(bold=True, color="00FF0000") 1078 | line += 1 1079 | # 1080 | # Adding filters 1081 | sheet.auto_filter.ref = f"A1:{ABC[sheet.max_column]}{line}" 1082 | sheet.auto_filter.add_filter_column(0, ["Max Utilization"]) 1083 | # 1084 | # RF Profile tab 1085 | workbook.create_sheet("RF Profiles") 1086 | sheet = workbook["RF Profiles"] 1087 | sheet["A1"] = "Organization Name" 1088 | sheet["B1"] = "Network Name" 1089 | sheet["C1"] = "RF Profile" 1090 | sheet["D1"] = "Result" 1091 | sheet["E1"] = "Minimum TX Power" 1092 | sheet["F1"] = "Minimum Bit Rate" 1093 | sheet["G1"] = "Channel Width" 1094 | sheet["H1"] = "RX-SOP" 1095 | line = 2 1096 | # 1097 | for network in results: 1098 | if "rf_profiles_check" in results[network].keys(): 1099 | for profile in results[network]["rf_profiles_check"]: 1100 | if profile == "is_ok": # skipping the is_ok key 1101 | continue 1102 | sheet[f"A{line}"] = org_name 1103 | sheet[f"B{line}"] = network 1104 | sheet[f"C{line}"] = profile 1105 | if results[network]["rf_profiles_check"][profile]["is_ok"]: 1106 | sheet[f"D{line}"] = "Pass" 1107 | else: 1108 | sheet[f"D{line}"] = "Fail" 1109 | sheet[f"D{line}"].font = Font(bold=True, color="00FF0000") 1110 | sheet[f"E{line}"] = results[network]["rf_profiles_check"][profile][ 1111 | "tests" 1112 | ]["min_power"]["value"] 1113 | if ( 1114 | results[network]["rf_profiles_check"][profile]["tests"][ 1115 | "min_power" 1116 | ]["is_ok"] 1117 | == False 1118 | ): 1119 | sheet[f"E{line}"].font = Font(bold=True, color="00FF0000") 1120 | # 1121 | sheet[f"F{line}"] = results[network]["rf_profiles_check"][profile][ 1122 | "tests" 1123 | ]["min_bitrate"]["value"] 1124 | if ( 1125 | results[network]["rf_profiles_check"][profile]["tests"][ 1126 | "min_bitrate" 1127 | ]["is_ok"] 1128 | == False 1129 | ): 1130 | sheet[f"F{line}"].font = Font(bold=True, color="00FF0000") 1131 | # 1132 | sheet[f"G{line}"] = results[network]["rf_profiles_check"][profile][ 1133 | "tests" 1134 | ]["channel_width"]["value"] 1135 | if ( 1136 | results[network]["rf_profiles_check"][profile]["tests"][ 1137 | "channel_width" 1138 | ]["is_ok"] 1139 | == False 1140 | ): 1141 | sheet[f"G{line}"].font = Font(bold=True, color="00FF0000") 1142 | # 1143 | sheet[f"H{line}"] = results[network]["rf_profiles_check"][profile][ 1144 | "tests" 1145 | ]["rxsop"]["value"] 1146 | if ( 1147 | results[network]["rf_profiles_check"][profile]["tests"]["rxsop"][ 1148 | "is_ok" 1149 | ] 1150 | == False 1151 | ): 1152 | sheet[f"H{line}"].font = Font(bold=True, color="00FF0000") 1153 | line += 1 1154 | # 1155 | # Adding filters 1156 | sheet.auto_filter.ref = f"A1:{ABC[sheet.max_column]}{line}" 1157 | sheet.auto_filter.add_filter_column(0, ["Result"]) 1158 | # 1159 | # Switch ports tab 1160 | workbook.create_sheet("Switch port counters") 1161 | sheet = workbook["Switch port counters"] 1162 | sheet["A1"] = "Organization Name" 1163 | sheet["B1"] = "Network Name" 1164 | sheet["C1"] = "Switch" 1165 | sheet["D1"] = "Result" 1166 | sheet["E1"] = "Ports with CRC errors" 1167 | sheet["F1"] = "Ports with collisions" 1168 | sheet["G1"] = "Multicasts exceeding threshold" 1169 | sheet["H1"] = "Broadcasts exceeding threshold" 1170 | sheet["I1"] = "Topology changes exceeding threshold" 1171 | line = 2 1172 | # 1173 | for network in results: 1174 | if "port_counters_check" in results[network].keys(): 1175 | for switch in results[network]["port_counters_check"]: 1176 | if switch == "is_ok": # skipping the is_ok key 1177 | continue 1178 | sheet[f"A{line}"] = org_name 1179 | sheet[f"B{line}"] = network 1180 | sheet[f"C{line}"] = switch 1181 | if results[network]["port_counters_check"][switch]["is_ok"]: 1182 | sheet[f"D{line}"] = "Pass" 1183 | else: 1184 | sheet[f"D{line}"] = "Fail" 1185 | sheet[f"D{line}"].font = Font(bold=True, color="00FF0000") 1186 | if results[network]["port_counters_check"][switch]["crc"] != []: 1187 | sheet[f"E{line}"] = str( 1188 | results[network]["port_counters_check"][switch]["crc"] 1189 | ) 1190 | sheet[f"E{line}"].font = Font(bold=True, color="00FF0000") 1191 | if results[network]["port_counters_check"][switch]["collision"] != []: 1192 | sheet[f"F{line}"] = str( 1193 | results[network]["port_counters_check"][switch]["collision"] 1194 | ) 1195 | sheet[f"F{line}"].font = Font(bold=True, color="00FF0000") 1196 | if results[network]["port_counters_check"][switch]["multicast"] != []: 1197 | sheet[f"G{line}"] = str( 1198 | results[network]["port_counters_check"][switch]["multicast"] 1199 | ) 1200 | sheet[f"G{line}"].font = Font(bold=True, color="00FF0000") 1201 | if results[network]["port_counters_check"][switch]["broadcast"] != []: 1202 | sheet[f"H{line}"] = str( 1203 | results[network]["port_counters_check"][switch]["broadcast"] 1204 | ) 1205 | sheet[f"H{line}"].font = Font(bold=True, color="00FF0000") 1206 | if ( 1207 | results[network]["port_counters_check"][switch]["topology_changes"] 1208 | != [] 1209 | ): 1210 | sheet[f"I{line}"] = str( 1211 | results[network]["port_counters_check"][switch][ 1212 | "topology_changes" 1213 | ] 1214 | ) 1215 | sheet[f"I{line}"].font = Font(bold=True, color="00FF0000") 1216 | line += 1 1217 | # 1218 | # Adding filters 1219 | sheet.auto_filter.ref = f"A1:{ABC[sheet.max_column]}{line}" 1220 | sheet.auto_filter.add_filter_column(0, ["Result"]) 1221 | # 1222 | # Formatting: increasing font size, adjusting column width 1223 | for sheet_name in workbook.sheetnames: 1224 | sheet = workbook[sheet_name] 1225 | for column in sheet.columns: 1226 | max_length = 0 1227 | for cell in column: 1228 | cell.font = Font(size=16, bold=cell.font.bold, color=cell.font.color) 1229 | if len(str(cell.value)) > max_length: 1230 | max_length = len(str(cell.value)) 1231 | if not "Introduction" in sheet_name: 1232 | sheet.column_dimensions[column[0].column_letter].width = max_length + 10 1233 | # 1234 | sheet = workbook["Introduction"] 1235 | sheet["B3"].font = Font(bold=True, size=36) 1236 | # 1237 | workbook.save(filename=f"{org_name}.xlsx") 1238 | 1239 | 1240 | async def main(): 1241 | async with meraki.aio.AsyncDashboardAPI( 1242 | output_log=False, 1243 | suppress_logging=True, 1244 | maximum_concurrent_requests=5, 1245 | wait_on_rate_limit=True, 1246 | nginx_429_retry_wait_time=2, 1247 | maximum_retries=100, 1248 | caller='Healthcheck/22.12 OBrigg' 1249 | ) as aiomeraki: 1250 | # 1251 | # Checking the organization has API enabled 1252 | try: 1253 | dashboard.organizations.getOrganizationNetworks(org_id) 1254 | except meraki.APIError as e: 1255 | pp(f"[red]An error has occured: \n\n{e}[/red]\n\n") 1256 | sys.exit(1) 1257 | # 1258 | # Run organization checks 1259 | await async_check_org_admins(aiomeraki) 1260 | # Get networks 1261 | try: 1262 | networks = await aiomeraki.organizations.getOrganizationNetworks(org_id, perPage=1000, total_pages=20) 1263 | except meraki.exceptions.AsyncAPIError as e: 1264 | pp( 1265 | f'[bold magenta]Meraki AIO API Error (OrgID "{ org_id }", OrgName "{ org_name }"): \n { e }' 1266 | ) 1267 | except Exception as e: 1268 | pp(f"[bold magenta]Some other ERROR: {e}") 1269 | 1270 | # Prepare results per network 1271 | for network in networks: 1272 | results[network["name"]] = {} 1273 | # 1274 | # General checks 1275 | check_network_health_tasks = [ 1276 | async_check_network_health_alerts(aiomeraki, net) for net in networks 1277 | ] 1278 | for task in asyncio.as_completed(check_network_health_tasks): 1279 | await task 1280 | check_firmware_tasks = [ 1281 | async_check_network_firmware(aiomeraki, net) for net in networks 1282 | ] 1283 | for task in asyncio.as_completed(check_firmware_tasks): 1284 | await task 1285 | # 1286 | # Wired checks 1287 | check_switch_port_tasks = [ 1288 | async_check_switches_port_counters(aiomeraki, net) 1289 | for net in networks 1290 | if "switch" in net["productTypes"] 1291 | ] 1292 | for task in asyncio.as_completed(check_switch_port_tasks): 1293 | await task 1294 | check_switch_stp_tasks = [ 1295 | async_check_switch_stp(aiomeraki, net) 1296 | for net in networks 1297 | if "switch" in net["productTypes"] 1298 | ] 1299 | for task in asyncio.as_completed(check_switch_stp_tasks): 1300 | await task 1301 | check_switch_mtu_tasks = [ 1302 | async_check_switch_mtu(aiomeraki, net) 1303 | for net in networks 1304 | if "switch" in net["productTypes"] 1305 | ] 1306 | for task in asyncio.as_completed(check_switch_mtu_tasks): 1307 | await task 1308 | check_switch_storm_control_tasks = [ 1309 | async_check_switch_storm_control(aiomeraki, net) 1310 | for net in networks 1311 | if "switch" in net["productTypes"] 1312 | ] 1313 | for task in asyncio.as_completed(check_switch_storm_control_tasks): 1314 | await task 1315 | # 1316 | # Wireless checks 1317 | check_channel_utilization_tasks = [ 1318 | async_check_wifi_channel_utilization(aiomeraki, net) 1319 | for net in networks 1320 | if "wireless" in net["productTypes"] 1321 | ] 1322 | for task in asyncio.as_completed(check_channel_utilization_tasks): 1323 | await task 1324 | check_rf_profiles_tasks = [ 1325 | async_check_wifi_rf_profiles(aiomeraki, net) 1326 | for net in networks 1327 | if "wireless" in net["productTypes"] 1328 | ] 1329 | for task in asyncio.as_completed(check_rf_profiles_tasks): 1330 | await task 1331 | check_ssid_amount_tasks = [ 1332 | async_check_wifi_ssid_amount(aiomeraki, net) 1333 | for net in networks 1334 | if "wireless" in net["productTypes"] 1335 | ] 1336 | for task in asyncio.as_completed(check_ssid_amount_tasks): 1337 | await task 1338 | # 1339 | check_wireless_ports(networks) 1340 | # TODO: wireless health 1341 | # 1342 | pp("\n", 100 * "*", "\n") 1343 | # Results cleanup 1344 | clean_results = {} 1345 | for result in results: 1346 | if results[result] != {}: 1347 | clean_results[result] = results[result] 1348 | 1349 | pp(clean_results) 1350 | generate_excel_report(clean_results) 1351 | pp("Done.") 1352 | 1353 | 1354 | if __name__ == "__main__": 1355 | # Thresholds 1356 | thresholds = { 1357 | "5G Channel Utilization": 20, # % 1358 | "5G Occurances Warning": 10, # times 1359 | "5G Occurances Alarm": 50, # times 1360 | "5G Min TX Power": 10, # dBm 1361 | "5G Min Bitrate": 12, # Mbps 1362 | "5G Max Channel Width": 40, # MHz 1363 | "broadcast_rate": 100, # pps 1364 | "multicast_rate": 100, # pps 1365 | "topology_changes": 10, 1366 | "ssid_amount": 4, 1367 | } 1368 | results = {} 1369 | 1370 | # Check for an envriomnet variable, if not set, ask for an API key 1371 | if os.environ.get("MERAKI_DASHBOARD_API_KEY"): 1372 | api_key = os.environ["MERAKI_DASHBOARD_API_KEY"] 1373 | else: 1374 | pp( 1375 | "[bold magenta]No API key found. Please enter your Meraki Dashboard API key:" 1376 | ) 1377 | api_key = getpass("Meraki Dashboard API Key: ") 1378 | os.environ["MERAKI_DASHBOARD_API_KEY"] = api_key 1379 | # Initializing Meraki SDK 1380 | dashboard = meraki.DashboardAPI(output_log=False, suppress_logging=True, caller='Healthcheck/22.12 OBrigg') 1381 | org_id, org_name = select_org() 1382 | 1383 | start = time.time() 1384 | loop = asyncio.new_event_loop() 1385 | loop = asyncio.get_event_loop() 1386 | loop.run_until_complete(main()) 1387 | 1388 | end = time.time() 1389 | pp(f"Total run time: {round(end - start, 2)} seconds") 1390 | -------------------------------------------------------------------------------- /example.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obrigg/meraki-health-check/a0da01057994c42a3efea054160910e5d20b487e/example.xlsx -------------------------------------------------------------------------------- /img/api_access.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obrigg/meraki-health-check/a0da01057994c42a3efea054160910e5d20b487e/img/api_access.png -------------------------------------------------------------------------------- /img/health_check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obrigg/meraki-health-check/a0da01057994c42a3efea054160910e5d20b487e/img/health_check.png -------------------------------------------------------------------------------- /img/meraki_health_check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obrigg/meraki-health-check/a0da01057994c42a3efea054160910e5d20b487e/img/meraki_health_check.png -------------------------------------------------------------------------------- /img/my_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obrigg/meraki-health-check/a0da01057994c42a3efea054160910e5d20b487e/img/my_profile.png -------------------------------------------------------------------------------- /img/network-firmware-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obrigg/meraki-health-check/a0da01057994c42a3efea054160910e5d20b487e/img/network-firmware-sample.png -------------------------------------------------------------------------------- /img/org_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obrigg/meraki-health-check/a0da01057994c42a3efea054160910e5d20b487e/img/org_settings.png -------------------------------------------------------------------------------- /img/rf_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obrigg/meraki-health-check/a0da01057994c42a3efea054160910e5d20b487e/img/rf_profile.png -------------------------------------------------------------------------------- /img/rf_utilization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obrigg/meraki-health-check/a0da01057994c42a3efea054160910e5d20b487e/img/rf_utilization.png -------------------------------------------------------------------------------- /img/summary-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obrigg/meraki-health-check/a0da01057994c42a3efea054160910e5d20b487e/img/summary-sample.png -------------------------------------------------------------------------------- /img/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obrigg/meraki-health-check/a0da01057994c42a3efea054160910e5d20b487e/img/summary.png -------------------------------------------------------------------------------- /img/switch_port_counters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/obrigg/meraki-health-check/a0da01057994c42a3efea054160910e5d20b487e/img/switch_port_counters.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | meraki>=1.20 2 | rich>=12.3.0 3 | openpyxl>=3.0.9 --------------------------------------------------------------------------------