├── requirements.txt ├── setup.cfg ├── .yamllint.yaml ├── pytest.ini ├── LICENSE ├── .github └── workflows │ └── main.yaml ├── Makefile ├── README.md ├── .gitignore └── examples ├── ios-config-diff-report.py ├── local-username-compliance.py └── all-ntp-servers.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | pprint 3 | black 4 | pylama 5 | colorama 6 | yamllint 7 | bandit 8 | pytest 9 | pytest-cov -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # Pylama setup configuration file 2 | [pylama] 3 | linters = pep8,mccabe,pyflakes 4 | skip = venv/* 5 | 6 | [pylama:pep8] 7 | max_line_length = 100 8 | # ignore E231 missing whitespace after ',' [pep8] 9 | # ignore W503 line break before binary operator 10 | ignore = E231, W503 11 | 12 | [pylama:pycodestyle] 13 | max_line_length = 100 -------------------------------------------------------------------------------- /.yamllint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # YAMLlint Configuration file 4 | # Ignore the virtual environment folder 5 | ignore: | 6 | venv/ 7 | .yamllint.yaml 8 | 9 | extends: default 10 | 11 | rules: 12 | # 120 chars should be enough, but don't fail if a line is longer 13 | line-length: 14 | max: 120 15 | level: warning 16 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | backend_core: Test core functionality of the backend functions 4 | frontend_core: Test core functionality of the frontend API 5 | frontend_offline: Test frontend API functionality in offline mode 6 | frontend_online: Test frontend API functionality in online mode 7 | frontend_custom: Test frontend API functionality with custom, inventory-specific values -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Daniel Teycheney 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Github actions workflow file 3 | name: net-api-tools 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | paths-ignore: 10 | - 'README.md' 11 | - 'TODO.md' 12 | pull_request: 13 | branches: 14 | - master 15 | paths-ignore: 16 | - 'README.md' 17 | - 'TODO.md' 18 | 19 | jobs: 20 | build: 21 | name: Lint and test 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | python-version: [3.6, 3.7, 3.8] 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v2 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Install dependencies 33 | run: | 34 | python -m pip install --upgrade pip 35 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 36 | pip freeze install 37 | - name: Execute black formatting check 38 | run: make black 39 | - name: Execute pylama check 40 | run: make pylama 41 | - name: Execute yamllint check 42 | run: make yamllint 43 | - name: Execute bandit check 44 | run: make bandit 45 | - name: Execute pytest (Github actions version) 46 | run: make pytest-gh-actions 47 | -------------------------------------------------------------------------------- /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[35m %-25s\033[0m %s\n", $$1, $$2}' 8 | 9 | .PHONY: lint-all 10 | lint-all: black pylama yamllint bandit ## Perform all linting and security checks (black, pylama, yamllint and bandit). 11 | 12 | .PHONY: black 13 | black: ## Format code using black 14 | @echo "--- Performing black reformatting ---" 15 | black . 16 | 17 | .PHONY: pylama 18 | pylama: ## Perform python linting using pylama 19 | @echo "--- Performing pylama linting ---" 20 | pylama . 21 | 22 | .PHONY: yamllint 23 | yamllint: ## Perform YAML linting using yamllint 24 | @echo "--- Performing yamllint linting ---" 25 | yamllint . 26 | 27 | .PHONY: bandit 28 | bandit: ## Perform python code security checks using bandit 29 | @echo "--- Performing bandit code security scanning ---" 30 | bandit -v --exclude ./venv --recursive --format json . --verbose -s B101 31 | 32 | .PHONY: venv 33 | venv: ## Install virtualenv, create virtualenv, install requirements for Python 3 34 | @echo "--- Creating virtual environment and installing requirements (Python3.x) ---" 35 | virtualenv --python=`which python3` venv 36 | source ./venv/bin/activate 37 | pip install -r ./requirements.txt 38 | 39 | .PHONY: pytest 40 | pytest: ## Perform testing using pytest 41 | @echo "--- Performing pytest ---" 42 | pytest . --cov-report term-missing -vs --pylama . --cache-clear -vvvvv 43 | 44 | .PHONY: pytest-gh-actions 45 | pytest-gh-actions: ## Perform testing using pytest on Github Actions 46 | @echo "--- Performing pytest on Github Action ---" 47 | pytest . --cov-report term-missing -vs --pylama . --cache-clear -vvvvv 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![net-api-tools](https://github.com/writememe/net-api-tools/workflows/net-api-tools/badge.svg?branch=master) 2 | [![Python 3.6](https://img.shields.io/badge/python-3.6-blue.svg)](https://www.python.org/downloads/release/python-360/) 3 | [![Python 3.7](https://img.shields.io/badge/python-3.7-blue.svg)](https://www.python.org/downloads/release/python-370/) 4 | [![Python 3.8](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/downloads/release/python-380/) 5 | [![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 6 | 7 | 8 | # Introduction 9 | 10 | This repository contains a reference set of tools and solutions which leverage the [`net-api`](https://github.com/writememe/net-api) platform. 11 | 12 | These tools have been developed with the intent to show how simple and flexible it is to consume `net-api` and write custom applications for your business needs. 13 | 14 | Whilst these solutions are written in Python, there is nothing preventing you from consuming `net-api` using any other language of your choice. 15 | 16 | 17 | ## Structure 18 | 19 | All examples are contained within the [examples/](examples/) directory. Below is a reference table detailing the high level examples: 20 | 21 | | Subject | Description | File | 22 | | ------- | --------------- | ---------------- | 23 | | Local username compliance | Validate that the local usernames defined on a host exactly match a pre-defined list | [local-username-compliance.py](examples/local-username-compliance.py)| 24 | | Running config versus startup config report | Validate that there are no differences between the running and startup config on an IOS host |[ios-config-diff-report.py](examples/ios-config-diff-report.py) | 25 | | All host NTP server compliance | Validate that all NTP servers defined on all hosts exactly match a pre-defined list |[all-ntp-servers.py](examples/all-ntp-servers.py) | 26 | 27 | **NOTE: All the examples provided are supported using Python 3.6 or higher** 28 | 29 | ## Contributing 30 | 31 | I will try to populate more examples as time dictates, however if you would like to contribute, that would be great! 32 | 33 | The more examples and use cases that are generated, the more useful the tool is for others throughout the community. 34 | 35 | If you need some help on how or where to start, please let me know. Thanks 36 | -------------------------------------------------------------------------------- /.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 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Ignore nornir logs 132 | nornir.log 133 | app/nornir.log -------------------------------------------------------------------------------- /examples/ios-config-diff-report.py: -------------------------------------------------------------------------------- 1 | """ 2 | This application will compare the running configuration against 3 | the startup configuration and print the difference to the screen. 4 | 5 | To adjust this to your environment, please change the `url` variable 6 | within the code. 7 | 8 | To execute this code, please run the following command: 9 | 10 | python ios-config-diff-report.py --help 11 | 12 | This will display what are the options to execute the application. 13 | """ 14 | 15 | # Import modules 16 | import requests 17 | import pprint 18 | from requests.exceptions import HTTPError 19 | from colorama import Fore, init 20 | import argparse 21 | 22 | # Auto-reset colorama colours back after each print statement 23 | init(autoreset=True) 24 | 25 | # Setup argparse parameters to take user input from the command line 26 | parser = argparse.ArgumentParser( 27 | description="Validate startup and running config differences on Cisco devices." 28 | ) 29 | parser.add_argument( 30 | "--host", 31 | action="store", 32 | help="Specify the hostname to be queried i.e. lab-host-01.lab.local", 33 | type=str, 34 | ) 35 | # Add debug argument, set to False by default unless required 36 | parser.add_argument( 37 | "--debug", 38 | action="store_true", 39 | help="Provide additional debugging output. True for debugging, set to False by default", 40 | ) 41 | args = parser.parse_args() 42 | 43 | print(Fore.CYAN + f"Validating config differences on {args.host}") 44 | # Take argparse input and assign to the hostname variable 45 | hostname = args.host 46 | # Define variables 47 | # Define the URL 48 | url = "http://10.0.0.54:5000" 49 | # Define the API path 50 | api_path = "/api/v1/nr/scrapli/genie/host?host=" 51 | command_api_path = "&command=" 52 | # The command to be parsed. 53 | command = "show archive config differences nvram:startup-config system:running-config" 54 | # Set indentation on pretty print 55 | pp = pprint.PrettyPrinter(indent=2) 56 | 57 | # Try/except block to validate a successful HTTP response code 58 | try: 59 | # If debug is set to True, provide the full API call string 60 | if args.debug is True: 61 | print( 62 | Fore.MAGENTA 63 | + f"Attempting API call - {url}{api_path}{hostname}{command_api_path}{command}" 64 | ) 65 | # Get the results of the API call 66 | req = requests.get(url + api_path + hostname + command_api_path + command) 67 | # If debug is set to True, printout the status code and the raw text response 68 | if args.debug is True: 69 | print(Fore.MAGENTA + f"Response code - {req.status_code}") 70 | print(Fore.MAGENTA + f"Response raw text - {req.text}") 71 | # If the response was successful, no exception will be raised 72 | req.raise_for_status() 73 | # Raise exception, print HTTP error 74 | except HTTPError as http_err: 75 | print(Fore.RED + f"HTTP error occurred: {http_err}") 76 | # Raise exception, print other error 77 | except Exception as err: 78 | print(Fore.RED + f"Other error occurred: {err}") 79 | # Proceed with the rest of the program 80 | else: 81 | """ 82 | Below is the JSON data which is presented: 83 | 84 | { 85 | 'command_output': 86 | {'diff': ['list of changes between files'] 87 | }, 88 | 'host': 'hostname' 89 | } 90 | 91 | Below we will access the usernames in the structure 92 | """ 93 | diff_list = req.json()["command_output"]["diff"] 94 | # Check which the differences are empty, which means there are no differences in the configs 95 | if not diff_list: 96 | print(Fore.GREEN + f"No configuration differences on : {hostname}") 97 | else: 98 | print(Fore.YELLOW + f"Configuration differences on : {hostname}") 99 | # Iterate over list of differences, printout to screen 100 | for i in diff_list: 101 | print(Fore.YELLOW + f"Differences : {i}") 102 | -------------------------------------------------------------------------------- /examples/local-username-compliance.py: -------------------------------------------------------------------------------- 1 | """ 2 | This application will validate all users on an individual Nornir 3 | host defined on `net-api` against a pre-defined list and print 4 | successes and/or failures. 5 | 6 | To adjust this to your environment, please change the `url` variable 7 | and the `allowed_users` variable within the code. 8 | 9 | To execute this code, please run the following command: 10 | 11 | python local-username-compliance.py --help 12 | 13 | This will display what are the options to execute the application. 14 | """ 15 | 16 | # Import modules 17 | import requests 18 | import pprint 19 | from requests.exceptions import HTTPError 20 | from colorama import Fore, init 21 | import argparse 22 | 23 | # Auto-reset colorama colours back after each print statement 24 | init(autoreset=True) 25 | 26 | # Setup argparse parameters to take user input from the command line 27 | parser = argparse.ArgumentParser( 28 | description="Validate local username(s) compliance on given host." 29 | ) 30 | parser.add_argument( 31 | "--host", 32 | action="store", 33 | help="Specify the hostname to be queried i.e. lab-host-01.lab.local", 34 | type=str, 35 | ) 36 | # Add debug argument, set to False by default unless required 37 | parser.add_argument( 38 | "--debug", 39 | action="store_true", 40 | help="Provide additional debugging output. True for debugging, set to False by default", 41 | ) 42 | args = parser.parse_args() 43 | 44 | print(Fore.CYAN + f"Validating local usernames on {args.host}") 45 | # Take argparse input and assign to the hostname variable 46 | hostname = args.host 47 | # Define the URL 48 | url = "http://10.0.0.54:5000" 49 | # Define the API path 50 | api_path = "/api/v1/nr/napalm/users/host?host=" 51 | # Define a list of allowed usernames 52 | allowed_users = ["admin", "svc-ansible", "svc-netbox-napalm"] 53 | # Set indentation on pretty print 54 | pp = pprint.PrettyPrinter(indent=2) 55 | 56 | # Try/except block to validate a successful HTTP response code 57 | try: 58 | # If debug is set to True, provide the full API call string 59 | if args.debug is True: 60 | print(Fore.MAGENTA + f"Attempting API call - {url}{api_path}{hostname}") 61 | # Get the results of the API call 62 | req = requests.get(url + api_path + hostname) 63 | # If debug is set to True, printout the status code and the raw text response 64 | if args.debug is True: 65 | print(Fore.MAGENTA + f"Response code - {req.status_code}") 66 | print(Fore.MAGENTA + f"Response raw text - {req.text}") 67 | # If the response was successful, no exception will be raised 68 | req.raise_for_status() 69 | # Raise exception, print HTTP error 70 | except HTTPError as http_err: 71 | print(Fore.RED + f"HTTP error occurred: {http_err}") 72 | # Raise exception, print other error 73 | except Exception as err: 74 | print(Fore.RED + f"Other error occurred: {err}") 75 | # Proceed with the rest of the program 76 | else: 77 | """ 78 | Below is the JSON data which is presented: 79 | 80 | { 81 | "hostname": { 82 | "users": { 83 | "username1": { 84 | "level": 15, 85 | "password": "$1$Xa0G$mSJ/Cp70.lRDgcn.SXL1r.", 86 | "sshkeys": [] 87 | }, 88 | "username2": { 89 | "level": 15, 90 | "password": "$1$oiur$guHYCob2ovmb5AB5ugDYw/", 91 | "sshkeys": [] 92 | }, 93 | "username3": { 94 | "level": 15, 95 | "password": "$1$Ln6l$1oPnApd2BSXchOQI3ifbd1", 96 | "sshkeys": [] 97 | } 98 | } 99 | } 100 | } 101 | 102 | Below we will access the usernames in the structure 103 | """ 104 | # Extract the JSON response and access the username level of the dictionary 105 | user_list = req.json()[hostname]["users"] 106 | # Create an empty list, for the username(s) on the device to be entered into 107 | configured_users = list() 108 | # Iterate over the keys in the list, which are the username(s) 109 | for key in user_list.keys(): 110 | # Append the list with the username entries 111 | configured_users.append(key) 112 | # Sort both lists, for comparison 113 | allowed_users.sort() 114 | configured_users.sort() 115 | # If/else list comparision to check that allowed 116 | # usernames exactly match what is on the device. 117 | if configured_users == allowed_users: 118 | print(Fore.GREEN + "SUCCESS : The configured users are compliant") 119 | print(Fore.GREEN + "Compliant user list : " + str(allowed_users)) 120 | print(Fore.GREEN + "Actual user list : " + str(configured_users)) 121 | else: 122 | print(Fore.RED + "FAILURE : The configured users are NOT compliant") 123 | print(Fore.RED + "Compliant user list : " + str(allowed_users)) 124 | print(Fore.RED + "Actual user list : " + str(configured_users)) 125 | -------------------------------------------------------------------------------- /examples/all-ntp-servers.py: -------------------------------------------------------------------------------- 1 | """ 2 | This application will validate all NTP servers on all hosts defined 3 | on `net-api` against a pre-defined list and print successes and/or 4 | failures. 5 | 6 | To adjust this to your environment, please change the `url` variable 7 | and the `allowed_ntp_servers` variable within the code. 8 | 9 | To execute this code, please run the following command: 10 | 11 | python all-ntp-servers.py --help 12 | 13 | This will display what are the options to execute the application. 14 | """ 15 | 16 | # Import modules 17 | import requests 18 | import pprint 19 | from requests.exceptions import HTTPError 20 | from colorama import Fore, init 21 | import argparse 22 | 23 | # Auto-reset colorama colours back after each print statement 24 | init(autoreset=True) 25 | 26 | # Setup argparse parameters to take user input from the command line 27 | parser = argparse.ArgumentParser( 28 | description="Collect all NTP servers configured on all hosts." 29 | ) 30 | # Add debug argument, set to False by default unless required 31 | parser.add_argument( 32 | "--debug", 33 | action="store_true", 34 | help="Provide additional debugging output. True for debugging, set to False by default", 35 | ) 36 | args = parser.parse_args() 37 | 38 | print(Fore.CYAN + "Collecting NTP servers, please wait ...") 39 | # Define the URL 40 | url = "http://10.0.0.54:5000" 41 | # Define the API path 42 | api_path = "/api/v1/nr/napalm/ntp_servers/all" 43 | # Define a list of allowed NTP servers 44 | allowed_ntp_servers = ["10.0.0.1", "8.8.8.8"] 45 | # Set indentation on pretty print 46 | pp = pprint.PrettyPrinter(indent=2) 47 | 48 | # Try/except block to validate a successful HTTP response code 49 | try: 50 | # If debug is set to True, provide the full API call string 51 | if args.debug is True: 52 | print(Fore.MAGENTA + f"Attempting API call - {url}{api_path}") 53 | # Get the results of the API call 54 | req = requests.get(url + api_path) 55 | # If debug is set to True, printout the status code and the raw text response 56 | if args.debug is True: 57 | print(Fore.MAGENTA + f"Response code - {req.status_code}") 58 | print(Fore.MAGENTA + f"Response raw text - {req.text}") 59 | # If the response was successful, no exception will be raised 60 | req.raise_for_status() 61 | # Raise exception, print HTTP error 62 | except HTTPError as http_err: 63 | print(Fore.RED + f"HTTP error occurred: {http_err}") 64 | # Raise exception, print other error 65 | except Exception as err: 66 | print(Fore.RED + f"Other error occurred: {err}") 67 | # Proceed with the rest of the program 68 | else: 69 | """ 70 | Below is the JSON data which is presented: 71 | 72 | { 'hostname-1.lab.acme.local': { 'ntp_servers': { '8.8.4.4': {}, 73 | '4.2.2.2': {}, 74 | '8.8.8.8': {}}}, 75 | 'hostname-2.lab.acme.local': { 'ntp_servers': { '8.8.4.4 prefer': {}, 76 | '8.8.8.8': {}}}, 77 | 'hostname-3.lab.acme.local': { 'ntp_servers': { '8.8.4.4': {}, 78 | '8.8.8.8': {}}}, 79 | 'hostname-4.lab.acme.local': {'ntp_servers': {'8.8.4.4': {}, '8.8.8.8': {}}}, 80 | 'hostname-5.lab.acme.local': { 'ntp_servers': { '8.8.4.4': {}, 81 | '8.8.8.8': {}}}, 82 | 'hostname-6.lab.acme.local': { 'ntp_servers': { '8.8.4.4': {}, 83 | '8.8.8.8': {}}}} 84 | Below we will access the NTP servers in the structure 85 | """ 86 | # Extract the JSON response 87 | ntp_results = req.json() 88 | # Iterate through NTP servers, using the host as the iterator 89 | for host in ntp_results.keys(): 90 | # print(Fore.GREEN + f"Hostname - {host}") 91 | # Assign NTP Server results inside the original JSON response to a variable 92 | ntp_servers = ntp_results[host]["ntp_servers"] 93 | # Create an empty list to append configured NTP servers into 94 | configured_ntp_servers = list() 95 | # For loop to iterate over NTP server results 96 | for server in ntp_servers.keys(): 97 | # Append to the list and right strip the " prefer" off entries such as "10.0.0.1 prefer" 98 | configured_ntp_servers.append(server.rstrip(" prefer")) 99 | # Sort both lists, for comparison 100 | allowed_ntp_servers.sort() 101 | configured_ntp_servers.sort() 102 | # If/else list comparision to check that allowed 103 | # NTP servers exactly match what is on the device. 104 | if configured_ntp_servers == allowed_ntp_servers: 105 | print( 106 | Fore.GREEN 107 | + f"SUCCESS : The configured NTP servers are compliant on {host}" 108 | ) 109 | print( 110 | Fore.GREEN + "Compliant NTP server list : " + str(allowed_ntp_servers) 111 | ) 112 | print( 113 | Fore.GREEN + "Actual NTP server list : " + str(configured_ntp_servers) 114 | ) 115 | else: 116 | print( 117 | Fore.RED 118 | + f"FAILURE : The configured NTP servers are NOT compliant on {host}" 119 | ) 120 | print(Fore.RED + "Compliant server list : " + str(allowed_ntp_servers)) 121 | print(Fore.RED + "Actual server list : " + str(configured_ntp_servers)) 122 | # Demarcation print 123 | print("=" * 80) 124 | --------------------------------------------------------------------------------