diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5bc272d..432522e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,6 +15,6 @@ repos: args: ["--profile", "black"] name: isort (python) - repo: https://github.com/asottile/pyupgrade - rev: v2.21.2 + rev: v2.22.0 hooks: - id: pyupgrade diff --git a/README.md b/README.md index be3e8c2..122b1b6 100644 --- a/README.md +++ b/README.md @@ -76,40 +76,35 @@ $ pip install -r requirements.txt --user path_to\subnetting> python main.py ``` -**macOS or Linux** +**macOS or Unix** ```bash $ python3 main.py ``` -You will be prompted to enter the name of the CSV file containing input subnets, the gateway IP address, a name for the Excel file to be created, and the name of the sheet within the Excel file. _(All inputs have default values)_. +You will be prompted to enter the name of the CSV file containing input subnets, the gateway, a name for the Excel file to be created. _(All inputs have default values)_. > A `subnets.csv` file can be found in the repo. This file is an entry point to get started using this program. It's prepopulated with three different subnets. _(Class A, B, and C)_. ```bash -- CSV file w/ extension? [Defaults to subnets.csv]: -- The gateway, first or last IP Address? [0/1] [Defaults to 0]: -- Excel file w/o extension? [Defaults to IP-Schema]: Test-Schema -- Worksheet name? [Defaults to IP Schema Worksheet]: Test Worksheet +- CSV file [subnets.csv]: +- The gateway, first or last IP Address [0/1] [0]: +- Excel file to create [New-Schema.xlsx]: ``` -> - Abbreviations:
- **w/: With**
- **w/o: Without** - Voila :sparkles: You have an Excel file that includes all required data about each subnet. ```bash -Please check Test-Schema_.xlsx in current working directory. +Please check New-Schema_.xlsx in current working directory. ``` > **Default behaviors:** -> 1. CIDR notation with no prefix length will be handled as /32.
- For example, if you enter `10.0.0.0` without a prefix length in the CSV file, the script will handle it like `10.0.0.0/32`. +> 1. CIDR notation with no prefix length will be handled as /32.
- For example, if you enter `10.0.0.1` without a prefix length in the CSV file, the script will handle it like `10.0.0.1/32`. > 2. The header line **`Subnets in CIDR Notation`** within the `subnets.csv` file is automatically skipped. So, there is no need to manually remove it. -> 3. Gateway input accepts 0 or 1 **ONLY** [Defaults to 0]. 0 picks the first IP address of the subnet, while 1 picks the last IP address. - -> 4. Microsoft Excel does not allow worksheet name longer than 31 characters. Worksheet names longer than 31 chars will be truncated. +> 3. The gateway input accepts 0 or 1 **ONLY** [Defaults to 0]. 0 picks the first IP address of the subnet, while 1 picks the last IP address. --- @@ -120,10 +115,6 @@ Finally, if you have a L3 switch and you want to create [SVI interfaces](https:/ ```bash $ python parse_excel.py --file .xlsx ``` -**OR** -``` -$ python parse_excel.py -f .xlsx -``` This Python script will generate a configuration file that includes all VLANs and their SVI interfaces. @@ -135,6 +126,7 @@ This Python script will generate a configuration file that includes all VLANs an **Terminal** ![Python CLI](assets/subnetting-cli.png) +_Elapsed time is about 9 seconds in here because a CIDR notation like 10.0.0.0/8 is a little bit extensive to process._ **CSV File (Input File)** ![CSV File](assets/subnets-csv.png) @@ -142,7 +134,7 @@ This Python script will generate a configuration file that includes all VLANs an **Excel File (Output File)** ![Excel Preview](assets/preview.png) -**python parse_excel.py --file .xlsx** +**python parse_excel.py -f .xlsx** ![SVI CLI](assets/svi.png) **SVI Template** diff --git a/assets/subnetting-cli.png b/assets/subnetting-cli.png index 3fc88af..a5237d8 100644 Binary files a/assets/subnetting-cli.png and b/assets/subnetting-cli.png differ diff --git a/export_subnets.py b/export_subnets.py index 9269101..9328297 100644 --- a/export_subnets.py +++ b/export_subnets.py @@ -1,7 +1,7 @@ #!usr/bin/env python3 from datetime import date -from typing import AnyStr, Dict, List +from typing import AnyStr, Dict, List, Optional from termcolor import colored, cprint from xlsxwriter import Workbook @@ -9,26 +9,30 @@ def export_subnets( subnets: List[Dict], - workbook_name: AnyStr = "IP-Schema", - worksheet_name: AnyStr = "IP Schema Worksheet", + workbook_name: Optional[AnyStr] = "New-Schema.xlsx", ): - """Export an Excel file of entered subnets - - Args: - subnets (List[Dict]): Processed subnets - workbook_name (AnyStr, optional): Name of the Excel file. Defaults to "IP-Schema". - worksheet_name (AnyStr, optional): Name of the sheet within the Excel file. Defaults to "IP Schema Worksheet". - - Raises: - SystemExit: TypeError + """Exports an Excel file of subnetting data + + Parameters + ---------- + subnets : List[Dict] + List of subnets went througth subnetting + workbook_name : Optional[AnyStr], optional + Name of Workbook to create, by default "New-Schema.xlsx" + + Raises + ------ + SystemExit + TypeError, KeyError """ - excel_fname = f"{workbook_name}_{date.today()}.xlsx" + wb_name, ext = workbook_name.split(".") + excel_fname = f"{wb_name}_{date.today()}.{ext}" # Create an Excel file with Workbook(filename=excel_fname) as workbook: # Create a sheet within the Excel file - worksheet = workbook.add_worksheet(name=worksheet_name) + worksheet = workbook.add_worksheet(name="Subnetting Results") # Filters worksheet.autofilter("A1:L1") # Freeze top row and 2 most left columns @@ -51,7 +55,7 @@ def export_subnets( } # Header line format - header_line_frmt = workbook.add_format( + h_frmt = workbook.add_format( properties={ "bold": True, "border": True, @@ -62,15 +66,11 @@ def export_subnets( # Create a header line row for cell, value in header_line.items(): - worksheet.write_string(cell, value, cell_format=header_line_frmt) + worksheet.write_string(cell, value, cell_format=h_frmt) # Generic cell format c_frmt = workbook.add_format( - properties={ - "border": True, - "align": "center", - "valign": "vcenter", - } + properties={"border": True, "align": "center", "valign": "vcenter"} ) # Format cell containing number @@ -106,8 +106,6 @@ def export_subnets( # Jump to next row row += 1 - except TypeError as e: - raise SystemExit(colored(f"export_subnets.py: {e}", "red")) - except KeyError as e: - raise SystemExit(colored(f"export_subnets.py: {e}", "red")) - cprint(f"\nPlease check {excel_fname} in the PWD.\n", "green") + except (TypeError, KeyError) as e: + raise SystemExit(colored(text=f"export_subnets.py: {e}", color="red")) + cprint(text=f"\nPlease check {excel_fname} in the PWD.\n", color="green") diff --git a/main.py b/main.py index 8a2f519..fa7e73b 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,10 @@ #!usr/bin/env python3 +import time from getpass import getuser from colorama import init -from termcolor import colored +from termcolor import colored, cprint from export_subnets import export_subnets from read_subnets import read_subnets @@ -15,59 +16,43 @@ def main(): try: # CSV file - input_subnets = ( - input(f"\n- CSV file w/ extension? [Defaults to subnets.csv]: ") - or "subnets.csv" - ) - if ".csv" not in input_subnets: - raise SystemExit( - colored("Sorry! The input file MUST include .csv extension", "red") - ) + input_csv = input(f"\n- CSV file [subnets.csv]: ") or "subnets.csv" gateway = int( - input("- The gateway, first or last IP Address? [0/1] [Defaults to 0]: ") - or "0" + input("- The gateway, first or last IP Address [0/1] [0]: ") or "0" ) if gateway not in (0, 1): raise SystemExit( - colored( - "0 and 1 are the only allowed values! 0: First IP, 1: Last IP", - "red", - ) + colored(text="0 and 1 are the only allowed values!", color="red") ) # Excel file name workbook_name = ( - input("- Excel file w/o extension? [Defaults to IP-Schema]: ") - or "IP-Schema" - ) - if workbook_name.endswith(".xlsx"): - raise SystemExit(colored("Oops! Please remove the .xlsx extension", "red")) - # Excel sheet name - worksheet_name = ( - input("- Worksheet name? [Defaults to IP Schema Worksheet]: ") - or "IP Schema Worksheet" + input("- Excel file to create [New-Schema.xlsx]: ") or "New-Schema.xlsx" ) + start = time.perf_counter() + # Read CSV file - subnets = read_subnets(file_path=input_subnets) + subnets = read_subnets(file_path=input_csv) # Do Subnetting network_subnets = subnetting(input_subnets=subnets, gateway=gateway) # Export subnetting results to an Excel file - export_subnets( - subnets=network_subnets, - workbook_name=workbook_name, - worksheet_name=worksheet_name[:31], - ) + export_subnets(subnets=network_subnets, workbook_name=workbook_name) + + end = time.perf_counter() + + delta = round(end - start, 2) + cprint(text=f"Finished in {delta} second(s)", on_color="on_blue") except FileNotFoundError: raise SystemExit( - colored(f"main.py: {input_subnets} file does not exist!", "red") + colored(text=f"`{input_csv}` file does not exist!", color="red") ) except KeyboardInterrupt: - raise SystemExit(colored(f"\nProcess interrupted by {getuser()}", "yellow")) - - print("Done") + raise SystemExit( + colored(text=f"Process interrupted by {getuser()}", color="yellow") + ) if __name__ == "__main__": diff --git a/parse_excel.py b/parse_excel.py index ecd4c2d..1bbed04 100644 --- a/parse_excel.py +++ b/parse_excel.py @@ -1,6 +1,5 @@ #!usr/bin/env python3 -import os from argparse import ArgumentParser from colorama import init @@ -32,14 +31,7 @@ args = parser.parse_args() -if not args.file.endswith(".xlsx"): - raise SystemExit(colored("\nInvalid input file. The file MUST be a .xlsx", "red")) -if not os.path.isfile(args.file): - raise SystemExit(colored(f"{args.file} does not exist!", "red")) - -svi_generator(args.file) # Execute the svi_generator - -cprint( - f'\nCreated {args.file.replace(".xlsx", "")}-svi-template.txt successfully.', - "green", -) +try: + svi_generator(excel_file=args.file) +except (FileNotFoundError, PermissionError) as e: + raise SystemExit(colored(text=e, color="red")) diff --git a/read_subnets.py b/read_subnets.py index 15c8612..a1bece1 100644 --- a/read_subnets.py +++ b/read_subnets.py @@ -1,29 +1,25 @@ #!usr/bin/env python3 import csv -from typing import AnyStr, List +from typing import AnyStr, Dict, List -def read_subnets(file_path: AnyStr = "subnets.csv") -> List[List]: +def read_subnets(file_path: AnyStr = "subnets.csv") -> List[Dict[AnyStr, AnyStr]]: """Reads CSV subnets file - Args: - file_path (AnyStr, optional): Path to subnets CSV file. Defaults to "subnets.csv". + Parameters + ---------- + file_path : AnyStr, optional + Name of a CSV file, by default "subnets.csv" - Returns: - List[List]: Subnets in CIDR notation representation + Returns + ------- + List[Dict[AnyStr, AnyStr]] + Subnets in CIDR Notation """ - # Define an empty list to hold all subnets - subnets = [] - # Read subnets CSV file with open(file=file_path, mode="r") as csvfile: next(csvfile) # Skip header line - csv_data = csv.reader( - csvfile, delimiter="\n", dialect="excel", doublequote=True - ) - for subnet in csv_data: - subnets.append(subnet[0]) - - return subnets + csv_data = csv.DictReader(f=csvfile, fieldnames={"cidr"}) + return [cidr for cidr in csv_data] diff --git a/subnetting.py b/subnetting.py index 009ce25..db052fd 100644 --- a/subnetting.py +++ b/subnetting.py @@ -2,89 +2,71 @@ import ipaddress from ipaddress import AddressValueError, NetmaskValueError -from typing import Dict, List +from typing import Any, AnyStr, Dict, List from termcolor import colored -def subnetting(input_subnets: List[List], gateway: int) -> List[Dict]: - """Does subnetting on each value entered by the user +def subnetting(input_subnets: List[Dict], gateway: int) -> List[Dict[AnyStr, Any]]: + """Does subnetting CIDR Notation - Args: - input_subnets (List[List]): Subnets from CSV file - gateway (int): An interger that decides which IP address is the gateway + Parameters + ---------- + input_subnets : List[Dict] + Input subnets in CSV file + gateway : int + The selected Gateway - Raises: - SystemExit: AddressValueError - SystemExit: NetmaskValueError - SystemExit: TypeError - SystemExit: ValueError - SystemExit: IndexError + Returns + ------- + List[Dict[AnyStr, Any]] + Result of subnetting - Returns: - List[Dict]: Networks details + Raises + ------ + SystemExit + AddressValueError, NetmaskValueError, ValueError, TypeError, IndexError """ - try: + # Empty list to hold all subnetting values results = [] # Loop over input_subnets for subnet in input_subnets: - cidr_notation = ipaddress.IPv4Network(subnet) + cidr = ipaddress.IPv4Network(subnet["cidr"]) # Find range of IP addresses - hosts = list(cidr_notation.hosts()) - start_ip = hosts[0] - end_ip = hosts[-1] + hosts = list(cidr.hosts()) + start_ip, end_ip = hosts[0], hosts[-1] # Evaluate wildcard mask from netmask - subnet_mask = str(cidr_notation.netmask).split(".") - wildcard_mask = [] - for octet in subnet_mask: - octet_value = 255 - int(octet) - wildcard_mask.append(octet_value) - + subnet_mask = str(cidr.netmask).split(".") + wildcard_mask = [255 - int(octet) for octet in subnet_mask] wildcard_mask = ".".join(map(str, wildcard_mask)) # Output - # Define an empty dictionary to hold all values - network_details = {} - - network_details["cidr"] = str(cidr_notation) - network_details["net_addr"] = str(cidr_notation.network_address) - network_details["prefix_len"] = str(cidr_notation.prefixlen) - network_details["broadcast_addr"] = str(cidr_notation.broadcast_address) - network_details["netmask"] = str(cidr_notation.netmask) - network_details["wildcard"] = wildcard_mask - network_details["num_hosts"] = len(hosts) - - if start_ip == end_ip: - network_details["range"] = str(start_ip) - else: - network_details["range"] = f"{start_ip} → {end_ip}" - - if gateway: # if gateway == 1: - network_details["gateway"] = str(end_ip) - else: - network_details["gateway"] = str(start_ip) + network_details = { + "cidr": str(cidr), + "net_addr": str(cidr.network_address), + "prefix_len": str(cidr.prefixlen), + "broadcast_addr": str(cidr.broadcast_address), + "netmask": str(cidr.netmask), + "wildcard": wildcard_mask, + "num_hosts": len(hosts), + "range": str(start_ip) + if start_ip == end_ip + else f"{start_ip} → {end_ip}", + "gateway": str(end_ip) if gateway else str(start_ip), + } results.append(network_details) return results - except AddressValueError as e: - raise SystemExit(colored(f"subnetting.py: {e}", "red")) - except NetmaskValueError as e: - raise SystemExit( - colored(f"subnetting.py: {e}. Please check {subnet} prefix length!", "red") - ) - except TypeError as e: + except ( + AddressValueError, + NetmaskValueError, + ValueError, + TypeError, + IndexError, + ) as e: raise SystemExit(colored(f"subnetting.py: {e}", "red")) - except ValueError as e: - raise SystemExit(colored(f"subnetting.py: {e}.", "red")) - except IndexError as e: - raise SystemExit( - colored( - f"subnetting.py:{e}. The input CSV file MUST contain at least one sunbet.", - "red", - ) - ) diff --git a/svi.j2 b/svi.j2 index b63da9c..96152c1 100644 --- a/svi.j2 +++ b/svi.j2 @@ -4,13 +4,13 @@ configure terminal ! {% for vlan in vlans %} vlan {{ vlan.id }} - name {{ vlan.name|replace(" ","_")|upper }} + name {{ vlan.name|replace(" ","_")|upper|truncate(32, False, "") }} exit ! {% endfor %} {% for vlan in vlans %} interface vlan {{ vlan.id }} - ip address {{ vlan.ipaddr }} {{ vlan.mask }} + ip address {{ vlan.ip }} {{ vlan.mask }} description *** {{ vlan.name|upper }} *** {% if vlan.helper_addr|string != "nan" %} ip helper-address {{ vlan.helper_addr }} diff --git a/svi_generator.py b/svi_generator.py index 11912fe..a009245 100644 --- a/svi_generator.py +++ b/svi_generator.py @@ -6,14 +6,16 @@ import pandas as pd from jinja2 import Environment, FileSystemLoader -from numpy import nan +from termcolor import cprint def svi_generator(excel_file: AnyStr) -> None: """Generates an SVI configuration template - Args: - excel_file (AnyStr): Path to the Excel file + Parameters + ---------- + excel_file : AnyStr + Name of an Excel file """ # Handle Jinja template @@ -29,23 +31,27 @@ def svi_generator(excel_file: AnyStr) -> None: data = pd.read_excel( io=os.path.join("./", excel_file), sheet_name=0, usecols="A:B,H:J" ) - df = pd.DataFrame(data=data) - - # Create a vlans List[Dict] from columns - vlans = df.rename( - columns={ - "VLAN ID": "id", - "VLAN Name": "name", - "Gateway": "ipaddr", - "Subnet Mask": "mask", - "IP Helper Address": "helper_addr", - } - ).to_dict(orient="records") + + vlans = ( + pd.DataFrame(data=data) + .rename( + columns={ + "VLAN ID": "id", + "VLAN Name": "name", + "Gateway": "ip", + "Subnet Mask": "mask", + "IP Helper Address": "helper_addr", + } + ) + .to_dict(orient="records") + ) # Render the templpate svi_cfg = template.render(vlans=vlans) # Export the template result to a text file - cfg_fname = f'{excel_file.replace(".xlsx", "")}_svi_template.txt' + cfg_fname = f'{excel_file.replace(".xlsx", "")}_svi.txt' with open(file=cfg_fname, mode="w", encoding="utf-8") as cfg_file: - cfg_file.write(svi_cfg.lstrip()) + cfg_file.write(svi_cfg) + + cprint(text=f"\nCreated {cfg_fname} successfully.", color="green")