├── .gitignore ├── LICENSE ├── README.md ├── collector.py ├── config.yaml ├── configer.py ├── groups.yaml ├── inventory.yaml ├── knetbox.py ├── knetbox_getter.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/settings.json 2 | __pycache__/configer.cpython-39.pyc 3 | __pycache__/pinger.cpython-39.pyc 4 | nornir.log 5 | .env 6 | __pycache__/collector.cpython-39.pyc 7 | __pycache__/knetbox_getter.cpython-39.pyc 8 | __pycache__/collector.cpython-39.pyc 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Cisco Systems, Inc. and/or its affiliates 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KNetbOX: Device Discovery and Creator for Netbox 2 | 3 | 1. KNetbOX prompts the user if they would like to scaffold their initial config files (YML) that will be used by Nornir 4 | 2. If y, KNetbOX prompts for a subnet to ping and find alive hosts 5 | 3. It will ask for default credentials in order to create the groups.yml file 6 | 4. It will then use Nornir and Scrapli to SSH into each device and collect facts needed to create a device 7 | 5. It will then perform REST API requests against Netbox to gather necessary info to build the payload 8 | 6. Then it will compare the gathered facts with the Netbox information. If a device already exists, it moves on 9 | 7. If the device needs to be added, it will prompt for Site and Role info (while giving contextual help) 10 | 8. It lastly performs the device creation 11 | 12 | # Contact 13 | - https://learn.gg/dataknox 14 | - https://twitter.com/data_knox 15 | - https://youtube.com/c/dataknox -------------------------------------------------------------------------------- /collector.py: -------------------------------------------------------------------------------- 1 | from configer import config_gen 2 | from nornir import InitNornir 3 | from nornir_scrapli.tasks import ( 4 | get_prompt, 5 | send_command, 6 | send_commands, 7 | send_configs 8 | ) 9 | from nornir_utils.plugins.functions import print_result 10 | from nornir.core.task import Task, Result 11 | import logging 12 | from nornir_scrapli.functions import print_structured_result 13 | 14 | 15 | # def config_checker(): 16 | # config_gen() 17 | 18 | 19 | def get_inv_details(task): 20 | response = task.run(task=send_command, 21 | command='show interfaces', severity_level=logging.DEBUG) 22 | int_results = response.scrapli_response.textfsm_parse_output() 23 | ver_response = task.run( 24 | task=send_command, command='show version', severity_level=logging.DEBUG) 25 | ver_results = ver_response.scrapli_response.textfsm_parse_output() 26 | results = {} 27 | results['hostname'] = ver_results[0]['hostname'] 28 | results['platform'] = ver_results[0]['rommon'] 29 | results['serial'] = ver_results[0]['serial'][0] 30 | results['interfaces'] = [] 31 | for int in int_results: 32 | interface = {} 33 | interface['name'] = int['interface'] 34 | interface['mac'] = int['bia'] 35 | interface['ip'] = int['ip_address'] 36 | interface['media'] = int['media_type'] 37 | interface['active'] = int['link_status'] 38 | results['interfaces'].append(interface) 39 | return results 40 | 41 | 42 | if __name__ == "__main__": 43 | response = input("Do you want to gen a config first? (y/N): ") 44 | if response == "y": 45 | config_gen() 46 | nr = InitNornir(config_file="config.yaml") 47 | results = nr.run(task=get_inv_details) 48 | print_result(results) 49 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | inventory: 2 | options: 3 | group_file: groups.yaml 4 | host_file: inventory.yaml 5 | plugin: SimpleInventory 6 | runner: 7 | options: 8 | num_workers: 1 9 | plugin: threaded 10 | -------------------------------------------------------------------------------- /configer.py: -------------------------------------------------------------------------------- 1 | from pypinger import pyping 2 | import yaml 3 | 4 | 5 | def config_gen(): 6 | config = { 7 | 'inventory': { 8 | 'plugin': 'SimpleInventory', 9 | 'options': { 10 | 'host_file': 'inventory.yaml', 11 | 'group_file': 'groups.yaml' 12 | } 13 | }, 14 | 'runner': { 15 | 'plugin': 'threaded', 16 | 'options': { 17 | 'num_workers': 1 18 | } 19 | } 20 | } 21 | f = open('config.yaml', 'w') 22 | yaml.dump(config, f, allow_unicode=True) 23 | hosts = pyping() 24 | user = input("Username (default to cisco): ") 25 | if not user: 26 | user = 'cisco' 27 | password = input("Passowrd (default to cisco): ") 28 | if not password: 29 | password = 'cisco' 30 | hosts_list = {} 31 | for host in hosts: 32 | host_data = { 33 | 'hostname': host, 34 | 'username': user, 35 | 'password': password, 36 | 'groups': ['cisco_group'] 37 | } 38 | hosts_list[host] = host_data 39 | print(yaml.dump(hosts_list)) 40 | f = open('inventory.yaml', 'w') 41 | yaml.dump(hosts_list, f, allow_unicode=True) 42 | group = { 43 | 'cisco_group': { 44 | 'username': 'cisco', 45 | 'password': 'cisco', 46 | 'connection_options': { 47 | 'scrapli': { 48 | 'platform': 'cisco_iosxe', 49 | 'port': 22, 50 | 'extras': {'ssh_config_file': True, 'auth_strict_key': False} 51 | }, 52 | 'scrapli_netconf': { 53 | 'port': 830, 54 | 'extras': {'ssh_config_file': True, 'auth_strict_key': False} 55 | } 56 | } 57 | } 58 | } 59 | f = open('groups.yaml', 'w') 60 | yaml.dump(group, f, allow_unicode=True) 61 | -------------------------------------------------------------------------------- /groups.yaml: -------------------------------------------------------------------------------- 1 | cisco_group: 2 | connection_options: 3 | scrapli: 4 | extras: 5 | auth_strict_key: false 6 | ssh_config_file: true 7 | platform: cisco_iosxe 8 | port: 22 9 | scrapli_netconf: 10 | extras: 11 | auth_strict_key: false 12 | ssh_config_file: true 13 | port: 830 14 | password: cisco 15 | username: cisco 16 | -------------------------------------------------------------------------------- /inventory.yaml: -------------------------------------------------------------------------------- 1 | 10.15.0.10: 2 | groups: 3 | - cisco_group 4 | hostname: 10.15.0.10 5 | password: cisco 6 | username: cisco 7 | 10.15.0.7: 8 | groups: 9 | - cisco_group 10 | hostname: 10.15.0.7 11 | password: cisco 12 | username: cisco 13 | 10.15.0.8: 14 | groups: 15 | - cisco_group 16 | hostname: 10.15.0.8 17 | password: cisco 18 | username: cisco 19 | 10.15.0.9: 20 | groups: 21 | - cisco_group 22 | hostname: 10.15.0.9 23 | password: cisco 24 | username: cisco 25 | -------------------------------------------------------------------------------- /knetbox.py: -------------------------------------------------------------------------------- 1 | import pynetbox 2 | from collector import get_inv_details 3 | from configer import config_gen 4 | from nornir import InitNornir 5 | from nornir_scrapli.tasks import ( 6 | get_prompt, 7 | send_command, 8 | send_commands, 9 | send_configs 10 | ) 11 | from nornir_utils.plugins.functions import print_result 12 | from nornir.core.task import Task, Result 13 | import logging 14 | from nornir_scrapli.functions import print_structured_result 15 | from knetbox_getter import ( 16 | get_dev_roles, get_dev_types, get_devices, get_manufacturers, get_sites, get_tenants) 17 | import json 18 | 19 | 20 | nb = pynetbox.api( 21 | 'http://10.10.21.196:8000', 22 | token='0123456789abcdef0123456789abcdef01234567') 23 | 24 | 25 | def device_creator(task): 26 | # Check if device already exists 27 | response = task.run(task=get_inv_details) 28 | # print(response) 29 | 30 | device = response[0].result 31 | #print(json.dumps(device, indent=2)) 32 | hostname = device['hostname'] 33 | serial = device['serial'] 34 | # Check if device exists 35 | existing_devices = get_devices() 36 | for ex_device in existing_devices: 37 | if ex_device == device['hostname']: 38 | return "Device already exists in inventory" 39 | 40 | # Handle site ID 41 | print(nb.dcim.sites.all()) 42 | site = input("Choose a site from above list: ") 43 | existing_sites = get_sites() 44 | for ex_site in existing_sites: 45 | if ex_site['name'] == site: 46 | site_id = ex_site['id'] 47 | else: 48 | print("Something went wrong with getting Site") 49 | 50 | # Handle Role ID 51 | existing_roles = get_dev_roles() 52 | role = 'virtual router' 53 | for ints in device['interfaces']: 54 | if ints['media'] != 'Virtual': 55 | print(ints['media']) 56 | role = ints['media'] 57 | if (role == 'virtual router') or (role == 'virtual switch'): 58 | tenants = get_tenants() 59 | for tenant in tenants: 60 | if tenant['name'] == 'Virtual': 61 | ten_id = tenant['id'] 62 | if (role == 'virtual router') or (role == 'virtual switch'): 63 | print('validating role') 64 | for ex_role in existing_roles: 65 | print(ex_role['name']) 66 | if ex_role['name'] == role: 67 | role_id = ex_role['id'] 68 | print(f"{ex_role['name']} DOES match {role}") 69 | else: 70 | print(f"{ex_role['name']} does not match {role}") 71 | 72 | # Handle manufacturer 73 | # if device['platform'] in ('IOS-XE', 'IOS', 'NX-OS'): 74 | # manu = 'Cisco' 75 | # existing_manu = get_manufacturers() 76 | # for ex_manu in existing_manu: 77 | # if ex_manu['name'] == manu: 78 | # manu_id = ex_manu['id'] 79 | # else: 80 | # print("Something went wrong with getting Manu ID") 81 | 82 | # Handle Device Type 83 | print(nb.dcim.device_types.all()) 84 | dev_type = input("Select type a Dev type from above: ") 85 | existing_types = get_dev_types() 86 | for ex_type in existing_types: 87 | if ex_type['model'] == dev_type: 88 | type_id = ex_type['id'] 89 | print(f"{ex_type['model']} matches {dev_type}") 90 | else: 91 | print("Something went wrong with getting Dev Type ID") 92 | 93 | # GO 94 | try: 95 | results = nb.dcim.devices.create(name=hostname, 96 | device_role=role_id, 97 | device_type=type_id, 98 | site=site_id, 99 | status="active", 100 | serial=serial, 101 | tenant=ten_id 102 | ) 103 | except Exception as err: 104 | results = err 105 | return Result(host=task.host, result=results) 106 | 107 | 108 | if __name__ == "__main__": 109 | response = input("Do you want to gen a config first? (y/N): ") 110 | if response == "y": 111 | config_gen() 112 | nr = InitNornir(config_file="config.yaml") 113 | results = nr.run(task=device_creator) 114 | print_result(results) 115 | -------------------------------------------------------------------------------- /knetbox_getter.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | base_url = "http://10.10.21.196:8000/api" 5 | 6 | headers = { 7 | 'accept': 'application/json', 8 | 'Authorization': 'Token 0123456789abcdef0123456789abcdef01234567', 9 | 10 | } 11 | 12 | # 13 | 14 | 15 | def get_dev_roles(): 16 | dev_roles_url = "/dcim/device-roles/" 17 | print(headers) 18 | try: 19 | dev_roles = requests.get( 20 | url=f"{base_url}{dev_roles_url}", headers=headers, verify=False) 21 | if dev_roles.status_code == 200: 22 | return json.loads(dev_roles.text)['results'] 23 | except Exception as err: 24 | return err 25 | 26 | 27 | def get_dev_types(): 28 | dev_types_url = "/dcim/device-types/" 29 | try: 30 | dev_types = requests.get( 31 | url=f"{base_url}{dev_types_url}", headers=headers, verify=False) 32 | if dev_types.status_code == 200: 33 | return json.loads(dev_types.text)['results'] 34 | except Exception as err: 35 | return err 36 | 37 | 38 | def get_manufacturers(): 39 | manu_url = '/dcim/manufacturers/' 40 | try: 41 | manus = requests.get( 42 | url=f"{base_url}{manu_url}", headers=headers, verify=False) 43 | if manus.status_code == 200: 44 | return json.loads(manus.text)['results'] 45 | except Exception as err: 46 | return err 47 | 48 | 49 | def get_sites(): 50 | sites_url = '/dcim/sites/' 51 | try: 52 | sites = requests.get( 53 | url=f"{base_url}{sites_url}", headers=headers, verify=False) 54 | if sites.status_code == 200: 55 | return json.loads(sites.text)['results'] 56 | except Exception as err: 57 | return err 58 | 59 | 60 | def get_devices(): 61 | devices_url = '/dcim/devices/' 62 | try: 63 | devices = requests.get( 64 | url=f"{base_url}{devices_url}", headers=headers, verify=False) 65 | if devices.status_code == 200: 66 | return json.loads(devices.text)['results'] 67 | except Exception as err: 68 | return err 69 | 70 | 71 | def get_tenants(): 72 | tenant_url = '/tenancy/tenants/' 73 | try: 74 | tenants = requests.get( 75 | url=f"{base_url}{tenant_url}", headers=headers, verify=False) 76 | if tenants.status_code == 200: 77 | return json.loads(tenants.text)['results'] 78 | except Exception as err: 79 | return err 80 | 81 | 82 | if __name__ == "__main__": 83 | # print(json.dumps(get_dev_roles(), indent=2)) 84 | print(json.dumps(get_dev_types(), indent=2)) 85 | # print(json.dumps(get_manufacturers(), indent=2)) 86 | # print(json.dumps(get_sites(), indent=2)) 87 | # print(json.dumps(get_devices(), indent=2)) 88 | # print(json.dumps(get_tenants(), indent=2)) 89 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | astroid==2.4.2 2 | asyncssh==2.4.2 3 | autopep8==1.5.4 4 | cffi==1.14.3 5 | colorama==0.4.4 6 | cryptography==3.2.1 7 | future==0.18.2 8 | isort==5.6.4 9 | lazy-object-proxy==1.4.3 10 | lxml==4.6.1 11 | mccabe==0.6.1 12 | mypy-extensions==0.4.3 13 | nornir==3.0.0 14 | nornir-scrapli==2020.11.1 15 | nornir-utils==0.1.1 16 | ntc-templates==1.6.0 17 | pycodestyle==2.6.0 18 | pycparser==2.20 19 | pylint==2.6.0 20 | pynetbox==5.1.0 21 | pypinger==2.0.3 22 | PyYAML==5.3.1 23 | ruamel.yaml==0.16.12 24 | scrapli==2020.10.10 25 | scrapli-asyncssh==2020.10.10 26 | scrapli-community==2020.9.19 27 | scrapli-netconf==2020.10.24 28 | six==1.15.0 29 | textfsm==1.1.0 30 | toml==0.10.2 31 | typing-extensions==3.7.4.3 32 | wrapt==1.12.1 --------------------------------------------------------------------------------