- 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.
173 lines
5.3 KiB
Python
173 lines
5.3 KiB
Python
"""
|
||
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
|