Skip to content

Commit 666e478

Browse files
committed
PUC-740: comparing data from Openstack Networks and Nautobot UCVNIs
1 parent e139ddc commit 666e478

File tree

12 files changed

+1529
-0
lines changed

12 files changed

+1529
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
cd python/diff-nautobot-understack
3+
python3 -m venv .venv
4+
source .venv/bin/activate
5+
poetry lock
6+
poetry install
7+
8+
export NB_TOKEN=<get_token_from_nautobot_dev>
9+
poetry run diff-network
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
clouds:
2+
uc-dev-infra:
3+
auth_type: v3websso
4+
identity_provider: sso
5+
protocol: openid
6+
auth:
7+
auth_url: https://keystone.dev.undercloud.rackspace.net/v3
8+
project_domain_name: infra
9+
project_name: baremetal
10+
uc-dev:
11+
auth_type: v3websso
12+
identity_provider: sso
13+
protocol: openid
14+
auth:
15+
auth_url: https://keystone.dev.undercloud.rackspace.net/v3
16+
project_domain_name: Default
17+
uc-staging-infra:
18+
auth_type: v3websso
19+
identity_provider: sso
20+
protocol: openid
21+
auth:
22+
auth_url: https://keystone.staging.undercloud.rackspace.net/v3
23+
project_domain_name: infra
24+
project_name: baremetal
25+
uc-staging:
26+
auth_type: v3websso
27+
identity_provider: sso
28+
protocol: openid
29+
auth:
30+
auth_url: https://keystone.staging.undercloud.rackspace.net/v3
31+
project_domain_name: Default

python/diff-nautobot-understack/diff_nautobot_understack/__init__.py

Whitespace-only changes.

python/diff-nautobot-understack/diff_nautobot_understack/network/__init__.py

Whitespace-only changes.

