diff --git a/README.md b/README.md index 2f7de84..d899d0b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Meraki EOL Manager + Obtain a lifecycle report from all of your Cisco Meraki organizations, detailing how many of your devices have EoL announcements published. # Table of Contents @@ -36,7 +37,12 @@ This script allows you to obtain a report of all of the End of Sales and End of ## How to Use 1. Clone repo to your working directory -2. Edit `config.py` with your API Key in between the quotation marks next to `api_key = ` +2. Edit `config.py` + - Set your API Key in between the quotation marks next to `api_key =` + - Set `html_output` to `TRUE` if you would like an HTML and PDF report + - Set `output_log` to `TRUE` if you would like an output file from all Meraki API calls + - Set `print_console` to `TRUE` if you would like to see Meraki API output on the screen during script run. + 3. Run `pip install -r requirements.txt` from your terminal 4. Run the script `python main.py` 5. You will be prompted with a list of the organizations your API Key has access to diff --git a/config.py b/config.py index 9ce29e4..86546a0 100644 --- a/config.py +++ b/config.py @@ -1 +1,4 @@ -api_key = 'MERAKI_API_KEY_HERE' +api_key = "MERAKI_API_KEY_HERE" +html_output = False # Set to True to output to HTML and PDF +output_log = False # Set to True to output to log file +print_console = False # Set to True to enable console output diff --git a/main.py b/main.py index 24aac5a..0c9b755 100644 --- a/main.py +++ b/main.py @@ -1,114 +1,141 @@ -import pandas as pd +import logging +import sys + import meraki +import pandas as pd import requests from bs4 import BeautifulSoup +from PyQt5 import QtWebEngineWidgets, QtWidgets + import config -import sys -import pathlib -from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets + +logging.basicConfig(filename="meraki_lifecycle.log", level=logging.INFO) +logger = logging.getLogger(__name__) api_key = config.api_key + def fetch_eol_data(): - url = 'https://documentation.meraki.com/General_Administration/Other_Topics/Meraki_End-of-Life_(EOL)_Products_and_Dates' + url = "https://documentation.meraki.com/General_Administration/Other_Topics/Meraki_End-of-Life_(EOL)_Products_and_Dates" dfs = pd.read_html(url) requested_url = requests.get(url) - soup = BeautifulSoup(requested_url.text, 'html.parser') - table = soup.find('table') + soup = BeautifulSoup(requested_url.text, "html.parser") + table = soup.find("table") links = [] - for row in table.find_all('tr'): - for td in row.find_all('td'): + for row in table.find_all("tr"): + for td in row.find_all("td"): sublinks = [] - if td.find_all('a'): - for a in td.find_all('a'): + if td.find_all("a"): + for a in td.find_all("a"): sublinks.append(str(a)) links.append(sublinks) eol_df = dfs[0] - eol_df['Upgrade Path'] = links + eol_df["Upgrade Path"] = links return eol_df + def get_inventory(dashboard, org_list): inventory_list = [] for org in org_list: - org_name = org['name'] - org_id = org['id'] - print(f"\nFetching networks and devices for organization: {org_name} (ID: {org_id})") + org_name = org["name"] + org_id = org["id"] + print( + f"\nFetching networks and devices for organization: {org_name} (ID: {org_id})" + ) try: # Fetch networks networks = dashboard.organizations.getOrganizationNetworks(org_id) - print(f"Networks found: {len(networks)}") - print(f"Network IDs: {[net['id'] for net in networks]}") + logger.info(f"Networks found: {len(networks)}") + logger.debug(f"Network IDs: {[net['id'] for net in networks]}") devices = dashboard.organizations.getOrganizationDevices(org_id) - print(f"Devices found: {len(devices)}") + logger.info(f"Devices found: {len(devices)}") if not devices: - print(f"No devices found for organization: {org_name}") + logger.warning(f"No devices found for organization: {org_name}") continue network_map = {net["id"]: net["name"] for net in networks} for device in devices: - device["networkName"] = network_map.get(device.get("networkId"), "Unassigned") + device["networkName"] = network_map.get( + device.get("networkId"), "Unassigned" + ) inventory_list.append({f"{org_name} - {org_id}": devices}) - print(f"Fetched {len(devices)} devices for {org_name}") + logger.info(f"Fetched {len(devices)} devices for {org_name}") except meraki.exceptions.APIError as e: - print(f"Meraki API error for {org_name}: {e}") + logger.error(f"Meraki API error for {org_name}: {e}") except Exception as ex: - print(f"General error for {org_name}: {ex}") - + logger.error(f"General error for {org_name}: {ex}") + return inventory_list + def process_inventory(inventory_list, eol_df): eol_report_list = [] for inventory in inventory_list: for key in inventory.keys(): if not inventory[key]: - print(f"Organization: {key}, Devices Count: 0") + logger.info(f"Organization: {key}, Devices Count: 0") continue print(f"Organization: {key}, Devices Count: {len(inventory[key])}") inventory_df = pd.DataFrame(inventory[key]) if inventory_df.empty: - print(f"Inventory DataFrame for {key} is empty. Skipping...") + logger.info(f"Inventory DataFrame for {key} is empty. Skipping...") continue - print(f"Inventory DataFrame for {key}:\n{inventory_df.head()}") + logger.debug(f"Inventory DataFrame for {key}:\n{inventory_df.head()}") - inventory_unassigned_df = inventory_df.loc[inventory_df['networkId'].isna()].copy() if 'networkId' in inventory_df else pd.DataFrame() - inventory_assigned_df = inventory_df.loc[~inventory_df['networkId'].isna()].copy() if 'networkId' in inventory_df else pd.DataFrame() + inventory_unassigned_df = ( + inventory_df.loc[inventory_df["networkId"].isna()].copy() + if "networkId" in inventory_df + else pd.DataFrame() + ) + inventory_assigned_df = ( + inventory_df.loc[~inventory_df["networkId"].isna()].copy() + if "networkId" in inventory_df + else pd.DataFrame() + ) if inventory_assigned_df.empty: - print(f"No assigned devices found for {key}. Skipping lifecycle processing...") + logger.info( + f"No assigned devices found for {key}. Skipping lifecycle processing..." + ) continue - inventory_assigned_df['lifecycle'] = "" + inventory_assigned_df["lifecycle"] = "" - inventory_assigned_df['model'].isin(eol_df['Product']).astype(int) + inventory_assigned_df["model"].isin(eol_df["Product"]).astype(int) eol_report = eol_df.copy() - eol_report['Total Units'] = eol_report['Product'].map(inventory_assigned_df['model'].value_counts()) - eol_report['Total Units'] = eol_report['Total Units'].fillna(0).astype(int) - eol_report = eol_report[eol_report['Total Units'] > 0] - eol_report = eol_report.sort_values(by=["Total Units"], ascending=False).reset_index(drop=True) + eol_report["Total Units"] = eol_report["Product"].map( + inventory_assigned_df["model"].value_counts() + ) + eol_report["Total Units"] = eol_report["Total Units"].fillna(0).astype(int) + eol_report = eol_report[eol_report["Total Units"] > 0] + eol_report = eol_report.sort_values( + by=["Total Units"], ascending=False + ).reset_index(drop=True) eol_report_list.append({"name": key, "report": eol_report}) - + return eol_report_list + def generate_html(eol_report_list): - page_title_text = 'Cisco Meraki Lifecycle Report' - title_text = 'Cisco Meraki Lifecycle Report' - text = ''' + page_title_text = "Cisco Meraki Lifecycle Report" + title_text = "Cisco Meraki Lifecycle Report" + text = """ This report lists all of your equipment currently in use that has an end of life announcement. They are ordered by the total units column, and the Upgrade Path column links you to the EoS announcement with recommendations on upgrade paths. -''' +""" - html = f''' + html = f"""