├── .env.example ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── config.yaml ├── data └── configs │ └── dummy.txt ├── requirements.txt ├── src ├── __init__.py ├── helpers.py ├── nr_backup_configs.py └── nr_print_inventory.py └── tests ├── test_001_helpers.py ├── test_002_backup_configs.py └── test_003_print_inventory.py /.env.example: -------------------------------------------------------------------------------- 1 | DEVICE_USERNAME= 2 | DEVICE_PASSWORD= 3 | NB_TOKEN= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data/configs/leaf* 2 | data/configs/spine* 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 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 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # celery beat schedule file 97 | celerybeat-schedule 98 | 99 | # SageMath parsed files 100 | *.sage.py 101 | 102 | # Environments 103 | .env 104 | .venv 105 | env/ 106 | venv*/ 107 | ENV/ 108 | env.bak/ 109 | venv.bak/ 110 | 111 | # Spyder project settings 112 | .spyderproject 113 | .spyproject 114 | 115 | # Rope project settings 116 | .ropeproject 117 | 118 | # mkdocs documentation 119 | /site 120 | 121 | # mypy 122 | .mypy_cache/ 123 | .dmypy.json 124 | dmypy.json 125 | 126 | # Pyre type checker 127 | .pyre/ 128 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.6 2 | 3 | RUN apt-get update -y ; apt-get install vim -y 4 | RUN pip install --upgrade pip 5 | RUN pip install pytest 6 | 7 | RUN mkdir /local 8 | 9 | COPY . /local 10 | WORKDIR /local 11 | 12 | RUN pip install -r requirements.txt --use-deprecated=legacy-resolver 13 | 14 | CMD /bin/bash -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | .PHONY: help 4 | help: 5 | @grep '^[a-zA-Z]' $(MAKEFILE_LIST) | \ 6 | sort | \ 7 | awk -F ':.*?## ' 'NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' 8 | 9 | build: ## Build 10 | docker build -t n3-guide . 11 | 12 | test: ## Test 13 | docker run -tv $(shell pwd):/source -w /local --env-file .env n3-guide:latest pytest -v tests/ 14 | 15 | cli: ## CLI 16 | docker run -it -v $(shell pwd):/source -w /local --env-file .env n3-guide:latest bash -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 2 | # Nornir, Netbox and NAPALM Guide 3 | Code, scripts and examples for the Packet Coders guide: 4 | > **How to Build a Network Automation Stack with Nornir, Napalm and Netbox**. 5 | 6 | The guide that accompanies this repo can be found [here](https://packetcoders.io/how-to-build-a-network-automation-stack-with-nornir-napalm-and-netbox/). 7 | 8 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | inventory: 3 | plugin: NetBoxInventory2 4 | options: 5 | #nb_url: 'http://127.0.0.1:8080/' 6 | nb_url: 'http://netbox.lab.packetcoders.io:8080/' 7 | ssl_verify: False 8 | 9 | logging: 10 | enabled: False -------------------------------------------------------------------------------- /data/configs/dummy.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/packetcoders/nornir-napalm-netbox-guide/70421b862f942b5f7296f5cfde3091a6c7fecefa/data/configs/dummy.txt -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs==21.4.0 2 | autoflake==1.4 3 | bcrypt==3.2.0 4 | black==21.9b0 5 | certifi==2021.5.30 6 | cffi==1.14.6 7 | charset-normalizer==2.0.6 8 | ciscoconfparse==1.5.46 9 | click==8.0.1 10 | colorama==0.4.4 11 | commonmark==0.9.1 12 | cryptography==35.0.0 13 | dnspython==2.1.0 14 | future==0.18.2 15 | idna==3.2 16 | iniconfig==1.1.1 17 | ipaddress==1.0.23 18 | isort==5.10.1 19 | Jinja2==3.0.2 20 | junos-eznc==2.6.3 21 | loguru==0.5.3 22 | lxml==4.9.1 23 | MarkupSafe==2.0.1 24 | mypy-extensions==0.4.3 25 | napalm==3.3.1 26 | ncclient==0.6.9 27 | netaddr==0.8.0 28 | netmiko==3.4.0 29 | nornir==3.1.1 30 | nornir-napalm==0.1.2 31 | nornir-netbox==0.3.0 32 | nornir-utils==0.1.2 33 | ntc-templates==2.3.2 34 | packaging==21.3 35 | paramiko==2.10.1 36 | passlib==1.7.4 37 | pathspec==0.9.0 38 | platformdirs==2.4.0 39 | pluggy==1.0.0 40 | py==1.11.0 41 | pycparser==2.20 42 | pyeapi==0.8.4 43 | pyflakes==2.4.0 44 | Pygments==2.10.0 45 | PyNaCl==1.4.0 46 | pyparsing==2.4.7 47 | pyserial==3.5 48 | pytest==6.2.5 49 | python-dotenv==0.19.2 50 | python-netbox==0.0.21 51 | PyYAML==5.4.1 52 | regex==2021.9.30 53 | requests==2.26.0 54 | rich==10.12.0 55 | ruamel.yaml==0.16.13 56 | ruamel.yaml.clib==0.2.6 57 | scp==0.14.1 58 | six==1.16.0 59 | tenacity==8.0.1 60 | textfsm==1.1.2 61 | toml==0.10.2 62 | tomli==1.2.1 63 | transitions==0.8.10 64 | typing-extensions==3.10.0.2 65 | urllib3==1.26.7 66 | yamlordereddictloader==0.4.0 67 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/packetcoders/nornir-napalm-netbox-guide/70421b862f942b5f7296f5cfde3091a6c7fecefa/src/__init__.py -------------------------------------------------------------------------------- /src/helpers.py: -------------------------------------------------------------------------------- 1 | #!./venv/bin/python 2 | 3 | import os 4 | from pathlib import Path 5 | 6 | from dotenv import load_dotenv 7 | from nornir import InitNornir 8 | 9 | load_dotenv() 10 | 11 | NORNIR_CONFIG_FILE = f"{Path(__file__).parent.parent}/config.yaml" 12 | 13 | 14 | def nornir_setup(): 15 | nr = InitNornir(config_file=NORNIR_CONFIG_FILE) 16 | 17 | nr.inventory.defaults.username = os.getenv("DEVICE_USERNAME") 18 | nr.inventory.defaults.password = os.getenv("DEVICE_PASSWORD") 19 | 20 | return nr 21 | -------------------------------------------------------------------------------- /src/nr_backup_configs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from nornir_napalm.plugins.tasks import napalm_get 4 | from nornir_utils.plugins.functions import print_result 5 | from nornir_utils.plugins.tasks.files import write_file 6 | 7 | from helpers import nornir_setup 8 | 9 | nr = nornir_setup() 10 | 11 | BACKUP_PATH = "./data/configs" 12 | 13 | 14 | def backup_config(task, path): 15 | device_config = task.run(task=napalm_get, getters=["config"]) 16 | task.run( 17 | task=write_file, 18 | content=device_config.result["config"]["running"], 19 | filename=f"{path}/{task.host}.txt", 20 | ) 21 | 22 | 23 | result = nr.run( 24 | name="Backup Device configurations", path=BACKUP_PATH, task=backup_config 25 | ) 26 | 27 | nr.close_connections() 28 | 29 | if __name__ == "__main__": 30 | print_result(result, vars=["stdout"]) 31 | -------------------------------------------------------------------------------- /src/nr_print_inventory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from rich import print as rprint 4 | 5 | from helpers import nornir_setup 6 | 7 | nr = nornir_setup() 8 | 9 | inventory = nr.inventory.dict() 10 | 11 | if __name__ == "__main__": 12 | rprint(inventory) 13 | -------------------------------------------------------------------------------- /tests/test_001_helpers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.append(f"{os.path.dirname(os.path.dirname(os.path.realpath(__file__)))}/src") 5 | 6 | from helpers import nornir_setup 7 | 8 | 9 | def test_nr_setup(): 10 | nr = nornir_setup() 11 | assert type(nr.inventory.defaults.username) == str 12 | assert type(nr.inventory.defaults.password) == str 13 | assert nr.config.inventory.plugin == "NetBoxInventory2" 14 | -------------------------------------------------------------------------------- /tests/test_002_backup_configs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import pytest 5 | 6 | sys.path.append(f"{os.path.dirname(os.path.dirname(os.path.realpath(__file__)))}/src") 7 | 8 | from nr_backup_configs import result 9 | 10 | 11 | @pytest.mark.parametrize("device", list(result.keys())) 12 | def test_backup_configs_get_config(device): 13 | assert not result[device][1].failed 14 | 15 | 16 | @pytest.mark.parametrize("device", list(result.keys())) 17 | def test_backup_configs_write_file(device): 18 | assert not result[device][2].failed 19 | -------------------------------------------------------------------------------- /tests/test_003_print_inventory.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import pytest 5 | 6 | sys.path.append(f"{os.path.dirname(os.path.dirname(os.path.realpath(__file__)))}/src") 7 | 8 | from nr_print_inventory import inventory 9 | 10 | 11 | @pytest.fixture 12 | def inventory_keys(): 13 | return ["hosts", "groups", "defaults"] 14 | 15 | 16 | def test_print_inventory(inventory_keys): 17 | assert list(inventory.keys()) == inventory_keys 18 | --------------------------------------------------------------------------------