ntermqt 0.1.6__py3-none-any.whl → 0.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nterm/parser/api_help_dialog.py +426 -223
- nterm/parser/tfsm_fire_tester.py +561 -731
- nterm/scripting/api.py +249 -641
- nterm/scripting/models.py +195 -0
- nterm/scripting/platform_data.py +272 -0
- nterm/scripting/platform_utils.py +330 -0
- nterm/scripting/repl.py +344 -103
- nterm/scripting/repl_interactive.py +331 -213
- nterm/scripting/ssh_connection.py +632 -0
- nterm/scripting/test_api_repl.py +290 -0
- {ntermqt-0.1.6.dist-info → ntermqt-0.1.8.dist-info}/METADATA +88 -28
- {ntermqt-0.1.6.dist-info → ntermqt-0.1.8.dist-info}/RECORD +15 -10
- {ntermqt-0.1.6.dist-info → ntermqt-0.1.8.dist-info}/WHEEL +0 -0
- {ntermqt-0.1.6.dist-info → ntermqt-0.1.8.dist-info}/entry_points.txt +0 -0
- {ntermqt-0.1.6.dist-info → ntermqt-0.1.8.dist-info}/top_level.txt +0 -0
|
@@ -7,11 +7,13 @@ Provides a safe, policy-controlled interface to network devices.
|
|
|
7
7
|
Same interface used by both humans (IPython) and MCP tools.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
from nterm.scripting.repl import NTermREPL, REPLPolicy
|
|
11
|
-
from .api import NTermAPI
|
|
12
|
-
from typing import Optional
|
|
13
10
|
import sys
|
|
14
11
|
import getpass
|
|
12
|
+
import json
|
|
13
|
+
from typing import Optional, Dict
|
|
14
|
+
|
|
15
|
+
from .repl import NTermREPL, REPLPolicy
|
|
16
|
+
from .api import NTermAPI
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
def start_repl(api: Optional[NTermAPI] = None, policy: Optional[REPLPolicy] = None):
|
|
@@ -39,8 +41,8 @@ def start_repl(api: Optional[NTermAPI] = None, policy: Optional[REPLPolicy] = No
|
|
|
39
41
|
api.repl(policy=policy)
|
|
40
42
|
"""
|
|
41
43
|
if api is None:
|
|
42
|
-
from
|
|
43
|
-
api =
|
|
44
|
+
from .api import NTermAPI
|
|
45
|
+
api = NTermAPI()
|
|
44
46
|
|
|
45
47
|
repl = NTermREPL(api=api, policy=policy)
|
|
46
48
|
|
|
@@ -67,7 +69,6 @@ def start_repl(api: Optional[NTermAPI] = None, policy: Optional[REPLPolicy] = No
|
|
|
67
69
|
print(f"\n⚠️ TextFSM database seems small ({db_info.get('db_size_mb', 0):.1f} MB)")
|
|
68
70
|
print(f" Expected ~0.3 MB. Use :dbinfo to check.")
|
|
69
71
|
except Exception:
|
|
70
|
-
# Don't crash startup if db_info fails
|
|
71
72
|
pass
|
|
72
73
|
|
|
73
74
|
print()
|
|
@@ -77,21 +78,13 @@ def start_repl(api: Optional[NTermAPI] = None, policy: Optional[REPLPolicy] = No
|
|
|
77
78
|
# Interactive loop
|
|
78
79
|
try:
|
|
79
80
|
while True:
|
|
80
|
-
|
|
81
|
-
if repl.state.connected_device:
|
|
82
|
-
mode_indicator = "📊" if repl.state.output_mode == "parsed" else "📄"
|
|
83
|
-
hint = f"[{repl.state.platform_hint}]" if repl.state.platform_hint else ""
|
|
84
|
-
prompt = f"{mode_indicator} {repl.state.connected_device}{hint}> "
|
|
85
|
-
else:
|
|
86
|
-
prompt = "nterm> "
|
|
81
|
+
prompt = _build_prompt(repl)
|
|
87
82
|
|
|
88
83
|
try:
|
|
89
84
|
line = input(prompt)
|
|
90
85
|
except EOFError:
|
|
91
|
-
# Ctrl+D
|
|
92
86
|
break
|
|
93
87
|
except KeyboardInterrupt:
|
|
94
|
-
# Ctrl+C
|
|
95
88
|
print()
|
|
96
89
|
continue
|
|
97
90
|
|
|
@@ -99,185 +92,329 @@ def start_repl(api: Optional[NTermAPI] = None, policy: Optional[REPLPolicy] = No
|
|
|
99
92
|
result = repl.handle_line(line)
|
|
100
93
|
|
|
101
94
|
# Display result
|
|
102
|
-
|
|
103
|
-
print(f"Error: {result.get('error')}")
|
|
104
|
-
continue
|
|
105
|
-
|
|
106
|
-
data = result.get("data", {})
|
|
107
|
-
cmd_type = data.get("type")
|
|
95
|
+
_display_result(repl, result)
|
|
108
96
|
|
|
109
|
-
|
|
97
|
+
# Check for exit
|
|
98
|
+
if result.get("ok") and result.get("data", {}).get("type") == "exit":
|
|
110
99
|
break
|
|
111
|
-
elif cmd_type == "unlock_prompt":
|
|
112
|
-
# Securely prompt for password
|
|
113
|
-
try:
|
|
114
|
-
password = getpass.getpass("Enter vault password: ")
|
|
115
|
-
unlock_result = repl.do_unlock(password)
|
|
116
|
-
if unlock_result.get("ok"):
|
|
117
|
-
unlock_data = unlock_result.get("data", {})
|
|
118
|
-
if unlock_data.get("vault_unlocked"):
|
|
119
|
-
print("Vault unlocked")
|
|
120
|
-
else:
|
|
121
|
-
print("Unlock failed - incorrect password")
|
|
122
|
-
else:
|
|
123
|
-
print(f"Error: {unlock_result.get('error')}")
|
|
124
|
-
except KeyboardInterrupt:
|
|
125
|
-
print("\nUnlock cancelled")
|
|
126
|
-
continue
|
|
127
|
-
elif cmd_type == "help":
|
|
128
|
-
print(data.get("text", ""))
|
|
129
|
-
elif cmd_type == "unlock":
|
|
130
|
-
status = "unlocked" if data.get("vault_unlocked") else "failed"
|
|
131
|
-
print(f"Vault: {status}")
|
|
132
|
-
elif cmd_type == "lock":
|
|
133
|
-
print("Vault locked")
|
|
134
|
-
elif cmd_type == "credentials":
|
|
135
|
-
creds = data.get("credentials", [])
|
|
136
|
-
if not creds:
|
|
137
|
-
print("No credentials found")
|
|
138
|
-
else:
|
|
139
|
-
print(f"\n{'Name':<20} {'Username':<20} {'Type':<15}")
|
|
140
|
-
print("-" * 55)
|
|
141
|
-
for cred in creds:
|
|
142
|
-
print(f"{cred['name']:<20} {cred.get('username', ''):<20} {cred.get('cred_type', 'ssh'):<15}")
|
|
143
|
-
print()
|
|
144
|
-
elif cmd_type == "devices":
|
|
145
|
-
devices = data.get("devices", [])
|
|
146
|
-
if not devices:
|
|
147
|
-
print("No devices found")
|
|
148
|
-
else:
|
|
149
|
-
print(f"\n{'Name':<20} {'Hostname':<20} {'Folder':<15}")
|
|
150
|
-
print("-" * 55)
|
|
151
|
-
for dev in devices:
|
|
152
|
-
print(f"{dev['name']:<20} {dev['hostname']:<20} {dev.get('folder', ''):<15}")
|
|
153
|
-
print()
|
|
154
|
-
elif cmd_type == "connect":
|
|
155
|
-
print(f"Connected to {data['device']} ({data['hostname']}:{data['port']})")
|
|
156
|
-
print(f"Platform: {data.get('platform', 'unknown')}")
|
|
157
|
-
print(f"Prompt: {data.get('prompt', '')}")
|
|
158
|
-
elif cmd_type == "disconnect":
|
|
159
|
-
print("Disconnected")
|
|
160
|
-
elif cmd_type == "policy":
|
|
161
|
-
print(f"Policy mode: {data.get('mode')}")
|
|
162
|
-
elif cmd_type == "mode":
|
|
163
|
-
mode = data.get('mode')
|
|
164
|
-
hint = data.get('platform_hint')
|
|
165
|
-
if mode:
|
|
166
|
-
print(f"Output mode: {mode}")
|
|
167
|
-
else:
|
|
168
|
-
print(f"Current mode: {mode}")
|
|
169
|
-
if hint:
|
|
170
|
-
print(f"Platform hint: {hint}")
|
|
171
|
-
elif cmd_type == "format":
|
|
172
|
-
fmt = data.get('format')
|
|
173
|
-
print(f"Output format: {fmt}")
|
|
174
|
-
elif cmd_type == "set_hint":
|
|
175
|
-
print(f"Platform hint set to: {data.get('platform_hint')}")
|
|
176
|
-
elif cmd_type == "clear_hint":
|
|
177
|
-
print("Platform hint cleared (using auto-detection)")
|
|
178
|
-
elif cmd_type == "debug":
|
|
179
|
-
status = "ON" if data.get("debug_mode") else "OFF"
|
|
180
|
-
print(f"Debug mode: {status}")
|
|
181
|
-
elif cmd_type == "dbinfo":
|
|
182
|
-
db_info = data.get("db_info", {})
|
|
183
|
-
print("\nTextFSM Database Info:")
|
|
184
|
-
print("=" * 60)
|
|
185
|
-
print(f"Engine Available: {db_info.get('engine_available', False)}")
|
|
186
|
-
print(f"Database Path: {db_info.get('db_path', 'unknown')}")
|
|
187
|
-
print(f"Database Exists: {db_info.get('db_exists', False)}")
|
|
188
|
-
|
|
189
|
-
if db_info.get('db_exists'):
|
|
190
|
-
db_size = db_info.get('db_size', 0)
|
|
191
|
-
db_size_mb = db_info.get('db_size_mb', 0.0)
|
|
192
|
-
print(f"Database Size: {db_size:,} bytes ({db_size_mb:.1f} MB)")
|
|
193
|
-
print(f"Absolute Path: {db_info.get('db_absolute_path', 'unknown')}")
|
|
194
|
-
|
|
195
|
-
# Health checks
|
|
196
|
-
if db_size == 0:
|
|
197
|
-
print("\n⚠️ WARNING: Database file is empty (0 bytes)!")
|
|
198
|
-
print(" Parsing will fail until you download templates.")
|
|
199
|
-
print(" Run: api.download_templates() or use the templates installer.")
|
|
200
|
-
elif db_size < 100000: # Less than 100KB
|
|
201
|
-
print(f"\n⚠️ WARNING: Database seems too small ({db_size_mb:.1f} MB)")
|
|
202
|
-
print(" Expected size is ~0.3 MB. May be corrupted or incomplete.")
|
|
203
|
-
else:
|
|
204
|
-
print("\n✓ Database appears healthy")
|
|
205
|
-
else:
|
|
206
|
-
print("\n❌ ERROR: Database file not found!")
|
|
207
|
-
print(" Run: api.download_templates() to create it.")
|
|
208
100
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
print(raw)
|
|
245
|
-
else:
|
|
246
|
-
# Fallback
|
|
247
|
-
raw = result_data.get("raw_output", "")
|
|
248
|
-
print(raw)
|
|
101
|
+
finally:
|
|
102
|
+
# Clean up
|
|
103
|
+
count = repl.state.api.disconnect_all()
|
|
104
|
+
if count > 0:
|
|
105
|
+
print(f"\nDisconnected {count} session(s)")
|
|
106
|
+
print("REPL closed")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _build_prompt(repl: NTermREPL) -> str:
|
|
110
|
+
"""Build the prompt string based on current state."""
|
|
111
|
+
if repl.state.connected_device:
|
|
112
|
+
mode_indicator = "📊" if repl.state.output_mode == "parsed" else "📄"
|
|
113
|
+
hint = f"[{repl.state.platform_hint}]" if repl.state.platform_hint else ""
|
|
114
|
+
return f"{mode_indicator} {repl.state.connected_device}{hint}> "
|
|
115
|
+
else:
|
|
116
|
+
return "nterm> "
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _display_result(repl: NTermREPL, result: Dict) -> None:
|
|
120
|
+
"""Display REPL result based on type."""
|
|
121
|
+
if not result.get("ok"):
|
|
122
|
+
print(f"Error: {result.get('error')}")
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
data = result.get("data", {})
|
|
126
|
+
cmd_type = data.get("type")
|
|
127
|
+
|
|
128
|
+
# ===== Vault Commands =====
|
|
129
|
+
if cmd_type == "unlock_prompt":
|
|
130
|
+
try:
|
|
131
|
+
password = getpass.getpass("Enter vault password: ")
|
|
132
|
+
unlock_result = repl.do_unlock(password)
|
|
133
|
+
if unlock_result.get("ok"):
|
|
134
|
+
if unlock_result.get("data", {}).get("vault_unlocked"):
|
|
135
|
+
print("✓ Vault unlocked")
|
|
249
136
|
else:
|
|
250
|
-
|
|
251
|
-
raw = result_data.get("raw_output", "")
|
|
252
|
-
print(raw)
|
|
253
|
-
|
|
254
|
-
# Show timing
|
|
255
|
-
elapsed = result_data.get("elapsed_seconds", 0)
|
|
256
|
-
print(f"\n[{elapsed}s]")
|
|
257
|
-
elif cmd_type == "noop":
|
|
258
|
-
pass
|
|
137
|
+
print("✗ Unlock failed - incorrect password")
|
|
259
138
|
else:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
139
|
+
print(f"Error: {unlock_result.get('error')}")
|
|
140
|
+
except KeyboardInterrupt:
|
|
141
|
+
print("\nUnlock cancelled")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
if cmd_type == "unlock":
|
|
145
|
+
status = "unlocked" if data.get("vault_unlocked") else "failed"
|
|
146
|
+
print(f"Vault: {status}")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
if cmd_type == "lock":
|
|
150
|
+
print("✓ Vault locked")
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
# ===== Inventory Commands =====
|
|
154
|
+
if cmd_type == "credentials":
|
|
155
|
+
creds = data.get("credentials", [])
|
|
156
|
+
if not creds:
|
|
157
|
+
print("No credentials found")
|
|
158
|
+
else:
|
|
159
|
+
print(f"\n{'Name':<20} {'Username':<20} {'Auth':<15} {'Default':<8}")
|
|
160
|
+
print("-" * 63)
|
|
161
|
+
for cred in creds:
|
|
162
|
+
auth_types = []
|
|
163
|
+
if cred.get('has_password'):
|
|
164
|
+
auth_types.append('pass')
|
|
165
|
+
if cred.get('has_key'):
|
|
166
|
+
auth_types.append('key')
|
|
167
|
+
auth = '+'.join(auth_types) if auth_types else 'none'
|
|
168
|
+
default = '★' if cred.get('is_default') else ''
|
|
169
|
+
print(f"{cred['name']:<20} {cred.get('username', ''):<20} {auth:<15} {default:<8}")
|
|
170
|
+
print()
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
if cmd_type == "devices":
|
|
174
|
+
devices = data.get("devices", [])
|
|
175
|
+
if not devices:
|
|
176
|
+
print("No devices found")
|
|
177
|
+
else:
|
|
178
|
+
print(f"\n{'Name':<25} {'Hostname':<20} {'Port':<6} {'Folder':<15}")
|
|
179
|
+
print("-" * 66)
|
|
180
|
+
for dev in devices:
|
|
181
|
+
print(f"{dev['name']:<25} {dev['hostname']:<20} {dev.get('port', 22):<6} {dev.get('folder', ''):<15}")
|
|
182
|
+
print(f"\n{len(devices)} device(s)")
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
if cmd_type == "folders":
|
|
186
|
+
folders = data.get("folders", [])
|
|
187
|
+
if not folders:
|
|
188
|
+
print("No folders found")
|
|
189
|
+
else:
|
|
190
|
+
print("\nFolders:")
|
|
191
|
+
for f in sorted(folders):
|
|
192
|
+
print(f" 📁 {f}")
|
|
193
|
+
print()
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
# ===== Session Commands =====
|
|
197
|
+
if cmd_type == "connect":
|
|
198
|
+
print(f"✓ Connected to {data['device']} ({data['hostname']}:{data['port']})")
|
|
199
|
+
print(f" Platform: {data.get('platform', 'unknown')}")
|
|
200
|
+
print(f" Prompt: {data.get('prompt', '')}")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
if cmd_type == "disconnect":
|
|
204
|
+
msg = data.get("message", "Disconnected")
|
|
205
|
+
print(f"✓ {msg}")
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
if cmd_type == "sessions":
|
|
209
|
+
sessions = data.get("sessions", [])
|
|
210
|
+
current = data.get("current")
|
|
211
|
+
if not sessions:
|
|
212
|
+
print("No active sessions")
|
|
213
|
+
else:
|
|
214
|
+
print(f"\n{'Device':<20} {'Hostname':<20} {'Platform':<15} {'Status':<10}")
|
|
215
|
+
print("-" * 65)
|
|
216
|
+
for s in sessions:
|
|
217
|
+
marker = "→ " if s['device'] == current else " "
|
|
218
|
+
status = "connected" if s.get('connected') else "stale"
|
|
219
|
+
print(f"{marker}{s['device']:<18} {s['hostname']:<20} {s.get('platform', 'unknown'):<15} {status:<10}")
|
|
220
|
+
print()
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
# ===== Settings Commands =====
|
|
224
|
+
if cmd_type == "policy":
|
|
225
|
+
mode = data.get('mode')
|
|
226
|
+
emoji = "🔒" if mode == "read_only" else "⚡"
|
|
227
|
+
print(f"Policy mode: {emoji} {mode}")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
if cmd_type == "mode":
|
|
231
|
+
mode = data.get('mode')
|
|
232
|
+
hint = data.get('platform_hint')
|
|
233
|
+
print(f"Output mode: {mode}")
|
|
234
|
+
if hint:
|
|
235
|
+
print(f"Platform hint: {hint}")
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
if cmd_type == "format":
|
|
239
|
+
print(f"Output format: {data.get('format')}")
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
if cmd_type == "set_hint":
|
|
243
|
+
print(f"✓ Platform hint set to: {data.get('platform_hint')}")
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
if cmd_type == "clear_hint":
|
|
247
|
+
print("✓ Platform hint cleared (using auto-detection)")
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
if cmd_type == "debug":
|
|
251
|
+
status = "ON" if data.get("debug_mode") else "OFF"
|
|
252
|
+
print(f"Debug mode: {status}")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
# ===== Info Commands =====
|
|
256
|
+
if cmd_type == "dbinfo":
|
|
257
|
+
_display_dbinfo(data.get("db_info", {}))
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
if cmd_type == "help":
|
|
261
|
+
print(data.get("text", ""))
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
if cmd_type == "exit":
|
|
265
|
+
count = data.get("disconnected", 0)
|
|
266
|
+
if count > 0:
|
|
267
|
+
print(f"Disconnected {count} session(s)")
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
if cmd_type == "noop":
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
# ===== Quick Commands =====
|
|
274
|
+
if cmd_type == "version":
|
|
275
|
+
result_data = data.get("result", {})
|
|
276
|
+
version_info = result_data.get("version_info", {})
|
|
277
|
+
|
|
278
|
+
print(f"\n{'─' * 50}")
|
|
279
|
+
print(f" Version: {version_info.get('version', 'unknown')}")
|
|
280
|
+
print(f" Hardware: {version_info.get('hardware', 'unknown')}")
|
|
281
|
+
print(f" Serial: {version_info.get('serial', 'unknown')}")
|
|
282
|
+
print(f" Uptime: {version_info.get('uptime', 'unknown')}")
|
|
283
|
+
print(f"{'─' * 50}")
|
|
284
|
+
|
|
285
|
+
elapsed = result_data.get("elapsed_seconds", 0)
|
|
286
|
+
print(f"[{elapsed}s]")
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
if cmd_type == "neighbors":
|
|
290
|
+
result_data = data.get("result", {})
|
|
291
|
+
neighbor_info = result_data.get("neighbor_info", [])
|
|
292
|
+
|
|
293
|
+
if not neighbor_info:
|
|
294
|
+
parsed = result_data.get("parsed_data", [])
|
|
295
|
+
if parsed:
|
|
296
|
+
# Fall back to raw parsed data
|
|
297
|
+
_display_parsed_result(parsed, repl.state.output_format)
|
|
298
|
+
else:
|
|
299
|
+
print("No neighbors found")
|
|
300
|
+
else:
|
|
301
|
+
print(f"\n{'Local Interface':<20} {'Neighbor':<30} {'Remote Port':<20} {'Platform':<20}")
|
|
302
|
+
print("-" * 90)
|
|
303
|
+
for n in neighbor_info:
|
|
304
|
+
print(f"{n.get('local_interface', 'unknown'):<20} "
|
|
305
|
+
f"{n.get('neighbor_device', 'unknown'):<30} "
|
|
306
|
+
f"{n.get('neighbor_interface', 'unknown'):<20} "
|
|
307
|
+
f"{n.get('platform', '')[:20]:<20}")
|
|
308
|
+
print(f"\n{len(neighbor_info)} neighbor(s)")
|
|
309
|
+
|
|
310
|
+
elapsed = result_data.get("elapsed_seconds", 0)
|
|
311
|
+
print(f"[{elapsed}s]")
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
# ===== Generic Result =====
|
|
315
|
+
if cmd_type == "result":
|
|
316
|
+
result_data = data.get("result", {})
|
|
317
|
+
_display_command_result(repl, result_data)
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
# Unknown type - dump as JSON
|
|
321
|
+
print(json.dumps(data, indent=2))
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _display_command_result(repl: NTermREPL, result_data: Dict) -> None:
|
|
325
|
+
"""Display a generic command result."""
|
|
326
|
+
# Debug mode: show full result dict
|
|
327
|
+
if repl.state.debug_mode:
|
|
328
|
+
print("\n[DEBUG - Full Result Dict]")
|
|
329
|
+
print("-" * 60)
|
|
330
|
+
debug_data = {k: v for k, v in result_data.items() if k != "raw_output"}
|
|
331
|
+
print(json.dumps(debug_data, indent=2, default=str))
|
|
332
|
+
print("-" * 60)
|
|
333
|
+
|
|
334
|
+
parsed = result_data.get("parsed_data")
|
|
335
|
+
parse_success = result_data.get("parse_success", False)
|
|
336
|
+
platform = result_data.get("platform", "")
|
|
337
|
+
command_type = result_data.get("command_type", "")
|
|
338
|
+
|
|
339
|
+
# Display based on mode
|
|
340
|
+
if repl.state.output_mode == "parsed":
|
|
341
|
+
if parsed and parse_success:
|
|
342
|
+
header = f"[Parsed: {platform}"
|
|
343
|
+
if command_type:
|
|
344
|
+
header += f" | {command_type}"
|
|
345
|
+
header += f" | format: {repl.state.output_format}]"
|
|
346
|
+
print(f"\n{header}")
|
|
347
|
+
print("-" * 60)
|
|
348
|
+
_display_parsed_result(parsed, repl.state.output_format)
|
|
349
|
+
print()
|
|
350
|
+
elif parse_success and not parsed:
|
|
351
|
+
print(f"\n[Parsed: {platform} - no structured data]")
|
|
352
|
+
raw = result_data.get("raw_output", "")
|
|
353
|
+
print(raw)
|
|
354
|
+
elif not parse_success:
|
|
355
|
+
print(f"\n[Parse failed - showing raw output]")
|
|
356
|
+
raw = result_data.get("raw_output", "")
|
|
357
|
+
print(raw)
|
|
358
|
+
else:
|
|
359
|
+
raw = result_data.get("raw_output", "")
|
|
360
|
+
print(raw)
|
|
361
|
+
else:
|
|
362
|
+
# Raw mode
|
|
363
|
+
raw = result_data.get("raw_output", "")
|
|
364
|
+
print(raw)
|
|
365
|
+
|
|
366
|
+
# Show timing
|
|
367
|
+
elapsed = result_data.get("elapsed_seconds", 0)
|
|
368
|
+
print(f"\n[{elapsed}s]")
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _display_dbinfo(db_info: Dict) -> None:
|
|
372
|
+
"""Display TextFSM database info."""
|
|
373
|
+
print("\nTextFSM Database Info:")
|
|
374
|
+
print("=" * 60)
|
|
375
|
+
print(f"Engine Available: {db_info.get('engine_available', False)}")
|
|
376
|
+
print(f"Database Path: {db_info.get('db_path', 'unknown')}")
|
|
377
|
+
print(f"Database Exists: {db_info.get('db_exists', False)}")
|
|
378
|
+
|
|
379
|
+
if db_info.get('db_exists'):
|
|
380
|
+
db_size = db_info.get('db_size', 0)
|
|
381
|
+
db_size_mb = db_info.get('db_size_mb', 0.0)
|
|
382
|
+
print(f"Database Size: {db_size:,} bytes ({db_size_mb:.1f} MB)")
|
|
383
|
+
print(f"Absolute Path: {db_info.get('db_absolute_path', 'unknown')}")
|
|
384
|
+
|
|
385
|
+
if db_size == 0:
|
|
386
|
+
print("\n⚠️ WARNING: Database file is empty (0 bytes)!")
|
|
387
|
+
print(" Parsing will fail until you download templates.")
|
|
388
|
+
elif db_size < 100000:
|
|
389
|
+
print(f"\n⚠️ WARNING: Database seems too small ({db_size_mb:.1f} MB)")
|
|
390
|
+
print(" Expected size is ~0.3 MB. May be corrupted or incomplete.")
|
|
391
|
+
else:
|
|
392
|
+
print("\n✓ Database appears healthy")
|
|
393
|
+
else:
|
|
394
|
+
print("\n✗ ERROR: Database file not found!")
|
|
395
|
+
print(" Run: api.download_templates() to create it.")
|
|
396
|
+
|
|
397
|
+
print()
|
|
263
398
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
399
|
+
|
|
400
|
+
def _display_parsed_result(data, output_format: str, max_rows: int = 20) -> None:
|
|
401
|
+
"""Display parsed data in the specified format."""
|
|
402
|
+
if output_format == "json":
|
|
403
|
+
print(json.dumps(data, indent=2, default=str))
|
|
404
|
+
elif output_format == "rich":
|
|
405
|
+
_print_parsed_data_rich(data, max_rows)
|
|
406
|
+
else:
|
|
407
|
+
_print_parsed_data_text(data, max_rows)
|
|
270
408
|
|
|
271
409
|
|
|
272
|
-
def
|
|
273
|
-
"""Pretty print parsed data
|
|
410
|
+
def _print_parsed_data_text(data, max_rows: int = 20) -> None:
|
|
411
|
+
"""Pretty print parsed data as text table."""
|
|
274
412
|
if not data:
|
|
275
413
|
print("(empty)")
|
|
276
414
|
return
|
|
277
415
|
|
|
278
416
|
if not isinstance(data, list):
|
|
279
|
-
|
|
280
|
-
print(json.dumps(data, indent=2))
|
|
417
|
+
print(json.dumps(data, indent=2, default=str))
|
|
281
418
|
return
|
|
282
419
|
|
|
283
420
|
# Get all unique keys
|
|
@@ -289,14 +426,11 @@ def _print_parsed_data(data, max_rows=20):
|
|
|
289
426
|
keys = sorted(all_keys)
|
|
290
427
|
|
|
291
428
|
if not keys:
|
|
292
|
-
|
|
293
|
-
print(json.dumps(data, indent=2))
|
|
429
|
+
print(json.dumps(data, indent=2, default=str))
|
|
294
430
|
return
|
|
295
431
|
|
|
296
432
|
# Calculate column widths
|
|
297
|
-
col_widths = {}
|
|
298
|
-
for key in keys:
|
|
299
|
-
col_widths[key] = len(key)
|
|
433
|
+
col_widths = {key: len(key) for key in keys}
|
|
300
434
|
|
|
301
435
|
for row in data[:max_rows]:
|
|
302
436
|
if isinstance(row, dict):
|
|
@@ -304,7 +438,7 @@ def _print_parsed_data(data, max_rows=20):
|
|
|
304
438
|
val = str(row.get(key, ""))
|
|
305
439
|
col_widths[key] = max(col_widths[key], len(val))
|
|
306
440
|
|
|
307
|
-
# Cap widths
|
|
441
|
+
# Cap widths at 30
|
|
308
442
|
for key in keys:
|
|
309
443
|
col_widths[key] = min(col_widths[key], 30)
|
|
310
444
|
|
|
@@ -334,14 +468,14 @@ def _print_parsed_data(data, max_rows=20):
|
|
|
334
468
|
shown += 1
|
|
335
469
|
|
|
336
470
|
|
|
337
|
-
def _print_parsed_data_rich(data, max_rows=20):
|
|
471
|
+
def _print_parsed_data_rich(data, max_rows: int = 20) -> None:
|
|
338
472
|
"""Pretty print parsed data using Rich library."""
|
|
339
473
|
try:
|
|
340
474
|
from rich.console import Console
|
|
341
475
|
from rich.table import Table
|
|
342
476
|
except ImportError:
|
|
343
477
|
print("⚠️ Rich library not available, falling back to text format")
|
|
344
|
-
|
|
478
|
+
_print_parsed_data_text(data, max_rows)
|
|
345
479
|
return
|
|
346
480
|
|
|
347
481
|
if not data:
|
|
@@ -349,8 +483,7 @@ def _print_parsed_data_rich(data, max_rows=20):
|
|
|
349
483
|
return
|
|
350
484
|
|
|
351
485
|
if not isinstance(data, list):
|
|
352
|
-
|
|
353
|
-
print(json.dumps(data, indent=2))
|
|
486
|
+
print(json.dumps(data, indent=2, default=str))
|
|
354
487
|
return
|
|
355
488
|
|
|
356
489
|
# Get all unique keys
|
|
@@ -362,19 +495,16 @@ def _print_parsed_data_rich(data, max_rows=20):
|
|
|
362
495
|
keys = sorted(all_keys)
|
|
363
496
|
|
|
364
497
|
if not keys:
|
|
365
|
-
|
|
366
|
-
print(json.dumps(data, indent=2))
|
|
498
|
+
print(json.dumps(data, indent=2, default=str))
|
|
367
499
|
return
|
|
368
500
|
|
|
369
501
|
# Create rich table
|
|
370
502
|
console = Console()
|
|
371
503
|
table = Table(show_header=True, header_style="bold cyan")
|
|
372
504
|
|
|
373
|
-
# Add columns
|
|
374
505
|
for key in keys:
|
|
375
506
|
table.add_column(key, style="white", no_wrap=False, max_width=30)
|
|
376
507
|
|
|
377
|
-
# Add rows
|
|
378
508
|
shown = 0
|
|
379
509
|
for row in data:
|
|
380
510
|
if not isinstance(row, dict):
|
|
@@ -391,28 +521,16 @@ def _print_parsed_data_rich(data, max_rows=20):
|
|
|
391
521
|
console.print(table)
|
|
392
522
|
|
|
393
523
|
|
|
394
|
-
def _print_parsed_data_json(data):
|
|
395
|
-
"""Print parsed data as JSON."""
|
|
396
|
-
import json
|
|
397
|
-
print(json.dumps(data, indent=2))
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
def _display_parsed_result(data, output_format, max_rows=20):
|
|
401
|
-
"""Display parsed data in the specified format."""
|
|
402
|
-
if output_format == "json":
|
|
403
|
-
_print_parsed_data_json(data)
|
|
404
|
-
elif output_format == "rich":
|
|
405
|
-
_print_parsed_data_rich(data, max_rows)
|
|
406
|
-
else: # text or fallback
|
|
407
|
-
_print_parsed_data(data, max_rows)
|
|
408
|
-
|
|
409
|
-
|
|
410
524
|
# Convenience function to add to API
|
|
411
|
-
def add_repl_to_api(api_instance):
|
|
525
|
+
def add_repl_to_api(api_instance: NTermAPI) -> None:
|
|
412
526
|
"""Add repl() method to API instance."""
|
|
413
527
|
|
|
414
528
|
def repl(policy: Optional[REPLPolicy] = None):
|
|
415
529
|
"""Start interactive REPL."""
|
|
416
530
|
start_repl(api=api_instance, policy=policy)
|
|
417
531
|
|
|
418
|
-
api_instance.repl = repl
|
|
532
|
+
api_instance.repl = repl
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# Type hint for display function
|
|
536
|
+
from typing import Dict
|