Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions projects/Douglas Mentoring/day1-rewrite.py
Original file line number Diff line number Diff line change
@@ -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}")
92 changes: 92 additions & 0 deletions projects/Douglas Mentoring/day1.py
Original file line number Diff line number Diff line change
@@ -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()
Comment on lines +9 to +11
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is generally a bad idea. below you used the with context manager like this:

with open(file, "r"):
    stuff....

And that is the "right" way. One of the main problems with this way here is that you never closed the file. If someone else tried to open that file even if you aren't currently running your script, they could run into errors because it's locked by you. The operating system should eventually realize your script is done and eventually unlock it, but it's a bad idea to cross your fingers and hope.

The context manager magically closes the file when the code leaves the manager (hits the unindent) so that is never an issue.

Which generally also means it should be inside a function or method. But you already know that :P

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually didn't know it was a best practice just felt right to move it to a variable and work with that after(LOL)

You mentioned "the context manager magically closes .." do you mean the way i did it, is good, it will always close automatically after "moving" the content to the variable, right?

just confirming if it go it right lol

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You used a context manager the second time, down when you saved the data.

This is a context manager:

with open(file, mode) as file:
    do_stuff()
    
do_more_stuff()

After do_stuff(), the manager will close the file before moving on to do_more_stuff().


username = input('User Name:')
password = getpass.getpass()
Comment on lines +8 to +14
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general rule, these should be inside a method. There isn't really any reason to have commands outside of a method. Save the top up here for declaring constants, and everything else gets wrapped up tidy.


# Variable for "append" the data later
formatted_data = []
Comment on lines +16 to +17
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would get moved into "main" when you implement it.


# for now just to name the file / Soon to add for user to add input :)
whid = 'YEG1'
Comment on lines +19 to +20
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By convention, names of constants are always in all caps

Suggested change
# for now just to name the file / Soon to add for user to add input :)
whid = 'YEG1'
WHID = 'YEG1'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, even if later I'll make it to ask user the "whid "input, I should make it all capital to mean it should not changed, right?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually talking about that in the other file I'm going to upload. it's an interesting one. In general, the value should never change so I'd say yes, all caps. BUT from the other point of view, it IS changing once because it is being set. In the end, it could go either way, I guess.



# Function to sorte the data
def light_level_check(connection):
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By convention, starting a method/function name with an underscore shows others that it's not meant for them to use. If someone builds on your package, would you expect them to want to use this function? If so, then this should return as you mentioned below. if not, then start it with an underscore and it can do whatever it wants because it's "private".

In this case, it's up to you. but defaulting to an underscore is never a bad idea.

# 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')
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the code for send_command, use_textfsmis a boolean so it expects True or False. I suspect you meant to use textfsm_template there?

You didn't get any errors here because Python is a "Truthy" language. When it expects a boolean and it gets pretty much anything, it assumes that is True. It's smart enough to know that an empty string is False, and the integer 0 is false, but the string "banana" or any number other than 0 is treated as a True.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if i get your question right, but zthe output from this command will be formatted as the textfsm template, which should look just like json.

here is the output how it looks like (ps: i added just 2 lines/interfaces output but it could have many)

output = [
{
    "iface": "Te1/0/39",
    "temperature": "36.1",
    "voltage": "3.29",
    "tx_pwr": "41.8",
    "rx_pwr": "-2.5"
  },
  {
    "iface": "Te1/0/40",
    "temperature": "35.1",
    "voltage": "3.30",
    "tx_pwr": "36.5",
    "rx_pwr": "-2.1"
  }
]

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://ktbyers.github.io/netmiko/docs/netmiko/base_connection.html#netmiko.base_connection.BaseConnection.send_command
image

In their documentation, they say that use_textfsm is a boolean (True or False) and it defaults to False. So your string '~/ntc-templates/ntc_templates/templates/cisco_ios_show_interface_transceiver.textfsm' that you have there is only being read as "True". I think what you meant was:

Suggested change
use_textfsm='~/ntc-templates/ntc_templates/templates/cisco_ios_show_interface_transceiver.textfsm')
transceiver_output = connection.send_command(
'show interface transceiver',
read_timeout=90,
use_textfsm=True,
textfsm_template= '~/ntc-templates/ntc_templates/templates/cisco_ios_show_interface_transceiver.textfsm'
)

Comment on lines +26 to +27
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python is super flexible on whitespace. the suggestion below is the same as far as python cares, but way easier for a human to read:

Suggested change
transceiver_output = connection.send_command('show interface transceiver', read_timeout=90,
use_textfsm='~/ntc-templates/ntc_templates/templates/cisco_ios_show_interface_transceiver.textfsm')
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:
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea what x is here, or what transceiver_output represents. Is x an individual SFP? is transceiver_output a list of all transceivers? With vague names and no type hinting, there is no real way to tell what I should expect this to do.

