Skip to content

Commit 46af6c4

Browse files
New Example06 - IP Prefixes (#111)
* first draft of example06 Co-authored-by: Glenn Matthews <[email protected]>
1 parent 3269198 commit 46af6c4

File tree

9 files changed

+360
-0
lines changed

9 files changed

+360
-0
lines changed

examples/06-ip-prefixes/README.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Example 06 - IP Prefixes
2+
3+
This example shows how to play around to IPAM systems which have a different implementation of an IP Prefix.
4+
5+
These IPAM systems, IPAM A and IPAM B, are simulated using two YAML files within the `data` folder. These files are dynamic, and they will be loaded and updated from diffsync.
6+
7+
## Test the example
8+
9+
You could simply run the `main.py` file, but to run step by step.
10+
11+
### Set up the environment
12+
13+
Install the dependencies (recommended into a virtual environment)
14+
15+
```
16+
pip3 install -r requirements.txt
17+
```
18+
19+
and go into a `python` interactive session:
20+
21+
```
22+
python3
23+
>>>
24+
```
25+
26+
### Import the DiffSync adapters
27+
28+
```py
29+
>>> from adapter_ipam_a import IpamA
30+
>>> from adapter_ipam_b import IpamB
31+
```
32+
33+
### Initialize and load adapter for IPAM A
34+
35+
```py
36+
>>> ipam_a = IpamA()
37+
>>> ipam_a.load()
38+
```
39+
40+
You can check the content loaded from IPAM A. Notice that the data has been transformed into the DiffSync model, which is different from the original YAML data.
41+
42+
```py
43+
>>> import pprint
44+
>>> pprint.pprint(ipam_a.dict())
45+
{'prefix': {'10.10.10.10/24': {'prefix': '10.10.10.10/24',
46+
'vlan_id': 10,
47+
'vrf': 'data'},
48+
'10.20.20.20/24': {'prefix': '10.20.20.20/24',
49+
'tenant': 'ABC corp',
50+
'vlan_id': 20,
51+
'vrf': 'voice'},
52+
'172.18.0.0/16': {'prefix': '172.18.0.0/16', 'vlan_id': 18}}}
53+
```
54+
55+
### Initialize and load adapter for IPAM B
56+
57+
```py
58+
>>> ipam_b = IpamB()
59+
>>> ipam_b.load()
60+
```
61+
62+
You can check the content loaded from IPAM B. Notice that the data has been transformed into the DiffSync model, which again is different from the original YAML format.
63+
64+
```py
65+
>>> pprint.pprint(ipam_b.dict())
66+
{'prefix': {'10.10.10.10/24': {'prefix': '10.10.10.10/24', 'vlan_id': 123},
67+
'2001:DB8::/32': {'prefix': '2001:DB8::/32',
68+
'tenant': 'XYZ Corporation',
69+
'vlan_id': 10,
70+
'vrf': 'data'}}}
71+
```
72+
73+
### Check the difference
74+
75+
We can use `diff_to` or `diff_from` to select, from the perspective of the calling adapter, who is the authoritative in each case.
76+
77+
```py
78+
>>> diff = ipam_a.diff_to(ipam_b)
79+
```
80+
81+
From this `diff`, we can check the summary of what would happen.
82+
83+
```py
84+
>>> diff.summary()
85+
{'create': 2, 'update': 1, 'delete': 1, 'no-change': 0}
86+
```
87+
88+
And, also go into the details. We can see how the `'+'` and + `'-'` represent the actual changes in the target adapter: create, delete or update (when both symbols appear).
89+
90+
```py
91+
>>> pprint.pprint(diff.dict())
92+
{'prefix': {'10.10.10.10/24': {'+': {'vlan_id': 10, 'vrf': 'data'},
93+
'-': {'vlan_id': 123, 'vrf': None}},
94+
'10.20.20.20/24': {'+': {'tenant': 'ABC corp',
95+
'vlan_id': 20,
96+
'vrf': 'voice'}},
97+
'172.18.0.0/16': {'+': {'tenant': None,
98+
'vlan_id': 18,
99+
'vrf': None}},
100+
'2001:DB8::/32': {'-': {'tenant': 'XYZ Corporation',
101+
'vlan_id': 10,
102+
'vrf': 'data'}}}}
103+
```
104+
105+
### Enforce synchronization
106+
107+
Simply transforming the `diff_to` to `sync_to`, we are going to change the state of the destination target.
108+
109+
```py
110+
>>> ipam_a.sync_to(ipam_b)
111+
```
112+
113+
### Validate synchronization
114+
115+
Now, if we reload the IPAM B, and try to check the difference, we should see no differences.
116+
117+
```py
118+
>>> new_ipam_b = IpamB()
119+
>>> new_ipam_b.load()
120+
>>> diff = ipam_a.diff_to(new_ipam_b)
121+
>>> diff.summary()
122+
{'create': 0, 'update': 0, 'delete': 0, 'no-change': 3}
123+
```
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""IPAM A adapter."""
2+
import os
3+
import ipaddress
4+
import yaml
5+
from models import Prefix # pylint: disable=no-name-in-module
6+
from diffsync import DiffSync
7+
8+
dirname = os.path.dirname(os.path.realpath(__file__))
9+
10+
11+
class IpamAPrefix(Prefix):
12+
"""Implementation of Prefix create/update/delete methods for IPAM A."""
13+
14+
@classmethod
15+
def create(cls, diffsync, ids, attrs):
16+
"""Create a Prefix record in IPAM A."""
17+
diffsync.data.append(
18+
{
19+
"cidr": ids["prefix"],
20+
"family": ipaddress.ip_address(ids["prefix"].split("/")[0]).version,
21+
"vrf": attrs["vrf"],
22+
"vlan": f'VLAN{attrs["vlan_id"]}',
23+
"customer_id": attrs["tenant"] if attrs["tenant"] else None,
24+
}
25+
)
26+
27+
return super().create(diffsync, ids=ids, attrs=attrs)
28+
29+
def update(self, attrs):
30+
"""Update a Prefix record in IPAM A."""
31+
for elem in self.diffsync.data:
32+
if elem["cidr"] == self.prefix:
33+
if "vrf" in attrs:
34+
elem["vrf"] = attrs["vrf"]
35+
if "vlan_id" in attrs:
36+
elem["vlan_id"] = f'VLAN{attrs["vlan_id"]}'
37+
if "tenant" in attrs:
38+
elem["customer_id"] = attrs["tenant"]
39+
break
40+
41+
return super().update(attrs)
42+
43+
def delete(self):
44+
"""Delete a Prefix record in IPAM A."""
45+
for index, elem in enumerate(self.diffsync.data):
46+
if elem["cidr"] == self.prefix:
47+
del self.diffsync.data[index]
48+
break
49+
50+
return super().delete()
51+
52+
53+
class IpamA(DiffSync):
54+
"""IPAM A DiffSync adapter implementation."""
55+
56+
prefix = IpamAPrefix
57+
58+
top_level = ["prefix"]
59+
60+
def __init__(self, *args, **kwargs):
61+
"""Initialize the IPAM A Adapter."""
62+
super().__init__(*args, **kwargs)
63+
64+
with open(os.path.join(dirname, "data", "ipam_a.yml"), encoding="utf-8") as data_file:
65+
self.data = yaml.safe_load(data_file)
66+
67+
def load(self):
68+
"""Load prefixes from IPAM A."""
69+
for subnet in self.data:
70+
prefix = self.prefix(
71+
prefix=subnet["cidr"],
72+
vrf=subnet["vrf"],
73+
vlan_id=int(subnet["vlan"].lstrip("VLAN")),
74+
tenant=subnet["customer_id"],
75+
)
76+
self.add(prefix)
77+
78+
def sync_complete(self, source, *args, **kwargs):
79+
"""Clean up function for DiffSync sync."""
80+
with open(os.path.join(dirname, "data", "ipam_a.yml"), encoding="utf-8", mode="w") as data_file:
81+
yaml.safe_dump(self.data, data_file)
82+
83+
return super().sync_complete(source, *args, **kwargs)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""IPAM B adapter."""
2+
import os
3+
import yaml
4+
from models import Prefix # pylint: disable=no-name-in-module
5+
from diffsync import DiffSync
6+
7+
dirname = os.path.dirname(os.path.realpath(__file__))
8+
9+
10+
class IpamBPrefix(Prefix):
11+
"""Implementation of Prefix create/update/delete methods for IPAM B."""
12+
13+
@classmethod
14+
def create(cls, diffsync, ids, attrs):
15+
"""Create a Prefix record in IPAM B."""
16+
diffsync.data.append(
17+
{
18+
"network": ids["prefix"].split("/")[0],
19+
"prefix_length": int(ids["prefix"].split("/")[1]),
20+
"vrf": attrs["vrf"],
21+
"vlan_id": attrs["vlan_id"],
22+
"tenant": attrs["tenant"] if attrs["tenant"] else None,
23+
}
24+
)
25+
26+
return super().create(diffsync, ids=ids, attrs=attrs)
27+
28+
def update(self, attrs):
29+
"""Update a Prefix record in IPAM B."""
30+
network = self.prefix.split("/")[0]
31+
prefix_length = int(self.prefix.split("/")[1])
32+
33+
for elem in self.diffsync.data:
34+
if elem["network"] == network and elem["prefix_length"] == prefix_length:
35+
if "vrf" in attrs:
36+
elem["vrf"] = attrs["vrf"]
37+
if "vlan_id" in attrs:
38+
elem["vlan_id"] = attrs["vlan_id"]
39+
if "tenant" in attrs:
40+
elem["tenant"] = attrs["tenant"]
41+
break
42+
43+
return super().update(attrs)
44+
45+
def delete(self):
46+
"""Update a Prefix record in IPAM B."""
47+
network = self.prefix.split("/")[0]
48+
prefix_length = int(self.prefix.split("/")[1])
49+
50+
for index, elem in enumerate(self.diffsync.data):
51+
if elem["network"] == network and elem["prefix_length"] == prefix_length:
52+
del self.diffsync.data[index]
53+
break
54+
55+
return super().delete()
56+
57+
58+
class IpamB(DiffSync):
59+
"""IPAM A DiffSync adapter implementation."""
60+
61+
prefix = IpamBPrefix
62+
63+
top_level = ["prefix"]
64+
65+
def __init__(self, *args, **kwargs):
66+
"""Initialize the IPAM B Adapter."""
67+
super().__init__(*args, **kwargs)
68+
69+
with open(os.path.join(dirname, "data", "ipam_b.yml"), encoding="utf-8") as data_file:
70+
self.data = yaml.safe_load(data_file)
71+
72+
def load(self):
73+
"""Initialize the Ipam B Object by loading from DATA."""
74+
for prefix_data in self.data:
75+
prefix = self.prefix(
76+
prefix=f"{prefix_data['network']}/{prefix_data['prefix_length']}",
77+
vrf=prefix_data["vrf"],
78+
vlan_id=prefix_data["vlan_id"],
79+
tenant=prefix_data["tenant"],
80+
)
81+
self.add(prefix)
82+
83+
def sync_complete(self, source, *args, **kwargs):
84+
"""Clean up function for DiffSync sync."""
85+
with open(os.path.join(dirname, "data", "ipam_b.yml"), encoding="utf-8", mode="w") as data_file:
86+
yaml.safe_dump(self.data, data_file)
87+
88+
return super().sync_complete(source, *args, **kwargs)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
- cidr: "10.10.10.10/24"
3+
family: 4
4+
vrf: "data"
5+
vlan: "VLAN10"
6+
customer_id: null
7+
- cidr: "10.20.20.20/24"
8+
family: 4
9+
vrf: "voice"
10+
vlan: "VLAN20"
11+
customer_id: "ABC corp"
12+
- cidr: "172.18.0.0/16"
13+
family: 4
14+
vrf: null
15+
vlan: "VLAN18"
16+
customer_id: null
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
- network: "10.10.10.10"
3+
prefix_length: 24
4+
tenant: null
5+
vlan_id: 123
6+
vrf: "voice"
7+
- network: "2001:DB8::"
8+
prefix_length: 32
9+
tenant: "XYZ Corporation"
10+
vlan_id: 10
11+
vrf: "data"

examples/06-ip-prefixes/main.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env python
2+
"""Main example."""
3+
from adapter_ipam_a import IpamA
4+
from adapter_ipam_b import IpamB
5+
6+
7+
if __name__ == "__main__":
8+
ipam_a = IpamA()
9+
ipam_b = IpamB()
10+
ipam_a.load()
11+
ipam_b.load()
12+
diff = ipam_a.diff_to(ipam_b)
13+
# ipam_a.sync_to(ipam_b)

examples/06-ip-prefixes/models.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""DiffSync models."""
2+
from typing import Optional
3+
from diffsync import DiffSyncModel
4+
5+
6+
class Prefix(DiffSyncModel):
7+
"""Example model of a Prefix."""
8+
9+
_modelname = "prefix"
10+
_identifiers = ("prefix",)
11+
_attributes = ("vrf", "vlan_id", "tenant")
12+
13+
prefix: str
14+
vrf: Optional[str]
15+
vlan_id: Optional[int]
16+
tenant: Optional[str]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
diffsync
2+
pyyaml

tests/unit/test_examples.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,11 @@ def test_example_4():
4343
example4_main = join(example4_dir, "main.py")
4444
# Run it and make sure it doesn't raise an exception or otherwise exit with a non-zero code.
4545
subprocess.run(example4_main, cwd=example4_dir, check=True)
46+
47+
48+
def test_example_6():
49+
"""Test that the "example6" script runs successfully."""
50+
example6_dir = join(EXAMPLES, "06-ip-prefixes")
51+
example6_main = join(example6_dir, "main.py")
52+
# Run it and make sure it doesn't raise an exception or otherwise exit with a non-zero code.
53+
subprocess.run(example6_main, cwd=example6_dir, check=True)

0 commit comments

Comments
 (0)