ntermqt 0.1.7__py3-none-any.whl → 0.1.9__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/scripting/api.py +421 -701
- nterm/scripting/models.py +195 -0
- nterm/scripting/platform_data.py +272 -0
- nterm/scripting/platform_utils.py +596 -0
- nterm/scripting/repl.py +527 -131
- nterm/scripting/repl_interactive.py +356 -213
- nterm/scripting/ssh_connection.py +632 -0
- nterm/scripting/test_api_repl.py +290 -0
- {ntermqt-0.1.7.dist-info → ntermqt-0.1.9.dist-info}/METADATA +89 -29
- {ntermqt-0.1.7.dist-info → ntermqt-0.1.9.dist-info}/RECORD +14 -9
- {ntermqt-0.1.7.dist-info → ntermqt-0.1.9.dist-info}/WHEEL +0 -0
- {ntermqt-0.1.7.dist-info → ntermqt-0.1.9.dist-info}/entry_points.txt +0 -0
- {ntermqt-0.1.7.dist-info → ntermqt-0.1.9.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,354 @@ 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
|
|
263
143
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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 == "switch":
|
|
204
|
+
msg = data.get('message', '')
|
|
205
|
+
if msg:
|
|
206
|
+
print(f"✓ {msg}")
|
|
207
|
+
else:
|
|
208
|
+
print(f"✓ Switched to {data['device']} ({data['hostname']}:{data['port']})")
|
|
209
|
+
print(f" Platform: {data.get('platform', 'unknown')}")
|
|
210
|
+
print(f" Prompt: {data.get('prompt', '')}")
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
if cmd_type == "disconnect":
|
|
214
|
+
disconnected = data.get("disconnected")
|
|
215
|
+
switched_to = data.get("switched_to")
|
|
216
|
+
msg = data.get("message")
|
|
217
|
+
|
|
218
|
+
if msg:
|
|
219
|
+
print(f"✓ {msg}")
|
|
220
|
+
elif disconnected:
|
|
221
|
+
print(f"✓ Disconnected from {disconnected}")
|
|
222
|
+
if switched_to:
|
|
223
|
+
print(f" Switched to: {switched_to}")
|
|
224
|
+
else:
|
|
225
|
+
print("✓ Disconnected")
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
if cmd_type == "disconnect_all":
|
|
229
|
+
count = data.get("count", 0)
|
|
230
|
+
print(f"✓ Disconnected {count} session(s)")
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
if cmd_type == "sessions":
|
|
234
|
+
sessions = data.get("sessions", [])
|
|
235
|
+
current = data.get("current")
|
|
236
|
+
if not sessions:
|
|
237
|
+
print("No active sessions")
|
|
238
|
+
else:
|
|
239
|
+
print(f"\n{'Device':<20} {'Hostname':<20} {'Platform':<15} {'Status':<10}")
|
|
240
|
+
print("-" * 65)
|
|
241
|
+
for s in sessions:
|
|
242
|
+
marker = "→ " if s['device'] == current else " "
|
|
243
|
+
status = "connected" if s.get('connected') else "stale"
|
|
244
|
+
print(f"{marker}{s['device']:<18} {s['hostname']:<20} {s.get('platform', 'unknown'):<15} {status:<10}")
|
|
245
|
+
print()
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
# ===== Settings Commands =====
|
|
249
|
+
if cmd_type == "policy":
|
|
250
|
+
mode = data.get('mode')
|
|
251
|
+
emoji = "🔒" if mode == "read_only" else "⚡"
|
|
252
|
+
print(f"Policy mode: {emoji} {mode}")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
if cmd_type == "mode":
|
|
256
|
+
mode = data.get('mode')
|
|
257
|
+
hint = data.get('platform_hint')
|
|
258
|
+
print(f"Output mode: {mode}")
|
|
259
|
+
if hint:
|
|
260
|
+
print(f"Platform hint: {hint}")
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
if cmd_type == "format":
|
|
264
|
+
print(f"Output format: {data.get('format')}")
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
if cmd_type == "set_hint":
|
|
268
|
+
print(f"✓ Platform hint set to: {data.get('platform_hint')}")
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
if cmd_type == "clear_hint":
|
|
272
|
+
print("✓ Platform hint cleared (using auto-detection)")
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
if cmd_type == "debug":
|
|
276
|
+
status = "ON" if data.get("debug_mode") else "OFF"
|
|
277
|
+
print(f"Debug mode: {status}")
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
# ===== Info Commands =====
|
|
281
|
+
if cmd_type == "dbinfo":
|
|
282
|
+
_display_dbinfo(data.get("db_info", {}))
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
if cmd_type == "help":
|
|
286
|
+
print(data.get("text", ""))
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
if cmd_type == "exit":
|
|
290
|
+
count = data.get("disconnected", 0)
|
|
291
|
+
if count > 0:
|
|
292
|
+
print(f"Disconnected {count} session(s)")
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
if cmd_type == "noop":
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
# ===== Quick Commands =====
|
|
299
|
+
if cmd_type == "version":
|
|
300
|
+
result_data = data.get("result", {})
|
|
301
|
+
version_info = result_data.get("version_info", {})
|
|
302
|
+
|
|
303
|
+
print(f"\n{'─' * 50}")
|
|
304
|
+
print(f" Version: {version_info.get('version', 'unknown')}")
|
|
305
|
+
print(f" Hardware: {version_info.get('hardware', 'unknown')}")
|
|
306
|
+
print(f" Serial: {version_info.get('serial', 'unknown')}")
|
|
307
|
+
print(f" Uptime: {version_info.get('uptime', 'unknown')}")
|
|
308
|
+
print(f"{'─' * 50}")
|
|
309
|
+
|
|
310
|
+
elapsed = result_data.get("elapsed_seconds", 0)
|
|
311
|
+
print(f"[{elapsed}s]")
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
if cmd_type == "neighbors":
|
|
315
|
+
result_data = data.get("result", {})
|
|
316
|
+
neighbor_info = result_data.get("neighbor_info", [])
|
|
317
|
+
|
|
318
|
+
if not neighbor_info:
|
|
319
|
+
parsed = result_data.get("parsed_data", [])
|
|
320
|
+
if parsed:
|
|
321
|
+
# Fall back to raw parsed data
|
|
322
|
+
_display_parsed_result(parsed, repl.state.output_format)
|
|
323
|
+
else:
|
|
324
|
+
print("No neighbors found")
|
|
325
|
+
else:
|
|
326
|
+
print(f"\n{'Local Interface':<20} {'Neighbor':<30} {'Remote Port':<20} {'Platform':<20}")
|
|
327
|
+
print("-" * 90)
|
|
328
|
+
for n in neighbor_info:
|
|
329
|
+
print(f"{n.get('local_interface', 'unknown'):<20} "
|
|
330
|
+
f"{n.get('neighbor_device', 'unknown'):<30} "
|
|
331
|
+
f"{n.get('neighbor_interface', 'unknown'):<20} "
|
|
332
|
+
f"{n.get('platform', '')[:20]:<20}")
|
|
333
|
+
print(f"\n{len(neighbor_info)} neighbor(s)")
|
|
334
|
+
|
|
335
|
+
elapsed = result_data.get("elapsed_seconds", 0)
|
|
336
|
+
print(f"[{elapsed}s]")
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
# ===== Generic Result =====
|
|
340
|
+
if cmd_type == "result":
|
|
341
|
+
result_data = data.get("result", {})
|
|
342
|
+
_display_command_result(repl, result_data)
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
# Unknown type - dump as JSON
|
|
346
|
+
print(json.dumps(data, indent=2))
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _display_command_result(repl: NTermREPL, result_data: Dict) -> None:
|
|
350
|
+
"""Display a generic command result."""
|
|
351
|
+
# Debug mode: show full result dict
|
|
352
|
+
if repl.state.debug_mode:
|
|
353
|
+
print("\n[DEBUG - Full Result Dict]")
|
|
354
|
+
print("-" * 60)
|
|
355
|
+
debug_data = {k: v for k, v in result_data.items() if k != "raw_output"}
|
|
356
|
+
print(json.dumps(debug_data, indent=2, default=str))
|
|
357
|
+
print("-" * 60)
|
|
358
|
+
|
|
359
|
+
parsed = result_data.get("parsed_data")
|
|
360
|
+
parse_success = result_data.get("parse_success", False)
|
|
361
|
+
platform = result_data.get("platform", "")
|
|
362
|
+
command_type = result_data.get("command_type", "")
|
|
363
|
+
|
|
364
|
+
# Display based on mode
|
|
365
|
+
if repl.state.output_mode == "parsed":
|
|
366
|
+
if parsed and parse_success:
|
|
367
|
+
header = f"[Parsed: {platform}"
|
|
368
|
+
if command_type:
|
|
369
|
+
header += f" | {command_type}"
|
|
370
|
+
header += f" | format: {repl.state.output_format}]"
|
|
371
|
+
print(f"\n{header}")
|
|
372
|
+
print("-" * 60)
|
|
373
|
+
_display_parsed_result(parsed, repl.state.output_format)
|
|
374
|
+
print()
|
|
375
|
+
elif parse_success and not parsed:
|
|
376
|
+
print(f"\n[Parsed: {platform} - no structured data]")
|
|
377
|
+
raw = result_data.get("raw_output", "")
|
|
378
|
+
print(raw)
|
|
379
|
+
elif not parse_success:
|
|
380
|
+
print(f"\n[Parse failed - showing raw output]")
|
|
381
|
+
raw = result_data.get("raw_output", "")
|
|
382
|
+
print(raw)
|
|
383
|
+
else:
|
|
384
|
+
raw = result_data.get("raw_output", "")
|
|
385
|
+
print(raw)
|
|
386
|
+
else:
|
|
387
|
+
# Raw mode
|
|
388
|
+
raw = result_data.get("raw_output", "")
|
|
389
|
+
print(raw)
|
|
390
|
+
|
|
391
|
+
# Show timing
|
|
392
|
+
elapsed = result_data.get("elapsed_seconds", 0)
|
|
393
|
+
print(f"\n[{elapsed}s]")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _display_dbinfo(db_info: Dict) -> None:
|
|
397
|
+
"""Display TextFSM database info."""
|
|
398
|
+
print("\nTextFSM Database Info:")
|
|
399
|
+
print("=" * 60)
|
|
400
|
+
print(f"Engine Available: {db_info.get('engine_available', False)}")
|
|
401
|
+
print(f"Database Path: {db_info.get('db_path', 'unknown')}")
|
|
402
|
+
print(f"Database Exists: {db_info.get('db_exists', False)}")
|
|
403
|
+
|
|
404
|
+
if db_info.get('db_exists'):
|
|
405
|
+
db_size = db_info.get('db_size', 0)
|
|
406
|
+
db_size_mb = db_info.get('db_size_mb', 0.0)
|
|
407
|
+
print(f"Database Size: {db_size:,} bytes ({db_size_mb:.1f} MB)")
|
|
408
|
+
print(f"Absolute Path: {db_info.get('db_absolute_path', 'unknown')}")
|
|
409
|
+
|
|
410
|
+
if db_size == 0:
|
|
411
|
+
print("\n⚠️ WARNING: Database file is empty (0 bytes)!")
|
|
412
|
+
print(" Parsing will fail until you download templates.")
|
|
413
|
+
elif db_size < 100000:
|
|
414
|
+
print(f"\n⚠️ WARNING: Database seems too small ({db_size_mb:.1f} MB)")
|
|
415
|
+
print(" Expected size is ~0.3 MB. May be corrupted or incomplete.")
|
|
416
|
+
else:
|
|
417
|
+
print("\n✓ Database appears healthy")
|
|
418
|
+
else:
|
|
419
|
+
print("\n✗ ERROR: Database file not found!")
|
|
420
|
+
print(" Run: api.download_templates() to create it.")
|
|
421
|
+
|
|
422
|
+
print()
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _display_parsed_result(data, output_format: str, max_rows: int = 20) -> None:
|
|
426
|
+
"""Display parsed data in the specified format."""
|
|
427
|
+
if output_format == "json":
|
|
428
|
+
print(json.dumps(data, indent=2, default=str))
|
|
429
|
+
elif output_format == "rich":
|
|
430
|
+
_print_parsed_data_rich(data, max_rows)
|
|
431
|
+
else:
|
|
432
|
+
_print_parsed_data_text(data, max_rows)
|
|
270
433
|
|
|
271
434
|
|
|
272
|
-
def
|
|
273
|
-
"""Pretty print parsed data
|
|
435
|
+
def _print_parsed_data_text(data, max_rows: int = 20) -> None:
|
|
436
|
+
"""Pretty print parsed data as text table."""
|
|
274
437
|
if not data:
|
|
275
438
|
print("(empty)")
|
|
276
439
|
return
|
|
277
440
|
|
|
278
441
|
if not isinstance(data, list):
|
|
279
|
-
|
|
280
|
-
print(json.dumps(data, indent=2))
|
|
442
|
+
print(json.dumps(data, indent=2, default=str))
|
|
281
443
|
return
|
|
282
444
|
|
|
283
445
|
# Get all unique keys
|
|
@@ -289,14 +451,11 @@ def _print_parsed_data(data, max_rows=20):
|
|
|
289
451
|
keys = sorted(all_keys)
|
|
290
452
|
|
|
291
453
|
if not keys:
|
|
292
|
-
|
|
293
|
-
print(json.dumps(data, indent=2))
|
|
454
|
+
print(json.dumps(data, indent=2, default=str))
|
|
294
455
|
return
|
|
295
456
|
|
|
296
457
|
# Calculate column widths
|
|
297
|
-
col_widths = {}
|
|
298
|
-
for key in keys:
|
|
299
|
-
col_widths[key] = len(key)
|
|
458
|
+
col_widths = {key: len(key) for key in keys}
|
|
300
459
|
|
|
301
460
|
for row in data[:max_rows]:
|
|
302
461
|
if isinstance(row, dict):
|
|
@@ -304,7 +463,7 @@ def _print_parsed_data(data, max_rows=20):
|
|
|
304
463
|
val = str(row.get(key, ""))
|
|
305
464
|
col_widths[key] = max(col_widths[key], len(val))
|
|
306
465
|
|
|
307
|
-
# Cap widths
|
|
466
|
+
# Cap widths at 30
|
|
308
467
|
for key in keys:
|
|
309
468
|
col_widths[key] = min(col_widths[key], 30)
|
|
310
469
|
|
|
@@ -334,14 +493,14 @@ def _print_parsed_data(data, max_rows=20):
|
|
|
334
493
|
shown += 1
|
|
335
494
|
|
|
336
495
|
|
|
337
|
-
def _print_parsed_data_rich(data, max_rows=20):
|
|
496
|
+
def _print_parsed_data_rich(data, max_rows: int = 20) -> None:
|
|
338
497
|
"""Pretty print parsed data using Rich library."""
|
|
339
498
|
try:
|
|
340
499
|
from rich.console import Console
|
|
341
500
|
from rich.table import Table
|
|
342
501
|
except ImportError:
|
|
343
502
|
print("⚠️ Rich library not available, falling back to text format")
|
|
344
|
-
|
|
503
|
+
_print_parsed_data_text(data, max_rows)
|
|
345
504
|
return
|
|
346
505
|
|
|
347
506
|
if not data:
|
|
@@ -349,8 +508,7 @@ def _print_parsed_data_rich(data, max_rows=20):
|
|
|
349
508
|
return
|
|
350
509
|
|
|
351
510
|
if not isinstance(data, list):
|
|
352
|
-
|
|
353
|
-
print(json.dumps(data, indent=2))
|
|
511
|
+
print(json.dumps(data, indent=2, default=str))
|
|
354
512
|
return
|
|
355
513
|
|
|
356
514
|
# Get all unique keys
|
|
@@ -362,19 +520,16 @@ def _print_parsed_data_rich(data, max_rows=20):
|
|
|
362
520
|
keys = sorted(all_keys)
|
|
363
521
|
|
|
364
522
|
if not keys:
|
|
365
|
-
|
|
366
|
-
print(json.dumps(data, indent=2))
|
|
523
|
+
print(json.dumps(data, indent=2, default=str))
|
|
367
524
|
return
|
|
368
525
|
|
|
369
526
|
# Create rich table
|
|
370
527
|
console = Console()
|
|
371
528
|
table = Table(show_header=True, header_style="bold cyan")
|
|
372
529
|
|
|
373
|
-
# Add columns
|
|
374
530
|
for key in keys:
|
|
375
531
|
table.add_column(key, style="white", no_wrap=False, max_width=30)
|
|
376
532
|
|
|
377
|
-
# Add rows
|
|
378
533
|
shown = 0
|
|
379
534
|
for row in data:
|
|
380
535
|
if not isinstance(row, dict):
|
|
@@ -391,28 +546,16 @@ def _print_parsed_data_rich(data, max_rows=20):
|
|
|
391
546
|
console.print(table)
|
|
392
547
|
|
|
393
548
|
|
|
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
549
|
# Convenience function to add to API
|
|
411
|
-
def add_repl_to_api(api_instance):
|
|
550
|
+
def add_repl_to_api(api_instance: NTermAPI) -> None:
|
|
412
551
|
"""Add repl() method to API instance."""
|
|
413
552
|
|
|
414
553
|
def repl(policy: Optional[REPLPolicy] = None):
|
|
415
554
|
"""Start interactive REPL."""
|
|
416
555
|
start_repl(api=api_instance, policy=policy)
|
|
417
556
|
|
|
418
|
-
api_instance.repl = repl
|
|
557
|
+
api_instance.repl = repl
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# Type hint for display function
|
|
561
|
+
from typing import Dict
|