python/diff-nautobot-understack/diff_nautobot_understack/network/adapters/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from diffsync import Adapter
2+
import openstack
3+
4+
from diff_nautobot_understack.network import models
5+
6+
7+
class Network(Adapter):
8+
network = models.NetworkModel
9+
10+
top_level = ["network"]
11+
type = "OpenstackNetwork"
12+
13+
def __init__(self, os_cloud, **kwargs):
14+
cloud_connection = openstack.connect(cloud=os_cloud)
15+
super().__init__(**kwargs)
16+
self.cloud = cloud_connection
17+
18+
def load(self):
19+
for network in self.cloud.network.networks():
20+
self.add(
21+
self.network(
22+
id=network.id,
23+
name=network.name,
24+
status=network.status.lower(),
25+
provider_physical_network=network.provider_physical_network,
26+
vni_id=network.provider_segmentation_id,
27+
)
28+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import os
2+
3+
import requests
4+
from diffsync import Adapter
5+
from pydantic import BaseModel
6+
import logging
7+
import inspect
8+
from urllib.parse import urljoin
9+
10+
from diff_nautobot_understack.network import models
11+
12+
NB_URL = "https://nautobot.dev.undercloud.rackspace.net"
13+
NB_TOKEN = os.environ.get("NB_TOKEN")
14+
15+
16+
class UcvniDetails(BaseModel):
17+
id: str
18+
name: str
19+
status: str
20+
ucvni_id: int
21+
ucvni_group: str
22+
vlan_group: str
23+
vlan_id: int
24+
25+
26+
class NautobotError(Exception):
27+
message = "Nautobot error"
28+
29+
30+
class APIClient:
31+
CALLER_FRAME = 1
32+
33+
def __init__(self):
34+
self.base_url = NB_URL
35+
self.s = requests.Session()
36+
self.token = NB_TOKEN
37+
self.s.headers.update({"Authorization": f"Token {NB_TOKEN}"})
38+
39+
def make_api_request(
40+
self, url: str, payload: dict | None = None, paginated: bool = False
41+
) -> dict | list:
42+
endpoint_url = urljoin(self.base_url, url)
43+
caller_function = inspect.stack()[self.CALLER_FRAME].function
44+
45+
logging.debug(
46+
"%(caller_function)s payload: %(payload)s",
47+
{"payload": payload, "caller_function": caller_function},
48+
)
49+
50+
if paginated:
51+
return self._fetch_paginated_data(endpoint_url, payload, caller_function)
52+
else:
53+
resp = self.s.get(endpoint_url, timeout=10, json=payload)
54+
return self._process_response(resp, caller_function)
55+
56+
def _fetch_paginated_data(
57+
self, endpoint_url: str, payload: dict | None, caller_function: str
58+
) -> list:
59+
response_items = []
60+
url = endpoint_url
61+
62+
while url is not None:
63+
resp = self.s.get(url, timeout=10, json=payload)
64+
resp_data = self._process_response(resp, caller_function)
65+
66+
response_items.extend(resp_data.get("results", []))
67+
url = resp_data.get("next")
68+
69+
return response_items
70+
71+
def _process_response(self, resp, caller_function: str) -> dict:
72+
if resp.content:
73+
resp_data = resp.json()
74+
else:
75+
resp_data = {"status_code": resp.status_code}
76+
77+
logging.debug(
78+
"%(caller_function)s resp: %(resp)s",
79+
{"resp": resp_data, "caller_function": caller_function},
80+
)
81+
82+
self._log_and_raise_for_status(resp)
83+
return resp_data
84+
85+
def _log_and_raise_for_status(self, resp):
86+
try:
87+
resp.raise_for_status()
88+
except Exception as e:
89+
logging.error(f"HTTP error occurred: {e}")
90+
raise
91+
92+
93+
class Network(Adapter):
94+
CALLER_FRAME = 1
95+
network = models.NetworkModel
96+
97+
top_level = ["network"]
98+
type = "UCVNI"
99+
100+
def __init__(self, **kwargs):
101+
super().__init__(**kwargs)
102+
self.api_client = APIClient()
103+
104+
def load(self):
105+
ucvni_data: list[UcvniDetails] = self.ucvni_get()
106+
for ucvni_item in ucvni_data:
107+
network = self.network(
108+
id=ucvni_item.id,
109+
name=ucvni_item.name,
110+
vni_id=ucvni_item.vlan_id,
111+
provider_physical_network=ucvni_item.vlan_group,
112+
status=ucvni_item.status,
113+
)
114+
self.add(network)
115+
116+
def ucvni_get(
117+
self,
118+
) -> list[UcvniDetails]:
119+
ucvni_detail_list: list[UcvniDetails] = []
120+
121+
url = "/api/plugins/undercloud-vni/ucvnis/?include=relationship"
122+
123+
ucvnis_response = self.api_client.make_api_request(
124+
f"{url}/?include=relationships", paginated=True
125+
)
126+
127+
for ucvni_item in ucvnis_response:
128+
ucvni_group = self.api_client.make_api_request(
129+
url=ucvni_item.get("ucvni_group", {}).get("url")
130+
)
131+
status = self.api_client.make_api_request(
132+
url=ucvni_item.get("status", {}).get("url")
133+
)
134+
vlan_uuid_objects = (
135+
ucvni_item.get("relationships", {})
136+
.get("ucvni_vlans", {})
137+
.get("destination")
138+
.get("objects")
139+
)
140+
vlan_details = self.get_vlan_details(vlan_uuid_objects)
141+
vlan_group, vlan_ids = next(iter(vlan_details.items()))
142+
ucvni_details = UcvniDetails(
143+
id=ucvni_item.get("id"),
144+
name=ucvni_item.get("name"),
145+
ucvni_id=ucvni_item.get("ucvni_id"),
146+
ucvni_group=ucvni_group.get("name"),
147+
status=status.get("name").lower(),
148+
vlan_group=vlan_group,
149+
vlan_id=int(vlan_ids[0]),
150+
)
151+
ucvni_detail_list.append(ucvni_details)
152+
return ucvni_detail_list
153+
154+
def get_vlan_details(self, vlan_uuid_objects):
155+
vlan_uuids = [vlan_uuid_object["id"] for vlan_uuid_object in vlan_uuid_objects]
156+
vlan_details = {}
157+
158+
vlan_uuids_query_params = "&".join(f"id={value}" for value in vlan_uuids)
159+
vlan_url = f"/api/ipam/vlans/?{vlan_uuids_query_params}"
160+
161+
vlans_response = self.api_client.make_api_request(url=vlan_url, paginated=True)
162+
163+
for vlan_response in vlans_response:
164+
vlan_group_url = vlan_response.get("vlan_group", {}).get("url")
165+
166+
if vlan_group_url:
167+
vlan_group_response = self.api_client.make_api_request(
168+
url=vlan_group_url
169+
)
170+
vlan_group_name = vlan_group_response.get("name")
171+
vlan_id = vlan_response.get("vid")
172+
173+
if vlan_group_name:
174+
vlan_details.setdefault(vlan_group_name, []).append(vlan_id)
175+
176+
return vlan_details
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from pprint import pprint
2+
from diffsync import Diff
3+
from diffsync.enum import DiffSyncFlags
4+
from diff_nautobot_understack.network.adapters.openstack_network import (
5+
Network as OpenstackNetwork,
6+
)
7+
from diff_nautobot_understack.network.adapters.ucvni import Network as UcvniNetwork
8+
9+
10+
class MyDiff(Diff):
11+
"""Custom Diff class to control the order of the site objects."""
12+
13+
@classmethod
14+
def order_children_site(cls, children):
15+
"""Return the site children ordered in alphabetical order."""
16+
keys = sorted(children.keys(), reverse=False)
17+
for key in keys:
18+
yield children[key]
19+
20+
21+
def openstack_network_diff_from_ucvni_network():
22+
openstack_network = OpenstackNetwork(os_cloud="uc-dev-infra")
23+
openstack_network.load()
24+
25+
ucvni_network = UcvniNetwork()
26+
ucvni_network.load()
27+
openstack_network_destination_ucvni_source = openstack_network.diff_from(
28+
ucvni_network, flags=DiffSyncFlags.CONTINUE_ON_FAILURE
29+
)
30+
pprint(" Nautobot ucvnis ⟹ Openstack networks ")
31+
summary = openstack_network_destination_ucvni_source.summary()
32+
pprint(summary, width=120)
33+
pprint(openstack_network_destination_ucvni_source.dict(), width=120)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from diffsync import DiffSyncModel
2+
3+
4+
class NetworkModel(DiffSyncModel):
5+
_modelname = "network"
6+
_identifiers = ("id",)
7+
_attributes = (
8+
"name",
9+
"status",
10+
"provider_physical_network",
11+
"vni_id",
12+
)
13+
14+
id: str
15+
name: str
16+
status: str
17+
provider_physical_network: str
18+
vni_id: int

0 commit comments

Comments
 (0)