test: jpath¶
jpath uses jsonpath in conjunction with assertpy to validate task data.
This module combines the power of jsonpath and assertpy. In order to use this module it is somewhat necessary to be familiar with both of those. Links to them can be found below.
jsonpath_ng <https://github.com/h2non/jsonpath-ng>assertpy <https://github.com/assertpy/assertpy>
Examples:
[17]:
from nornir import InitNornir
from nornir_napalm.plugins.tasks import napalm_get, napalm_cli
from nornir_tests.plugins.functions import print_result
from nornir_tests.plugins.tasks import wrap_task
from nornir_tests.plugins.tests import jpath
nr = InitNornir(
inventory={
"plugin": "SimpleInventory",
"options": {
"host_file": "inventory/hosts.yaml",
"group_file": "inventory/groups.yaml",
"defaults_file": "inventory/defaults.yaml",
}
},
dry_run=True,
)
When running a task, validations usually if done in Nornir can be executed as additional logic implemented in python or with running of actual tasks. Using nornir_tests moves the logic into the task and provides a way to impact the success of a task based on its validations.
[18]:
results = nr.run(
wrap_task(napalm_get), getters=['facts'],
tests=[
jpath(path='$..os_version', value='4.14.3-2329074.gaatlantarel')
]
)
[19]:
print_result(results, vars=['tests', 'highlit'])
print(results['rtr00'][0].result)
napalm_get**********************************************************************
* rtr00 ** changed : False *****************************************************
vvvv napalm_get ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
P JpathRecord - {'assertion': 'is_equal_to',
'path': '$..os_version',
'result_attr': 'result',
'value': '4.14.3-2329074.gaatlantarel'}
{'matches': ['facts.os_version']}
{
"facts": {
"uptime": 151005.57332897186,
"vendor": "Arista",
"os_version": "4.14.3-2329074.gaatlantarel",
"serial_number": "SN0123A34AS",
"model": "vEOS",
"hostname": "eos-router",
"fqdn": "eos-router",
"interface_list": [
"Ethernet2",
"Management1",
"Ethernet1",
"Ethernet3"
]
}
}
^^^^ END napalm_get ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* rtr01 ** changed : False *****************************************************
vvvv napalm_get ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
P JpathRecord - {'assertion': 'is_equal_to',
'path': '$..os_version',
'result_attr': 'result',
'value': '4.14.3-2329074.gaatlantarel'}
{'matches': ['facts.os_version']}
{
"facts": {
"uptime": 151005.57332897186,
"vendor": "Arista",
"os_version": "4.14.3-2329074.gaatlantarel",
"serial_number": "SN0123A34AS",
"model": "vEOS",
"hostname": "eos-router2",
"fqdn": "eos-router2",
"interface_list": [
"Ethernet2",
"Management1",
"Ethernet1",
"Ethernet3",
"Ethernet4"
]
}
}
^^^^ END napalm_get ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
{'facts': {'uptime': 151005.57332897186, 'vendor': 'Arista', 'os_version': '4.14.3-2329074.gaatlantarel', 'serial_number': 'SN0123A34AS', 'model': 'vEOS', 'hostname': 'eos-router', 'fqdn': 'eos-router', 'interface_list': ['Ethernet2', 'Management1', 'Ethernet1', 'Ethernet3']}}
The first example was pretty simple but the next will have many tests run in validating interface data. It will also use @ decorator syntax.
[20]:
@jpath(path='$..ipv6', assertion='contains', value="1::1")
@jpath(path='$.interfaces_ip', assertion='is_length', value=3)
@jpath(path='$..FastEthernet8..prefix_length', value=22)
def get_interface_ips(task):
return napalm_get(task, getters=['interfaces_ip'])
results = nr.run(get_interface_ips)
[21]:
print_result(results, vars=['tests', 'highlit'])
get_interface_ips***************************************************************
* rtr00 ** changed : False *****************************************************
vvvv get_interface_ips ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
P JpathRecord - {'assertion': 'is_equal_to',
'path': '$..FastEthernet8..prefix_length',
'result_attr': 'result',
'value': 22}
{'matches': ['interfaces_ip.FastEthernet8.ipv4.10.66.43.169.prefix_length']}
F JpathRecord - {'assertion': 'is_length',
'path': '$.interfaces_ip',
'result_attr': 'result',
'value': 3}
{'exception': TypeError("'str' object does not support item assignment"),
'matches': ['interfaces_ip']}
P JpathRecord - {'assertion': 'contains',
'path': '$..ipv6',
'result_attr': 'result',
'value': '1::1'}
{'matches': ['interfaces_ip.Loopback555.ipv6']}
"\"{\\n \\\"interfaces_ip\\\": {\\n \\\"FastEthernet8\\\": {\\n \\\"ipv4\\\": {\\n
\\\"10.66.43.169\\\": {\\n \\\"prefix_length\\\": \\\"22\\\"\\n }\\n
}\\n },\\n \\\"Loopback555\\\": {\\n \\\"ipv4\\\": {\\n
\\\"192.168.1.1\\\": {\\n \\\"prefix_length\\\": 24\\n }\\n },\\n
\\\"ipv6\\\": {\\n \\\"1::1\\\": {\\n \\\"prefix_length\\\": 64\\n
},\\n \\\"2001:DB8:1::1\\\": {\\n \\\"prefix_length\\\": 64\\n },\\n
\\\"2::\\\": {\\n \\\"prefix_length\\\": 64\\n },\\n \\\"FE80::3\\\":
{\\n \\\"prefix_length\\\": \\\"N/A\\\"\\n }\\n }\\n },\\n
\\\"Tunnel0\\\": {\\n \\\"ipv4\\\": {\\n \\\"10.63.100.9\\\": {\\n
\\\"prefix_length\\\": 24\\n }\\n }\\n }\\n }\\n}\""
^^^^ END get_interface_ips ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* rtr01 ** changed : False *****************************************************
vvvv get_interface_ips ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
P JpathRecord - {'assertion': 'is_equal_to',
'path': '$..FastEthernet8..prefix_length',
'result_attr': 'result',
'value': 22}
{'matches': ['interfaces_ip.FastEthernet8.ipv4.10.66.43.170.prefix_length']}
F JpathRecord - {'assertion': 'is_length',
'path': '$.interfaces_ip',
'result_attr': 'result',
'value': 3}
{'exception': TypeError("'str' object does not support item assignment"),
'matches': ['interfaces_ip']}
F JpathRecord - {'assertion': 'contains',
'path': '$..ipv6',
'result_attr': 'result',
'value': '1::1'}
{'exception': Exception(AssertionError("Expected <{'1::2': {'prefix_length': 64}, '2001:DB8:1::2': {'prefix_length': 64}, '2::99': {'prefix_length': 64}, 'FE80::5': {'prefix_length': 'N/A'}}> to contain key <1::1>, but did not."))}
"\"{\\n \\\"interfaces_ip\\\": {\\n \\\"FastEthernet8\\\": {\\n \\\"ipv4\\\": {\\n
\\\"10.66.43.170\\\": {\\n \\\"prefix_length\\\": \\\"22\\\"\\n }\\n
}\\n },\\n \\\"Loopback555\\\": {\\n \\\"ipv4\\\": {\\n
\\\"192.168.1.2\\\": {\\n \\\"prefix_length\\\": 24\\n }\\n },\\n
\\\"ipv6\\\": {\\n \\\"1::2\\\": {\\n \\\"prefix_length\\\": 64\\n
},\\n \\\"2001:DB8:1::2\\\": {\\n \\\"prefix_length\\\": 64\\n },\\n
\\\"2::99\\\": {\\n \\\"prefix_length\\\": 64\\n },\\n
\\\"FE80::5\\\": {\\n \\\"prefix_length\\\": \\\"N/A\\\"\\n }\\n }\\n
},\\n \\\"Tunnel0\\\": {\\n \\\"ipv4\\\": {\\n \\\"10.63.100.10\\\": {\\n
\\\"prefix_length\\\": 24\\n }\\n }\\n }\\n }\\n}\""
^^^^ END get_interface_ips ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The next example will show how a task can be set to failed based on the validation that is performed. If the task has fail_task set to true and ends up with passed=False in the test it will mark the overall task as failed.
[22]:
@jpath(path='$..connection_state', value="Established", fail_task=True)
@jpath(path='$..remote_as', value='8121')
def check_bgp_neighbors(task):
return napalm_get(task, getters=['bgp_neighbors_detail'])
results = nr.run(check_bgp_neighbors)
print_result(results, vars=['tests', 'highlit'])
check_bgp_neighbors*************************************************************
* rtr00 ** changed : False *****************************************************
vvvv check_bgp_neighbors ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
F JpathRecord - {'assertion': 'is_equal_to',
'path': '$..remote_as',
'result_attr': 'result',
'value': '8121'}
{'exception': Exception(AssertionError('Expected <8121> to be equal to <8121>, but was not.'))}
P JpathRecord - {'assertion': 'is_equal_to',
'fail_task': True,
'path': '$..connection_state',
'result_attr': 'result',
'value': 'Established'}
{'matches': ['bgp_neighbors_detail.global.8121.[0].connection_state']}
"{\n \"bgp_neighbors_detail\": {\n \"global\": {\n \"8121\": [\n {\n
\"up\": true,\n \"local_as\": 13335,\n \"remote_as\": \"8121\",\n
\"local_address\": \"172.101.76.1\",\n \"local_address_configured\": true,\n
\"local_port\": 179,\n \"routing_table\": \"inet.0\",\n \"remote_address\":
\"192.247.78.0\",\n \"remote_port\": 58380,\n \"multihop\": false,\n
\"multipath\": true,\n \"remove_private_as\": true,\n \"import_policy\":
\"4-NTT-TRANSIT-IN\",\n \"export_policy\": \"4-NTT-TRANSIT-OUT\",\n
\"input_messages\": 123,\n \"output_messages\": 13,\n \"input_updates\":
123,\n \"output_updates\": 5,\n \"messages_queued_out\": 23,\n
\"connection_state\": \"Established\",\n \"previous_connection_state\":
\"EstabSync\",\n \"last_event\": \"RecvKeepAlive\",\n
\"suppress_4byte_as\": false,\n \"local_as_prepend\": false,\n
\"holdtime\": 90,\n \"configured_holdtime\": 90,\n \"keepalive\": 30,\n
\"configured_keepalive\": 30,\n \"active_prefix_count\": 132808,\n
\"received_prefix_count\": 566739,\n \"accepted_prefix_count\": 566479,\n
\"suppressed_prefix_count\": 0,\n \"advertised_prefix_count\": 0,\n
\"flap_count\": 27\n }\n ]\n }\n }\n}"
^^^^ END check_bgp_neighbors ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* rtr01 ** changed : False *****************************************************
vvvv check_bgp_neighbors ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ERROR
F JpathRecord - {'assertion': 'is_equal_to',
'path': '$..remote_as',
'result_attr': 'result',
'value': '8121'}
{'exception': Exception(AssertionError('Expected <8121> to be equal to <8121>, but was not.'))}
F JpathRecord - {'assertion': 'is_equal_to',
'fail_task': True,
'path': '$..connection_state',
'result_attr': 'result',
'value': 'Established'}
{'exception': Exception(AssertionError('Expected <EstabSync> to be equal to <Established>, but was not.'))}
"{\n \"bgp_neighbors_detail\": {\n \"global\": {\n \"8121\": [\n {\n
\"up\": true,\n \"local_as\": 13335,\n \"remote_as\": \"8121\",\n
\"local_address\": \"172.101.77.1\",\n \"local_address_configured\": true,\n
\"local_port\": 179,\n \"routing_table\": \"inet.0\",\n \"remote_address\":
\"192.247.78.1\",\n \"remote_port\": 58381,\n \"multihop\": false,\n
\"multipath\": true,\n \"remove_private_as\": true,\n \"import_policy\":
\"4-NTT-TRANSIT-IN\",\n \"export_policy\": \"4-NTT-TRANSIT-OUT\",\n
\"input_messages\": 123,\n \"output_messages\": 13,\n \"input_updates\":
123,\n \"output_updates\": 5,\n \"messages_queued_out\": 23,\n
\"connection_state\": \"EstabSync\",\n \"previous_connection_state\":
\"Established\",\n \"last_event\": \"RecvKeepAlive\",\n
\"suppress_4byte_as\": false,\n \"local_as_prepend\": false,\n
\"holdtime\": 90,\n \"configured_holdtime\": 90,\n \"keepalive\": 30,\n
\"configured_keepalive\": 30,\n \"active_prefix_count\": 132808,\n
\"received_prefix_count\": 566739,\n \"accepted_prefix_count\": 566479,\n
\"suppressed_prefix_count\": 0,\n \"advertised_prefix_count\": 0,\n
\"flap_count\": 27\n }\n ]\n }\n }\n}"
^^^^ END check_bgp_neighbors ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The last example will use host_data in order to find some data specific to the host to validate against. The host_data is anything from inventory perhaps obviously in the data dictionary.
[23]:
nr.data.reset_failed_hosts()
results = nr.run(
wrap_task(napalm_get), getters=['interfaces'],
tests=[
jpath(path='$.interfaces', assertion='contains', host_data='$.mgmt_port')
]
)
print_result(results, vars=['tests', 'highlit'])
napalm_get**********************************************************************
* rtr00 ** changed : False *****************************************************
vvvv napalm_get ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
P JpathRecord - {'assertion': 'contains',
'host_data': '$.mgmt_port',
'path': '$.interfaces',
'result_attr': 'result',
'value': 'Management1'}
{'matches': ['interfaces']}
{
"interfaces": {
"Management1": {
"is_up": false,
"is_enabled": false,
"description": "",
"last_flapped": -1.0,
"speed": 1000,
"mtu": 1500,
"mac_address": "FA:16:3E:57:33:61"
},
"Ethernet1": {
"is_up": true,
"is_enabled": true,
"description": "foo",
"last_flapped": 1429978575.1554043,
"speed": 1000,
"mtu": 1500,
"mac_address": "FA:16:3E:57:33:62"
},
"Ethernet2": {
"is_up": true,
"is_enabled": true,
"description": "bla",
"last_flapped": 1429978575.1555667,
"speed": 1000,
"mtu": 1500,
"mac_address": "FA:16:3E:57:33:63"
},
"Ethernet3": {
"is_up": false,
"is_enabled": true,
"description": "bar",
"last_flapped": -1.0,
"speed": 1000,
"mtu": 1500,
"mac_address": "FA:16:3E:57:33:64"
}
}
}
^^^^ END napalm_get ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* rtr01 ** changed : False *****************************************************
vvvv napalm_get ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
P JpathRecord - {'assertion': 'contains',
'host_data': '$.mgmt_port',
'path': '$.interfaces',
'result_attr': 'result',
'value': 'Management2'}
{'matches': ['interfaces']}
{
"interfaces": {
"Management2": {
"is_up": false,
"is_enabled": false,
"description": "",
"last_flapped": -1.0,
"speed": 1000,
"mtu": 1500,
"mac_address": "FA:16:3E:57:33:61"
},
"Ethernet1": {
"is_up": true,
"is_enabled": true,
"description": "foo",
"last_flapped": 1429978575.1554043,
"speed": 1000,
"mtu": 1500,
"mac_address": "FA:16:3E:57:33:62"
},
"Ethernet2": {
"is_up": true,
"is_enabled": true,
"description": "bla",
"last_flapped": 1429978575.1555667,
"speed": 1000,
"mtu": 1500,
"mac_address": "FA:16:3E:57:33:63"
},
"Ethernet3": {
"is_up": false,
"is_enabled": true,
"description": "bar",
"last_flapped": -1.0,
"speed": 1000,
"mtu": 1500,
"mac_address": "FA:16:3E:57:33:64"
},
"Ethernet4": {
"is_up": false,
"is_enabled": true,
"description": "bar",
"last_flapped": -1.0,
"speed": 1000,
"mtu": 1500,
"mac_address": "FA:16:3E:57:33:65"
}
}
}
^^^^ END napalm_get ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In these examples ‘contains’, ‘is_equal’, and ‘is_length’ were used for assertions. Many other possibilities are available from the assertpy module. Not saying all of them make sense to use or that they all work as expected but they should. Too many to validate to be honest. Some other that would certainly work fine would be ‘is_true’, ‘is_empty’, etc.
A few more things about how it all works. The one_of argument isn’t always needed but it could be. If the match was intended to turn up many of something and something like is_equal assertion is used, if one_of is not true then it will fail if all matches don’t meet the assertion. This is kind of confusing and I should prob show an example here.
[24]:
@jpath(path='$..ipv4', assertion='contains', value="10.66.43.169")
@jpath(path='$..ipv4', assertion='contains', value="10.66.43.169", one_of=True)
def get_interface_ips(task):
return napalm_get(task, getters=['interfaces_ip'])
rtr00 = nr.filter(name='rtr00')
results = rtr00.run(get_interface_ips)
print_result(results, vars=['tests', 'highlit'])
get_interface_ips***************************************************************
* rtr00 ** changed : False *****************************************************
vvvv get_interface_ips ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
P JpathRecord - {'assertion': 'contains',
'one_of': True,
'path': '$..ipv4',
'result_attr': 'result',
'value': '10.66.43.169'}
{'matches': ['interfaces_ip.FastEthernet8.ipv4']}
F JpathRecord - {'assertion': 'contains',
'path': '$..ipv4',
'result_attr': 'result',
'value': '10.66.43.169'}
{'exception': Exception(AssertionError("Expected <{'192.168.1.1': {'prefix_length': 24}}> to contain key <10.66.43.169>, but did not.")),
'matches': ['interfaces_ip.FastEthernet8.ipv4']}
"{\n \"interfaces_ip\": {\n \"FastEthernet8\": {\n \"ipv4\": {\n
\"10.66.43.169\": {\n \"prefix_length\": 22\n }\n }\n },\n
\"Loopback555\": {\n \"ipv4\": {\n \"192.168.1.1\": {\n
\"prefix_length\": 24\n }\n },\n \"ipv6\": {\n \"1::1\": {\n
\"prefix_length\": 64\n },\n \"2001:DB8:1::1\": {\n \"prefix_length\":
64\n },\n \"2::\": {\n \"prefix_length\": 64\n },\n
\"FE80::3\": {\n \"prefix_length\": \"N/A\"\n }\n }\n },\n
\"Tunnel0\": {\n \"ipv4\": {\n \"10.63.100.9\": {\n \"prefix_length\":
24\n }\n }\n }\n }\n}"
^^^^ END get_interface_ips ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
So what happened is the first test passed as it found a bunch of paths that ended with ‘ipv4’ and it only needed one of them to contain the value of “10.66.43.169”. The second one failed due to the fact that it wanted all the paths to contain that value and they did not.