├── .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 | [](https://developer.cisco.com/devenv/?id=devenv-vscode-base&GITHUB_SOURCE_REPO=https://github.com/obrigg/meraki-health-check) [](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 | [](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 | 
159 | 
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
--------------------------------------------------------------------------------