Skip to content

Commit 5abdb7b

Browse files
committed
PUC-740: pretty output
1 parent 28c869c commit 5abdb7b

File tree

15 files changed

+471
-454
lines changed

15 files changed

+471
-454
lines changed
+33-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,35 @@
1+
# Nautobot vs OpenStack Comparison Tool
12

2-
cd python/diff-nautobot-understack
3-
python3 -m venv .venv
4-
source .venv/bin/activate
5-
poetry lock
6-
poetry install
3+
This tool compares data between Nautobot and OpenStack.
74

8-
export NB_TOKEN=<get_token_from_nautobot_dev>
9-
poetry run diff-networks
10-
poetry run diff-projects
5+
## Currently Supported Comparisons
6+
7+
| OpenStack | Nautobot |
8+
|-----------|-----------------------|
9+
| Project | Tenant |
10+
| Network | UCVNI & Namespace |
11+
12+
13+
---
14+
15+
## Setup Instructions
16+
17+
1. cd python/diff-nautobot-understack
18+
2. `poetry install`
19+
1. poetry will handle the creation of this virtual environment for you. It'll use .venv in the project if you configure it to do so locally on your machine with `poetry config virtualenvs.in-project true`.
20+
2. user can create a shell with poetry shell and then when they exit it will clean up auth variables. Or they can run source .venv/bin/activate or poetry run <commands-below>
21+
22+
3. Export environment variables (or add them to a .env file):
23+
1. export NAUTOBOT_URL=https://nautobot.url.here
24+
2. users should browse to https://nautobot.url.here/user/api-tokens/ and generate an API token
25+
3. export NAUTOBOT_TOKEN= <generated token from above step>
26+
4. export OS_CLOUD=uc-dev-infra
27+
5. export OS_CLIENT_CONFIG_FILE=./my_clouds.yaml (set this if it is in any other location not [defined here](https://opendev.org/openstack/openstacksdk#getting-started))
28+
29+
30+
- Below are some example commands
31+
```
32+
uc-diff --help
33+
uc-diff project undercloud -v
34+
uc-diff network
35+
```

python/diff-nautobot-understack/clouds.yaml

-31
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import os
2+
import sys
3+
4+
import typer
5+
from diffsync.diff import Diff
6+
from rich import print
7+
from rich.console import Console
8+
from rich.table import Table
9+
10+
from diff_nautobot_understack.network.main import (
11+
openstack_network_diff_from_ucvni_network,
12+
)
13+
from diff_nautobot_understack.project.main import (
14+
openstack_project_diff_from_nautobot_tenant,
15+
)
16+
from diff_nautobot_understack.settings import app_settings as settings
17+
18+
required_env_vars = ["NAUTOBOT_TOKEN", "NAUTOBOT_URL", "OS_CLOUD"]
19+
20+
21+
app = typer.Typer(
22+
name="diff",
23+
add_completion=False,
24+
help="compare data between Openstack and Nautobot.",
25+
)
26+
diff_outputs = {
27+
"project": {"title": "Project Diff", "id_column_name": "Project ID"},
28+
"network": {"title": "Network Diff", "id_column_name": "Network ID"},
29+
}
30+
31+
32+
def display_output(
33+
diff_result: Diff, diff_output: str, output_format: str | None = None
34+
):
35+
print(diff_result.summary())
36+
__output_format = (
37+
output_format if output_format is not None else settings.output_format
38+
)
39+
if __output_format == "table":
40+
diff_output_props = diff_outputs.get(diff_output)
41+
tabular_output(
42+
diff_result.dict().get(diff_output, {}),
43+
diff_output_props.get("title"),
44+
diff_output_props.get("id_column_name"),
45+
)
46+
else:
47+
print(diff_result.dict())
48+
49+
50+
def tabular_output(diffs, title, id_column_name):
51+
table = Table(title=title, show_lines=True)
52+
53+
table.add_column(id_column_name, style="cyan", no_wrap=True)
54+
table.add_column("Change Type", style="magenta")
55+
table.add_column("Details", style="yellow")
56+
57+
for diff_id, changes in diffs.items():
58+
for change_type, details in changes.items():
59+
table.add_row(diff_id, change_type, str(details))
60+
61+
console = Console()
62+
console.print(table)
63+
64+
65+
@app.command()
66+
def project(
67+
name: str,
68+
debug: bool = typer.Option(False, "--debug", "-v", help="Enable debug mode"),
69+
output_format: str = typer.Option(
70+
"json", "--format", help="Available formats json, table"
71+
),
72+
):
73+
"""Nautobot tenants ⟹ Openstack projects"""
74+
settings.debug = debug
75+
diff_result = openstack_project_diff_from_nautobot_tenant(os_project=name)
76+
display_output(diff_result, "project", output_format)
77+
78+
79+
@app.command()
80+
def network(
81+
debug: bool = typer.Option(False, "--debug", "-v", help="Enable debug mode"),
82+
output_format: str = typer.Option(
83+
"json", "--format", help="Available formats json, table"
84+
),
85+
):
86+
"""Nautobot ucvnis ⟹ Openstack networks"""
87+
settings.debug = debug
88+
diff_result = openstack_network_diff_from_ucvni_network()
89+
display_output(diff_result, "network", output_format)
90+
91+
92+
def check_env_vars(required_vars):
93+
missing_vars = [var for var in required_vars if var not in os.environ]
94+
95+
if missing_vars:
96+
print(f"Error: Missing environment variables: {', '.join(missing_vars)}")
97+
sys.exit(1)
98+
else:
99+
print("All required environment variables are set.")
100+
101+
102+
check_env_vars(required_env_vars)

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

+7-9
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
1-
import os
2-
3-
import requests
4-
import logging
51
import inspect
2+
import logging
63
from urllib.parse import urljoin
74

8-
NB_URL = os.environ.get("NB_URL", "https://nautobot.dev.undercloud.rackspace.net")
9-
NB_TOKEN = os.environ.get("NB_TOKEN")
5+
import requests
6+
7+
from diff_nautobot_understack.settings import app_settings as settings
108

119

1210
class API:
1311
CALLER_FRAME = 1
1412

1513
def __init__(self):
16-
self.base_url = NB_URL
14+
self.base_url = settings.nautobot_url
1715
self.s = requests.Session()
18-
self.token = NB_TOKEN
19-
self.s.headers.update({"Authorization": f"Token {NB_TOKEN}"})
16+
self.token = settings.nautobot_token
17+
self.s.headers.update({"Authorization": f"Token {self.token}"})
2018

2119
def make_api_request(
2220
self, url: str, payload: dict | None = None, paginated: bool = False
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import os
2-
31
import openstack
42

5-
OS_CLOUD = os.environ.get("OS_CLOUD", "uc-dev-infra")
6-
7-
8-
openstack.enable_logging(debug=True)
3+
from diff_nautobot_understack.settings import app_settings as settings
94

105

116
class API:
127
def __init__(self):
13-
self.cloud_connection = openstack.connect(cloud=OS_CLOUD)
8+
cloud_name = settings.os_cloud
9+
debug = settings.debug
10+
11+
openstack.enable_logging(debug=debug)
12+
self.cloud_connection = openstack.connect(cloud=cloud_name)

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from diffsync import Adapter
2-
from diff_nautobot_understack.clients.openstack import API
32

3+
from diff_nautobot_understack.clients.openstack import API
44
from diff_nautobot_understack.network import models
55

66

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from diffsync import Adapter
22
from pydantic import BaseModel
3-
from diff_nautobot_understack.clients.nautobot import API
43

4+
from diff_nautobot_understack.clients.nautobot import API
55
from diff_nautobot_understack.network import models
66

77

Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
1-
from pprint import pprint
1+
from diffsync.diff import Diff
22
from diffsync.enum import DiffSyncFlags
3+
from rich import print
4+
35
from diff_nautobot_understack.network.adapters.openstack_network import (
46
Network as OpenstackNetwork,
57
)
68
from diff_nautobot_understack.network.adapters.ucvni import Network as UcvniNetwork
79

810

9-
def openstack_network_diff_from_ucvni_network():
11+
def openstack_network_diff_from_ucvni_network() -> Diff:
1012
openstack_network = OpenstackNetwork()
11-
openstack_network.load()
12-
13+
try:
14+
openstack_network.load()
15+
except Exception:
16+
print("Error retrieving networks from Openstack")
1317
ucvni_network = UcvniNetwork()
14-
ucvni_network.load()
15-
openstack_network_destination_ucvni_source = openstack_network.diff_from(
16-
ucvni_network, flags=DiffSyncFlags.CONTINUE_ON_FAILURE
18+
try:
19+
ucvni_network.load()
20+
except Exception:
21+
print("Error retrieving ucvnis from Nautobot")
22+
ucvni_network_destination_openstack_source = ucvni_network.diff_from(
23+
openstack_network, flags=DiffSyncFlags.CONTINUE_ON_FAILURE
1724
)
18-
pprint(" Nautobot ucvnis ⟹ Openstack networks ")
19-
summary = openstack_network_destination_ucvni_source.summary()
20-
pprint(summary, width=120)
21-
pprint(openstack_network_destination_ucvni_source.dict(), width=120)
25+
return ucvni_network_destination_openstack_source

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ class NetworkModel(DiffSyncModel):
1414
id: str
1515
name: str
1616
status: str
17-
provider_physical_network: str
18-
vni_id: int
17+
provider_physical_network: str | None = None
18+
vni_id: int | None = None

python/diff-nautobot-understack/diff_nautobot_understack/project/adapters/nautobot_tenant.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1+
import uuid
2+
13
from diffsync import Adapter
2-
from diff_nautobot_understack.clients.nautobot import API
34

5+
from diff_nautobot_understack.clients.nautobot import API
46
from diff_nautobot_understack.project import models
57

68

9+
def _remove_hyphens(tenant_id: str):
10+
uuid_obj = uuid.UUID(tenant_id)
11+
return str(uuid_obj.hex)
12+
13+
714
class Tenant(Adapter):
815
project = models.ProjectModel
916

@@ -13,16 +20,17 @@ class Tenant(Adapter):
1320
def __init__(self, **kwargs):
1421
super().__init__(**kwargs)
1522
self.api_client = API()
23+
self.tenant_name = kwargs["name"]
1624

1725
def load(self):
18-
url = "/api/tenancy/tenants/?include=relationships"
26+
url = f"/api/tenancy/tenants/?name={self.tenant_name}&include=relationships"
1927

2028
tenants_response = self.api_client.make_api_request(url, paginated=True)
2129

2230
for tenant in tenants_response:
2331
self.add(
2432
self.project(
25-
id=tenant.get("id"),
33+
id=_remove_hyphens(tenant.get("id")),
2634
name=tenant.get("name"),
2735
description=tenant.get("description"),
2836
)
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import logging
2+
13
from diffsync import Adapter
2-
from diff_nautobot_understack.clients.openstack import API
34

5+
from diff_nautobot_understack.clients.openstack import API
46
from diff_nautobot_understack.project import models
57

8+
logger = logging.getLogger(__name__)
9+
610

711
class Project(Adapter):
812
project = models.ProjectModel
@@ -13,14 +17,18 @@ class Project(Adapter):
1317
def __init__(self, **kwargs):
1418
super().__init__(**kwargs)
1519
openstack_api = API()
20+
self.project_name = kwargs["name"]
1621
self.cloud = openstack_api.cloud_connection
1722

1823
def load(self):
19-
for project in self.cloud.identity.projects():
20-
self.add(
21-
self.project(
22-
id=project.id,
23-
name=project.name,
24-
description=project.description,
25-
)
24+
os_project = self.cloud.get_project(name_or_id=self.project_name)
25+
if not os_project:
26+
logger.error(f"Project '{self.project_name}' not found.")
27+
return
28+
self.add(
29+
self.project(
30+
id=os_project.id,
31+
name=os_project.name,
32+
description=os_project.description,
2633
)
34+
)
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
from pprint import pprint
1+
from diffsync.diff import Diff
22
from diffsync.enum import DiffSyncFlags
3-
from diff_nautobot_understack.project.adapters.openstack_project import Project
3+
44
from diff_nautobot_understack.project.adapters.nautobot_tenant import Tenant
5+
from diff_nautobot_understack.project.adapters.openstack_project import Project
6+
from diff_nautobot_understack.settings import app_settings as settings
57

68

7-
def openstack_project_diff_from_nautobot_tenant():
8-
openstack_project = Project()
9+
def openstack_project_diff_from_nautobot_tenant(os_project=None) -> Diff:
10+
project_name = os_project if os_project is not None else settings.os_project
11+
openstack_project = Project(name=project_name)
912
openstack_project.load()
1013

11-
nautobot_tenant = Tenant()
14+
nautobot_tenant = Tenant(name=project_name)
1215
nautobot_tenant.load()
13-
openstack_project_destination_tenant_source = openstack_project.diff_from(
14-
nautobot_tenant, flags=DiffSyncFlags.CONTINUE_ON_FAILURE
16+
tenant_destination_openstack_project_source = nautobot_tenant.diff_from(
17+
openstack_project, flags=DiffSyncFlags.CONTINUE_ON_FAILURE
1518
)
16-
pprint(" Nautobot tenants ⟹ Openstack projects ")
17-
summary = openstack_project_destination_tenant_source.summary()
18-
pprint(summary, width=120)
19-
pprint(openstack_project_destination_tenant_source.dict(), width=120)
19+
return tenant_destination_openstack_project_source

0 commit comments

Comments
 (0)