""" 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: – 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