├── 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 | if {$NED_TYPE = "cli"} ?>
12 |
13 | {$NED_ID}:{$NED_ID}
14 | {$PROTOCOL}
15 |
16 | else ?>
17 | if {$NED_TYPE = "generic"} ?>
18 |
19 | {$NED_ID}:{$NED_ID}
20 |
21 | end ?>
22 | end ?>
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 |
--------------------------------------------------------------------------------