From fb939227b35c107f8b6b5b4b12bf061d92241dd9 Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 24 Jan 2025 07:14:28 -0500 Subject: [PATCH 1/2] remove meraki logs, move to python logging and add ability to choose all orgs --- config.py | 5 +- main.py | 150 +++++++++++++++++++++++++++++++++++------------------- 2 files changed, 101 insertions(+), 54 deletions(-) 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"""