diff --git a/projects/Douglas Mentoring/day1-rewrite.py b/projects/Douglas Mentoring/day1-rewrite.py new file mode 100644 index 0000000..758ef06 --- /dev/null +++ b/projects/Douglas Mentoring/day1-rewrite.py @@ -0,0 +1,168 @@ +# Python 3.10 has some nice features, but I only have 3.8 installed. +# Importing __future__ lets us use the features that won't break anything. +# It isn't as good as updating and getting ALL the features, but it's nice to have some. +from __future__ import annotations + +import csv +from getpass import getpass +from typing import Any, Dict, List, Tuple + +from netmiko import BaseConnection, ConnectHandler, ReadTimeout + +DEFAULT_DEVICE_TYPE: str = 'cisco_ios' +DEFAULT_WHID: str = "YEG1" +IF_DETAIL_CMD: str = "sh idprom interface {iface} detail" +HOSTS_FILE = "/home/segattod/segattod-workspace/src/Segattod-package/myproject/Light_level_checks/list.txt" +MIN_LIGHT_LEVEL: float = -10 +MODEL_LINE_PREFIX: str = "Vendor Name" +SPEED_LINE_PREFIX: str = "Administrative Speed" + +SFP_SPEEDS: Dict[int, str] = {10000: "10G", 1000: "1G"} + + +def _login() -> Tuple[str, str]: + # A tuple lets us return two different values at once! Making this a method means + # it is easier to change later on if we ever decide to change how we log in. Maybe + # we can use SSO or something one day? That would be cool. + return input('User Name:'), getpass() + + +def _get_whid() -> str: + # You said you wanted to replace that hardcoded string with a user input or something + # later, now you can just change this here and not mess with your `main` code. This + # is called the Interface Design Principle. You can write a dozen different ways to get + # a WHID and add code here that says "if on the VPN then use SSO; if there is no keyboard + # then default to "YEG1" otherwise ask the user for input and each of those options can + # be in their own methods... Once you have this interface class, main doesn't care HOW + # you get the WHID, as long as this method returns a string. + return DEFAULT_WHID + + +def _create_connection( + _host: str, + _username: str, + _password: str, + # If the user doesn't provide a secret then we will default to using the password below. + _secret: str | None = None, + # If the user doesn't provide a device type then use the default. + _device_type: str = DEFAULT_DEVICE_TYPE, +) -> BaseConnection | None: + try: + _connection: BaseConnection = ConnectHandler( + host=_host, + device_type=_device_type, + username=_username, + password=_password, + secret=_secret or _password # If _secret is None then use _password here instead + ) + _connection.send_command('terminal length 0') + _connection.enable() + return _connection + + except ReadTimeout as raised: + print(f'Failed connection {host}: {raised}') + return None + except ValueError as raised: + print(f'ValueError raised connecting to {host}: {raised}: ') + return None + + +def _parse_response_data(conn: BaseConnection, iface: str) -> Tuple[str, str]: + """I have no way of testing if this will actually work, it's just here as an example.""" + response: str = conn.send_command(IF_DETAIL_CMD.format(iface=iface)) + + # Initialize these to blank strings to stat with. + # Python treats a blank string as False in an if statement. + model: str = "" + speed: str = "" + # For each line in the response: + for line in response.splitlines(): + # Split at the colon, save the right side, and strip any extra blank spaces. + line_data: str = line.split(":")[1].rstrip() + # If the left side startswith something we want, save it. + if line.startswith(MODEL_LINE_PREFIX): + model = line_data + elif line.startswith(SPEED_LINE_PREFIX): + speed = line_data + + # If we have both values then return them. There is no reason to look at the rest of the lines. + if model and speed: + return model, speed + + # If we finished looking at every line and have not found both values, we have a problem. + raise ValueError( + f"Provided data did not include model and speed information in the expected format. {response}" + ) + + +def _get_response_data(conn: BaseConnection, iface: str) -> Tuple[str, str]: + """ + TECHNICALLY I think it is best practice to call just the base command and + pull out the data you need from the full response. That is preferred because + this is making two network calls instead of one. In our case it's trivial, + but if you were connecting to a service that charged per connection or had + a max number of connections per hour or something, getting it in one call + then parsing it would save time and heartache. It's a good habit to be in. + + See _parse_response_data() above for a suggestion. + """ + + # Take the command from up above and replace the {iface} placeholder: + base_cmd: str = IF_DETAIL_CMD.format(iface=iface) + # Then use that to build your two commands. + model_data: str = conn.send_command(f'{base_cmd} | i Vendor Name') + speed_data: str = conn.send_command(f'{base_cmd} | i Administrative Speed') + + return model_data, speed_data + + +def _light_level_check(conn: BaseConnection) -> None: + transceiver_output: Dict[str, Any] = conn.send_command( + 'show interface transceiver', + read_timeout=90, + # I can't actually run this script, so you may need to tinker a little. + # I suspect you can drop the following line + use_textfsm=True + ) + + for transceiver in transceiver_output: + interface: str = transceiver['iface'] + model_data, speed_data = _get_response_data(connection, interface) + model: str = 'Cisco' if 'CISCO' in model_data.upper() else 'Non-Cisco' + try: + speed_as_str: str = speed_data.split(":")[1] + speed = SFP_SPEEDS[int(speed_as_str)] + except (ValueError, IndexError): + # Return "Unknown" if we get an integer we don't recognize or a non-integer value. + speed = "Unknown" + + pwr: str = transceiver['rx_pwr'] + try: + state = 'Pass' if float(pwr) >= MIN_LIGHT_LEVEL else 'Fail' + except ValueError: # Could not convert to a float + state = 'No Connection' + + formatted_data.append((host, interface, pwr, state, model, speed)) + + +if __name__ == '__main__': + username, password = _login() + formatted_data: List = [] + whid: str = _get_whid() + output_filename = f"{whid}_output_data.csv" + + print('Beginning Script') + with open(HOSTS_FILE, 'r') as file: + for host in file.readlines(): + print(f"Connecting to {host}") + connection = _create_connection(host, username, password) + _light_level_check(connection) + + with open(output_filename, "w", newline="") as csvfile: + csv_writer = csv.writer(csvfile) + # Write CSV header. + csv_writer.writerow(["Host", "interface", "rx_pwr", "result", "SFP Model", "SFP Speed"]) + # Write the data. + csv_writer.writerows(formatted_data) + + print(f"Data exported to {output_filename}") diff --git a/projects/Douglas Mentoring/day1.py b/projects/Douglas Mentoring/day1.py new file mode 100644 index 0000000..d9a2756 --- /dev/null +++ b/projects/Douglas Mentoring/day1.py @@ -0,0 +1,92 @@ +import getpass +import netmiko +# json was here bc as I'm using textfsm to pull data from cisco devices +# it comes in json i needed to show the output before deciding how to tread the date +import json +import csv + +# Where i'm pulling the devices list / Soon I'll make this automated which will pull direct from NARF +file = open(r'/home/segattod/segattod-workspace/src/Segattod-package/myproject/Light_level_checks/list.txt', + 'r') +file = file.read().splitlines() + +username = input('User Name:') +password = getpass.getpass() + +# Variable for "append" the data later +formatted_data = [] + +# for now just to name the file / Soon to add for user to add input :) +whid = 'YEG1' + + +# Function to sorte the data +def light_level_check(connection): + # Command to checks light level per interface + transceiver_output = connection.send_command('show interface transceiver', read_timeout=90, + use_textfsm='~/ntc-templates/ntc_templates/templates/cisco_ios_show_interface_transceiver.textfsm') + + for x in transceiver_output: + # added both variables below to do a check at that moment based on the "output" for that device interface the loop is going through + # why? bc was the way i could think to append(look below) all data to export later in CSV + # basically i use the "entry" information from "x" do all the checks and move to the next, and so on. + sfpmodel = connection.send_command(f'sh idprom interface {x["iface"]} detail | i Vendor Name') + sfpspeed = connection.send_command( + f'sh idprom interface {x["iface"]} detail | i Administrative Speed') + + # Checking if that entry maches with 'cisco' + if 'CISCO' in sfpmodel.upper(): + sfpM = 'Cisco' + else: + sfpM = 'Non-Cisco' + # Checking if that entry has a match with 10000, then with 1000 + if '10000' in sfpspeed: + sfpS = '10GB' + elif '1000' in sfpspeed: + sfpS = '1GB' + else: + sfpS = 'Unknown' + # checking if the light level for fiber connections are acceptable + # AND add all that compiled data to "formatted_data" + ## I know I could have used "return" but when i did it it was not "appending" + ## it was adding all for one device and erasing and add the new ones, should be simple mistake from my end but i could not figure lol + if x['rx_pwr'] == 'N/A': + formatted_data.append((host, x['iface'], x["rx_pwr"], 'No Connection', sfpM, sfpS)) + elif float(x['rx_pwr']) >= -10: + formatted_data.append((host, x['iface'], x["rx_pwr"], 'Pass', sfpM, sfpS)) + else: + formatted_data.append((host, x['iface'], x["rx_pwr"], 'Fail', sfpM, sfpS)) + + +# where script "starts", i could have done a __MAIN__ here but didn't get much how it works yet, currently learning this ;P +print('Beginning Script') + +# Loop to go over device list linked on the beginning +## I know I can add the connection to a funcion as well, i have it on another script just didn't get to do it as this looks cleaner for me(but i know should be updated) +for host in file: + print(f'Connecting to {host}') + try: + connection = netmiko.ConnectHandler(host=host, device_type='cisco_ios', username=username, + password=password, secret=password) + connection.send_command('terminal length 0') + connection.enable() + except Exception as e: + print(f'Failed connection {host}: {e}') + continue + # call the funcion (and only one lol) + light_level_check(connection) + +# Define CSV file name + WHID +csv_filename = (f"{whid}_output_data.csv") + +# Export formatted data to CSV +with open(csv_filename, "w", newline="") as csvfile: + csv_writer = csv.writer(csvfile) + csv_writer.writerow( + ["Host", "interface", "rx_pwr", "result", "SFP Model", "SFP Speed"]) # Write CSV header + csv_writer.writerows(formatted_data) # Write the data + +print(f"Data exported to {csv_filename}") + +## I know i could make a main function and invoke others but im still going through it in my learning path lol +## Also should use a more structured start for the script using main or other way. \ No newline at end of file diff --git a/projects/Douglas Mentoring/response_parse_oneliner.py b/projects/Douglas Mentoring/response_parse_oneliner.py new file mode 100644 index 0000000..1ca2562 --- /dev/null +++ b/projects/Douglas Mentoring/response_parse_oneliner.py @@ -0,0 +1,35 @@ +from result import result + +MODEL_LINE_PREFIX: str = "Vendor Name" +SPEED_LINE_PREFIX: str = "Administrative Speed" + +""" +These two lines have the same result as the _parse_response_data() method in day1_rewrite.py. +Instead of getting the data from send_command(), I'm importing it from result.py so you can +actually run it and see that it's not just gibberish. :P The `data = ` line is what is called a +"dictionary comprehension". Super powerful, and often super confusing. Don't use this kind of +thing when someone else may need to read your code, but once you learn them they are really fun +to play with. + +Line 29 does all the following in one command: + +create a new dictionary named data +for each line in result: + if the line starts with MODEL_LINE_PREFIX or SPEED_LINE_PREFIX: + add an entry to the dictionary with the matching PREFIX as key and the rest of the line as the value + + +Then line 30 pulls those two values out of the dictionary and returns them as a tuple. + +Of course, when it takes more lines to explain it than to do it, you are likely something it wrong. +""" + + +def _parse(): + data = {line.split(":")[0].strip() : line.split(":")[1].strip() for line in result.splitlines() if line.startswith((MODEL_LINE_PREFIX, SPEED_LINE_PREFIX))} + return data[MODEL_LINE_PREFIX], data[SPEED_LINE_PREFIX] + + +model, speed = _parse() +print(f'Model: {model}') +print(f'Speed: {speed}') diff --git a/projects/Douglas Mentoring/result.py b/projects/Douglas Mentoring/result.py new file mode 100644 index 0000000..82384fe --- /dev/null +++ b/projects/Douglas Mentoring/result.py @@ -0,0 +1,65 @@ +result = """ +General SFP Information +----------------------------------------------- +Identifier : SFP/SFP+ +Ext.Identifier : SFP function is defined by two-wire interface ID only +Connector : LC connector +Transceiver + 10GE Comp code : Unknown + SONET Comp code : OC 48 short reach + GE Comp code : Unknown + Link length : Unknown + Technology : Unknown + Media : Single Mode + Speed : Unknown +Encoding : 64B/66B +BR_Nominal : 10300 Mbps +Length(9um)-km : 10 km +Length(9um) : 10000 m +Length(50um) : GBIC does not support 50 micron multi mode OM2 fibre +Length(62.5um) : GBIC does not support 62.5 micron multi mode OM1 fibre +Length(Copper) : GBIC does not support 50 micron multi mode OM4 fibre +Vendor Name : CISCO-FINISAR +Vendor Part Number : FTLX1475D3BCL-C2 +Vendor Revision : 0x41 0x20 0x20 0x20 +Vendor Serial Number : FNS24150LJV +Wavelength : 1310 nm +CC_BASE : 0xC5 +----------------------------------------------- + +Extended ID Fields +----------------------------------------------- +Options : 0x00 0x1A +BR, max : 0x00 +BR, min : 0x00 +Date code : 200409 +Diag monitoring : Implemented +Internally calibrated : Yes +Exeternally calibrated: No +Rx.Power measurement : Avg.Power +Address Change : Not Required +CC_EXT : 0x56 +----------------------------------------------- + +Other Information +----------------------------------------------- +Chk for link status : 00 +Flow control Receive : Off +Flow control Send : Off +Administrative Speed : 10000 +Administrative Duplex : full +Operational Speed : 10000 +Operational Duplex : full +----------------------------------------------- + +SEEPROM contents (hex): + 0x00: 03 04 07 20 00 00 00 00 00 00 00 06 67 00 0A 64 + 0x10: 00 00 00 00 43 49 53 43 4F 2D 46 49 4E 49 53 41 + 0x20: 52 20 20 20 00 00 90 65 46 54 4C 58 31 34 37 35 + 0x30: 44 33 42 43 4C 2D 43 32 41 20 20 20 05 1E 00 C5 + 0x40: 00 1A 00 00 46 4E 53 32 34 31 35 30 4C 4A 56 20 + 0x50: 20 20 20 20 32 30 30 34 30 39 20 20 68 F0 06 56 + 0x60: 00 00 02 C2 4C 1D 0E 2B 09 6C 77 24 44 8C BD 46 + 0x70: F5 F7 3F 00 00 00 00 00 00 00 00 00 94 1C CE 86 +----------------------------------------------- +""" \ No newline at end of file