├── python └── nso_netbox │ ├── __init__.py │ ├── actions.py │ ├── netbox_inventory.py │ ├── main.py │ ├── netbox_utilities.py │ └── netbox_inventory_actions.py ├── src ├── requirements.txt ├── Makefile └── yang │ └── nso-netbox.yang ├── templates ├── add-device-group.xml └── add-device.xml ├── test ├── internal │ ├── lux │ │ ├── service │ │ │ ├── pyvm.xml │ │ │ ├── dummy-device.xml │ │ │ ├── dummy-service.xml │ │ │ ├── Makefile │ │ │ └── run.lux │ │ └── Makefile │ └── Makefile └── Makefile ├── templates-ned-settings ├── add-cisco-fmc.xml └── add-vmware-vsphere-gen.xml ├── package-meta-data.xml ├── LICENSE └── README.md /python/nso_netbox/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | pynetbox==5.3.1 2 | -------------------------------------------------------------------------------- /templates/add-device-group.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {$DEVICE_GROUP_NAME} 4 | 5 | {$DEVICE_GROUP_LOCATION} 6 | 7 | {$DEVICE_NAME} 8 | 9 | -------------------------------------------------------------------------------- /test/internal/lux/service/pyvm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./logs/ncs-python-vm 5 | level-info 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/internal/lux/service/dummy-device.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | eth 5 |
192.168.110.1
6 | 7 | southbound-locked 8 | 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /test/internal/Makefile: -------------------------------------------------------------------------------- 1 | DIRS = lux 2 | 3 | build: 4 | @for d in $(DIRS) ; do \ 5 | $(MAKE) -C $$d build || exit 1; \ 6 | done 7 | 8 | clean: 9 | @for d in $(DIRS) ; do \ 10 | $(MAKE) -C $$d clean || exit 1; \ 11 | done 12 | 13 | test: 14 | @for d in $(DIRS) ; do \ 15 | $(MAKE) -C $$d test || exit 1; \ 16 | done 17 | 18 | desc: 19 | @for d in $(DIRS) ; do \ 20 | $(MAKE) -C $$d desc || exit 1; \ 21 | done 22 | -------------------------------------------------------------------------------- /test/internal/lux/Makefile: -------------------------------------------------------------------------------- 1 | DIRS = service 2 | 3 | build: 4 | @for d in $(DIRS) ; do \ 5 | $(MAKE) -C $$d build || exit 1; \ 6 | done 7 | 8 | clean: 9 | @for d in $(DIRS) ; do \ 10 | $(MAKE) -C $$d clean || exit 1; \ 11 | done 12 | 13 | test: 14 | @for d in $(DIRS) ; do \ 15 | $(MAKE) -C $$d test || exit 1; \ 16 | done 17 | 18 | desc: 19 | @for d in $(DIRS) ; do \ 20 | $(MAKE) -C $$d desc || exit 1; \ 21 | done 22 | -------------------------------------------------------------------------------- /test/internal/lux/service/dummy-service.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 33 9 | eth 10 | 127.0.0.1 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/internal/lux/service/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # The 'lux' test tool can be obtained from: 3 | # 4 | # https://github.com/hawk/lux.git 5 | # 6 | 7 | # Make sure the TARGET_DIR has got the following make targets: 8 | .PHONY: clean build start stop 9 | 10 | export TARGET_DIR=../../../../../.. 11 | 12 | .PHONY: test 13 | test: 14 | lux run.lux 15 | 16 | clean: 17 | $(MAKE) -C $(TARGET_DIR) clean 18 | 19 | build: 20 | $(MAKE) -C $(TARGET_DIR) build 21 | 22 | start: 23 | $(MAKE) -C $(TARGET_DIR) start 24 | 25 | stop: 26 | $(MAKE) -C $(TARGET_DIR) stop 27 | -------------------------------------------------------------------------------- /templates-ned-settings/add-cisco-fmc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {$DEVICE_NAME} 4 | 5 | 6 | 7 | 8 | 9 | 10 | {$BYPASS_CERT_VERIFY} 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /package-meta-data.xml: -------------------------------------------------------------------------------- 1 | 2 | nso-netbox 3 | 1.0 4 | This will be an NSO package for interacting with NetBox as a source of truth. Example use cases include: 5 | 6 | * Generating a device inventory from NetBox devices 7 | * IP Address/Prefix Allocation 8 | * Gathering data for verification checks 9 | 5.4 10 | 11 | 12 | main 13 | 14 | nso_netbox.main.Main 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /templates-ned-settings/add-vmware-vsphere-gen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {$DEVICE_NAME} 4 | 5 | 6 | 7 | 60 8 | 60 9 | 10 | 11 | 12 | portgroup-cfg 13 | 14 | TLSv1.2 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | DIRS = external internal 2 | 3 | ifeq ($(BUILD_JOB),external) 4 | DIR = external 5 | endif 6 | 7 | ifeq ($(BUILD_JOB),internal_realhw) 8 | DIR = internal 9 | JOB_DIR = realhw 10 | endif 11 | 12 | ifeq ($(BUILD_JOB),internal_simulated) 13 | DIR = internal 14 | JOB_DIR = simulated 15 | endif 16 | 17 | ifeq ($(BUILD_JOB),) 18 | DIR = internal 19 | JOB_DIR = simulated 20 | endif 21 | 22 | all: test 23 | 24 | build: 25 | $(MAKE) -C $(DIR) build JOB_DIR=$(JOB_DIR) || exit 1 26 | 27 | clean: 28 | $(MAKE) -C $(DIR) clean JOB_DIR=$(JOB_DIR) || exit 1 29 | 30 | test: 31 | $(MAKE) -C $(DIR) test JOB_DIR=$(JOB_DIR) || exit 1 32 | 33 | desc: 34 | @echo "==Test Cases for NED==" 35 | @for d in $(DIRS) ; do \ 36 | $(MAKE) -sC $$d desc || exit 1; \ 37 | done 38 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | all: fxs 2 | .PHONY: all 3 | 4 | # Include standard NCS examples build definitions and rules 5 | include $(NCS_DIR)/src/ncs/build/include.ncs.mk 6 | 7 | SRC = $(wildcard yang/*.yang) 8 | DIRS = ../load-dir java/src/$(JDIR)/$(NS) 9 | FXS = $(SRC:yang/%.yang=../load-dir/%.fxs) 10 | 11 | ## Uncomment and patch the line below if you have a dependency to a NED 12 | ## or to other YANG files 13 | # YANGPATH += ../..//src/ncsc-out/modules/yang \ 14 | # ../..//src/yang 15 | 16 | NCSCPATH = $(YANGPATH:%=--yangpath %) 17 | YANGERPATH = $(YANGPATH:%=--path %) 18 | 19 | fxs: $(DIRS) $(FXS) 20 | 21 | $(DIRS): 22 | mkdir -p $@ 23 | 24 | ../load-dir/%.fxs: yang/%.yang 25 | $(NCSC) `ls $*-ann.yang > /dev/null 2>&1 && echo "-a $*-ann.yang"` \ 26 | $(NCSCPATH) -c -o $@ $< 27 | 28 | clean: 29 | rm -rf $(DIRS) 30 | .PHONY: clean 31 | -------------------------------------------------------------------------------- /python/nso_netbox/actions.py: -------------------------------------------------------------------------------- 1 | import ncs 2 | from ncs.dp import Action 3 | 4 | # from _ncs import decrypt 5 | from .netbox_utilities import verify_netbox 6 | 7 | 8 | class NetboxServerAction(Action): 9 | @Action.action 10 | def cb_action(self, uinfo, name, kp, action_input, action_output, trans): 11 | self.log.info("NetboxAction: ", name) 12 | service = ncs.maagic.get_node(trans, kp) 13 | root = ncs.maagic.get_root(trans) 14 | trans.maapi.install_crypto_keys() 15 | 16 | if name == "verify-status": 17 | self.verify_status(service, root, action_output) 18 | 19 | def verify_status(self, service, root, action_output): 20 | """Perform a status check that the NetBox server is reachable.""" 21 | netbox_status = verify_netbox(service) 22 | action_output.success = netbox_status["status"] 23 | action_output.output = netbox_status["message"] 24 | self.log.info( 25 | f'Verification Results: {netbox_status["status"]} {netbox_status["message"]}' 26 | ) 27 | -------------------------------------------------------------------------------- /templates/add-device.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {$DEVICE_NAME} 4 |
{$DEVICE_ADDRESS}
5 | {$PORT} 6 | {$DEVICE_DESCRIPTION} 7 | {$AUTH_GROUP} 8 | 9 | 10 | 11 | 12 | 13 | {$NED_ID}:{$NED_ID} 14 | {$PROTOCOL} 15 | 16 | 17 | 18 | 19 | {$NED_ID}:{$NED_ID} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {$ADMIN_STATE} 28 | 29 | 30 | {$SOURCE_CONTEXT} 31 | {$SOURCE_WHEN} 32 | {$SOURCE_SOURCE} 33 | 34 |
35 |
36 | -------------------------------------------------------------------------------- /test/internal/lux/service/run.lux: -------------------------------------------------------------------------------- 1 | # 2 | # The 'lux' test tool can be obtained from: 3 | # 4 | # https://github.com/hawk/lux.git 5 | # 6 | [global target_dir=../../../../../..] 7 | [config skip_unless=PYTHON] 8 | 9 | [shell top] 10 | !make stop build 11 | !echo ==$$?== 12 | ?==0== 13 | ?SH-PROMPT: 14 | 15 | !rm ${target_dir}/ncs-cdb/* 16 | ?SH-PROMPT: 17 | !cp pyvm.xml ${target_dir}/ncs-cdb/. 18 | ?SH-PROMPT: 19 | 20 | !make start 21 | !echo ==$$?== 22 | ?==0== 23 | ?SH-PROMPT: 24 | 25 | [progress \nCreate a dummy device...\n] 26 | !ncs_load -lm dummy-device.xml 27 | ?SH-PROMPT: 28 | [progress \nCreate a dummy device...ok\n] 29 | 30 | [sleep 3] 31 | 32 | [progress \nCreate the dummy service...\n] 33 | !ncs_load -lm dummy-service.xml 34 | ?SH-PROMPT: 35 | [progress \nCreate the dummy service...ok\n] 36 | 37 | 38 | [shell log] 39 | !cd ${target_dir} 40 | ?SH-PROMPT: 41 | 42 | [progress \nVerify that the service code has been invoked...\n] 43 | !tail -f ./logs/ncs-python-vm-nso-netbox.log 44 | ?.*Worker RUNNING.* 45 | ?.*Service create.* 46 | [progress \nVerify that the service code has been invoked...ok\n] 47 | 48 | 49 | [cleanup] 50 | !make stop 51 | !echo ==$$?== 52 | ?==0== 53 | ?SH-PROMPT: 54 | -------------------------------------------------------------------------------- /python/nso_netbox/netbox_inventory.py: -------------------------------------------------------------------------------- 1 | # -*- mode: python; python-indent: 4 -*- 2 | import ncs 3 | from ncs.application import Service 4 | from .netbox_utilities import verify_netbox, devicelist_netbox 5 | from ipaddress import ip_address 6 | 7 | 8 | # ------------------------ 9 | # SERVICE CALLBACK EXAMPLE 10 | # ------------------------ 11 | class NetboxInventoryServiceCallbacks(Service): 12 | 13 | # The create() callback is invoked inside NCS FASTMAP and 14 | # must always exist. 15 | @Service.create 16 | def cb_create(self, tctx, root, service, proplist): 17 | self.log.info("Service create(service=", service._path, ")") 18 | 19 | # TODO: How can I fire off and verify an action before commit? pre_modification? 20 | 21 | if not service.update_nso_devices: 22 | self.log.info( 23 | f"NSO Inventory {service.name} has update-nso-devices set to {service.update_nso_devices}. No NSO will be created." 24 | ) 25 | return 26 | 27 | # The pre_modification() and post_modification() callbacks are optional, 28 | # and are invoked outside FASTMAP. pre_modification() is invoked before 29 | # create, update, or delete of the service, as indicated by the enum 30 | # ncs_service_operation op parameter. Conversely 31 | # post_modification() is invoked after create, update, or delete 32 | # of the service. These functions can be useful e.g. for 33 | # allocations that should be stored and existing also when the 34 | # service instance is removed. 35 | 36 | # @Service.pre_lock_create 37 | # def cb_pre_lock_create(self, tctx, root, service, proplist): 38 | # self.log.info('Service plcreate(service=', service._path, ')') 39 | 40 | # @Service.pre_modification 41 | # def cb_pre_modification(self, tctx, op, kp, root, proplist): 42 | # self.log.info('Service premod(service=', kp, ')') 43 | 44 | # @Service.post_modification 45 | # def cb_post_modification(self, tctx, op, kp, root, proplist): 46 | # self.log.info('Service postmod(service=', kp, ')') 47 | -------------------------------------------------------------------------------- /python/nso_netbox/main.py: -------------------------------------------------------------------------------- 1 | # -*- mode: python; python-indent: 4 -*- 2 | import ncs 3 | from ncs.application import Service 4 | from .actions import NetboxServerAction 5 | from .netbox_inventory import NetboxInventoryServiceCallbacks 6 | from .netbox_inventory_actions import NetboxInventoryAction 7 | 8 | 9 | # ------------------------ 10 | # SERVICE CALLBACK EXAMPLE 11 | # ------------------------ 12 | class NetboxServerServiceCallbacks(Service): 13 | 14 | # The create() callback is invoked inside NCS FASTMAP and 15 | # must always exist. 16 | @Service.create 17 | def cb_create(self, tctx, root, service, proplist): 18 | self.log.info("Service create(service=", service._path, ")") 19 | 20 | # Use the provided connection details for the server to craft the URL to connect to 21 | if service.fqdn: 22 | netbox_url = f"{service.protocol}://{service.fqdn}:{service.port}" 23 | else: 24 | netbox_url = f"{service.protocol}://{service.address}:{service.port}" 25 | self.log.info(f"NetBox url: {netbox_url}") 26 | # TODO: After restarting NSO this value seems to go away until re-commit 27 | service.url = netbox_url 28 | 29 | # vars = ncs.template.Variables() 30 | # vars.add('DUMMY', '127.0.0.1') 31 | # template = ncs.template.Template(service) 32 | # template.apply('nso-netbox-template', vars) 33 | 34 | # The pre_modification() and post_modification() callbacks are optional, 35 | # and are invoked outside FASTMAP. pre_modification() is invoked before 36 | # create, update, or delete of the service, as indicated by the enum 37 | # ncs_service_operation op parameter. Conversely 38 | # post_modification() is invoked after create, update, or delete 39 | # of the service. These functions can be useful e.g. for 40 | # allocations that should be stored and existing also when the 41 | # service instance is removed. 42 | 43 | # @Service.pre_lock_create 44 | # def cb_pre_lock_create(self, tctx, root, service, proplist): 45 | # self.log.info('Service plcreate(service=', service._path, ')') 46 | 47 | # @Service.pre_modification 48 | # def cb_pre_modification(self, tctx, op, kp, root, proplist): 49 | # self.log.info('Service premod(service=', kp, ')') 50 | 51 | # @Service.post_modification 52 | # def cb_post_modification(self, tctx, op, kp, root, proplist): 53 | # self.log.info('Service postmod(service=', kp, ')') 54 | 55 | 56 | # --------------------------------------------- 57 | # COMPONENT THREAD THAT WILL BE STARTED BY NCS. 58 | # --------------------------------------------- 59 | class Main(ncs.application.Application): 60 | def setup(self): 61 | # The application class sets up logging for us. It is accessible 62 | # through 'self.log' and is a ncs.log.Log instance. 63 | self.log.info("Main RUNNING") 64 | 65 | # Service callbacks require a registration for a 'service point', 66 | # as specified in the corresponding data model. 67 | # 68 | self.register_service( 69 | "nso-netbox-server-servicepoint", NetboxServerServiceCallbacks 70 | ) 71 | self.register_service( 72 | "nso-netbox-inventory-servicepoint", NetboxInventoryServiceCallbacks 73 | ) 74 | self.register_action("netbox-verify-status", NetboxServerAction) 75 | self.register_action("netbox-inventory-build", NetboxInventoryAction) 76 | self.register_action("netbox-inventory-connect", NetboxInventoryAction) 77 | self.register_action("netbox-inventory-remove", NetboxInventoryAction) 78 | self.register_action("netbox-inventory-verify", NetboxInventoryAction) 79 | 80 | # If we registered any callback(s) above, the Application class 81 | # took care of creating a daemon (related to the service/action point). 82 | 83 | # When this setup method is finished, all registrations are 84 | # considered done and the application is 'started'. 85 | 86 | def teardown(self): 87 | # When the application is finished (which would happen if NCS went 88 | # down, packages were reloaded or some error occurred) this teardown 89 | # method will be called. 90 | 91 | self.log.info("Main FINISHED") 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CISCO SAMPLE CODE LICENSE 2 | Version 1.0 3 | Copyright (c) 2017 Cisco and/or its affiliates 4 | 5 | These terms govern this Cisco example or demo source code and its 6 | associated documentation (together, the "Sample Code"). By downloading, 7 | copying, modifying, compiling, or redistributing the Sample Code, you 8 | accept and agree to be bound by the following terms and conditions (the 9 | "License"). If you are accepting the License on behalf of an entity, you 10 | represent that you have the authority to do so (either you or the entity, 11 | "you"). Sample Code is not supported by Cisco TAC and is not tested for 12 | quality or performance. This is your only license to the Sample Code and 13 | all rights not expressly granted are reserved. 14 | 15 | 1. LICENSE GRANT: Subject to the terms and conditions of this License, 16 | Cisco hereby grants to you a perpetual, worldwide, non-exclusive, non- 17 | transferable, non-sublicensable, royalty-free license to copy and 18 | modify the Sample Code in source code form, and compile and 19 | redistribute the Sample Code in binary/object code or other executable 20 | forms, in whole or in part, solely for use with Cisco products and 21 | services. For interpreted languages like Java and Python, the 22 | executable form of the software may include source code and 23 | compilation is not required. 24 | 25 | 2. CONDITIONS: You shall not use the Sample Code independent of, or to 26 | replicate or compete with, a Cisco product or service. Cisco products 27 | and services are licensed under their own separate terms and you shall 28 | not use the Sample Code in any way that violates or is inconsistent 29 | with those terms (for more information, please visit: 30 | www.cisco.com/go/terms. 31 | 32 | 3. OWNERSHIP: Cisco retains sole and exclusive ownership of the Sample 33 | Code, including all intellectual property rights therein, except with 34 | respect to any third-party material that may be used in or by the 35 | Sample Code. Any such third-party material is licensed under its own 36 | separate terms (such as an open source license) and all use must be in 37 | full accordance with the applicable license. This License does not 38 | grant you permission to use any trade names, trademarks, service 39 | marks, or product names of Cisco. If you provide any feedback to Cisco 40 | regarding the Sample Code, you agree that Cisco, its partners, and its 41 | customers shall be free to use and incorporate such feedback into the 42 | Sample Code, and Cisco products and services, for any purpose, and 43 | without restriction, payment, or additional consideration of any kind. 44 | If you initiate or participate in any litigation against Cisco, its 45 | partners, or its customers (including cross-claims and counter-claims) 46 | alleging that the Sample Code and/or its use infringe any patent, 47 | copyright, or other intellectual property right, then all rights 48 | granted to you under this License shall terminate immediately without 49 | notice. 50 | 51 | 4. LIMITATION OF LIABILITY: CISCO SHALL HAVE NO LIABILITY IN CONNECTION 52 | WITH OR RELATING TO THIS LICENSE OR USE OF THE SAMPLE CODE, FOR 53 | DAMAGES OF ANY KIND, INCLUDING BUT NOT LIMITED TO DIRECT, INCIDENTAL, 54 | AND CONSEQUENTIAL DAMAGES, OR FOR ANY LOSS OF USE, DATA, INFORMATION, 55 | PROFITS, BUSINESS, OR GOODWILL, HOWEVER CAUSED, EVEN IF ADVISED OF THE 56 | POSSIBILITY OF SUCH DAMAGES. 57 | 58 | 5. DISCLAIMER OF WARRANTY: SAMPLE CODE IS INTENDED FOR EXAMPLE PURPOSES 59 | ONLY AND IS PROVIDED BY CISCO "AS IS" WITH ALL FAULTS AND WITHOUT 60 | WARRANTY OR SUPPORT OF ANY KIND. TO THE MAXIMUM EXTENT PERMITTED BY 61 | LAW, ALL EXPRESS AND IMPLIED CONDITIONS, REPRESENTATIONS, AND 62 | WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OR 63 | CONDITION OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON- 64 | INFRINGEMENT, SATISFACTORY QUALITY, NON-INTERFERENCE, AND ACCURACY, 65 | ARE HEREBY EXCLUDED AND EXPRESSLY DISCLAIMED BY CISCO. CISCO DOES NOT 66 | WARRANT THAT THE SAMPLE CODE IS SUITABLE FOR PRODUCTION OR COMMERCIAL 67 | USE, WILL OPERATE PROPERLY, IS ACCURATE OR COMPLETE, OR IS WITHOUT 68 | ERROR OR DEFECT. 69 | 70 | 6. GENERAL: This License shall be governed by and interpreted in 71 | accordance with the laws of the State of California, excluding its 72 | conflict of laws provisions. You agree to comply with all applicable 73 | United States export laws, rules, and regulations. If any provision of 74 | this License is judged illegal, invalid, or otherwise unenforceable, 75 | that provision shall be severed and the rest of the License shall 76 | remain in full force and effect. No failure by Cisco to enforce any of 77 | its rights related to the Sample Code or to a breach of this License 78 | in a particular situation will act as a waiver of such rights. In the 79 | event of any inconsistencies with any other terms, this License shall 80 | take precedence. -------------------------------------------------------------------------------- /python/nso_netbox/netbox_utilities.py: -------------------------------------------------------------------------------- 1 | """Set of reusable functions for NetBox 2 | 3 | """ 4 | 5 | import pynetbox 6 | from requests import exceptions 7 | from _ncs import decrypt 8 | from pynetbox.core.query import RequestError 9 | 10 | 11 | def verify_netbox(netbox_server): 12 | """Verify a NetBox Server is reachable""" 13 | nb = pynetbox.api(netbox_server.url, token=decrypt(netbox_server.api_token)) 14 | try: 15 | status = nb.status() 16 | except RequestError as e: 17 | # Older versions of NetBox don't have the status api, try to retrieve devices 18 | devices = nb.dcim.devices.all() 19 | return { 20 | "status": True, 21 | "message": f"Successfully connected to NetBox to query devices." 22 | } 23 | except exceptions.ConnectionError: 24 | return { 25 | "status": False, 26 | "message": f"Error connecting to NetBox Server at url {netbox_server.url}.", 27 | } 28 | 29 | status_message = f"NetBox Version: {status['netbox-version']}, Python Version: {status['python-version']}, Plugins: {status['plugins']}, Workers Running: {status['rq-workers-running']}" 30 | return {"status": True, "message": status_message} 31 | 32 | 33 | def query_netbox(object, log=False, **query): 34 | """Send a filter query to NetBox for an object""" 35 | results = object.filter(**query) 36 | if log: 37 | log.info(f" Results: {results}") 38 | 39 | return results 40 | 41 | 42 | def devicelist_netbox(netbox_inventory, netbox_server, log=False): 43 | """Retrieve matching devices from NetBox for an inventory""" 44 | 45 | try: 46 | nb = pynetbox.api(netbox_server.url, token=decrypt(netbox_server.api_token)) 47 | 48 | # Build the device query from the provided attributes to the inventory instance 49 | device_query = {} 50 | if netbox_inventory.site: 51 | if log: 52 | log.info("Looking up NetBox Sites to Filter.") 53 | sites = query_netbox( 54 | nb.dcim.sites, log, name=[site for site in netbox_inventory.site] 55 | ) 56 | device_query["site_id"] = [site.id for site in sites] 57 | 58 | if netbox_inventory.tenant: 59 | if log: 60 | log.info("Looking up NetBox Tenants to Filter.") 61 | tenants = query_netbox( 62 | object=nb.tenancy.tenants, 63 | log=log, 64 | name=[tenant for tenant in netbox_inventory.tenant], 65 | ) 66 | device_query["tenant_id"] = [tenant.id for tenant in tenants] 67 | 68 | if netbox_inventory.device_type: 69 | if log: 70 | log.info("Looking up NetBox Device-Types to Filter.") 71 | device_types = query_netbox( 72 | object=nb.dcim.device_types, 73 | log=log, 74 | model=[ 75 | device_type.model for device_type in netbox_inventory.device_type 76 | ], 77 | ) 78 | device_query["device_type_id"] = [ 79 | device_type.id for device_type in device_types 80 | ] 81 | 82 | if netbox_inventory.device_role: 83 | if log: 84 | log.info("Looking up NetBox Device-Roles to Filter.") 85 | device_roles = query_netbox( 86 | object=nb.dcim.device_roles, 87 | log=log, 88 | name=[device_role for device_role in netbox_inventory.device_role], 89 | ) 90 | device_query["role_id"] = [device_role.id for device_role in device_roles] 91 | 92 | if log: 93 | log.info(f"Looking up NetBox Devices for Filter: {device_query}") 94 | devices = query_netbox(object=nb.dcim.devices, log=log, **device_query) 95 | 96 | return {"status": True, "result": devices} 97 | except Exception as e: 98 | if log: 99 | log.error(f"Lookup failed: {e}") 100 | return {"status": False, "result": e} 101 | 102 | 103 | def vmlist_netbox(netbox_inventory, netbox_server, log=False): 104 | """Retrieve matching Virtual Machines from NetBox for an inventory""" 105 | 106 | try: 107 | nb = pynetbox.api(netbox_server.url, token=decrypt(netbox_server.api_token)) 108 | 109 | # Build the VM query from the provided attributes to the inventory instance 110 | vm_query = {} 111 | if netbox_inventory.site: 112 | if log: 113 | log.info("Looking up NetBox Sites to Filter.") 114 | sites = query_netbox( 115 | nb.dcim.sites, log, name=[site for site in netbox_inventory.site] 116 | ) 117 | vm_query["site_id"] = [site.id for site in sites] 118 | 119 | if netbox_inventory.tenant: 120 | if log: 121 | log.info("Looking up NetBox Tenants to Filter.") 122 | tenants = query_netbox( 123 | object=nb.tenancy.tenants, 124 | log=log, 125 | name=[tenant for tenant in netbox_inventory.tenant], 126 | ) 127 | vm_query["tenant_id"] = [tenant.id for tenant in tenants] 128 | 129 | if netbox_inventory.vm_role: 130 | if log: 131 | log.info("Looking up NetBox Virtual Machine Roles to Filter.") 132 | vm_roles = query_netbox( 133 | object=nb.dcim.device_roles, 134 | log=log, 135 | name=[vm_role.role for vm_role in netbox_inventory.vm_role], 136 | vm_role=True, 137 | ) 138 | vm_query["role_id"] = [vm_role.id for vm_role in vm_roles] 139 | 140 | if log: 141 | log.info(f"Looking up NetBox vms for Filter: {vm_query}") 142 | vms = query_netbox(object=nb.virtualization.virtual_machines, log=log, **vm_query) 143 | 144 | return {"status": True, "result": vms} 145 | except Exception as e: 146 | if log: 147 | log.error(f"Lookup failed: {e}") 148 | return {"status": False, "result": e} 149 | -------------------------------------------------------------------------------- /src/yang/nso-netbox.yang: -------------------------------------------------------------------------------- 1 | module nso-netbox { 2 | 3 | namespace "http://learning.cisco.com/nso-netbox"; 4 | prefix nso-netbox; 5 | yang-version 1.1; 6 | 7 | import ietf-inet-types { 8 | prefix inet; 9 | } 10 | import tailf-common { 11 | prefix tailf; 12 | } 13 | import tailf-ncs { 14 | prefix ncs; 15 | } 16 | 17 | description 18 | "This will be an NSO package for interacting with NetBox as a source of truth."; 19 | 20 | revision 2021-03-05 { 21 | description 22 | "Initial package"; 23 | } 24 | 25 | revision 2021-03-23 { 26 | description 27 | "Add support for NetBox VM-Roles in addition to Device Types."; 28 | } 29 | 30 | list netbox-server { 31 | description "NetBox server instance."; 32 | 33 | key name; 34 | leaf name { 35 | tailf:info "Friendly name for NetBox server."; 36 | type string; 37 | } 38 | 39 | choice server-address { 40 | case dns { 41 | leaf fqdn { 42 | tailf:info "DNS Name for NetBox server"; 43 | type inet:domain-name; 44 | } 45 | } 46 | 47 | case ip { 48 | leaf address { 49 | tailf:info "IP address for NetBox server"; 50 | type inet:ipv4-address; 51 | } 52 | } 53 | } 54 | 55 | 56 | leaf url { 57 | tailf:info "Crafted URL for NetBox server"; 58 | type string; 59 | config false; 60 | tailf:cdb-oper { tailf:persistent true; } 61 | } 62 | 63 | leaf port { 64 | tailf:info "TCP Port NetBox server listening on"; 65 | type uint16 { 66 | range "0 .. 65535"; 67 | } 68 | default 443; 69 | } 70 | 71 | leaf protocol { 72 | tailf:info "HTTP or HTTPS used to connect to NetBox"; 73 | type enumeration { 74 | enum http; 75 | enum https; 76 | } 77 | default https; 78 | } 79 | 80 | leaf api-token { 81 | tailf:info "API Token for interacting with NetBox Server."; 82 | type tailf:aes-cfb-128-encrypted-string; 83 | } 84 | 85 | action verify-status { 86 | tailf:actionpoint netbox-verify-status; 87 | tailf:info "Perform a status check that the NetBox server is reachable."; 88 | output { 89 | leaf output { type string; } 90 | leaf success { type boolean; } 91 | } 92 | } 93 | 94 | uses ncs:service-data; 95 | ncs:servicepoint nso-netbox-server-servicepoint; 96 | 97 | } 98 | 99 | list netbox-inventory { 100 | description "Definition of a NetBox lookup of devices/vms to add to NSO as devices."; 101 | 102 | key name; 103 | leaf name { 104 | tailf:info "Name for this set of devices."; 105 | type string; 106 | } 107 | 108 | leaf auth-group { 109 | tailf:info "The authgroup to use for this set of devices."; 110 | type leafref { 111 | path "/ncs:devices/ncs:authgroups/ncs:group/ncs:name"; 112 | } 113 | mandatory true; 114 | } 115 | 116 | leaf connection-protocol { 117 | tailf:info "The type of connection protocol to connect to the device with."; 118 | type enumeration { 119 | enum ssh; 120 | enum telnet; 121 | enum http; 122 | enum https; 123 | } 124 | default ssh; 125 | } 126 | 127 | leaf bypass-certificate-verification { 128 | tailf:info "Whether to verify keys for SSL type connections. A setting of 'true' is equivalent to 'verify = False'"; 129 | type boolean; 130 | default true; 131 | } 132 | 133 | leaf admin-state { 134 | tailf:info "The admin-state to add devices to NSO in."; 135 | type enumeration { 136 | enum config-locked; 137 | enum locked; 138 | enum southbound-locked; 139 | enum unlocked; 140 | } 141 | } 142 | 143 | leaf update-nso-devices { 144 | tailf:info "Whether to add/update the NSO devices based on this query. If false, inventory can be used for verifications."; 145 | type boolean; 146 | default false; 147 | } 148 | 149 | leaf netbox-server { 150 | tailf:info "The NetBox Server to query for this inventory lookup."; 151 | type leafref { 152 | path "/nso-netbox:netbox-server/name"; 153 | } 154 | mandatory true; 155 | } 156 | 157 | leaf-list site { 158 | tailf:info "Name of a NetBox Site to limit query to."; 159 | type string; 160 | } 161 | 162 | leaf-list tenant { 163 | tailf:info "Name of a NetBox Tenant to limit query to."; 164 | type string; 165 | } 166 | 167 | list device-type { 168 | tailf:info "NetBox Device Types to include in the inventory."; 169 | 170 | key model; 171 | leaf model { 172 | tailf:info "The Model Name for the Device-Type."; 173 | type string; 174 | } 175 | 176 | leaf ned { 177 | tailf:info "The NED to use for this device type."; 178 | type string; 179 | tailf:non-strict-leafref { 180 | path "/ncs:packages/ncs:package/ncs:build-info/ncs:package/ncs:name"; 181 | } 182 | mandatory true; 183 | } 184 | } 185 | 186 | leaf-list device-role { 187 | tailf:info "Name of a NetBox Device Role to limit query to."; 188 | type string; 189 | } 190 | 191 | list vm-role { 192 | tailf:info "NetBox VM Roles to include in the inventory. Explicit vm-role -> NED definitions required."; 193 | 194 | key role; 195 | leaf role { 196 | tailf:info "The name of the VM Role to include."; 197 | type string; 198 | } 199 | 200 | leaf ned { 201 | tailf:info "The NED to use for this VM Role."; 202 | type string; 203 | tailf:non-strict-leafref { 204 | path "/ncs:packages/ncs:package/ncs:build-info/ncs:package/ncs:name"; 205 | } 206 | mandatory true; 207 | } 208 | } 209 | 210 | // Constraint - each inventory must have at least 1 device-type or vm-role defined 211 | must "count(device-type) + count(vm-role) >= 1" { 212 | error-message "Every netbox-inventory must have at least 1 device-type or vm-role defined."; 213 | } 214 | 215 | uses ncs:service-data; 216 | ncs:servicepoint nso-netbox-inventory-servicepoint; 217 | 218 | action verify-inventory { 219 | tailf:actionpoint netbox-inventory-verify; 220 | tailf:info "Verify that the NetBox Devices for the Inventory are present in NSO as Devices."; 221 | 222 | output { 223 | leaf output { type string; } 224 | leaf success { type boolean; } 225 | } 226 | } 227 | 228 | action build-inventory { 229 | tailf:actionpoint netbox-inventory-build; 230 | tailf:info "Deploy NSO devices based on the NetBox Devices for the described query."; 231 | 232 | input { 233 | leaf commit { 234 | tailf:info "Whether to commit/merge the changes to the devices into NSO."; 235 | type boolean; 236 | default false; 237 | } 238 | } 239 | 240 | output { 241 | leaf output { type string; } 242 | leaf success { type boolean; } 243 | } 244 | } 245 | 246 | action connect-inventory { 247 | tailf:actionpoint netbox-inventory-connect; 248 | tailf:info "Establish connection to each inventory device and optionally sync-from."; 249 | 250 | input { 251 | leaf sync-from { 252 | tailf:info "Whether to perform a sync-from on devices."; 253 | type boolean; 254 | default false; 255 | } 256 | } 257 | 258 | output { 259 | leaf output { type string; } 260 | leaf success { type boolean; } 261 | } 262 | 263 | } 264 | 265 | action remove-inventory { 266 | tailf:actionpoint netbox-inventory-remove; 267 | tailf:info "Remove NSO devices based on the NetBox Devices for the described query."; 268 | 269 | input { 270 | leaf commit { 271 | tailf:info "Whether to commit/merge the changes to the devices into NSO."; 272 | type boolean; 273 | default false; 274 | } 275 | } 276 | 277 | output { 278 | leaf output { type string; } 279 | leaf success { type boolean; } 280 | } 281 | } 282 | 283 | 284 | } 285 | 286 | } 287 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NSO package for interacting with NetBox 2 | 3 | This will be an NSO package for interacting with NetBox as a source of truth. Example use cases include: 4 | 5 | * Generating a device inventory from NetBox devices 6 | * IP Address/Prefix Allocation 7 | * Gathering data for verification checks 8 | 9 | ## NSO and NetBox Version Info 10 | This package has been tested with the following versions of Cisco NSO and NetBox. It may work with other versions of the products, but be prepared for potential troubleshooting. 11 | 12 | * Cisco NSO 13 | * 5.5 14 | * 5.4.1 15 | * NetBox 16 | * v2.10.4 17 | * v2.8.4 18 | 19 | ## Installing the nso-netbox package 20 | > These instructions assume you already have installed NSO and know the basics of setting up an NSO local instance. For a detailed walkthrough of these steps, see the documentation [Getting and Installing NSO](https://developer.cisco.com/docs/nso/#!getting-and-installing-nso/getting-nso) on DevNet. 21 | 22 | Installing the `nso-netbox` package is straightforward and follows the same process as most NSO packages. Follow these steps to get started with a local-instance of NSO. 23 | 24 | 1. Clone down this repository to your NSO server. 25 | 26 | ``` 27 | https://gitlab.com/nso-developer/nso-netbox.git 28 | cd nso-netbox 29 | ``` 30 | 31 | 1. Now you need to compile the YANG models for the package for your NSO version. 32 | 33 | ``` 34 | cd src 35 | make 36 | cd .. 37 | ``` 38 | 39 | 1. `nso-netbox` is a Python package and leverages the [`pynetbox`](https://pypi.org/project/pynetbox/) library for interacting with NetBox. While any version of the library may work, it is recommended to install the same version that the package was developed and tested with. The file `src\requirements.txt` is included with the package and has the version specified. 40 | 41 | ``` 42 | python3 -m pip install -r src/requirements.txt 43 | ``` 44 | 45 | > Note: Current versions of Cisco NSO will default to using `python3` to startup Python packages. If you are using a custom Python interpreter or Virtual Environment for your packages, just be sure to install the requirements into the appropriate place. 46 | 47 | 1. Now you're ready to setup a new NSO instance using this package. 48 | * The example command makes the following assumptions. Adjust as necessary for your environment. 49 | * You have cloned the `nso-netbox` repository to your home directory 50 | * Your local-installation of NSO is located at `~/nso` 51 | * The example command also shows adding packages for `cisco-nx-cli` and `cisco-ios-cli`. An NSO installation includes these demo NED packages. More details can be found at [Getting and Installing NSO](https://developer.cisco.com/docs/nso/#!getting-and-installing-nso/getting-nso) 52 | 53 | ``` 54 | ncs-setup --package ~/nso-netbox \ 55 | --package ~/nso/packages/neds/cisco-nx-cli-3.0 56 | --package ~/nso/packages/neds/cisco-ios-cli-3.8 57 | --dest ~/nso-instance 58 | ``` 59 | 60 | 1. Now startup the new NSO instance 61 | 62 | ``` 63 | cd ~/nso-instance 64 | ncs 65 | ``` 66 | 67 | 1. After NSO starts, you can verify the package started correctly from `ncs_cli` 68 | 69 | ``` 70 | ncs_cli -u admin -C 71 | show packages package oper-status 72 | 73 | # Example Output 74 | packages package cisco-ios-cli-6.67 75 | oper-status up 76 | packages package cisco-nx-cli-5.20 77 | oper-status up 78 | packages package nso-netbox 79 | oper-status up 80 | ``` 81 | 82 | ## Using the nso-netbox package 83 | This short walkthrough will show how the nso-netbox package can be used to integrate NetBox with Cisco NSO. This walkthrough is intended to highlight the use of the package, **NOT** how the package was built. 84 | 85 | ## Add a NetBox Server to NSO 86 | 1. The first step for any interaction involves adding a NetBox Server instance to to NSO. To add the server, you'll need the following information. 87 | * The FQDN or IP Address for the NetBox server 88 | * An API token for a user. Read-Only permissions on the token should be sufficient 89 | * The protocol (http / https) and port (80 / 443) that NetBox is listening on. 90 | 1. With that information, you can add this block of configuration to NSO. 91 | 92 | ``` 93 | netbox-server example-vm-netbox-01.example.net 94 | fqdn example-netbox-01.example.net 95 | port 80 96 | protocol http 97 | api-token uana9a8nakduandkadda68c6885383b16feefe3 98 | ``` 99 | 100 | > Note: If you are using IP address, you'd configure the `address 172.23.80.1` instead of `fqdn`. 101 | > Note: The service defaults to `protocol https` and `port 443` 102 | 103 | 1. Once committed to NSO, an additional value for the `netbox-server` is constructed to represent the URL for the server. 104 | 105 | ``` 106 | localadmin@ncs# show netbox-server example-vm-netbox-01.example.net url 107 | 108 | url http://example-netbox-01.example.net:80 109 | ``` 110 | 111 | 1. It is always a good idea to verify that NSO can communicate with NetBox after adding a new server. You can do so with this action. 112 | 113 | ``` 114 | netbox-server example-vm-netbox-01.example.net verify-status 115 | 116 | # Sample Output 117 | output NetBox Version: 2.10.4, Python Version: 3.8.7, Plugins: {}, Workers Running: 1 118 | success true 119 | ``` 120 | 121 | > Note: A communciation verification to the server is run by most other actions before attempting to take any action. 122 | 123 | ## Verifying the NSO Devices match NetBox 124 | As the "Source of Truth", the devices listed in NetBox (including thier address, types, and status) should drive what NSO has in it's own inventory. We can do this check and verification by adding a `netbox-inventory` object to NSO. 125 | 126 | 1. A `netbox-inventory` configuration is a query or filter that NSO will use to lookup a subset of devices from NetBox to work with. This filter can include the following characteristics. 127 | * `site` - The NetBox Site the devices are a member of 128 | * `tenant` - The NetBox Tenant that the device is associated to 129 | * `device-role` - The NetBox Device Role assigned to the device 130 | * `device-type` - The NetBox Device Type of the device 131 | * `vm-role` - The NetBox Device Role for Virtual Machines to be added to inventory. 132 | * *See below for more details* 133 | 134 | > Note: Each characteristic can be added more than once. A device would simply need to match one of the possible values listed to be selected. 135 | 1. Every `netbox-inventory` requires at least one `device-type` or `vm-role` be configured. Furthermore, for each of these configured, you must specify the NSO NED that corresponds to this device. 136 | 1. In addition to the filters used to query NetBox, you also need to specify the following values for a `netbox-inventory`. 137 | * `auth-group` - The NSO `devices auth-group` that should be used for the devices that match the query 138 | * `netbox-server` - The `netbox-server` instance you are querying with this inventory 139 | 1. With this information available, you can create the following configuration. 140 | 141 | ``` 142 | netbox-inventory router-verify 143 | auth-group localadmin 144 | netbox-server example-vm-netbox-01.example.net 145 | site [ MYDC ] 146 | tenant [ example-admin ] 147 | device-type "CSR 1000v-physical" 148 | ned cisco-ios-cli-6.69 149 | ``` 150 | 151 | 1. After you've committed this configuration, you can now check to see if the NSO `` that match this query are configured correctly with this NSO action. 152 | 153 | ``` 154 | netbox-inventory router-verify verify-inventory 155 | 156 | # Example Output when all is correct 157 | output 158 | success true 159 | ``` 160 | 161 | 1. In the output above, everything is correct in NSO. But suppose something didn't match. You'd get an output such as this. 162 | 163 | ``` 164 | netbox-inventory router-verify verify-inventory 165 | 166 | # Output with errors 167 | output Device example-rtr-dmz-01 not found in NSO . 168 | Device example-rtr-edge-02 has a NetBox Primary IP of 172.31.128.14, NSO device is configured for address 1.1.1.1 169 | Device example-rtr-edge-02 has a NetBox Role of Virtual/Physical Router, which doesn't match NSO description of None 170 | success false 171 | ``` 172 | 173 | > Note: The `success false` indicates at least one difference was found. And the `output` highlights the differences. 174 | 175 | 1. You could manually fix these errors, or move to the next section to see how this can be done automatically. 176 | 177 | ## Building NSO `` from NetBox 178 | Verifying that the data in NSO matches the Source of Truth is a great first step, but wouldn't it be great to just enforce the configuration from NetBox to NSO? Of course it would. 179 | 180 | 1. Start by updating the `netbox-inventory` configuration to support updating NSO by setting the `update-nso-devices` value to `true`. The default value is `false` to prevent unintended configuration updates. 181 | 182 | ``` 183 | netbox-inventory router-verify 184 | update-nso-devices true 185 | ``` 186 | 187 | 1. Now you can take advantage of the `build-inventory` action. 188 | 189 | ```yaml 190 | netbox-inventory router-verify build-inventory 191 | 192 | # Sample Output 193 | output # Adding Devices to NSO from NetBox inventory. 194 | devices: 195 | - device: example-rtr-dmz-01 196 | address: 172.31.128.21 197 | description: Virtual/Physical Router 198 | auth-group: localadmin 199 | device-type: 200 | cli: 201 | ned-id: cisco-ios-cli-6.69 202 | protocol: ssh 203 | state: unlocked 204 | source: 205 | context: {"web": "http://example-netbox-01.example.net/dcim/devices/205/", "api": "http://example-netbox-01.example.net/api/dcim/devices/205/"} 206 | when: 2021-03-18T13:55:02 207 | source: /nso-netbox:netbox-inventory{router-verify} 208 | device-groups: 209 | - NetBoxInventory router-verify: 210 | - NetBoxInventory router-verify CSR 1000v-physical: 211 | - NetBoxInventory router-verify Virtual/Physical Router: 212 | - NetBoxInventory router-verify example-admin: 213 | 214 | 215 | # Action input commit: False. Devices will NOT be added to NSO. 216 | - device: example-rtr-dmz-02 217 | address: 172.31.128.22 218 | description: Virtual/Physical Router 219 | auth-group: localadmin 220 | device-type: 221 | cli: 222 | ned-id: cisco-ios-cli-6.69 223 | protocol: ssh 224 | state: unlocked 225 | source: 226 | context: {"web": "http://example-netbox-01.example.net/dcim/devices/206/", "api": "http://example-netbox-01.example.net/api/dcim/devices/206/"} 227 | when: 2021-03-18T13:55:02 228 | source: /nso-netbox:netbox-inventory{router-verify} 229 | device-groups: 230 | - NetBoxInventory router-verify: 231 | - NetBoxInventory router-verify CSR 1000v-physical: 232 | - NetBoxInventory router-verify Virtual/Physical Router: 233 | - NetBoxInventory router-verify example-admin: 234 | 235 | 236 | # Action input commit: False. Devices will NOT be added to NSO. 237 | - device: example-rtr-edge-01 238 | address: 172.31.128.13 239 | description: Virtual/Physical Router 240 | auth-group: localadmin 241 | device-type: 242 | cli: 243 | ned-id: cisco-ios-cli-6.69 244 | protocol: ssh 245 | state: unlocked 246 | source: 247 | context: {"web": "http://example-netbox-01.example.net/dcim/devices/201/", "api": "http://example-netbox-01.example.net/api/dcim/devices/201/"} 248 | when: 2021-03-18T13:55:02 249 | source: /nso-netbox:netbox-inventory{router-verify} 250 | device-groups: 251 | - NetBoxInventory router-verify: 252 | - NetBoxInventory router-verify CSR 1000v-physical: 253 | - NetBoxInventory router-verify Virtual/Physical Router: 254 | - NetBoxInventory router-verify example-admin: 255 | 256 | 257 | # Action input commit: False. Devices will NOT be added to NSO. 258 | - device: example-rtr-edge-02 259 | address: 172.31.128.14 260 | description: Virtual/Physical Router 261 | auth-group: localadmin 262 | device-type: 263 | cli: 264 | ned-id: cisco-ios-cli-6.69 265 | protocol: ssh 266 | state: unlocked 267 | source: 268 | context: {"web": "http://example-netbox-01.example.net/dcim/devices/202/", "api": "http://example-netbox-01.example.net/api/dcim/devices/202/"} 269 | when: 2021-03-18T13:55:02 270 | source: /nso-netbox:netbox-inventory{router-verify} 271 | device-groups: 272 | - NetBoxInventory router-verify: 273 | - NetBoxInventory router-verify CSR 1000v-physical: 274 | - NetBoxInventory router-verify Virtual/Physical Router: 275 | - NetBoxInventory router-verify example-admin: 276 | 277 | 278 | # Action input commit: False. Devices will NOT be added to NSO. 279 | success false 280 | ``` 281 | 282 | 1. In the output above, notice the final line of output. There is a flag to the `build-inventory` action for `commit`. If you don't explicitly set this to `commit true`, it is kinda like a `dry-run`. You'll see the configuration to be applied, but it won't actually apply the configuration. This is a second protection from unintended configuration changes to NSO. 283 | 1. In addition to adding the devices that match the inventory, several `device-groups` are created for the inventory. There will be groups for: 284 | * The entire `netbox-inventory` instance 285 | * The `device-types` of each device 286 | * The `device-roles` for each device 287 | * THe `tenants` for each device 288 | 289 | > Note: even if the attributes are NOT used as filters in the inventory, the groups are still created 290 | 291 | 1. To actually apply the configuration, run the `build-inventory commit true` command. 292 | 293 | ```yaml 294 | netbox-inventory router-verify build-inventory commit true 295 | 296 | # Sample output - some content removed for brevity 297 | output # Adding Devices to NSO from NetBox inventory. 298 | devices: 299 | - device: example-rtr-dmz-01 300 | address: 172.31.128.21 301 | description: Virtual/Physical Router 302 | auth-group: localadmin 303 | device-type: 304 | cli: 305 | ned-id: cisco-ios-cli-6.69 306 | protocol: ssh 307 | state: unlocked 308 | source: 309 | context: {"web": "http://example-netbox-01.example.net/dcim/devices/205/", "api": "http://example-netbox-01.example.net/api/dcim/devices/205/"} 310 | when: 2021-03-18T14:00:45 311 | source: /nso-netbox:netbox-inventory{router-verify} 312 | device-groups: 313 | - NetBoxInventory router-verify: 314 | - NetBoxInventory router-verify CSR 1000v-physical: 315 | - NetBoxInventory router-verify Virtual/Physical Router: 316 | - NetBoxInventory router-verify example-admin: 317 | . 318 | . 319 | . 320 | success true 321 | localadmin@ncs# 322 | System message at 2021-03-18 14:00:45... 323 | Commit performed by localadmin via tcp using build-inventory. 324 | ``` 325 | 326 | 1. This time you see `success true` at the end of the output. Also there is the message from NSO that a commit was performed. 327 | 1. To verify the inventory was applied correctly, you can run the `verify-inventory` once again. 328 | 329 | ## Adding SSH Keys, Verifying Connection to Devices, and initial Sync-From 330 | The first time a new device is added to NSO there are a few steps that need to be taken to complete the connection. These include: 331 | 332 | * Fetching SSH Host-Keys (if SSH is used instead of Telnet) 333 | * Attempting to `connect` to the device to make sure the credentials are working 334 | * Performing a `sync-from` to update the NSO CDB with the initial configuration for the device. 335 | 336 | These steps could be done manually, but there is an action on the `netbox-inventory` that will make this easier. 337 | 338 | 1. First, let's run the `connect-inventory` action. 339 | > Note: this action can take a long time if you have many devices that match the filter 340 | 341 | ``` 342 | netbox-inventory router-verify connect-inventory 343 | 344 | # Sample Output 345 | output Connecting to devices from inventory router-verify 346 | Connecting to device example-rtr-dmz-01 347 | - Fetching SSH Host-Keys 348 | result: updated None 349 | - Testing Connecting to Device 350 | result: True (localadmin) Connected to example-rtr-dmz-01 - 172.31.128.21:22 351 | Connecting to device example-rtr-dmz-02 352 | - Fetching SSH Host-Keys 353 | result: unchanged None 354 | - Testing Connecting to Device 355 | result: True (localadmin) Connected to example-rtr-dmz-02 - 172.31.128.22:22 356 | Connecting to device example-rtr-edge-01 357 | - Fetching SSH Host-Keys 358 | result: unchanged None 359 | - Testing Connecting to Device 360 | result: True (localadmin) Connected to example-rtr-edge-01 - 172.31.128.13:22 361 | Connecting to device example-rtr-edge-02 362 | - Fetching SSH Host-Keys 363 | result: unchanged None 364 | - Testing Connecting to Device 365 | result: True (localadmin) Connected to example-rtr-edge-02 - 172.31.128.14:22 366 | success true 367 | ``` 368 | 369 | * You'll get status messages letting you know the results of fetching keys and connecting. If an error is found in this step you should work to fix the configuration for the inventory. The most likely candidates for problems are incorrect `primary-ips` for devices in NetBox or devices where the credentials in the configured `auth-group` aren't correct. 370 | 371 | 1. But there is nothing about a `sync-from` in the output above. By default `connect-inventory` will only grab SSH keys and try to connect. To `sync-from` you need to set this true. 372 | > Note: this action can take a long time if you have many devices that match the filter 373 | 374 | ``` 375 | netbox-inventory router-verify connect-inventory sync-from true 376 | 377 | # Sample output - edited for brevity 378 | output Connecting to devices from inventory router-verify 379 | Connecting to device example-rtr-dmz-01 380 | - Fetching SSH Host-Keys 381 | result: unchanged None 382 | - Testing Connecting to Device 383 | result: True (localadmin) Connected to example-rtr-dmz-01 - 172.31.128.21:22 384 | - Performing sync-from 385 | result: True None 386 | . 387 | . 388 | . 389 | success true 390 | ``` 391 | 392 | * Now you see the `Performing sync-from` in the output 393 | 394 | ## Using nso-netbox after initial inventory setup 395 | While this package is clearly useful to get the initial devices setup correctly in NSO to match NetBox, the ability to run the `verify-inventory` action at anytime is key to making sure NSO stays aligned to the Source of Truth. Some ideas on how to do this would be: 396 | 397 | * Scheduling regular runs of `verify-inventory` to occur using the RESTCONF or NETCONF API for NSO 398 | * If the action `fails`, an alert should be generated 399 | * Optionally, if your team is very disciplined with Source of Truth based automation, you could automatically run the `build-inventory` and `connect-inventory` actions when `verify-inventory` fails 400 | 401 | ## NetBox Virtual Machine and NSO 402 | While the majority of devices within NSO will be represented by NetBox devices, there are some that maybe in NetBox as Virtual Machines. For example, Cisco Firepower Management Center and vCenter Applicances can be added to Cisco NSO but they are more likely to be represented in NetBox as VMs than devices. 403 | 404 | Within NetBox, there is no concept of `device-types` for VMs. But a `device-role` can be associated to Virtual Machines. To support VMs within `netbox-inventory`, you can add a `vm-role`. Like `device-type`, `vm-role` takes a required `ned` to link the NetBox VM Role to a NED. 405 | 406 | Here is an example for a FirewPower Management Center 407 | 408 | ``` 409 | netbox-inventory firepower-management-center 410 | auth-group fmc 411 | connection-protocol https 412 | update-nso-devices true 413 | netbox-server example-vm-netbox-01.example.net 414 | site [ MYDC ] 415 | tenant [ example-admin ] 416 | vm-role "Firepower Management Center (FMC)" 417 | ned cisco-fmc-gen-1.5 418 | ``` 419 | 420 | ## NED Specific Settings 421 | In addition to device configurations that are fairly common across all devices, NSO supports specific settings per NED. For these NED/Device specific configurations, you can either configure them manually after running the `build-inventory` process, or update the service/package to configure them for you. 422 | 423 | There are some common ned-settings included with the package already. These settings are implemented with two parts of the package: 424 | 425 | 1. An XML template specific to the device. The templates are named `add-NED-NAME.xml` and are located in the folder `nso-netbox/templates-ned-settings`. 426 | > *To take advantage of these optional settings, you need to move the templates into the `templates` directory. They are delivered in this way because any `ned-setting` template that is loaded must have the related NEDs loaded into NSO or the package will not successfully load.* 427 | 428 | ``` 429 | ls -l packages/nso-netbox/templates-ned-settings/ 430 | 431 | -rw-rw-r-- 1 hapresto hapresto 533 Mar 22 17:09 add-cisco-fmc.xml 432 | -rw-rw-r-- 1 hapresto hapresto 640 Mar 22 17:06 add-vmware-vsphere-gen.xml 433 | ``` 434 | 435 | 1. Logic in the `build-inventory` action code in the `netbox_inventory_actions.py` file that looks at the NED for a device and applies the NED specific configuration if needed. 436 | 437 | ``` 438 | # Device/NED specific configuratons 439 | if "cisco-fmc" in ned: 440 | self.log.info( 441 | f"Device {device.name} uses ned {ned}. Applying Cisco FMC specific configurations." 442 | ) 443 | template.apply("add-cisco-fmc", vars) 444 | elif "vmware-vsphere" in ned: 445 | self.log.info( 446 | f"Device {device.name} uses ned {ned}. Applying VMware specific configurations." 447 | ) 448 | template.apply("add-vmware-vsphere-gen", vars) 449 | ``` 450 | 451 | The following optional ned-settings are included with the package. 452 | 453 | ### cisco-fmc 454 | 455 | ``` 456 | devices device dev01-z0-vm-fmc-01 457 | ned-settings cisco-fmc cisco-fmc-connection ssl accept-any true 458 | ``` 459 | 460 | ### vmware-vsphere 461 | 462 | ``` 463 | devices device dev01-z0-vm-vcenter-02 464 | read-timeout 60 465 | write-timeout 60 466 | ned-settings vmware-vsphere device-flavor portgroup-cfg 467 | ned-settings vmware-vsphere connection ssl-version TLSv1.2 468 | ``` 469 | -------------------------------------------------------------------------------- /python/nso_netbox/netbox_inventory_actions.py: -------------------------------------------------------------------------------- 1 | import ncs 2 | from ncs.dp import Action 3 | 4 | # from _ncs import decrypt 5 | import ncs.maapi as maapi 6 | import _ncs.maapi as _maapi 7 | from .netbox_utilities import verify_netbox, devicelist_netbox, vmlist_netbox 8 | from ipaddress import ip_address 9 | from datetime import datetime 10 | import json 11 | from _ncs.error import Error 12 | import pynetbox 13 | 14 | # TODO: Move to utilities file 15 | def get_users_groups(trans, uinfo): 16 | # Get the maapi socket 17 | s = trans.maapi.msock 18 | auth = _maapi.get_authorization_info(s, uinfo.usid) 19 | return list(auth.groups) 20 | 21 | 22 | # Constancs and Values for use 23 | PROTOCOL_PORTS = { 24 | "ssh": 22, 25 | "telnet": 23, 26 | "http": 80, 27 | "https": 443, 28 | } 29 | 30 | 31 | class NetboxInventoryAction(Action): 32 | @Action.action 33 | def cb_action(self, uinfo, name, kp, action_input, action_output, trans): 34 | self.log.info("NetboxAction: ", name) 35 | service = ncs.maagic.get_node(trans, kp) 36 | root = ncs.maagic.get_root(trans) 37 | netbox_server = root.netbox_server[service.netbox_server] 38 | trans.maapi.install_crypto_keys() 39 | 40 | # Find groups user is a member of 41 | ugroups = get_users_groups(trans, uinfo) 42 | self.log.info("groups = ", ugroups) 43 | 44 | if name == "verify-inventory": 45 | self.verify_inventory( 46 | name, service, root, netbox_server, action_input, action_output 47 | ) 48 | if name == "build-inventory": 49 | self.build_inventory( 50 | uinfo, 51 | ugroups, 52 | name, 53 | service, 54 | root, 55 | netbox_server, 56 | action_input, 57 | action_output, 58 | ) 59 | if name == "connect-inventory": 60 | self.connect_inventory( 61 | uinfo, 62 | ugroups, 63 | name, 64 | service, 65 | root, 66 | netbox_server, 67 | action_input, 68 | action_output, 69 | ) 70 | 71 | def build_inventory( 72 | self, 73 | uinfo, 74 | ugroups, 75 | name, 76 | service, 77 | root, 78 | netbox_server, 79 | action_input, 80 | action_output, 81 | ): 82 | """Add NetBox Devices for the Inventory to NSO as Devices.""" 83 | 84 | build_status = True 85 | build_messages = [] 86 | 87 | # See if the inventory service is configured to allow updating NSO Devices 88 | # TODO: Consider allowing a "dry-run" of building config even if false 89 | if not service.update_nso_devices: 90 | build_messages.append( 91 | f"NSO Inventory {service.name} has update-nso-devices set to {service.update_nso_devices}. No NSO will be created." 92 | ) 93 | build_status = False 94 | 95 | # Check if NetBox Server reachable 96 | netbox_status = verify_netbox(netbox_server) 97 | if not netbox_status["status"]: 98 | build_messages.append(netbox_status["message"]) 99 | build_status = False 100 | 101 | # Check if should proceed with building 102 | if not build_status: 103 | self.log.info("\n".join(build_messages)) 104 | action_output.success = build_status 105 | action_output.output = "\n".join(build_messages) 106 | return 107 | 108 | # Create an output message that will be nice YAML friendly 109 | build_messages.append("# Adding Devices to NSO from NetBox inventory.") 110 | build_messages.append("devices: ") 111 | 112 | # Things good to build the inventory 113 | # Start a new Transaction Session 114 | with ncs.maapi.Maapi() as m: 115 | with ncs.maapi.Session( 116 | m, user=uinfo.username, context=name, groups=ugroups 117 | ): 118 | with m.start_write_trans() as t: 119 | 120 | # Create new service object from a writeable transaction 121 | writeable_service = ncs.maagic.get_node(t, service._path) 122 | template = ncs.template.Template(writeable_service) 123 | 124 | devices = [] 125 | 126 | # Only lookup devices if device_types provided 127 | if service.device_type: 128 | query = devicelist_netbox(service, netbox_server, log=self.log) 129 | if query["status"]: 130 | devices += query["result"] 131 | else: 132 | build_messages.append( 133 | f"Unable to query to netbox server {netbox_server.url}" 134 | ) 135 | build_messages.append(query["result"]) 136 | build_status = False 137 | self.log.error("\n".join(build_messages)) 138 | action_output.success = build_status 139 | action_output.output = "\n".join(build_messages) 140 | return 141 | 142 | # Lookup VMs if used in inventory 143 | if service.vm_role: 144 | vms_query = vmlist_netbox(service, netbox_server, log=self.log) 145 | 146 | if vms_query["status"]: 147 | # devices.append(vms_query["result"]) 148 | devices += vms_query["result"] 149 | else: 150 | action_output.output = vms_query["result"] 151 | action_output.success = vms_query["status"] 152 | return 153 | 154 | for device in devices: 155 | self.log.info(f"Processing device {device.name}") 156 | 157 | # Verify mandatory attributes for devices are available 158 | if not device.primary_ip: 159 | build_messages.append(f"# Device {device.name} is missing the mandatory field primary_ip. Skipping device.") 160 | continue 161 | 162 | 163 | # NetBox Status Field will affect results 164 | # Active > Add / unlocked 165 | # Staged > Add / unlocked 166 | # Offline > Add / locked 167 | # Failed > Add / locked 168 | # Decommissioning > Add / locked 169 | # Planned > Add / southbound-locked 170 | # Inventory > Remove 171 | # 172 | # If the service.admin_state is set, this overrides settings based on status 173 | vars = ncs.template.Variables() 174 | device_admin_state = None 175 | if service.admin_state: 176 | self.log.info( 177 | f"Admin State configured to be {service.admin_state}, overriding status from NetBox" 178 | ) 179 | device_admin_state = service.admin_state 180 | else: 181 | if device.status.value in ["active", "staged"]: 182 | device_admin_state = "unlocked" 183 | elif device.status.value in [ 184 | "offline", 185 | "failed", 186 | "decommissioning", 187 | ]: 188 | device_admin_state = "locked" 189 | elif device.status.value in ["planned"]: 190 | device_admin_state = "southbound-locked" 191 | elif device.status.value in ["inventory"]: 192 | self.log.info( 193 | f"NetBox status is {device.status}, removing device from NSO devices if found" 194 | ) 195 | # TODO: Write removal code 196 | continue 197 | 198 | self.log.info( 199 | f"NetBox status is {device.status}, setting Admin State to match" 200 | ) 201 | 202 | # TODO: Create NSO Device Groups for Types/Models/Tenants that consolidate the groups from any NetBox Inventory Instance 203 | # What NSO Device Groups to add device 204 | nso_groups = [ 205 | f"NetBoxInventory {service.name}", 206 | ] 207 | 208 | # Device vs VM differences 209 | if isinstance(device, pynetbox.models.dcim.Devices): 210 | role = device.device_role 211 | ned = service.device_type[device.device_type.model].ned 212 | nso_groups.append( 213 | f"NetBoxInventory {service.name} {device.device_type.model}" 214 | ) 215 | elif isinstance(device, pynetbox.models.virtualization.VirtualMachines): 216 | role = device.role 217 | ned = service.vm_role[device.role.name].ned 218 | 219 | # Add role based group 220 | nso_groups.append( 221 | f"NetBoxInventory {service.name} {role.name}", 222 | ) 223 | 224 | # Add role based on tenant 225 | if device.tenant: 226 | nso_groups.append( 227 | f"NetBoxInventory {service.name} {device.tenant.name}" 228 | ) 229 | 230 | # Set Metadata on device for source of inventory info 231 | source = { 232 | "context": None, 233 | "when": datetime.utcnow().isoformat(timespec="seconds"), 234 | "source": service._path, 235 | } 236 | 237 | # If a URL address for device returned, apply it 238 | # TODO: Add logic to construct address for older NetBox servers 239 | if device.url: 240 | source["context"] = { 241 | "web": device.url.replace("/api", ""), 242 | "api": device.url, 243 | } 244 | 245 | # Populate Variables for creating device 246 | vars.add("DEVICE_NAME", device.name) 247 | vars.add( 248 | "DEVICE_ADDRESS", 249 | ip_address(device.primary_ip.address.split("/")[0]), 250 | ) 251 | vars.add("DEVICE_DESCRIPTION", role.name) 252 | vars.add("AUTH_GROUP", service.auth_group) 253 | 254 | # Determine if this NED is a "cli" or "generic" 255 | device_package = root.packages.package[ned] 256 | for component in device_package.component: 257 | # Find the component that ties to the NED name 258 | # Note: The cisco-iosxr ned uses cisco-ios-xr as the component name. Pulling out "-"'s for this test 259 | if component.name.replace("-", "") in ned.replace("-", ""): 260 | # TODO: There is likely a better way to do this, but component.ned.cli is an ncs.maagic.Container, and can't figure out how to see if "empty" in Python 261 | # Is it a CLI ned? 262 | try: 263 | component.ned.cli.ned_id 264 | ned_type = "cli" 265 | except Error: 266 | pass 267 | # Is it a Generic NED 268 | try: 269 | component.ned.generic.ned_id 270 | ned_type = "generic" 271 | except Error: 272 | pass 273 | 274 | # Port/Protocol Conversion Logic 275 | if service.connection_protocol.string in PROTOCOL_PORTS.keys(): 276 | connection_port = PROTOCOL_PORTS[ 277 | service.connection_protocol.string 278 | ] 279 | else: 280 | connection_port = "" 281 | 282 | # Remaining Variables for template 283 | vars.add("NED_ID", ned) 284 | vars.add("NED_TYPE", ned_type) 285 | vars.add("PROTOCOL", service.connection_protocol) 286 | vars.add("PORT", connection_port) 287 | vars.add("ADMIN_STATE", device_admin_state) 288 | vars.add("SOURCE_CONTEXT", json.dumps(source["context"])) 289 | vars.add("SOURCE_SOURCE", source["source"]) 290 | vars.add("SOURCE_WHEN", source["when"]) 291 | vars.add( 292 | "BYPASS_CERT_VERIFY", 293 | service.bypass_certificate_verification, 294 | ) 295 | 296 | # Create output message for user 297 | build_messages.append(f"- device: {device.name}") 298 | build_messages.append( 299 | f" address: {ip_address(device.primary_ip.address.split('/')[0])}" 300 | ) 301 | build_messages.append(f" port: {connection_port}") 302 | build_messages.append(f" description: {role.name}") 303 | build_messages.append(f" auth-group: {service.auth_group}") 304 | 305 | build_messages.append(" device-type: ") 306 | build_messages.append(f" {ned_type}:") 307 | build_messages.append(f" ned-id: {ned}") 308 | build_messages.append( 309 | f" protocol: {service.connection_protocol}" 310 | ) 311 | 312 | build_messages.append(f" state: {device_admin_state}") 313 | build_messages.append(" source:") 314 | build_messages.append( 315 | f" context: {json.dumps(source['context'])}" 316 | ) 317 | build_messages.append(f" when: {source['when']}") 318 | build_messages.append(f" source: {source['source']}") 319 | 320 | # Add devices to NSO Device-Groups 321 | self.log.info(f"Device will be added to groups: {nso_groups}") 322 | group_vars = ncs.template.Variables() 323 | group_vars.add("DEVICE_NAME", device.name) 324 | group_vars.add("DEVICE_GROUP_LOCATION", service._path) 325 | build_messages.append(" device-groups: ") 326 | for group in nso_groups: 327 | group_vars.add("DEVICE_GROUP_NAME", group) 328 | build_messages.append(f" - {group}") 329 | 330 | self.log.info( 331 | f"Device {device.name} Group {group}: {group_vars}" 332 | ) 333 | 334 | if action_input.commit: 335 | self.log.info( 336 | "Applying template to add device to device-groups." 337 | ) 338 | template.apply("add-device-group", group_vars) 339 | 340 | self.log.info(f"Device {device.name}: {vars}") 341 | 342 | # TODO: Look at using NSO "dry-run" feature 343 | if action_input.commit: 344 | self.log.info("Applying template to add device to NSO.") 345 | template.apply("add-device", vars) 346 | 347 | # Device/NED specific configuratons 348 | if "cisco-fmc" in ned: 349 | self.log.info( 350 | f"Device {device.name} uses ned {ned}. Applying Cisco FMC specific configurations." 351 | ) 352 | template.apply("add-cisco-fmc", vars) 353 | elif "vmware-vsphere" in ned: 354 | self.log.info( 355 | f"Device {device.name} uses ned {ned}. Applying VMware specific configurations." 356 | ) 357 | template.apply("add-vmware-vsphere-gen", vars) 358 | else: 359 | build_messages.append( 360 | f"\n\n# Action input commit: {action_input.commit}. Devices will NOT be added to NSO." 361 | ) 362 | build_status = False 363 | 364 | t.apply() 365 | 366 | action_output.output = "\n".join(build_messages) 367 | action_output.success = build_status 368 | 369 | def connect_inventory( 370 | self, 371 | uinfo, 372 | ugroups, 373 | name, 374 | service, 375 | root, 376 | netbox_server, 377 | action_input, 378 | action_output, 379 | ): 380 | """Perform connection to devices in inventory. Include fetching ssh keys and optional sync-from.""" 381 | 382 | connect_status = True 383 | connect_messages = [] 384 | 385 | connect_messages.append(f"Connecting to devices from inventory {service.name}") 386 | 387 | # See if the inventory service is configured to allow updating NSO Devices 388 | if not service.update_nso_devices: 389 | connect_messages.append( 390 | f"NSO Inventory {service.name} has update-nso-devices set to {service.update_nso_devices}. No NSO will be created." 391 | ) 392 | connect_status = False 393 | 394 | # Check if NetBox Server reachable 395 | netbox_status = verify_netbox(netbox_server) 396 | if not netbox_status["status"]: 397 | connect_messages.append(netbox_status["message"]) 398 | connect_status = False 399 | 400 | devices = [] 401 | 402 | # Only lookup devices if device_types provided 403 | if service.device_type: 404 | query = devicelist_netbox(service, netbox_server, log=self.log) 405 | if query["status"]: 406 | devices = query["result"] 407 | else: 408 | connect_messages.append( 409 | f"Unable to query to netbox server {netbox_server.url}" 410 | ) 411 | connect_messages.append(query["result"]) 412 | connect_status = False 413 | self.log.error("\n".join(connect_messages)) 414 | action_output.success = connect_status 415 | action_output.output = "\n".join(connect_messages) 416 | return 417 | 418 | # Lookup VMs if used in inventory 419 | if service.vm_role: 420 | vms_query = vmlist_netbox(service, netbox_server, log=self.log) 421 | 422 | if vms_query["status"]: 423 | devices += vms_query["result"] 424 | else: 425 | action_output.output = vms_query["result"] 426 | action_output.success = vms_query["status"] 427 | return 428 | 429 | for device in devices: 430 | self.log.info(f"Processing device {device.name}") 431 | 432 | # Verify mandatory attributes for devices are available 433 | if not device.primary_ip: 434 | connect_messages.append(f"# Device {device.name} is missing the mandatory field primary_ip. Skipping device.") 435 | continue 436 | 437 | 438 | # TODO: Verify device in NSO first 439 | 440 | connect_messages.append(f"Connecting to device {device.name}") 441 | 442 | if service.connection_protocol.string == "ssh": 443 | connect_messages.append(" - Fetching SSH Host-Keys") 444 | ssh_fetch = root.devices.device[device.name].ssh.fetch_host_keys() 445 | connect_messages.append( 446 | f" result: {ssh_fetch.result} {ssh_fetch.info}" 447 | ) 448 | self.log.info( 449 | f"{device.name} fetch ssh host key result: {ssh_fetch.result} {ssh_fetch.info}" 450 | ) 451 | 452 | connect_messages.append(" - Testing Connecting to Device") 453 | connect = root.devices.device[device.name].connect() 454 | connect_messages.append(f" result: {connect.result} {connect.info}") 455 | self.log.info( 456 | f"{device.name} connect result: {connect.result} {connect.info}" 457 | ) 458 | 459 | if action_input.sync_from and connect.result: 460 | connect_messages.append(" - Performing sync-from") 461 | syncfrom = root.devices.device[device.name].sync_from() 462 | connect_messages.append( 463 | f" result: {syncfrom.result} {syncfrom.info}" 464 | ) 465 | self.log.info( 466 | f"{device.name} sync-from result: {syncfrom.result} {syncfrom.info}" 467 | ) 468 | 469 | action_output.output = "\n".join(connect_messages) 470 | action_output.success = connect_status 471 | 472 | def verify_inventory( 473 | self, name, service, root, netbox_server, action_input, action_output 474 | ): 475 | """Verify that the NetBox Devices for the Inventory are present in NSO as Devices.""" 476 | netbox_status = verify_netbox(netbox_server) 477 | if not netbox_status["status"]: 478 | action_output.success = netbox_status["status"] 479 | action_output.output = netbox_status["message"] 480 | return 481 | 482 | devices = [] 483 | 484 | # Only lookup devices if device_types provided 485 | if service.device_type: 486 | query = devicelist_netbox(service, netbox_server, log=self.log) 487 | 488 | if query["status"]: 489 | devices += query["result"] 490 | else: 491 | action_output.output = query["result"] 492 | action_output.success = query["status"] 493 | return 494 | 495 | # Lookup VMs if used in inventory 496 | if service.vm_role: 497 | vms_query = vmlist_netbox(service, netbox_server, log=self.log) 498 | 499 | if vms_query["status"]: 500 | devices += vms_query["result"] 501 | else: 502 | action_output.output = vms_query["result"] 503 | action_output.success = vms_query["status"] 504 | return 505 | 506 | # Look for each NetBox device in the inventory 507 | verify_status = True 508 | verify_messages = [] 509 | 510 | # if service.device_type: 511 | for device in devices: 512 | self.log.info(f"Testing device {device.name}") 513 | 514 | # Verify mandatory attributes for devices are available 515 | if not device.primary_ip: 516 | verify_messages.append(f"# Device {device.name} is missing the mandatory field primary_ip. Skipping device.") 517 | continue 518 | 519 | # Does the device exist 520 | try: 521 | nso_device = root.devices.device[device.name] 522 | except KeyError: 523 | # If NetBox lists device as "inventory" it shouldn't be found 524 | if device.status.value in ["inventory"]: 525 | verify_messages.append( 526 | f"Device {device.name} has a NetBox Status of {device.status.value}, it is not in NSO." 527 | ) 528 | else: 529 | verify_messages.append( 530 | f"Device {device.name} not found in NSO ." 531 | ) 532 | verify_status = False 533 | continue 534 | 535 | # If NetBox lists device as "inventory" it shouldn't be found 536 | if device.status.value in ["inventory"]: 537 | verify_messages.append( 538 | f"Device {device.name} has a NetBox Status of {device.status.value}, it should NOT be in NSO but it is." 539 | ) 540 | verify_status = False 541 | 542 | # Verify admin-state status of devices match NetBox status 543 | if ( 544 | service.admin_state 545 | and nso_device.state.admin_state != service.admin_state 546 | ): 547 | verify_messages.append( 548 | f"Device {device.name} has an admin_state of {nso_device.state.admin_state} which differs from service admin-state of {service.admin_state}" 549 | ) 550 | verify_status = False 551 | else: 552 | if ( 553 | ( 554 | device.status.value in ["active", "staged"] 555 | and nso_device.state.admin_state != "unlocked" 556 | ) 557 | or ( 558 | device.status.value in ["offline", "failed", "decommissioning"] 559 | and nso_device.state.admin_state != "locked" 560 | ) 561 | or ( 562 | device.status.value in ["planned"] 563 | and nso_device.state.admin_state != "southbound-locked" 564 | ) 565 | ): 566 | verify_messages.append( 567 | f"Device {device.name} has an admin_state of {nso_device.state.admin_state} which differs from NetBox status of {device.status.value}" 568 | ) 569 | verify_status = False 570 | 571 | # Verify Address of device 572 | if ip_address(device.primary_ip.address.split("/")[0]) != ip_address( 573 | nso_device.address 574 | ): 575 | verify_messages.append( 576 | f"Device {device.name} has a NetBox Primary IP of {device.primary_ip.address.split('/')[0]}, NSO device is configured for address {nso_device.address}" 577 | ) 578 | verify_status = False 579 | 580 | # Device vs VM differences 581 | if isinstance(device, pynetbox.models.dcim.Devices): 582 | role = device.device_role 583 | ned = service.device_type[device.device_type.model].ned 584 | elif isinstance(device, pynetbox.models.virtualization.VirtualMachines): 585 | role = device.role 586 | ned = service.vm_role[device.role.name].ned 587 | 588 | # Verify Description of Device 589 | if role.name != nso_device.description: 590 | verify_messages.append( 591 | f"Device {device.name} has a NetBox Role of {role.name}, which doesn't match NSO description of {nso_device.description}" 592 | ) 593 | verify_status = False 594 | 595 | # Determine if this NED is a "cli" or "generic" 596 | device_package = root.packages.package[ned] 597 | for component in device_package.component: 598 | # Find the component that ties to the NED name 599 | if component.name.replace("-", "") in ned.replace("-", ""): 600 | # TODO: There is likely a better way to do this, but component.ned.cli is an ncs.maagic.Container, and can't figure out how to see if "empty" in Python 601 | # Is it a CLI ned? 602 | try: 603 | component.ned.cli.ned_id 604 | ned_type = "cli" 605 | except Error: 606 | pass 607 | # Is it a Generic NED 608 | try: 609 | component.ned.generic.ned_id 610 | ned_type = "generic" 611 | except Error: 612 | pass 613 | 614 | # Verify NED_ID 615 | if ned != nso_device.device_type[ned_type].ned_id.split(":")[1]: 616 | verify_messages.append( 617 | f"Device {device.name} has a NetBox Device Type of {device.device_type.model} which should use NED {service.device_type[device.device_type.model].ned}, but is configured for NSO NED {nso_device.device_type.cli.ned_id.split(':')[1]}" 618 | ) 619 | verify_status = False 620 | 621 | # Verify Protocol 622 | if ned_type == "cli" and str(service.connection_protocol) != str( 623 | nso_device.device_type.cli.protocol 624 | ): 625 | verify_messages.append( 626 | f"Device {device.name} should use a connection protocol of {service.connection_protocol}, but is configured for {nso_device.device_type.cli.protocol}" 627 | ) 628 | verify_status = False 629 | 630 | # Verify Port 631 | if nso_device.port != PROTOCOL_PORTS[service.connection_protocol.string]: 632 | verify_messages.append( 633 | f"Device {device.name} should use a port of {PROTOCOL_PORTS[service.connection_protocol.string]}, but is configured for {nso_device.port}" 634 | ) 635 | verify_status = False 636 | 637 | action_output.output = "\n".join(verify_messages) 638 | action_output.success = verify_status 639 | --------------------------------------------------------------------------------