├── .flake8 ├── netlib ├── __init__.py ├── util.py └── arista_pyeapi.py ├── requirements.txt ├── README.md ├── get_flash_storage.py ├── .gitignore ├── get_mlag_status.py ├── get_overlay_outputs.py ├── get_switch_port_errors.py ├── get_fpga_error.py ├── get_power_status.py ├── get_upgrade_checks.py ├── get_reload_cause.py ├── get_show_tech_and_agent_logs.py ├── get_errors_and_discards.py ├── LICENSE.md └── get_port_data.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | max-complexity = 18 4 | -------------------------------------------------------------------------------- /netlib/__init__.py: -------------------------------------------------------------------------------- 1 | from netlib.arista_pyeapi import AristaPyeapi 2 | from netlib.util import cleanup_log_if_empty, get_credentials, setup_logging 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | black==23.9.1 2 | flake8==6.1.0 3 | isort==5.12.0 4 | netaddr==0.9.0 5 | numpy==1.26.0 6 | pexpect==4.8.0 7 | pyeapi==1.0.2 8 | rich==13.6.0 9 | termcolor==2.3.0 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nanog-api-troubleshooting 2 | Scripts that go along with the talk "Simplified Troubleshooting Through API Scripting" that was presented at NANOG 87 by Cat Gurinsky on Monday, February 13th, 2023. This talk was updated and presented again at the Network Automation Forum inaugural conference AutoCon0 on November 14th, 2023. 3 | 4 | View the NANOG 87 talk here: https://www.youtube.com/watch?v=ne_4-5rdL_M 5 | 6 | View the updated AutoCon0 Network Automation Forum Talk here: https://www.youtube.com/watch?v=BYAwFvWvDiE 7 | 8 | View the NANOG 87 version of the slides here: https://storage.googleapis.com/site-media-prod/meetings/NANOG87/4617/20230213_Gurinsky_Simplified_Troubleshooting_Through_v1.pdf 9 | 10 | ## Full Abstract 11 | How often do you find yourself doing the same set of commands when troubleshooting issues in your network? I am willing to bet the answer to this is quite often! Usually we have a list of our favorite commands that we will always use to quickly narrow down a specific problem type. 12 | 13 | Switch reloaded unexpectedly? "show reload cause" 14 | Fan failure? "show environment power" 15 | Fiber link reporting high errors or down on your monitoring system? "show interface counters errors", "show interface transceiver", "show interface mac detail" 16 | 17 | Outputs like the above examples help you quickly pinpoint the source of your failures for remediation. SSH'ing into the boxes and running these commands by hand is time consuming, especially if you are for example a NOC dealing with numerous failures throughout the day. Most switch platforms have API's now and you can instead program against them to get these outputs in seconds. I will go over a variety of examples and creative ways to use these scripts for optimal use of your troubleshooting time and to get you away from continually doing these repetitive tasks by hand. 18 | 19 | NOTE: My tutorial examples will be using python and the Arista pyeapi module with Arista examples, but the concepts can easily be transferred to other platforms and languages. 20 | -------------------------------------------------------------------------------- /get_flash_storage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | try: 16 | # importing libraries 17 | import argparse 18 | import os 19 | 20 | import netlib 21 | except ImportError as error: 22 | print(error) 23 | quit() 24 | except Exception as exception: 25 | print(exception) 26 | 27 | parser = argparse.ArgumentParser() 28 | parser.add_argument( 29 | "-s", 30 | "--switch", 31 | type=str, 32 | required=True, 33 | metavar="somehostname.com", 34 | help="Hostname of the switch", 35 | ) 36 | parser.add_argument( 37 | "-o", 38 | "--output_directory", 39 | type=str, 40 | required=False, 41 | default=os.environ.get("HOME", "/tmp"), 42 | metavar="some_folder", 43 | help="Folder for all outputs from the script", 44 | ) 45 | args = parser.parse_args() 46 | switch_hostname = args.switch 47 | output_dir = args.output_directory 48 | username, password = netlib.get_credentials("TACACS") 49 | 50 | # open a file for logging errors 51 | start_logger = netlib.setup_logging("get_flash", output_dir) 52 | logger = start_logger[0] 53 | logger_full_path = start_logger[1] 54 | 55 | # instantiate the eAPI library 56 | eapi = netlib.AristaPyeapi(username, password, switch_hostname, logger) 57 | 58 | 59 | def get_storage(): 60 | get_storage = eapi.get_storage() 61 | print(get_storage) 62 | 63 | 64 | show_version = eapi.get_version() 65 | version = show_version["eos_version"] 66 | serial_number = show_version["serial_number"] 67 | model = show_version["model"] 68 | hardware_rev = show_version["hardware_rev"] 69 | hostname_short = eapi.get_hostname_short() 70 | 71 | print(f"Hostname: {switch_hostname}") 72 | print(f"Model: {model} | Hardware Revision: {hardware_rev}") 73 | print(f"Serial Number: {serial_number}") 74 | print(f"OS Version: {version}") 75 | 76 | print(f"\n{hostname_short}# bash timeout 2 sudo df -lh") 77 | get_storage() 78 | 79 | netlib.cleanup_log_if_empty(logger_full_path) 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # MacOS 41 | .DS_Store 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | -------------------------------------------------------------------------------- /get_mlag_status.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | try: 16 | # importing libraries 17 | import argparse 18 | import os 19 | 20 | from termcolor import colored 21 | 22 | import netlib 23 | except ImportError as error: 24 | print(error) 25 | quit() 26 | except Exception as exception: 27 | print(exception) 28 | 29 | parser = argparse.ArgumentParser() 30 | parser.add_argument( 31 | "-s", 32 | "--switch", 33 | type=str, 34 | required=True, 35 | metavar="switch.hostname.com", 36 | help="Hostname of the switch", 37 | ) 38 | parser.add_argument( 39 | "-o", 40 | "--output_directory", 41 | type=str, 42 | required=False, 43 | default=os.environ.get("HOME", "/tmp"), 44 | metavar="some_folder", 45 | help="Folder for all outputs from the script", 46 | ) 47 | args = parser.parse_args() 48 | switch_hostname = args.switch 49 | output_dir = args.output_directory 50 | username, password = netlib.get_credentials("TACACS") 51 | 52 | # open a file for logging errors 53 | start_logger = netlib.setup_logging("get_mlag", output_dir) 54 | logger = start_logger[0] 55 | logger_full_path = start_logger[1] 56 | 57 | # instantiate the eAPI library 58 | eapi = netlib.AristaPyeapi(username, password, switch_hostname, logger) 59 | 60 | show_mlag = eapi.get_mlag() 61 | 62 | mlag_neg_status = show_mlag["neg_status"] 63 | mlag_peer_link = show_mlag["peer_link"] 64 | mlag_peer_link_status = show_mlag["peer_link_status"] 65 | mlag_state = show_mlag["state"] 66 | if mlag_peer_link_status == "up": 67 | mlag_config_sanity = show_mlag["config_sanity"] 68 | print(f"Mlag Config Sanity: {mlag_config_sanity}") 69 | 70 | print(f"Mlag Peer Link: {mlag_peer_link}") 71 | print(f"Mlag Peer Link Status: {mlag_peer_link_status}") 72 | print(f"Mlag Negotiation Status: {mlag_neg_status}") 73 | print(f"Mlag State: {mlag_state}") 74 | 75 | 76 | show_port_channel = eapi.get_port_channel(mlag_peer_link) 77 | port_channel_active_ports = show_port_channel["activePorts"] 78 | port_channel_inactive_ports = show_port_channel["inactivePorts"] 79 | print("\nActive Ports: ") 80 | for port in port_channel_active_ports: 81 | print(colored(port, "green")) 82 | print("\nInactive Ports: ") 83 | for port in port_channel_inactive_ports: 84 | print(colored(port, "red")) 85 | 86 | netlib.cleanup_log_if_empty(logger_full_path) 87 | -------------------------------------------------------------------------------- /get_overlay_outputs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | try: 16 | # importing libraries 17 | import argparse 18 | import os 19 | 20 | import netlib 21 | except ImportError as error: 22 | print(error) 23 | quit() 24 | except Exception as exception: 25 | print(exception) 26 | 27 | parser = argparse.ArgumentParser() 28 | parser.add_argument( 29 | "-s", 30 | "--switch", 31 | type=str, 32 | required=True, 33 | metavar="switch.hostname.com", 34 | help="Hostname of the switch", 35 | ) 36 | parser.add_argument( 37 | "-o", 38 | "--output_directory", 39 | type=str, 40 | required=False, 41 | default=os.environ.get("HOME", "/tmp"), 42 | metavar="some_folder", 43 | help="Folder for all outputs from the script", 44 | ) 45 | args = parser.parse_args() 46 | switch_hostname = args.switch 47 | output_dir = args.output_directory 48 | 49 | # retrieve the username and password from environment variables if present 50 | username, password = netlib.get_credentials("TACACS") 51 | 52 | # open a file for logging errors 53 | start_logger = netlib.setup_logging("get_flash", output_dir) 54 | logger = start_logger[0] 55 | logger_full_path = start_logger[1] 56 | 57 | # instantiate the eAPI library 58 | eapi = netlib.AristaPyeapi(username, password, switch_hostname, logger) 59 | 60 | sudo_du = eapi.try_eapi_command( 61 | "bash timeout 2 sudo du -sch /.overlay/* | sort -k1 -hr", 62 | "enable", 63 | )["messages"] 64 | 65 | sudo_ls = eapi.try_eapi_command( 66 | "bash timeout 2 sudo ls -alSRh /.overlay", 67 | "enable", 68 | )["messages"] 69 | 70 | sudo_lsof = eapi.try_eapi_command( 71 | "bash timeout 30 sudo lsof -nP | grep '(deleted)'", 72 | "enable", 73 | )["messages"] 74 | 75 | 76 | def json_to_file(filename, json_source): 77 | new_filename = f"{output_dir}/{switch_hostname.strip()}_{filename}" 78 | with open(new_filename, "w") as my_file: 79 | if filename.startswith("sudo"): 80 | for each_line in json_source: 81 | my_file.write("%s\n" % each_line) 82 | else: 83 | my_file.write(json_source) 84 | print(f"Outputs written to {new_filename}") 85 | 86 | 87 | json_to_file("sudo_du.txt", sudo_du) 88 | json_to_file("sudo_ls.txt", sudo_ls) 89 | json_to_file("sudo_lsof.txt", sudo_lsof) 90 | 91 | netlib.cleanup_log_if_empty(logger_full_path) 92 | -------------------------------------------------------------------------------- /get_switch_port_errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | try: 16 | # importing libraries 17 | import argparse 18 | import os 19 | import sys 20 | import time 21 | 22 | import netlib 23 | except ImportError as error: 24 | print(error) 25 | quit() 26 | except Exception as exception: 27 | print(exception) 28 | 29 | parser = argparse.ArgumentParser() 30 | parser.add_argument( 31 | "-s", 32 | "--switch", 33 | type=str, 34 | required=True, 35 | metavar="switch.hostname.com", 36 | help="Hostname of the switch", 37 | ) 38 | parser.add_argument( 39 | "-o", 40 | "--output_directory", 41 | type=str, 42 | required=False, 43 | default=os.environ.get("HOME", "/tmp"), 44 | metavar="some_folder", 45 | help="Folder for all outputs from the script", 46 | ) 47 | args = parser.parse_args() 48 | switch_hostname = args.switch 49 | output_dir = args.output_directory 50 | # check for environment variables for TACACS username & password, prompt if missing 51 | username, password = netlib.get_credentials("TACACS") 52 | 53 | # open a file for logging errors 54 | start_logger = netlib.setup_logging("get_switch_port_errors", output_dir) 55 | logger = start_logger[0] 56 | logger_full_path = start_logger[1] 57 | 58 | # instantiate the eAPI library 59 | eapi = netlib.AristaPyeapi(username, password, switch_hostname, logger) 60 | 61 | try: 62 | show_version = eapi.get_version() 63 | hostname_short = eapi.get_hostname_short() 64 | print(f"\nHostname: {hostname_short}") 65 | print( 66 | f'Model: {show_version["model"]} ' 67 | f'Hardware Revision: {show_version["hardware_rev"]}' 68 | ) 69 | print(f'Serial Number: {show_version["serial_number"]}') 70 | print(f'OS Version: {show_version["eos_version"]}\n') 71 | port_errors_nz = eapi.try_eapi_command( 72 | "show interfaces counters errors | nz", "enable", "text" 73 | ) 74 | print(f"{hostname_short}# show interface counters errors | nz\n") 75 | print(port_errors_nz) 76 | print("Sleeping for 20 seconds and trying again.") 77 | time.sleep(20) 78 | port_errors_nz = eapi.try_eapi_command( 79 | "show interfaces counters errors | nz", "enable", "text" 80 | ) 81 | print(f"\n{hostname_short}# show interface counters errors | nz\n") 82 | print(port_errors_nz) 83 | 84 | except KeyboardInterrupt: 85 | print("Caught Keyboard Interrupt - Exiting the program.") 86 | sys.exit() 87 | 88 | netlib.cleanup_log_if_empty(logger_full_path) 89 | -------------------------------------------------------------------------------- /netlib/util.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | try: 14 | # importing libraries 15 | import datetime 16 | import getpass 17 | import subprocess 18 | import logging 19 | import os 20 | from subprocess import check_output 21 | import sys 22 | except ImportError as error: 23 | print(error) 24 | quit() 25 | except Exception as exception: 26 | print(exception) 27 | 28 | 29 | def setup_logging(script_function, output_dir=os.environ.get("HOME", "/tmp")): 30 | if not os.path.exists(output_dir): 31 | os.mkdir(output_dir) 32 | print(f"\nIf log entries are present, the file will be saved under: {output_dir}\n") 33 | script_time_stamp = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S") 34 | log_filename = f"{output_dir}/{script_function}_log_{script_time_stamp}.log" 35 | logger = logging.getLogger(script_function) 36 | # create file handler which logs even debug messages 37 | fh = logging.FileHandler(log_filename) 38 | fh.setLevel(logging.WARNING) 39 | # create formatter and add it to the handlers 40 | formatter = logging.Formatter("%(asctime)-15s %(levelname)-8s %(message)s") 41 | fh.setFormatter(formatter) 42 | # add the handlers to the logger 43 | logger.addHandler(fh) 44 | return [logger, log_filename] 45 | 46 | 47 | def cleanup_log_if_empty(logger_full_path): 48 | logging.shutdown() 49 | if os.stat(logger_full_path).st_size == 0: 50 | print(f"\nLog file was empty, removing file: {logger_full_path}") 51 | os.remove(logger_full_path) 52 | else: 53 | print(f"\nScript has log entries, find log here: {logger_full_path}") 54 | 55 | 56 | def get_credentials(prefix="TACACS"): 57 | # Usage example: 58 | # username, password = get_credentials() 59 | try: 60 | user = os.environ.get(f"{prefix}_USERNAME", None) 61 | if user is None: 62 | user = input(f"Enter your {prefix} username: ") 63 | password = os.environ.get(f"{prefix}_PASSWORD", None) 64 | if password is None: 65 | password = getpass.getpass( 66 | prompt=f"{prefix} password for {user}: ", 67 | ) 68 | return (user, password) 69 | except KeyboardInterrupt: 70 | print("\n\nCaught Keyboard Interrupt - Exiting the program.") 71 | sys.exit() 72 | 73 | 74 | def check_ping(device_hostname): 75 | try: 76 | check_ping = check_output(f"ping -c 2 {device_hostname}", shell=True).decode( 77 | "utf-8" 78 | ) 79 | # search for percentage packet loss 80 | for ping_stat in check_ping.split("\n"): 81 | if "% packet loss" in ping_stat: 82 | ping_ret = int( 83 | ping_stat.split(",")[-2].strip().split(" ")[0].rstrip("%") 84 | ) 85 | return ping_ret 86 | except subprocess.CalledProcessError: 87 | ping_ret = int(100) 88 | return ping_ret 89 | print(f"{device_hostname} is not pinging, ping returned error code.") 90 | -------------------------------------------------------------------------------- /get_fpga_error.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | try: 16 | # importing libraries 17 | import argparse 18 | import datetime 19 | import os 20 | from datetime import timedelta 21 | 22 | from rich.console import Console 23 | from rich.table import Table 24 | 25 | import netlib 26 | except ImportError as error: 27 | print(error) 28 | quit() 29 | except Exception as exception: 30 | print(exception) 31 | 32 | parser = argparse.ArgumentParser() 33 | parser.add_argument( 34 | "-s", 35 | "--switch", 36 | type=str, 37 | required=True, 38 | metavar="switch.hostname.com", 39 | help="Hostname of the switch", 40 | ) 41 | parser.add_argument( 42 | "-o", 43 | "--output_directory", 44 | type=str, 45 | required=False, 46 | default=os.environ.get("HOME", "/tmp"), 47 | metavar="some_folder", 48 | help="Folder for all outputs from the script", 49 | ) 50 | args = parser.parse_args() 51 | switch_hostname = args.switch 52 | output_dir = args.output_directory 53 | username, password = netlib.get_credentials("TACACS") 54 | 55 | # open a file for logging errors 56 | start_logger = netlib.setup_logging("get_fpga_errors", output_dir) 57 | logger = start_logger[0] 58 | logger_full_path = start_logger[1] 59 | 60 | console = Console() 61 | 62 | # instantiate the eAPI library 63 | eapi = netlib.AristaPyeapi(username, password, switch_hostname, logger) 64 | 65 | show_version = eapi.get_version() 66 | fpga_error = eapi.try_eapi_command("show hardware fpga error", "enable") 67 | fpga_uncorrected = fpga_error["uncorrectableCrcs"] 68 | 69 | version = show_version["eos_version"] 70 | serial_number = show_version["serial_number"] 71 | model = show_version["model"] 72 | hardware_rev = show_version["hardware_rev"] 73 | uptime = str(timedelta(seconds=show_version["uptime"])) 74 | 75 | print(f"Hostname: {switch_hostname}") 76 | print(f"Model: {model}, Hardware Revision: {hardware_rev}") 77 | print(f"Serial Number: {serial_number}") 78 | print(f"OS Version: {version}") 79 | print(f"Uptime: {uptime}") 80 | 81 | fpga_table = Table(show_header=False) 82 | fpga_table.add_column("Query", justify="left", style="cyan", no_wrap=True) 83 | fpga_table.add_column("Data", justify="right", style="magenta") 84 | 85 | for value in fpga_uncorrected: 86 | error_count = fpga_error_count = fpga_uncorrected[value]["count"] 87 | fpga_table.add_row( 88 | f"{value} Error Count", 89 | str(error_count), 90 | ) 91 | if error_count > 0: 92 | fpga_error_first = str( 93 | datetime.datetime.utcfromtimestamp( 94 | fpga_uncorrected[value]["firstOccurrence"] 95 | ) 96 | ) 97 | fpga_error_last = str( 98 | datetime.datetime.utcfromtimestamp( 99 | fpga_uncorrected[value]["lastOccurrence"] 100 | ) 101 | ) 102 | fpga_table.add_row( 103 | f"{value} First Occurence (UTC)", 104 | str(fpga_error_first), 105 | ) 106 | fpga_table.add_row( 107 | f"{value} Last Occurence (UTC)", 108 | str(fpga_error_last), 109 | ) 110 | 111 | console.print(fpga_table) 112 | 113 | netlib.cleanup_log_if_empty(logger_full_path) 114 | -------------------------------------------------------------------------------- /get_power_status.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | try: 16 | # importing libraries 17 | import argparse 18 | import os 19 | 20 | from rich.console import Console 21 | from rich.table import Table 22 | 23 | import netlib 24 | except ImportError as error: 25 | print(error) 26 | quit() 27 | except Exception as exception: 28 | print(exception) 29 | 30 | parser = argparse.ArgumentParser() 31 | parser.add_argument( 32 | "-s", 33 | "--switch", 34 | type=str, 35 | required=True, 36 | metavar="switch.hostname.com", 37 | help="Hostname of the switch", 38 | ) 39 | parser.add_argument( 40 | "-o", 41 | "--output_directory", 42 | type=str, 43 | required=False, 44 | default=os.environ.get("HOME", "/tmp"), 45 | metavar="some_folder", 46 | help="Folder for all outputs from the script", 47 | ) 48 | args = parser.parse_args() 49 | switch_hostname = args.switch 50 | output_dir = args.output_directory 51 | username, password = netlib.get_credentials("TACACS") 52 | 53 | # open a file for logging errors 54 | start_logger = netlib.setup_logging("port_data", output_dir) 55 | logger = start_logger[0] 56 | logger_full_path = start_logger[1] 57 | 58 | # instantiate the eAPI library 59 | eapi = netlib.AristaPyeapi(username, password, switch_hostname, logger) 60 | 61 | show_version = eapi.get_version() 62 | power_status = eapi.try_eapi_command("show environment power", "enable", "text") 63 | power_status_json = eapi.get_power() 64 | get_hostname = eapi.get_hostname_short() 65 | get_power = eapi.get_inventory("power") 66 | 67 | if show_version["cli_commands"] == "old": 68 | location = eapi.try_eapi_command("show snmp location", "enable")["location"] 69 | else: 70 | location = eapi.try_eapi_command("show snmp v2-mib location", "enable")["location"] 71 | 72 | try: 73 | pdu_table = Table() 74 | pdu_table.add_column("PSU Number", justify="center", style="cyan", no_wrap=True) 75 | pdu_table.add_column("PSU Model", justify="center") 76 | pdu_table.add_column("Serial Number", justify="center", style="magenta") 77 | console = Console() 78 | 79 | for pdu in get_power: 80 | pdu_model = get_power[pdu]["name"] 81 | pdu_serial_number = get_power[pdu]["serialNum"] 82 | if "PWR" in pdu_model: 83 | pdu_table.add_row( 84 | pdu, 85 | pdu_model, 86 | pdu_serial_number, 87 | ) 88 | console.print(pdu_table) 89 | except NameError: 90 | print("No PDU data found, please investigate.") 91 | print(f"\nHostname: {switch_hostname}") 92 | print( 93 | f'Model: {show_version["model"]} Hardware Revision: {show_version["hardware_rev"]}' 94 | ) 95 | print(f'Serial Number: {show_version["serial_number"]}') 96 | print(f"Location: {location}") 97 | print(f'OS Version: {show_version["eos_version"]}') 98 | for power_supply in power_status_json: 99 | if power_status_json[power_supply]["state"] != "ok": 100 | console.print( 101 | f"[red]PSU{power_supply} is reporting a state of " 102 | f'{power_status_json[power_supply]["state"]} - ' 103 | f'{get_power[power_supply]["name"]}, SN: ' 104 | f'{get_power[power_supply]["serialNum"]}', 105 | highlight=False, 106 | ) 107 | print(f"\n{get_hostname}# show environment power\n") 108 | things_to_color = { 109 | "Power Loss": "[red]Power Loss[/red]", 110 | "Offline": "[red]Offline[/red]", 111 | "Ok": "[green]Ok[/green]", 112 | } 113 | for old, new in things_to_color.items(): 114 | power_status = power_status.replace(old, new) 115 | console.print(power_status, highlight=False) 116 | 117 | netlib.cleanup_log_if_empty(logger_full_path) 118 | -------------------------------------------------------------------------------- /get_upgrade_checks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | try: 16 | # importing libraries 17 | import argparse 18 | import os 19 | import sys 20 | from datetime import datetime 21 | 22 | import netlib 23 | except ImportError as error: 24 | print(error) 25 | quit() 26 | except Exception as exception: 27 | print(exception) 28 | 29 | parser = argparse.ArgumentParser() 30 | parser.add_argument( 31 | "-s", 32 | "--switch", 33 | type=str, 34 | required=True, 35 | metavar="switch.hostname.com", 36 | help="Hostname of the switch", 37 | ) 38 | parser.add_argument( 39 | "-o", 40 | "--output_directory", 41 | type=str, 42 | required=False, 43 | default=os.environ.get("HOME", "/tmp"), 44 | metavar="some_folder", 45 | help="Folder for all outputs from the script", 46 | ) 47 | args = parser.parse_args() 48 | switch_hostname = args.switch 49 | output_dir = args.output_directory 50 | # check for environment variables for TACACS username & password, prompt if missing 51 | username, password = netlib.get_credentials("TACACS") 52 | 53 | # open a file for logging errors 54 | start_logger = netlib.setup_logging("get_upgrade_checks", output_dir) 55 | logger = start_logger[0] 56 | logger_full_path = start_logger[1] 57 | 58 | # instantiate the eAPI library 59 | eapi = netlib.AristaPyeapi(username, password, switch_hostname, logger) 60 | logs_time_stamp = datetime.now().strftime("%Y-%m-%d_%H:%M:%S") 61 | 62 | try: 63 | switch_hostname_short = eapi.get_hostname_short() 64 | show_command_array = [ 65 | "show ip route vrf all", 66 | "show ipv6 route vrf all", 67 | "show ip route summary", 68 | "show ipv6 route summary", 69 | "show ip bgp summary vrf all", 70 | "show ipv6 bgp summary vrf all", 71 | "show ip ospf neighbor", 72 | "show ipv6 ospf neighbor", 73 | "show ospfv3 neighbor", 74 | "show interfaces status", 75 | "show interfaces counters errors", 76 | "show interfaces counters discards", 77 | "show interfaces transceiver", 78 | "show port-channel summary", 79 | "show lldp neighbors", 80 | "show mlag detail", 81 | "show mlag interfaces", 82 | "show ip arp", 83 | "show inventory", 84 | "show version", 85 | "show extensions", 86 | "show boot-extensions", 87 | ] 88 | 89 | show_version = eapi.get_version() 90 | switch_eos_version = show_version["eos_version"] 91 | switch_model = show_version["model"] 92 | if ("DCS-750" in switch_model) or ("DCS-780" in switch_model): 93 | show_command_array.insert(8, "show module") 94 | filename = ( 95 | f"{output_dir}/{switch_hostname_short}_{switch_eos_version}_" 96 | f"{logs_time_stamp}_upgrade_checks.txt" 97 | ) 98 | 99 | for show_command in show_command_array: 100 | print(f"Grabbing outputs for [{show_command}]") 101 | command_output = eapi.try_eapi_command(show_command, "enable", "text") 102 | with open(filename, "a+") as my_file: 103 | my_file.write( 104 | "\n##############################################################\n" 105 | ) 106 | my_file.write(f"{switch_hostname_short}# {show_command}") 107 | my_file.write( 108 | "\n##############################################################\n\n" 109 | ) 110 | if command_output is None: 111 | my_file.write("Command returned no data.\n") 112 | else: 113 | my_file.write(command_output) 114 | print(f"Finished outputing all commands to {filename}") 115 | 116 | except KeyboardInterrupt: 117 | print("Caught Keyboard Interrupt - Exiting the program.") 118 | sys.exit() 119 | 120 | netlib.cleanup_log_if_empty(logger_full_path) 121 | -------------------------------------------------------------------------------- /get_reload_cause.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | try: 16 | # importing libraries 17 | import argparse 18 | import datetime 19 | import os 20 | from datetime import timedelta 21 | 22 | from dateutil.relativedelta import relativedelta 23 | from rich.console import Console 24 | from rich.table import Table 25 | 26 | import netlib 27 | except ImportError as error: 28 | print(error) 29 | quit() 30 | except Exception as exception: 31 | print(exception) 32 | 33 | parser = argparse.ArgumentParser() 34 | parser.add_argument( 35 | "-s", 36 | "--switch", 37 | type=str, 38 | required=True, 39 | metavar="switch.hostname.com", 40 | help="Hostname of the switch", 41 | ) 42 | parser.add_argument( 43 | "-o", 44 | "--output_directory", 45 | type=str, 46 | required=False, 47 | default=os.environ.get("HOME", "/tmp"), 48 | metavar="some_folder", 49 | help="Folder for all outputs from the script", 50 | ) 51 | args = parser.parse_args() 52 | switch_hostname = args.switch 53 | output_dir = args.output_directory 54 | username, password = netlib.get_credentials("TACACS") 55 | 56 | # open a file for logging errors 57 | start_logger = netlib.setup_logging("port_data", output_dir) 58 | logger = start_logger[0] 59 | logger_full_path = start_logger[1] 60 | 61 | ping_results = netlib.util.check_ping(switch_hostname) 62 | 63 | if ping_results < 10: 64 | # instantiate the eAPI library 65 | eapi = netlib.AristaPyeapi(username, password, switch_hostname, logger) 66 | 67 | show_version = eapi.get_version() 68 | reload_cause = eapi.get_reload_cause() 69 | if reload_cause["resetCauses"]: 70 | reload_reason = reload_cause["resetCauses"][0]["description"] 71 | reload_timestamp = str( 72 | datetime.datetime.utcfromtimestamp( 73 | reload_cause["resetCauses"][0]["timestamp"] 74 | ) 75 | ) 76 | now = datetime.datetime.utcnow() 77 | then = datetime.datetime.utcfromtimestamp( 78 | reload_cause["resetCauses"][0]["timestamp"] 79 | ) 80 | difference = str(timedelta(seconds=(now - then).total_seconds())) 81 | rel_diff = relativedelta(now, then) 82 | reload_recommendation = reload_cause["resetCauses"][0]["recommendedAction"] 83 | show_version_uptime = show_version["uptime"] 84 | show_version_uptime = str(timedelta(seconds=show_version_uptime)) 85 | 86 | print(f"Hostname: {switch_hostname}") 87 | print( 88 | f'Model: {show_version["model"]} ' 89 | f'Hardware Revision: {show_version["hardware_rev"]}' 90 | ) 91 | print(f'Serial Number: {show_version["serial_number"]}') 92 | print(f'OS Version: {show_version["eos_version"]}') 93 | 94 | console = Console() 95 | reload_table = Table(show_header=False) 96 | reload_table.add_column("Query", justify="left") 97 | reload_table.add_column("Data", justify="left", max_width=40) 98 | 99 | reload_table.add_row( 100 | "Reload Reason", 101 | reload_reason, 102 | ) 103 | reload_table.add_row( 104 | "Recommendation", 105 | reload_recommendation, 106 | ) 107 | reload_table.add_row( 108 | "Last Reboot Date (UTC)", 109 | reload_timestamp, 110 | ) 111 | reload_table.add_row( 112 | "Time Since Last Reboot", 113 | difference, 114 | ) 115 | reload_table.add_row( 116 | "Total Current Uptime", 117 | show_version_uptime, 118 | ) 119 | 120 | if reload_cause["resetCauses"]: 121 | console.print(reload_table) 122 | print( 123 | "Switch online for:", 124 | rel_diff.years, 125 | "years,", 126 | rel_diff.months, 127 | "months,", 128 | rel_diff.days, 129 | "days,", 130 | rel_diff.hours, 131 | "hours,", 132 | rel_diff.minutes, 133 | "minutes", 134 | ) 135 | if "debugInfo" in reload_cause["resetCauses"][0]: 136 | print("Debug Information will be written to debug.txt file.") 137 | with open("debug.txt", "w") as file: 138 | for each_line in reload_cause["resetCauses"][0]["debugInfo"]: 139 | file.write("%s\n" % each_line) 140 | else: 141 | print("No debug information available") 142 | else: 143 | print("No information available about reload.") 144 | elif ping_results >= 10: 145 | print(f"{switch_hostname} is not pinging, please check console and inspect device.") 146 | 147 | netlib.cleanup_log_if_empty(logger_full_path) 148 | -------------------------------------------------------------------------------- /get_show_tech_and_agent_logs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | try: 16 | # importing libraries 17 | import argparse 18 | import os 19 | import sys 20 | import tarfile 21 | from datetime import datetime 22 | 23 | import pexpect 24 | 25 | import netlib 26 | except ImportError as error: 27 | print(error) 28 | quit() 29 | except Exception as exception: 30 | print(exception) 31 | 32 | parser = argparse.ArgumentParser() 33 | parser.add_argument( 34 | "-s", 35 | "--switch", 36 | type=str, 37 | required=True, 38 | metavar="switch.hostname.com", 39 | help="Hostname of the switch", 40 | ) 41 | parser.add_argument( 42 | "-t", 43 | "--ticket", 44 | type=str, 45 | required=True, 46 | metavar="123456", 47 | help="Ticket number (INC or SR)", 48 | ) 49 | parser.add_argument( 50 | "-o", 51 | "--output_directory", 52 | type=str, 53 | required=False, 54 | default=os.environ.get("HOME", "/tmp"), 55 | metavar="some_folder", 56 | help="Folder for all outputs from the script", 57 | ) 58 | args = parser.parse_args() 59 | switch_hostname = args.switch 60 | ticket_number = args.ticket 61 | output_dir = args.output_directory 62 | username, password = netlib.get_credentials("TACACS") 63 | 64 | # open a file for logging errors 65 | start_logger = netlib.setup_logging("get_show_tech", output_dir) 66 | logger = start_logger[0] 67 | logger_full_path = start_logger[1] 68 | 69 | # instantiate the eAPI library 70 | eapi = netlib.AristaPyeapi(username, password, switch_hostname, logger) 71 | logs_time_stamp = datetime.now().strftime("%Y-%m-%d_%H:%M:%S") 72 | 73 | try: 74 | time_start = datetime.now() 75 | switch_hostname_short = eapi.get_hostname_short() 76 | base_filename = f"{ticket_number}_{switch_hostname_short}_{logs_time_stamp}_" 77 | print("Generating show tech-support file.") 78 | show_tech = eapi.try_eapi_command("show tech-support", "enable", "text") 79 | print("Generating show agent logs file.") 80 | agent_logs = eapi.try_eapi_command("show agent logs", "enable")["output"] 81 | print("Generating show agent qtrace file.") 82 | qtrace_filename = f"{base_filename}agent_qtrace.log.gz" 83 | eapi.try_eapi_command( 84 | f"show agent qtrace | gzip >/mnt/flash/{qtrace_filename}", "enable", "text" 85 | ) 86 | print("Zipping up /var/qt/log into a tar.") 87 | qtlog_filename = f"{base_filename}qt_logs.tar.gz" 88 | eapi.try_eapi_command( 89 | f"bash timeout 10 sudo tar -czvf - /var/log/qt/ > /mnt/flash/{qtlog_filename}", 90 | "enable", 91 | "text", 92 | ) 93 | print("Zipping up /mnt/flash/schedule/tech-support/* into a tar.") 94 | historical_techs_filename = f"{base_filename}historical_techs.tar.gz" 95 | eapi.try_eapi_command( 96 | f"bash timeout 10 tar -cvf - /mnt/flash/schedule/tech-support/* > " 97 | f"/mnt/flash/{historical_techs_filename}", 98 | "enable", 99 | "text", 100 | ) 101 | print("Generating show logging system file.") 102 | logging_system = eapi.try_eapi_command("show logging system", "enable")["output"] 103 | print("Generating show aaa accounting logs file.") 104 | aaa_accounting = eapi.try_eapi_command("show aaa accounting logs", "enable")[ 105 | "output" 106 | ] 107 | print("Generating output for bash df -h.") 108 | disk_free = eapi.try_eapi_command("bash timeout 2 df -h", "enable")["messages"] 109 | print("Generating output for dir all-filesystems.") 110 | dir_all_file_sys = eapi.try_eapi_command("dir all-filesystems ", "enable")[ 111 | "messages" 112 | ] 113 | 114 | except KeyboardInterrupt: 115 | print("Caught Keyboard Interrupt - Exiting the program.") 116 | sys.exit() 117 | 118 | array_of_files = [] 119 | 120 | 121 | def json_to_file(filename, json_source): 122 | new_filename = ( 123 | f"{output_dir}/{ticket_number}_{switch_hostname_short}" 124 | f"_{logs_time_stamp}_{filename}" 125 | ) 126 | with open(new_filename, "w") as my_file: 127 | if filename == "disk_free.txt" or filename == "dir_all_filesystems.txt": 128 | for each_line in json_source: 129 | my_file.write("%s\n" % each_line) 130 | else: 131 | my_file.write(json_source) 132 | print(f"Saved {new_filename} to this server.") 133 | array_of_files.append(new_filename) 134 | 135 | 136 | def scp_transfer(filename): 137 | scp_command = f"scp {username}@{switch_hostname}:/mnt/flash/{filename} {output_dir}" 138 | print(f"Attempting to SCP {filename} from the switch to this server.") 139 | try: 140 | pexpect.run(scp_command) 141 | child = pexpect.spawn(scp_command) 142 | child.expect(".*assw.*") 143 | child.sendline(password) 144 | child.expect(pexpect.EOF, timeout=5) 145 | except Exception as exception: 146 | print(f"WARNING!! Copy failed for {filename}: please check logs!") 147 | logger.warning(f"{switch_hostname} copy failed: {str(exception)}") 148 | else: 149 | print(f"Copy successful for {filename}.") 150 | print(f"Removing {filename} from the switch /mnt/flash now") 151 | eapi.try_eapi_command(f"delete flash:{filename}", "enable") 152 | array_of_files.append(f"{output_dir}/{filename}") 153 | 154 | 155 | scp_transfer(qtrace_filename) 156 | scp_transfer(qtlog_filename) 157 | scp_transfer(historical_techs_filename) 158 | 159 | json_to_file("tech_support.txt", show_tech) 160 | json_to_file("disk_free.txt", disk_free) 161 | json_to_file("agent_logs.txt", agent_logs) 162 | json_to_file("logging_system.txt", logging_system) 163 | json_to_file("aaa_accounting.txt", aaa_accounting) 164 | json_to_file("dir_all_filesystems.txt", dir_all_file_sys) 165 | 166 | 167 | try: 168 | tar = tarfile.open(f"{output_dir}/{base_filename}tac_pack.tar.gz", "w:gz") 169 | for tac_file in array_of_files: 170 | tar.add(tac_file) 171 | tar.close() 172 | except Exception as exception: 173 | print(exception) 174 | else: 175 | print( 176 | f"Successfully tar'ed up all files into {output_dir}/{base_filename}" 177 | f"tac_pack.tar.gz and now removing individual files." 178 | ) 179 | for tac_file in array_of_files: 180 | os.remove(tac_file) 181 | print("Done! Please don't forget to upload this TAC pack to Arista.") 182 | time_finish = datetime.now() 183 | time_end = (time_finish - time_start).total_seconds() 184 | print( 185 | "{}: Took {:.2f} seconds to copy all needed files for TAC.".format( 186 | switch_hostname, time_end 187 | ) 188 | ) 189 | 190 | netlib.cleanup_log_if_empty(logger_full_path) 191 | -------------------------------------------------------------------------------- /get_errors_and_discards.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | try: 16 | # importing libraries 17 | import argparse 18 | import datetime 19 | import os 20 | import sys 21 | import time 22 | 23 | import numpy as np 24 | from rich.console import Console 25 | from rich.table import Table 26 | 27 | import netlib 28 | except ImportError as error: 29 | print(error) 30 | quit() 31 | except Exception as exception: 32 | print(exception) 33 | 34 | 35 | def read_the_errors(): 36 | get_errors = eapi.get_interface_errors() 37 | interface_counters_table = Table(title="show interfaces counters errors | nz") 38 | interface_counters_table.add_column("Port", justify="right", style="magenta") 39 | interface_counters_table.add_column("FCS", justify="right") 40 | interface_counters_table.add_column("Align", justify="right") 41 | interface_counters_table.add_column("Symbol", justify="right") 42 | interface_counters_table.add_column("Rx", justify="right") 43 | interface_counters_table.add_column("Runts", justify="right") 44 | interface_counters_table.add_column("Giants", justify="right") 45 | interface_counters_table.add_column("Tx", justify="right") 46 | interface_errors_array = {} 47 | number_of_errors = 0 48 | for interface in get_errors: 49 | interface_errors = get_errors[interface] 50 | port_alignment_errors = interface_errors["alignmentErrors"] 51 | port_fcs_errors = interface_errors["fcsErrors"] 52 | port_frame_too_longs = interface_errors["frameTooLongs"] 53 | port_frame_too_shorts = interface_errors["frameTooShorts"] 54 | port_in_errors = interface_errors["inErrors"] 55 | port_out_errors = interface_errors["outErrors"] 56 | port_symbol_errors = interface_errors["symbolErrors"] 57 | if ( 58 | port_alignment_errors != 0 59 | or port_fcs_errors != 0 60 | or port_frame_too_longs != 0 61 | or port_frame_too_shorts != 0 62 | or port_in_errors != 0 63 | or port_out_errors != 0 64 | or port_symbol_errors != 0 65 | ): 66 | number_of_errors += 1 67 | interface_counters_table.add_row( 68 | interface, 69 | str(port_fcs_errors), 70 | str(port_alignment_errors), 71 | str(port_symbol_errors), 72 | str(port_in_errors), 73 | str(port_frame_too_shorts), 74 | str(port_frame_too_longs), 75 | str(port_out_errors), 76 | ) 77 | interface_errors_array[interface] = [ 78 | port_fcs_errors, 79 | port_alignment_errors, 80 | port_symbol_errors, 81 | port_in_errors, 82 | port_frame_too_shorts, 83 | port_frame_too_longs, 84 | port_out_errors, 85 | ] 86 | if number_of_errors > 0: 87 | console.print(interface_counters_table) 88 | else: 89 | print(f"No errors seen currently for {hostname_short}.") 90 | return interface_errors_array 91 | 92 | 93 | def read_the_discards(): 94 | get_discards = eapi.get_interface_discards() 95 | interface_discard_array = {} 96 | interface_discard_table = Table(title="show interfaces counters discards | nz") 97 | interface_discard_table.add_column("Port", justify="right", style="magenta") 98 | interface_discard_table.add_column("In Discards", justify="right") 99 | interface_discard_table.add_column("Out Discards", justify="right") 100 | number_of_discards = 0 101 | for interface in get_discards: 102 | interface_discards = get_discards[interface] 103 | port_in_discards = interface_discards["inDiscards"] 104 | port_out_discards = interface_discards["outDiscards"] 105 | if port_in_discards != 0 or port_out_discards != 0: 106 | number_of_discards += 1 107 | interface_discard_table.add_row( 108 | interface, str(port_in_discards), str(port_out_discards) 109 | ) 110 | interface_discard_array[interface] = [port_in_discards, port_out_discards] 111 | if number_of_discards > 0: 112 | console.print(interface_discard_table) 113 | else: 114 | print(f"No discards seen currently for {hostname_short}.") 115 | return interface_discard_array 116 | 117 | 118 | parser = argparse.ArgumentParser() 119 | parser.add_argument( 120 | "-s", 121 | "--switches", 122 | type=str, 123 | required=True, 124 | metavar="switches.txt", 125 | help="A list of switch hostnames (FQDN) one per line", 126 | ) 127 | parser.add_argument( 128 | "-o", 129 | "--output_directory", 130 | type=str, 131 | required=False, 132 | default=os.environ.get("HOME", "/tmp"), 133 | metavar="some_folder", 134 | help="Folder for all outputs from the script", 135 | ) 136 | 137 | console = Console() 138 | 139 | args = parser.parse_args() 140 | switch_list = open(args.switches) 141 | output_dir = args.output_directory 142 | username, password = netlib.get_credentials("TACACS") 143 | 144 | # open a file for logging errors 145 | start_logger = netlib.setup_logging("get_errors_and_discards", output_dir) 146 | logger = start_logger[0] 147 | logger_full_path = start_logger[1] 148 | 149 | all_switch_errors = {} 150 | all_switch_discards = {} 151 | 152 | print("Please wait while first scans for all complete.") 153 | 154 | for switch_hostname in switch_list: 155 | # instantiate the eAPI library 156 | eapi = netlib.AristaPyeapi(username, password, switch_hostname, logger) 157 | hostname_short = eapi.get_hostname_short() 158 | current_time = datetime.datetime.fromtimestamp(time.time()).strftime( 159 | "%Y-%m-%d %H:%M:%S" 160 | ) 161 | print( 162 | "\n##########################################" 163 | "#############################################" 164 | ) 165 | print( 166 | f"Checking {hostname_short} for errors and discards " 167 | f"the first time @ {current_time}" 168 | ) 169 | print( 170 | "\n##########################################" 171 | "#############################################" 172 | ) 173 | 174 | try: 175 | all_switch_errors[hostname_short] = read_the_errors() 176 | all_switch_discards[hostname_short] = read_the_discards() 177 | 178 | current_time = datetime.datetime.fromtimestamp(time.time()).strftime( 179 | "%Y-%m-%d %H:%M:%S" 180 | ) 181 | print( 182 | "\n##########################################" 183 | "#############################################" 184 | ) 185 | print( 186 | f"Checking {hostname_short} for errors and discards again @ {current_time}" 187 | ) 188 | print( 189 | "\n##########################################" 190 | "#############################################" 191 | ) 192 | 193 | second_interface_errors_array = read_the_errors() 194 | second_interface_discard_array = read_the_discards() 195 | compare_interface_errors_change = {} 196 | compare_interface_discards_change = {} 197 | for interface in all_switch_errors[hostname_short]: 198 | compare_interface_errors_change[interface] = np.subtract( 199 | second_interface_errors_array[interface], 200 | all_switch_errors[hostname_short][interface], 201 | ) 202 | for interface in all_switch_discards[hostname_short]: 203 | compare_interface_discards_change[interface] = np.subtract( 204 | second_interface_discard_array[interface], 205 | all_switch_discards[hostname_short][interface], 206 | ) 207 | for interface in compare_interface_errors_change: 208 | int_values = compare_interface_errors_change[interface] 209 | if np.any(int_values): 210 | print("!!!!!!!!!!!!!!!!!!!! ATTENTION !!!!!!!!!!!!!!!!!!!!") 211 | print( 212 | f"Interface: {interface} saw new errors. FCS: {str(int_values[0])} " 213 | f"Alignment: {str(int_values[1])} Symbol: {str(int_values[2])} Rx: " 214 | f"{str(int_values[3])} Runts: {str(int_values[4])} Giants: " 215 | f"{str(int_values[5])} Tx: {str(int_values[6])}" 216 | ) 217 | for interface in compare_interface_discards_change: 218 | int_values = compare_interface_discards_change[interface] 219 | if np.any(int_values): 220 | print("!!!!!!!!!!!!!!!!!!!! ATTENTION !!!!!!!!!!!!!!!!!!!!") 221 | print( 222 | f"Interface: {interface} saw new discards. Discards In: " 223 | f"{str(int_values[0])} Discards Out: {str(int_values[1])}" 224 | ) 225 | 226 | except KeyboardInterrupt: 227 | print("Caught Keyboard Interrupt - Exiting the program.") 228 | sys.exit() 229 | 230 | netlib.cleanup_log_if_empty(logger_full_path) 231 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /get_port_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | try: 16 | # importing libraries 17 | import argparse 18 | import os 19 | import re 20 | 21 | from rich.console import Console 22 | from rich.table import Table 23 | 24 | import netlib 25 | except ImportError as error: 26 | print(error) 27 | quit() 28 | except Exception as exception: 29 | print(exception) 30 | 31 | parser = argparse.ArgumentParser() 32 | parser.add_argument( 33 | "-p", "--port", type=str, required=True, metavar="32/1", help="The port to look at" 34 | ) 35 | parser.add_argument( 36 | "-s", 37 | "--switch", 38 | type=str, 39 | required=True, 40 | metavar="switch.hostname.com", 41 | help="Hostname of the switch", 42 | ) 43 | parser.add_argument( 44 | "-o", 45 | "--output_directory", 46 | type=str, 47 | required=False, 48 | default=os.environ.get("HOME", "/tmp"), 49 | metavar="some_folder", 50 | help="Folder for all outputs from the script", 51 | ) 52 | args = parser.parse_args() 53 | switch_hostname = args.switch 54 | port_name = args.port 55 | output_dir = args.output_directory 56 | username, password = netlib.get_credentials("TACACS") 57 | 58 | 59 | start_logger = netlib.setup_logging("port_data", output_dir) 60 | logger = start_logger[0] 61 | logger_full_path = start_logger[1] 62 | 63 | console = Console() 64 | 65 | eapi = netlib.AristaPyeapi(username, password, switch_hostname, logger) 66 | 67 | port_name_long = f"Ethernet{port_name}" 68 | if "/1" in port_name: 69 | base_port = re.sub(r"\/1$", "", port_name) 70 | dash2 = re.sub(r"\/1$", "/2", port_name_long) 71 | dash3 = re.sub(r"\/1$", "/3", port_name_long) 72 | dash4 = re.sub(r"\/1$", "/4", port_name_long) 73 | else: 74 | base_port = port_name 75 | dash2 = base_port 76 | dash3 = base_port 77 | dash4 = base_port 78 | 79 | 80 | def show_switch_version(): 81 | show_version = eapi.get_version() 82 | switch_version_table = Table() 83 | switch_version_table.add_column( 84 | "Hostname", justify="left", style="magenta", no_wrap=True, vertical="middle" 85 | ) 86 | switch_version_table.add_column("Model", justify="center", no_wrap=True) 87 | switch_version_table.add_column("OS\nVersion", justify="center", no_wrap=True) 88 | switch_version_table.add_column("Serial\nNumber", justify="center", no_wrap=True) 89 | 90 | switch_version_table.add_row( 91 | hostname_short, 92 | show_version["model"], 93 | show_version["eos_version"], 94 | show_version["serial_number"], 95 | ) 96 | print("") 97 | console.print(switch_version_table) 98 | 99 | 100 | def interface_inventory(base_port, port_name_long): 101 | get_interface_inventory = eapi.get_inventory("interfaces")[base_port] 102 | inventory_vendor = get_interface_inventory["mfgName"] 103 | port_inventory_table = Table() 104 | port_inventory_table.add_column( 105 | "Hostname", justify="left", style="magenta", no_wrap=True, vertical="middle" 106 | ) 107 | port_inventory_table.add_column("Port", justify="center", no_wrap=True) 108 | port_inventory_table.add_column("Manufacturer", justify="center", no_wrap=True) 109 | port_inventory_table.add_column("Model", justify="center", no_wrap=True) 110 | port_inventory_table.add_column("Serial\nNumber", justify="center", no_wrap=True) 111 | 112 | port_inventory_table.add_row( 113 | hostname_short, 114 | port_name_long, 115 | inventory_vendor, 116 | get_interface_inventory["modelName"], 117 | get_interface_inventory["serialNum"], 118 | ) 119 | print("") 120 | print(f"{hostname_short} # show inventory | inc {port_name}") 121 | console.print(port_inventory_table) 122 | return inventory_vendor 123 | 124 | 125 | def interface_errors(port_name_long): 126 | get_errors = eapi.try_eapi_command("show interfaces counters errors", "enable")[ 127 | "interfaceErrorCounters" 128 | ][port_name_long] 129 | interface_counters_table = Table() 130 | interface_counters_table.add_column( 131 | "Port", justify="left", style="magenta", no_wrap=True 132 | ) 133 | interface_counters_table.add_column("FCS", justify="center", no_wrap=True) 134 | interface_counters_table.add_column("Align", justify="center", no_wrap=True) 135 | interface_counters_table.add_column("Symbol", justify="center", no_wrap=True) 136 | interface_counters_table.add_column("RX", justify="center", no_wrap=True) 137 | interface_counters_table.add_column("Runts", justify="center", no_wrap=True) 138 | interface_counters_table.add_column("Giants", justify="center", no_wrap=True) 139 | interface_counters_table.add_column("TX", justify="center", no_wrap=True) 140 | 141 | interface_counters_table.add_row( 142 | port_name_long, 143 | str(get_errors["fcsErrors"]), 144 | str(get_errors["alignmentErrors"]), 145 | str(get_errors["symbolErrors"]), 146 | str(get_errors["inErrors"]), 147 | str(get_errors["frameTooShorts"]), 148 | str(get_errors["frameTooLongs"]), 149 | str(get_errors["outErrors"]), 150 | ) 151 | print(f"\n{hostname_short}# show interfaces counters errors") 152 | console.print(interface_counters_table) 153 | 154 | 155 | def interface_transceiver(port_name_long, dash2, dash3, dash4): 156 | get_interface_transceiver = eapi.try_eapi_command( 157 | "show interfaces transceiver", "enable" 158 | )["interfaces"] 159 | transceiver_results_table = Table() 160 | transceiver_results_table.add_column( 161 | "Port", justify="left", style="magenta", no_wrap=True 162 | ) 163 | transceiver_results_table.add_column("Temp(C)", justify="center", no_wrap=True) 164 | transceiver_results_table.add_column("Voltage", justify="center", no_wrap=True) 165 | transceiver_results_table.add_column("Bias Current", justify="center", no_wrap=True) 166 | transceiver_results_table.add_column("Tx (dBm)", justify="center", no_wrap=True) 167 | transceiver_results_table.add_column("Rx (dBm)", justify="center", no_wrap=True) 168 | 169 | def get_transceiver(port): 170 | levels = get_interface_transceiver[port] 171 | transceiver_results_table.add_row( 172 | port, 173 | f'{levels["temperature"]:.2f}', 174 | f'{levels["voltage"]:.2f}', 175 | f'{levels["txBias"]:.2f}', 176 | f'{levels["txPower"]:.2f}', 177 | f'{levels["rxPower"]:.2f}', 178 | ) 179 | 180 | get_transceiver(port_name_long) 181 | if "/1" in port_name_long: 182 | get_transceiver(dash2) 183 | get_transceiver(dash3) 184 | get_transceiver(dash4) 185 | print("") 186 | print(f"{hostname_short}# show interfaces transceiver") 187 | transceiver_results_table.float_format = ".2" 188 | console.print(transceiver_results_table) 189 | 190 | 191 | def mac_details(port_name): 192 | mac_detail_command = f"show interfaces Ethernet {port_name} mac detail" 193 | get_mac_detail = eapi.try_eapi_command(mac_detail_command, "enable", "text") 194 | print(f"\n{hostname_short}# {mac_detail_command}") 195 | print(get_mac_detail) 196 | 197 | 198 | show_version = eapi.get_version() 199 | hostname_short = eapi.get_hostname_short() 200 | lookup_lldp_command = str(f"show lldp neighbors Ethernet {port_name}") 201 | results_lldp = eapi.try_eapi_command(lookup_lldp_command, "enable")["lldpNeighbors"] 202 | current_port_status = eapi.get_interfaces_status()[port_name_long] 203 | current_port_desc = current_port_status["description"] 204 | current_port_link_status = current_port_status["linkStatus"] 205 | current_port_line_protocol = current_port_status["lineProtocolStatus"] 206 | if not results_lldp: 207 | # if LLDP returned no data, print an error to the screen and the log file 208 | print( 209 | f"\nATTN: {port_name} has no LLDP neighbor. " 210 | f"Current description: {current_port_desc}" 211 | ) 212 | print( 213 | f"Current link status: {current_port_link_status} line protocol " 214 | f"status: {current_port_line_protocol}" 215 | ) 216 | logger.warning( 217 | f"{switch_hostname} {port_name} has no LLDP neighbor. " 218 | f"Current description: {current_port_desc}" 219 | ) 220 | else: 221 | # get the neighbor information from LLDP 222 | my_neighbor_port_long = results_lldp[0]["neighborPort"] 223 | my_neighbor_port = my_neighbor_port_long.replace("Ethernet", "Et") 224 | my_neighbor_port = my_neighbor_port_long.replace("Management", "Ma") 225 | my_neighbor_device = results_lldp[0]["neighborDevice"] 226 | lldp_neighbor_table = Table() 227 | lldp_neighbor_table.add_column( 228 | "Hostname", justify="left", style="magenta", no_wrap=True, vertical="middle" 229 | ) 230 | lldp_neighbor_table.add_column("Local Port", justify="center") 231 | lldp_neighbor_table.add_column("LLDP Neighbor", justify="left") 232 | lldp_neighbor_table.add_column("Remote Port", justify="center") 233 | lldp_neighbor_table.add_row( 234 | hostname_short, port_name_long, my_neighbor_device, my_neighbor_port_long 235 | ) 236 | print(f"{hostname_short}# show lldp neighbors") 237 | console.print(lldp_neighbor_table) 238 | 239 | print("\n**************************************************************************") 240 | show_switch_version() 241 | inventory_vendor = interface_inventory(base_port, port_name_long) 242 | interface_errors(port_name_long) 243 | if "Mellanox" not in inventory_vendor and "Amphenol" not in inventory_vendor: 244 | interface_transceiver(port_name_long, dash2, dash3, dash4) 245 | mac_details(port_name) 246 | 247 | print("**************************************************************************") 248 | 249 | if ( 250 | results_lldp 251 | and "netapp" not in my_neighbor_device 252 | and "Mellanox" not in inventory_vendor 253 | and "Amphenol" not in inventory_vendor 254 | ): 255 | eapi.reset(username, password, my_neighbor_device, logger) 256 | 257 | port_name = my_neighbor_port.replace("Ethernet", "") 258 | port_name_long = f"Ethernet{port_name}" 259 | hostname_short = eapi.get_hostname_short() 260 | if "/1" in port_name: 261 | base_port = re.sub(r"\/1$", "", port_name) 262 | dash2 = re.sub(r"\/1$", "/2", port_name_long) 263 | dash3 = re.sub(r"\/1$", "/3", port_name_long) 264 | dash4 = re.sub(r"\/1$", "/4", port_name_long) 265 | else: 266 | base_port = port_name 267 | show_switch_version() 268 | interface_inventory(base_port, port_name_long) 269 | interface_errors(port_name_long) 270 | interface_transceiver(port_name_long, dash2, dash3, dash4) 271 | mac_details(port_name) 272 | 273 | netlib.cleanup_log_if_empty(logger_full_path) 274 | -------------------------------------------------------------------------------- /netlib/arista_pyeapi.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | try: 14 | # importing libraries 15 | import datetime 16 | import json 17 | import logging 18 | import os 19 | import socket 20 | import sys 21 | 22 | import pyeapi 23 | 24 | import netlib.util 25 | except ImportError as error: 26 | print(error) 27 | quit() 28 | except Exception as exception: 29 | print(exception) 30 | 31 | 32 | class AristaPyeapi: 33 | def __init__(self, username, password, switch_hostname, logger=None, timeout=180): 34 | self.switch_hostname = switch_hostname.strip() 35 | self.node = pyeapi.connect( 36 | transport="https", 37 | host=self.switch_hostname, 38 | username=username, 39 | password=password, 40 | timeout=timeout, 41 | return_node=True, 42 | ) 43 | if logger is None: 44 | self.logger = netlib.util.setup_logging("generic_eapi")[0] 45 | else: 46 | self.logger = logger 47 | 48 | def reset(self, username, password, switch_hostname, logger): 49 | self.__init__(username, password, switch_hostname, logger) 50 | 51 | def try_eapi_command( 52 | self, 53 | command, 54 | run_method="run_commands", 55 | encoding_type="json", 56 | ): 57 | try: 58 | pyeapi_log_level = pyeapi.eapilib._LOGGER.getEffectiveLevel() 59 | pyeapi.eapilib._LOGGER.setLevel(logging.CRITICAL) 60 | # lower layer function, using enable or config mode is preferred 61 | if run_method == "run_commands": 62 | command_output = self.node.run_commands(command)[0] 63 | # this one is best for getting show commands, use text encoding for getting 64 | # back the same output as you'd see on the switch screen, default is json 65 | # which will give you back the json formatted show values for better variable 66 | # maninpulation with your scripts 67 | elif run_method == "enable": 68 | if encoding_type == "json": 69 | command_output = self.node.enable(command)[0]["result"] 70 | elif encoding_type == "text": 71 | command_output = self.node.enable(command, encoding="text")[0][ 72 | "result" 73 | ]["output"] 74 | elif run_method == "config": 75 | command_output = self.node.config(command) 76 | elif run_method == "api": 77 | command_output = eval(command) 78 | return command_output 79 | except KeyboardInterrupt: 80 | print("Caught Keyboard Interrupt - Exiting the program.") 81 | sys.exit() 82 | except ImportError as import_error: 83 | print(f"Could not import module: {import_error}") 84 | self.logger.warning(f"Import error: {import_error}") 85 | except json.decoder.JSONDecodeError as json_error: 86 | print("Received unexpected JSON error, but non impacting. Continuing.") 87 | except KeyError as key_error: 88 | print(f"{self.switch_hostname} has a key error.") 89 | self.logger.warning( 90 | f"Key error while running script against {self.switch_hostname}." 91 | ) 92 | except pyeapi.eapilib.CommandError as command_error: 93 | print( 94 | f"Command error while executing [{command}] for {self.switch_hostname}:" 95 | ) 96 | self.logger.warning( 97 | f"Command error on {self.switch_hostname}: for [{command}]\n {str(command_error.command_error)}" 98 | ) 99 | except pyeapi.eapilib.ConnectionError as conn_error: 100 | print( 101 | f"########## WARNING ##########\nError connecting to the eAPI for {self.switch_hostname}:\n--{str(conn_error.message)}\n#############################" 102 | ) 103 | self.logger.warning( 104 | f"Connection error on {self.switch_hostname}:\n--{str(conn_error.message)}" 105 | ) 106 | if "Name or service not known" in str(conn_error.message): 107 | sys.exit(1) 108 | except TypeError as type_error: 109 | print(f"Ran into a Type error: {type_error}") 110 | self.logger.warning( 111 | f"Type error while running scipt against {self.switch_hostname}." 112 | ) 113 | except Exception as exception: 114 | print(f"Hit some other exception: {exception}") 115 | self.logger.warning( 116 | f"Hit another exception against {self.switch_hostname}.\n {str(exception)}" 117 | ) 118 | finally: 119 | if pyeapi_log_level != pyeapi.eapilib._LOGGER.getEffectiveLevel(): 120 | pyeapi.eapilib._LOGGER.setLevel(pyeapi_log_level) 121 | 122 | def cleanup_config_sessions(self): 123 | config_sessions = self.try_eapi_command( 124 | "show configuration sessions", "enable" 125 | )["sessions"] 126 | for session in config_sessions: 127 | removal_command = f"no configure session {session}" 128 | self.try_eapi_command(removal_command, "enable") 129 | 130 | def copy_run_start(self): 131 | self.try_eapi_command("copy running-config startup-config", "enable") 132 | 133 | def get_arp(self): 134 | arp_entries = self.try_eapi_command("show ip arp", "enable")["ipV4Neighbors"] 135 | return arp_entries 136 | 137 | def get_extensions(self): 138 | extensions = self.try_eapi_command("show extensions", "enable")["extensions"] 139 | return extensions 140 | 141 | def get_file_date(self, filename): 142 | get_file_date = self.try_eapi_command( 143 | f"bash timeout 2 sudo date -r /mnt/flash/{filename}", "enable", "text" 144 | ) 145 | if get_file_date: 146 | get_file_date = get_file_date.strip() 147 | return get_file_date 148 | 149 | def get_flash_storage(self): 150 | get_flash_storage = self.try_eapi_command("dir flash:", "enable", "text") 151 | return get_flash_storage 152 | 153 | def get_hostname_short(self): 154 | hostname_short = self.try_eapi_command("show hostname", "enable")["hostname"] 155 | return hostname_short 156 | 157 | def get_interface_discards(self): 158 | get_discards = self.try_eapi_command( 159 | "show interfaces counters discards", "enable" 160 | )["interfaces"] 161 | return get_discards 162 | 163 | def get_interface_errors(self): 164 | get_errors = self.try_eapi_command("show interfaces counters errors", "enable")[ 165 | "interfaceErrorCounters" 166 | ] 167 | return get_errors 168 | 169 | def get_interfaces_status(self): 170 | interfaces_status = self.try_eapi_command("show interfaces status", "enable")[ 171 | "interfaceStatuses" 172 | ] 173 | return interfaces_status 174 | 175 | def get_inventory(self, type): 176 | inventory = self.try_eapi_command("show inventory", "enable") 177 | if type == "interfaces": 178 | return inventory["xcvrSlots"] 179 | elif type == "power": 180 | return inventory["powerSupplySlots"] 181 | elif type == "storage": 182 | return inventory["storageDevices"] 183 | elif type == "system": 184 | return inventory["systemInformation"] 185 | elif type == "linecards": 186 | return inventory["cardSlots"] 187 | else: 188 | return inventory 189 | 190 | def get_ipv6_neighbors(self): 191 | ipv6_neighbors = self.try_eapi_command("show ipv6 neighbors", "enable")[ 192 | "ipV6Neighbors" 193 | ] 194 | return ipv6_neighbors 195 | 196 | def get_lldp_neighbors(self): 197 | lldp_neighbors = self.try_eapi_command("show lldp neighbors", "enable")[ 198 | "lldpNeighbors" 199 | ] 200 | return lldp_neighbors 201 | 202 | def get_mlag(self): 203 | mlag_status = self.try_eapi_command("show mlag", "enable") 204 | state = mlag_status["state"] 205 | neg_status = mlag_status["negStatus"] 206 | local_if_status = mlag_status["localIntfStatus"] 207 | peer_link_status = mlag_status["peerLinkStatus"] 208 | peer_link = mlag_status["peerLink"] 209 | peer_address = mlag_status["peerAddress"] 210 | config_sanity = mlag_status["configSanity"] 211 | mlag_dict = { 212 | "state": state, 213 | "neg_status": neg_status, 214 | "local_if_status": local_if_status, 215 | "peer_link_status": peer_link_status, 216 | "peer_link": peer_link, 217 | "peer_address": peer_address, 218 | "config_sanity": config_sanity, 219 | } 220 | return mlag_dict 221 | 222 | def get_port_channel(self, port_channel): 223 | port_channel_num = port_channel.split("Port-Channel")[1] 224 | port_channel_data = self.try_eapi_command( 225 | f"show port-channel {port_channel_num}", "enable" 226 | )["portChannels"][port_channel] 227 | return port_channel_data 228 | 229 | def get_port_channel_summary(self): 230 | port_channel_summary = self.try_eapi_command( 231 | "show port-channel summary", "enable" 232 | )["portChannels"] 233 | return port_channel_summary 234 | 235 | def get_power(self): 236 | power_status = self.try_eapi_command("show environment power", "enable")[ 237 | "powerSupplies" 238 | ] 239 | return power_status 240 | 241 | def get_reload_cause(self): 242 | show_reload_cause = self.try_eapi_command("show reload cause", "enable") 243 | return show_reload_cause 244 | 245 | def get_revision_number(self, banner_type): 246 | show_banner = self.try_eapi_command(f"show banner {banner_type}", "enable") 247 | if banner_type == "login": 248 | banner = show_banner["loginBanner"] 249 | elif banner_type == "motd": 250 | banner = show_banner["motd"] 251 | if banner: 252 | try: 253 | revision = banner.split("revision ")[1] 254 | revision = revision.split()[0] 255 | except: 256 | revision = "wrong" 257 | else: 258 | revision = "missing" 259 | return revision 260 | 261 | def get_storage(self): 262 | get_storage = self.try_eapi_command( 263 | "bash timeout 2 sudo df -lh", "enable", "text" 264 | ) 265 | return get_storage 266 | 267 | def get_storage_overlay(self): 268 | get_storage_overlay = self.try_eapi_command( 269 | "bash timeout 2 sudo df -lh /.overlay", "enable", "text" 270 | ) 271 | return get_storage_overlay 272 | 273 | def get_version(self): 274 | show_version = self.try_eapi_command("show version", "enable") 275 | switch_eos_version = show_version["version"] 276 | switch_hardware_rev = show_version["hardwareRevision"] 277 | switch_model = show_version["modelName"] 278 | switch_serial_number = show_version["serialNumber"] 279 | switch_system_mac = show_version["systemMacAddress"] 280 | switch_uptime = show_version["uptime"] 281 | if switch_eos_version.startswith(("4.19", "4.20")) is True: 282 | switch_cli_commands = "old" 283 | else: 284 | switch_cli_commands = "new" 285 | show_version_dict = { 286 | "eos_version": switch_eos_version, 287 | "hardware_rev": switch_hardware_rev, 288 | "model": switch_model, 289 | "serial_number": switch_serial_number, 290 | "system_mac": switch_system_mac, 291 | "uptime": switch_uptime, 292 | "cli_commands": switch_cli_commands, 293 | } 294 | return show_version_dict 295 | 296 | def get_vrf(self): 297 | which_commands = self.get_version()["cli_commands"] 298 | if which_commands == "old": 299 | configured_vrfs = self.try_eapi_command("show vrf", "enable", "text") 300 | if "management" in configured_vrfs: 301 | vrf = "management" 302 | else: 303 | vrf = "default" 304 | elif which_commands == "new": 305 | configured_vrfs = self.try_eapi_command("show vrf", "enable") 306 | if "management" in configured_vrfs["vrfs"]: 307 | vrf = "management" 308 | else: 309 | vrf = "default" 310 | else: 311 | vrf = "default" 312 | return vrf 313 | 314 | def peer_supervisor_command(self, peer_command): 315 | run_peer_command = self.try_eapi_command( 316 | f'bash timeout 10 Cli -p15 -c "session peer-supervisor {peer_command}"', 317 | "enable", 318 | "text", 319 | ) 320 | return run_peer_command 321 | 322 | def set_lacp_timeout(self, port_channel, timeout_value): 323 | set_command = f'self.node.api("interfaces").set_lacp_timeout("Port-Channel{port_channel}", {timeout_value})' 324 | result = self.try_eapi_command(set_command, "api") 325 | return result 326 | --------------------------------------------------------------------------------