Files
nsx_fed_sync_script/nsx_sync/interactive.py
T
brandon 5b76457ad8 feat: initial implementation of NSX Security Group IP Sync tool
- Added main script (main.py) for syncing IPs between NSX managers.
- Implemented logging setup and YAML configuration loading.
- Created NSXClient for interacting with NSX Policy API.
- Developed SyncEngine for comparing and syncing IPs.
- Added interactive group selection feature.
- Implemented data export functionality for JSON and CSV formats.
- Created requirements.txt for dependencies.
- Added __init__.py for package versioning.
- Included error handling and validation for configuration.
2026-05-01 09:46:04 -05:00

173 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Interactive mode: list all Security Groups from the source manager and
allow the user to select which ones to sync via a terminal UI.
This is a nice-to-have feature and only runs when --interactive is passed.
Requires no additional dependencies beyond the standard library.
"""
import logging
from typing import List, Optional
from .nsx_client import NSXClient
logger = logging.getLogger(__name__)
_PAGE = 20 # groups per screen page
def _print_page(groups: List[dict], page: int, selected: set) -> None:
start = page * _PAGE
end = min(start + _PAGE, len(groups))
total_pages = (len(groups) + _PAGE - 1) // _PAGE
print(f"\n Security Groups (page {page + 1}/{total_pages}) "
f" {len(selected)} selected")
print(f" {'#':<5} {'*':<3} {'Display Name':<50} {'ID'}")
print(f" {'-'*5} {'-'*3} {'-'*50} {'-'*36}")
for i, g in enumerate(groups[start:end], start=start):
marker = "*" if g["id"] in selected else " "
name = g.get("display_name", g["id"])[:50]
print(f" {i:<5} {marker:<3} {name:<50} {g['id']}")
def _prompt(msg: str, default: str = "") -> str:
try:
val = input(msg).strip()
return val if val else default
except (KeyboardInterrupt, EOFError):
return "q"
def select_groups(
source: NSXClient,
domain: str = "default",
preselected: Optional[List[str]] = None,
) -> List[str]:
"""
Interactive terminal selector. Returns a list of group names (display_name
if unique, otherwise id) the user wants to sync.
Navigation:
<number(s)> toggle selection by row index (comma-separated)
a select all on current page
n next page
p previous page
s show currently selected list
d done / confirm
q quit without selecting
"""
print(f"\nFetching Security Groups from [{source.label}]...")
try:
all_groups = source.list_groups(domain)
except Exception as exc:
logger.error("Failed to list groups: %s", exc)
print(f"\nError: {exc}")
return []
if not all_groups:
print("No Security Groups found.")
return []
all_groups.sort(key=lambda g: g.get("display_name", g["id"]).lower())
selected: set = set()
if preselected:
for g in all_groups:
if g.get("display_name") in preselected or g["id"] in preselected:
selected.add(g["id"])
page = 0
total_pages = (len(all_groups) + _PAGE - 1) // _PAGE
print(
"\nControls: enter row number(s) to toggle | 'a' all on page | "
"'n' next | 'p' prev | 's' show selected | 'd' done | 'q' quit"
)
while True:
_print_page(all_groups, page, selected)
cmd = _prompt("\n > ").lower()
if cmd == "q":
print("Cancelled.")
return []
if cmd == "d":
break
if cmd == "n":
if page < total_pages - 1:
page += 1
else:
print(" Already on last page.")
continue
if cmd == "p":
if page > 0:
page -= 1
else:
print(" Already on first page.")
continue
if cmd == "a":
start = page * _PAGE
end = min(start + _PAGE, len(all_groups))
for g in all_groups[start:end]:
selected.add(g["id"])
continue
if cmd == "s":
if not selected:
print(" Nothing selected yet.")
else:
print(f"\n Selected ({len(selected)}):")
for gid in sorted(selected):
for g in all_groups:
if g["id"] == gid:
print(f" - {g.get('display_name', gid)}")
break
continue
# Try to parse as comma-separated indices
tokens = [t.strip() for t in cmd.replace(" ", ",").split(",") if t.strip()]
valid = True
indices = []
for tok in tokens:
if tok.isdigit():
idx = int(tok)
if 0 <= idx < len(all_groups):
indices.append(idx)
else:
print(f" Index {idx} is out of range.")
valid = False
else:
print(f" Unknown command or index: '{tok}'")
valid = False
if valid and indices:
for idx in indices:
gid = all_groups[idx]["id"]
if gid in selected:
selected.discard(gid)
print(
f" Deselected: {all_groups[idx].get('display_name', gid)}"
)
else:
selected.add(gid)
print(
f" Selected: {all_groups[idx].get('display_name', gid)}"
)
if not selected:
print("No groups selected nothing to sync.")
return []
# Return display_names if they're unique, otherwise IDs
display_names = [g.get("display_name", g["id"]) for g in all_groups if g["id"] in selected]
print(f"\nConfirmed {len(display_names)} group(s) for sync:")
for name in display_names:
print(f" - {name}")
return display_names