Skip to content

Commit bc57e6a

Browse files
authored
Merge pull request #73 from networktocode-llc/develop
Rename lib from netcompare into jdiff
2 parents 1604af7 + 96b3c91 commit bc57e6a

32 files changed

+875
-674
lines changed

.bandit.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ skips: []
33
# No need to check for security issues in the test scripts!
44
exclude_dirs:
55
- "./tests/"
6-
- "./netcompare/tests/"
6+
- "./jdiff/tests/"

.github/ISSUE_TEMPLATE/bug_report.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
---
22
name: 🐛 Bug Report
3-
about: Report a reproducible bug in the current release of netcompare
3+
about: Report a reproducible bug in the current release of jdiff
44
---
55

66
### Environment
77
* Python version: <!-- Example: 3.7.7 -->
8-
* netcompare version: <!-- Example: 1.0.0 -->
8+
* jdiff version: <!-- Example: 1.0.0 -->
99

1010
<!-- What did you expect to happen? -->
1111
### Expected Behavior

.github/ISSUE_TEMPLATE/feature_request.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ about: Propose a new feature or enhancement
55
---
66

77
### Environment
8-
* netcompare version: <!-- Example: 1.0.0 -->
8+
* jdiff version: <!-- Example: 1.0.0 -->
99

1010
<!--
1111
Describe in detail the new functionality you are proposing.

.github/workflows/ci.yml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on: # yamllint disable-line rule:truthy rule:comments
55
- "pull_request"
66

77
env:
8-
IMAGE_NAME: "netcompare"
8+
IMAGE_NAME: "jdiff"
99

1010
jobs:
1111
black:
@@ -19,6 +19,17 @@ jobs:
1919
uses: "networktocode/gh-action-setup-poetry-environment@v2"
2020
- name: "Linting: black"
2121
run: "poetry run invoke black"
22+
mypy:
23+
runs-on: "ubuntu-20.04"
24+
env:
25+
INVOKE_LOCAL: "True"
26+
steps:
27+
- name: "Check out repository code"
28+
uses: "actions/checkout@v2"
29+
- name: "Setup environment"
30+
uses: "networktocode/gh-action-setup-poetry-environment@v2"
31+
- name: "Linting: mypy"
32+
run: "poetry run invoke mypy"
2233
bandit:
2334
runs-on: "ubuntu-20.04"
2435
env:
@@ -75,7 +86,7 @@ jobs:
7586
strategy:
7687
fail-fast: true
7788
matrix:
78-
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
89+
python-version: ["3.7", "3.8", "3.9", "3.10"]
7990
runs-on: "ubuntu-20.04"
8091
env:
8192
PYTHON_VER: "${{ matrix.python-version }}"
@@ -114,6 +125,7 @@ jobs:
114125
python-version: ["3.7"]
115126
env:
116127
PYTHON_VER: "${{ matrix.python-version }}"
128+
INVOKE_LOCAL: "True"
117129
steps:
118130
- name: "Check out repository code"
119131
uses: "actions/checkout@v2"
@@ -147,10 +159,11 @@ jobs:
147159
strategy:
148160
fail-fast: true
149161
matrix:
150-
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
162+
python-version: ["3.7", "3.8", "3.9", "3.10"]
151163
runs-on: "ubuntu-20.04"
152164
env:
153165
PYTHON_VER: "${{ matrix.python-version }}"
166+
INVOKE_LOCAL: "True"
154167
steps:
155168
- name: "Check out repository code"
156169
uses: "actions/checkout@v2"

README.md

Lines changed: 107 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,59 @@
1-
# netcompare
1+
# jdiff
22

3-
This library is meant to be a light-weight way to compare structured output from network devices `show` commands. `netcompare` is a python library targeted at intelligently deep diffing structured data objects of json type. In addition, `netcompare` can also provide some basic testing of key/values within a data structure.
3+
The `jdiff` is a light-weight library to examine structured data. `jdiff` provides an interface to intelligently compare json data objects as well as test for the presence (or absence) of keys, and to examine and compare their values.
44

