Split monolithic script into focused modules
Refactored the single 1200-line es24n_conf.py into six modules plus a slim entry point, in preparation for the upcoming network-based config workflow. Each file has a clear, single responsibility: ui.py — ANSI colours, display primitives, input prompts serial_port.py — SerialPort class (termios/fcntl/select) models.py — IOMConfig and ShelfConfig dataclasses redfish.py — Redfish API client (shared by all workflows) workflow_serial.py — Serial-based IOM network configuration workflow workflow_firmware.py — IOM and Fabric Card firmware update workflow es24n_conf.py — Entry point and main menu only No functional changes. All imports verified. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
120
ui.py
Normal file
120
ui.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
ui.py — ANSI colour helpers, display primitives, and input prompts.
|
||||
Shared by all ES24N workflows.
|
||||
"""
|
||||
|
||||
import ipaddress
|
||||
import sys
|
||||
|
||||
|
||||
# ── ANSI colour helpers ───────────────────────────────────────────────────────
|
||||
class C:
|
||||
RED = "\033[0;31m"
|
||||
GRN = "\033[0;32m"
|
||||
YEL = "\033[1;33m"
|
||||
CYN = "\033[0;36m"
|
||||
WHT = "\033[1;37m"
|
||||
DIM = "\033[2m"
|
||||
BOLD = "\033[1m"
|
||||
RESET = "\033[0m"
|
||||
CLEAR = "\033[2J\033[H"
|
||||
|
||||
|
||||
def _c(colour: str, text: str) -> str:
|
||||
return f"{colour}{text}{C.RESET}"
|
||||
|
||||
|
||||
def info(msg: str): print(f" {_c(C.CYN, 'i')} {msg}")
|
||||
def ok(msg: str): print(f" {_c(C.GRN, 'OK')} {msg}")
|
||||
def warn(msg: str): print(f" {_c(C.YEL, '!')} {msg}")
|
||||
def error(msg: str): print(f" {_c(C.RED, 'X')} {msg}")
|
||||
|
||||
|
||||
def banner():
|
||||
print(C.CLEAR, end="")
|
||||
w = 60
|
||||
print(_c(C.CYN, " +" + "-" * w + "+"))
|
||||
print(_c(C.CYN, " |") + _c(C.WHT + C.BOLD,
|
||||
" TrueNAS ES24N IOM Configuration Tool ") + _c(C.CYN, " |"))
|
||||
print(_c(C.CYN, " |") + _c(C.DIM,
|
||||
" Serial Config & Firmware Updates (stdlib only) ") + _c(C.CYN, " |"))
|
||||
print(_c(C.CYN, " +" + "-" * w + "+"))
|
||||
print()
|
||||
|
||||
|
||||
def rule(title: str = ""):
|
||||
width = 60
|
||||
if title:
|
||||
pad = max(0, width - len(title) - 2)
|
||||
left = pad // 2
|
||||
right = pad - left
|
||||
line = f"{'-' * left} {title} {'-' * right}"
|
||||
else:
|
||||
line = "-" * width
|
||||
print(f"\n {_c(C.YEL, line)}\n")
|
||||
|
||||
|
||||
def draw_table(headers: list, rows: list, col_widths: list):
|
||||
sep = " +-" + "-+-".join("-" * w for w in col_widths) + "-+"
|
||||
|
||||
def fmt_row(cells):
|
||||
return " | " + " | ".join(
|
||||
str(c).ljust(w) for c, w in zip(cells, col_widths)
|
||||
) + " |"
|
||||
|
||||
print(_c(C.DIM, sep))
|
||||
print(_c(C.BOLD, fmt_row(headers)))
|
||||
print(_c(C.DIM, sep))
|
||||
for row in rows:
|
||||
print(fmt_row(row))
|
||||
print(_c(C.DIM, sep))
|
||||
|
||||
|
||||
def draw_box(lines: list, colour: str = C.CYN):
|
||||
width = max(len(l) for l in lines) + 4
|
||||
print(f" {_c(colour, '+' + '-' * width + '+')}")
|
||||
for line in lines:
|
||||
pad = width - len(line) - 2
|
||||
print(f" {_c(colour, '|')} {line}{' ' * pad} {_c(colour, '|')}")
|
||||
print(f" {_c(colour, '+' + '-' * width + '+')}")
|
||||
|
||||
|
||||
# ── Input helpers ─────────────────────────────────────────────────────────────
|
||||
def prompt(label: str, default: str = "") -> str:
|
||||
display = f" {_c(C.CYN, label)}"
|
||||
if default:
|
||||
display += f" {_c(C.DIM, f'[{default}]')}"
|
||||
display += ": "
|
||||
|
||||
sys.stdout.write(display)
|
||||
sys.stdout.flush()
|
||||
val = sys.stdin.readline().strip()
|
||||
return val if val else default
|
||||
|
||||
|
||||
def prompt_ip(label: str) -> str:
|
||||
while True:
|
||||
val = prompt(label)
|
||||
try:
|
||||
ipaddress.IPv4Address(val)
|
||||
return val
|
||||
except ValueError:
|
||||
warn(f"'{val}' is not a valid IPv4 address — please try again.")
|
||||
|
||||
|
||||
def prompt_yn(label: str, default: bool = True) -> bool:
|
||||
hint = "Y/n" if default else "y/N"
|
||||
val = prompt(f"{label} [{hint}]").strip().lower()
|
||||
if not val:
|
||||
return default
|
||||
return val in ("y", "yes")
|
||||
|
||||
|
||||
def prompt_password() -> str:
|
||||
while True:
|
||||
val = prompt(
|
||||
"Admin password (BMC/chassis serial, e.g. MXE3000043CHA007)",
|
||||
)
|
||||
if val:
|
||||
return val
|
||||
warn("Password cannot be empty.")
|
||||
Reference in New Issue
Block a user