# 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')
Comment on lines +33 to +35
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a design Principle called DRY: "Don't Repeat Yourself". If you re typing the same thing more than once, it's likely faster and easier to use a variable. Try this

Suggested change
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')
base_command = f'sh idprom interface {x["iface"]} detail | i "
sfpmodel = connection.send_command(f"{base_command}Vendor Name')
sfpspeed = connection.send_command(f"{base_command}Administrative Speed')

Similarly, sftmodel and sftspeed are not very "pythonic" variable names. At a miniumumm, I'd suggest sfp_model and sfp_speed but really, that isn't what these are and you've painted yourself in a corner below trying to think of new names. Perhaps try model_data and speed_data or even raw_model_info and raw_speed_info.

Don't be afraid of long names, they make life easier when you come back to this in a month and forget what everything meant.


# Checking if that entry maches with 'cisco'
if 'CISCO' in sfpmodel.upper():
sfpM = 'Cisco'
else:
sfpM = 'Non-Cisco'
Comment on lines +37 to +41
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When there are only two possible outcomes like this, you can use a Ternary Operator:

Suggested change
# Checking if that entry maches with 'cisco'
if 'CISCO' in sfpmodel.upper():
sfpM = 'Cisco'
else:
sfpM = 'Non-Cisco'
# Checking if that entry maches with 'cisco'
sfpM = 'Cisco' if 'CISCO' in sfpmodel.upper() else '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'
Comment on lines +42 to +48
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting one. The way you did it is quite clever, and nothing wrong with that. I may try to come up with another way.

# 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
Comment on lines +51 to +52
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, using append is perfectly fine here. I suspect the reason it was overwriting is because append() returns None. if you wanted this to return a value then it should return (host, (sfp['iface']), pwr, state, model, speed) and below where you call it, it would be something like formatted_data.append(light_level_check(connection))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't roll back to what i did to confirm, but i do believe it did exactly what you suggested,but if you think the way it is, looks good and not like a "remediate way to make it work" then should be fine ;)

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))
Comment on lines +53 to +58
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stay DRY:

        pwr = x['rx_pwr']
        iface = x['iface']
        if pwr == 'N/A':
            formatted_data.append((host, iface, pwr, 'No Connection', sfpM, sfpS))
        elif float(x['rx_pwr']) >= -10:
            formatted_data.append((host, iface, pwr, 'Pass', sfpM, sfpS))
        else:
            formatted_data.append((host, iface, pwr, 'Fail', sfpM, sfpS))

BUT that can be simplified even more using try/except as control logic:

Suggested change
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))
pwr: str = sfp['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, (sfp['iface']), pwr, state, model, speed))



# where script "starts", i could have done a __MAIN__ here but didn't get much how it works yet, currently learning this ;P
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it should :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)
Comment on lines +69 to +70
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
connection = netmiko.ConnectHandler(host=host, device_type='cisco_ios', username=username,
password=password, secret=password)
connection = netmiko.ConnectHandler(
host=host,
device_type='cisco_ios',
username=username,
password=password,
secret=password
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one i was lazy, i did this way on the test but didn't change here

when first writing script/code for me straight line felt easier

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All in one line is fine if it fits and you like it. My team uses 120 character as the maximum line length, as a guide for "fits on one line"

connection.send_command('terminal length 0')
connection.enable()
except Exception as e:
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's considered poor form to catch Exception, you should always be as explicit as possible here so you don't accidentally swallow an error you didn't mean to.

It looks like ConnectHandler and enable could raise ValueError, send_command can raise ReadTimeout. You could replace except Exception with except ValueError, ReadTimeout, or you could catch them separately, depending on whether you want to take the same action for all possible failures. For example, perhaps a ReadTimeout could be a network issue and you want to retry once before giving up.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my idea was to have the device and error shown, which i could later come back and check the error from the output and try is later.

but yeah its a good idea to have it retry depending on the error! noted!

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.
Comment on lines +91 to +92
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of these are true statements :P

Also, by convention, every python script/file should end in a blank line. The reason for it is "old technology", it's a throwback to when most people used a terminal. when you cat the python file, having a blank line at the end of the file makes it a little easier to read. But either way it's a standard that pretty much everyone does, and many linters will enforce it.

Which brings me to Linters. You should install one. A linter is a script that will go through your code and clean it up. Depending on your settings it can do thins like make sure you always use the same quotes (python doesn't care if you use 'single' or "double" quotes, but it's generally expected that you will pick one and stick with it) and it can make sure you have the right number of blank lines before a method or class, etc. most of those things they can even fix for you magically, so you just run the command and it makes your code pretty. my team uses ruff for most things: https://beta.ruff.rs/docs/tutorial/

35 changes: 35 additions & 0 deletions projects/Douglas Mentoring/response_parse_oneliner.py
Original file line number Diff line number Diff line change
@@ -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}')
65 changes: 65 additions & 0 deletions projects/Douglas Mentoring/result.py
Original file line number Diff line number Diff line change
@@ -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
-----------------------------------------------
"""