5-
The library heavily relies on [jmespath](https://jmespath.org/) for traversing the json object and finding the value(s) to be evaluated. More on that later.
5+
The library heavily relies on [jmespath](https://jmespath.org/) for traversing the json object and finding the value(s) to be evaluated. More on that [here](#customized-jmespath).
66

7-
## Use Case
7+
## Usage
88

9-
`netcompare` enables an easy and direct way to see the outcome of network configuration or operational status change. The intended usage is to collect structured `show` command output before and after a change window. Prior to closing the change window, the results are compared to help determine if the change was successful as intended and if the network is in an acceptable state. The output can be stored with the change's documentation for easy reference and proof of completion.
9+
A `jdiff` Check accepts two objects as input: the reference object, and the comparison object. The reference object is used as the intended or accepted state and it's keys and values are compared against the comparison object.
10+
11+
`jdiff` does not collect the data for you, it simply works on data passed into it. This allows for maximum flexibility in collecting the data.
12+
13+
For instance, the reference state can be collected from the network directly using any method that returns structured data: ansible, napalm, nornir to name a few. You could also choose to generate the reference state from an SoT, such as [Nautobot](https://github.com/nautobot/nautobot/), and have a true intended state.
14+
15+
`jdiff` is perfectly suited to work with data gathered from network devices via show commands, Ansible playbooks, as well as in applications such as [Nautobot](https://github.com/nautobot/nautobot/), or [Netbox](https://github.com/netbox-community/netbox). `jdiff` is focused on being the 'plumbing' behind a full network automation validation solution.
16+
17+
### Testing data structures
18+
19+
Briefly, these tests, or `CheckTypes`, are provided to test the objects, to aide in determining the status of the data.
20+
21+
- `exact_match`: the keys and values much match, exactly, between the two objects
22+
- `tolerance`: the keys must match and the values can differ according to the 'tolerance' value provided
23+
- `parameter_match`: a reference key and value is provided and it's presence (or absence) is checked in the provided object
24+
- `regex`: a reference regex pattern is provided and it's presence (or absence) is checked for a match in the provided object
25+
- `operator`: similar to parameter match, but the reference includes several different possible operators: 'in', 'bool', 'string', and numerical comparison with 'int' and 'float' to check against
26+
27+
`CheckTypes` are explained in more detail in the [CheckTypes Explained section](#check-types-explained).
28+
29+
30+
## Workflow
31+
32+
| ![jdiff workflow](./docs/images/workflow.png) |
33+
|:---:|
34+
| **`jdiff` workflow** |
35+
36+
37+
1. The reference state object is assembled. The structured data may be collected from:
38+
39+
- an SoT
40+
- Directly from the network using any Automation that returns structured data
41+
42+
2. The Network Engineer makes changes to the network, whether it is an upgrade, peering change, or migration.
43+
3. The comparison state is collected, typically directly from the network, but any method is acceptable.
44+
4. The reference state is then compared to the current state using the jdiff library.
1045

1146
## Library Architecture
1247

13-
![netcompare HLD](./docs/images/hld.png)
48+
| ![jdiff HLD](./docs/images/hld.png) |
49+
|:---:|
50+
| **`jdiff` architecture** |
1451

1552
An instance of `CheckType` object must be created first before passing one of the below check types as an argument:
1653

1754
- `exact_match`
1855
- `tolerance`
19-
- `parameters`
56+
- `parameter_match`
2057
- `regex`
2158
- `operator`
2259

@@ -26,7 +63,7 @@ my_check = "exact_match"
2663
check = CheckType.init(my_check)
2764
```
2865

29-
Next, define a json object as reference data, as well as a JMESPATH expression to extract the value wanted and pass them to `get_value` method. Be aware! `netcompare` works with a customized version of JMESPATH. More on that later.
66+
Next, define a json object as reference data, as well as a JMESPATH expression to extract the value wanted and pass them to `get_value` method. Be aware! `jdiff` works with a customized version of JMESPATH. More on that [below](#customized-jmespath).
3067

3168
```python
3269
bgp_pre_change = "./pre/bgp.json"
@@ -51,7 +88,7 @@ results = check.evaluate(post_value, pre_value, **evaluate_args)
5188

5289
## Customized JMESPATH
5390

54-
Since `netcompare` works with json objects as data inputs, JMESPATH was the obvious choice for traversing the data and extracting the value(s) to compare.
91+
Since `jdiff` works with json objects as data inputs, JMESPATH was the obvious choice for traversing the data and extracting the value(s) to compare.
5592

5693
However, JMESPATH comes with a limitation where is not possible to define a `key` to which the `value` belongs to.
5794

@@ -105,7 +142,7 @@ A JMESPATH expression to extract `state` is shown below.
105142
```
106143

107144
How can we understand that `Idle` is relative to peer 7.7.7.7 and `Connected` to peer `10.1.0.0` ?
108-
We could index the output but that would require some post-processing of the data. For that reason, `netcompare` use a customized version of JMESPATH where it is possible to define a reference key for the value(s) wanted. The reference key must be within `$` sign anchors and defined in a list, together with the value(s):
145+
We could index the output but that would require some post-processing of the data. For that reason, `jdiff` use a customized version of JMESPATH where it is possible to define a reference key for the value(s) wanted. The reference key must be within `$` sign anchors and defined in a list, together with the value(s):
109146

110147
```python
111148
"result[0].vrfs.default.peerList[*].[$peerAddress$,state]
@@ -118,7 +155,7 @@ That would give us...
118155

119156
```
120157

121-
## Check Types Explained
158+
## `CheckTypes` Explained
122159

123160
### exact_match
124161

@@ -129,7 +166,7 @@ Examples:
129166

130167

131168
```python
132-
>>> from netcompare import CheckType
169+
>>> from jdiff import CheckType
133170
>>> pre_data = {
134171
"jsonrpc": "2.0",
135172
"id": "EapiExplorer-1",
@@ -187,7 +224,7 @@ Examples:
187224
>>> # Create an instance of CheckType object with 'exact_match' as check-type argument.
188225
>>> my_check = CheckType.init(check_type="exact_match")
189226
>>> my_check
190-
>>> <netcompare.check_types.ExactMatchType object at 0x10ac00f10>
227+
>>> <jdiff.check_types.ExactMatchType object at 0x10ac00f10>
191228
>>> # Extract the wanted value from pre_dat to later compare with post_data. As we want compare all the body (excluding "interfaceStatistics"), we do not need to define any reference key
192229
>>> pre_value = my_check.get_value(output=pre_data, path=my_jmspath, exclude=exclude_fields)
193230
>>> pre_value
@@ -219,60 +256,8 @@ Let's see a better way to run `exact_match` for this specific case. Since we are
219256
```
220257
Targeting only the `interfaceStatus` key, we would need to define a reference key (in this case `$name$`), we would not define any exclusion list.
221258

222-
The anchor logic for the reference key applies to all check-types available in `netcompare`
223-
259+
The anchor logic for the reference key applies to all check-types available in `jdiff`
224260

225-
### parameter_match
226-
227-
parameter_match provides a way to match keys and values in the output with known good values.
228-
229-
The test defines key/value pairs known to be the good value - type `dict()` - as well as a mode - `match`, `no-match` - to match or not against the parsed output. The test fails if any status has changed based on what is defined in pre/post. If there are new values not contained in the input/test value, that will not count as a failure.
230-
231-
232-
Examples:
233-
234-
```python
235-
>>> post_data = {
236-
... "jsonrpc": "2.0",
237-
... "id": "EapiExplorer-1",
238-
... "result": [
239-
... {
240-
... "interfaces": {
241-
... "Management1": {
242-
... "lastStatusChangeTimestamp": 1626247821.123456,
243-
... "lanes": 0,
244-
... "name": "Management1",
245-
... "interfaceStatus": "down",
246-
... "autoNegotiate": "success",
247-
... "burnedInAddress": "08:00:27:e6:b2:f8",
248-
... "loopbackMode": "loopbackNone",
249-
... "interfaceStatistics": {
250-
... "inBitsRate": 3403.4362520883615,
251-
... "inPktsRate": 3.7424095978179257,
252-
... "outBitsRate": 16249.69114419833,
253-
... "updateInterval": 300,
254-
... "outPktsRate": 2.1111866059750692
255-
... }
256-
... }
257-
... }
258-
... }
259-
... ]
260-
>>> my_check = CheckType.init(check_type="parameter_match")
261-
>>> my_jmspath = "result[*].interfaces.*.[$name$,interfaceStatus,autoNegotiate]"
262-
>>> post_value = my_check.get_value(output=post_data, path=my_jmspath)
263-
>>> # mode: match - Match in the ouptut what is defined under 'params'
264-
>>> my_parameter_match = {"mode": "match", "params": {"interfaceStatus": "connected", "autoNegotiate": "success"}}
265-
>>> actual_results = my_check.evaluate(post_value, **my_parameter_match)
266-
>>> actual_results
267-
({'Management1': {'interfaceStatus': 'down'}}, False)
268-
>>> # mode: no-match - Return what does nto match in the ouptut as defined under 'params'
269-
>>> my_parameter_match = {"mode": "no-match", "params": {"interfaceStatus": "connected", "autoNegotiate": "success"}}
270-
>>> actual_results = my_check.evaluate(post_value, **my_parameter_match)
271-
>>> actual_results
272-
({'Management1': {'autoNegotiate': 'success'}}, False
273-
```
274-
275-
In network data, this could be a state of bgp neighbors being Established or the connectedness of certain interfaces being up.
276261

277262
### Tolerance
278263

@@ -344,7 +329,7 @@ Lets have a look to a couple of examples:
344329
>>> pre_value = my_check.get_value(pre_data, my_jmspath)
345330
>>> post_value = my_check.get_value(post_data, my_jmspath)
346331
>>> actual_results = my_check.evaluate(post_value, pre_value, **my_tolerance_arguments)
347-
>>> # Netcompare returns the value that are not within the 10%
332+
>>> # jdiff returns the value that are not within the 10%
348333
>>> actual_results
349334
({'10.1.0.0': {'accepted_prefixes': {'new_value': 500, 'old_value': 900}, 'received_prefixes': {'new_value': 599, 'old_value': 999}, 'sent_prefixes': {'new_value': 511, 'old_value': 1011}}}, False)
350335
>>> # Let's difine a higher tolerance
@@ -358,6 +343,59 @@ Lets have a look to a couple of examples:
358343
This test can test the tolerance for changing quantities of certain things such as routes, or L2 or L3 neighbors. It could also test actual outputted values such as transmitted light levels for optics.
359344

360345

346+
### Parameter_match
347+
348+
parameter_match provides a way to match keys and values in the output with known good values.
349+
350+
The test defines key/value pairs known to be the good value - type `dict()` - as well as a mode - `match`, `no-match` - to match or not against the parsed output. The test fails if any status has changed based on what is defined in pre/post. If there are new values not contained in the input/test value, that will not count as a failure.
351+
352+
353+
Examples:
354+
355+
```python
356+
>>> post_data = {
357+
... "jsonrpc": "2.0",
358+
... "id": "EapiExplorer-1",
359+
... "result": [
360+
... {
361+
... "interfaces": {
362+
... "Management1": {
363+
... "lastStatusChangeTimestamp": 1626247821.123456,
364+
... "lanes": 0,
365+
... "name": "Management1",
366+
... "interfaceStatus": "down",
367+
... "autoNegotiate": "success",
368+
... "burnedInAddress": "08:00:27:e6:b2:f8",
369+
... "loopbackMode": "loopbackNone",
370+
... "interfaceStatistics": {
371+
... "inBitsRate": 3403.4362520883615,
372+
... "inPktsRate": 3.7424095978179257,
373+
... "outBitsRate": 16249.69114419833,
374+
... "updateInterval": 300,
375+
... "outPktsRate": 2.1111866059750692
376+
... }
377+
... }
378+
... }
379+
... }
380+
... ]
381+
>>> my_check = CheckType.init(check_type="parameter_match")
382+
>>> my_jmspath = "result[*].interfaces.*.[$name$,interfaceStatus,autoNegotiate]"
383+
>>> post_value = my_check.get_value(output=post_data, path=my_jmspath)
384+
>>> # mode: match - Match in the ouptut what is defined under 'params'
385+
>>> my_parameter_match = {"mode": "match", "params": {"interfaceStatus": "connected", "autoNegotiate": "success"}}
386+
>>> actual_results = my_check.evaluate(post_value, **my_parameter_match)
387+
>>> actual_results
388+
({'Management1': {'interfaceStatus': 'down'}}, False)
389+
>>> # mode: no-match - Return what does nto match in the ouptut as defined under 'params'
390+
>>> my_parameter_match = {"mode": "no-match", "params": {"interfaceStatus": "connected", "autoNegotiate": "success"}}
391+
>>> actual_results = my_check.evaluate(post_value, **my_parameter_match)
392+
>>> actual_results
393+
({'Management1': {'autoNegotiate': 'success'}}, False
394+
```
395+
396+
In network data, this could be a state of bgp neighbors being Established or the connectedness of certain interfaces being up.
397+
398+
361399
### Regex
362400

363401
The `regex` check type evaluates data against a python regular expression defined as check-type argument. As per `parameter_match` the option `match`, `no-match` is also supported.
@@ -405,7 +443,7 @@ Let's run an example where we want to check the `burnedInAddress` key has a stri
405443
>>> # What if we want "no-match"?
406444
>>> regex_args = {"regex": "(?:[0-9a-fA-F]:?){12}", "mode": "no-match"}
407445
>>> result = check.evaluate(value, **regex_args)
408-
>>> # Netcompare return the failing data as the regex match the value
446+
>>> # jdiff return the failing data as the regex match the value
409447
>>> result
410448
({'Management1': {'burnedInAddress': '08:00:27:e6:b2:f8'}}, False)
411449
```
@@ -428,7 +466,7 @@ Operator is a check which includes an array of different evaluation logic. Here
428466

429467
3. in-range: Check if the value of a specified element is in the given numeric range.
430468
- in-range: [20, 70]
431-
check if value is in range between 20 nad 70
469+
check if value is in range between 20 and 70
432470

433471
4. not-range: Check if the value of a specified element is outside of a given numeric range.
434472
- not-range: [5 , 40]

docs/images/workflow.png

61.1 KB
Loading
File renamed without changes.

0 commit comments

Comments
 (0)