ntermqt 0.1.4__py3-none-any.whl → 0.1.6__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/__main__.py +48 -0
- nterm/parser/__init__.py +0 -0
- nterm/parser/api_help_dialog.py +607 -0
- nterm/parser/ntc_download_dialog.py +372 -0
- nterm/parser/tfsm_engine.py +246 -0
- nterm/parser/tfsm_fire.py +237 -0
- nterm/parser/tfsm_fire_tester.py +2329 -0
- nterm/scripting/__init__.py +8 -6
- nterm/scripting/api.py +926 -19
- nterm/scripting/repl.py +406 -0
- nterm/scripting/repl_interactive.py +418 -0
- nterm/session/local_terminal.py +1 -0
- {ntermqt-0.1.4.dist-info → ntermqt-0.1.6.dist-info}/METADATA +7 -5
- {ntermqt-0.1.4.dist-info → ntermqt-0.1.6.dist-info}/RECORD +17 -10
- nterm/examples/basic_terminal.py +0 -415
- {ntermqt-0.1.4.dist-info → ntermqt-0.1.6.dist-info}/WHEEL +0 -0
- {ntermqt-0.1.4.dist-info → ntermqt-0.1.6.dist-info}/entry_points.txt +0 -0
- {ntermqt-0.1.4.dist-info → ntermqt-0.1.6.dist-info}/top_level.txt +0 -0
nterm/__main__.py
CHANGED
|
@@ -20,6 +20,8 @@ from nterm.manager import (
|
|
|
20
20
|
SessionTreeWidget, SessionStore, SavedSession, QuickConnectDialog,
|
|
21
21
|
SettingsDialog, ExportDialog, ImportDialog, ImportTerminalTelemetryDialog
|
|
22
22
|
)
|
|
23
|
+
from nterm.parser.ntc_download_dialog import NTCDownloadDialog
|
|
24
|
+
from nterm.parser.api_help_dialog import APIHelpDialog
|
|
23
25
|
from nterm.terminal.widget import TerminalWidget
|
|
24
26
|
from nterm.session.ssh import SSHSession
|
|
25
27
|
from nterm.session.local_terminal import LocalTerminal
|
|
@@ -457,6 +459,25 @@ class MainWindow(QMainWindow):
|
|
|
457
459
|
shell_window_action.triggered.connect(lambda: self._open_local("Shell", LocalTerminal(), "window"))
|
|
458
460
|
shell_menu.addAction(shell_window_action)
|
|
459
461
|
|
|
462
|
+
# Separator before tools
|
|
463
|
+
dev_menu.addSeparator()
|
|
464
|
+
|
|
465
|
+
# Download NTC Templates
|
|
466
|
+
download_templates_action = QAction("Download &NTC Templates...", self)
|
|
467
|
+
download_templates_action.triggered.connect(self._on_download_ntc_templates)
|
|
468
|
+
dev_menu.addAction(download_templates_action)
|
|
469
|
+
|
|
470
|
+
# TextFSM Template Tester
|
|
471
|
+
template_tester_action = QAction("TextFSM &Template Tester...", self)
|
|
472
|
+
template_tester_action.triggered.connect(self._on_textfsm_tester)
|
|
473
|
+
dev_menu.addAction(template_tester_action)
|
|
474
|
+
|
|
475
|
+
# API Help
|
|
476
|
+
api_help_action = QAction("&API Help...", self)
|
|
477
|
+
api_help_action.setShortcut(QKeySequence("F1"))
|
|
478
|
+
api_help_action.triggered.connect(self._on_api_help)
|
|
479
|
+
dev_menu.addAction(api_help_action)
|
|
480
|
+
|
|
460
481
|
def _on_import_sessions(self):
|
|
461
482
|
"""Show import dialog."""
|
|
462
483
|
dialog = ImportDialog(self.session_store, self)
|
|
@@ -474,6 +495,33 @@ class MainWindow(QMainWindow):
|
|
|
474
495
|
if dialog.exec():
|
|
475
496
|
self.session_tree.refresh()
|
|
476
497
|
|
|
498
|
+
def _on_download_ntc_templates(self):
|
|
499
|
+
"""Show NTC template download dialog."""
|
|
500
|
+
# Use the same db path as the TextFSM engine would use
|
|
501
|
+
db_path = Path.cwd() / "tfsm_templates.db"
|
|
502
|
+
dialog = NTCDownloadDialog(self, str(db_path))
|
|
503
|
+
dialog.exec()
|
|
504
|
+
|
|
505
|
+
def _on_textfsm_tester(self):
|
|
506
|
+
"""Launch TextFSM Template Tester."""
|
|
507
|
+
import subprocess
|
|
508
|
+
import sys
|
|
509
|
+
|
|
510
|
+
# Launch as separate process
|
|
511
|
+
try:
|
|
512
|
+
subprocess.Popen([sys.executable, "-m", "nterm.parser.tfsm_fire_tester"])
|
|
513
|
+
except Exception as e:
|
|
514
|
+
QMessageBox.critical(
|
|
515
|
+
self,
|
|
516
|
+
"Launch Error",
|
|
517
|
+
f"Failed to launch TextFSM Template Tester:\n{e}"
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
def _on_api_help(self):
|
|
521
|
+
"""Show API help dialog."""
|
|
522
|
+
dialog = APIHelpDialog(self)
|
|
523
|
+
dialog.exec()
|
|
524
|
+
|
|
477
525
|
def _on_settings(self):
|
|
478
526
|
"""Show settings dialog."""
|
|
479
527
|
dialog = SettingsDialog(self.theme_engine, self.current_theme, self)
|
nterm/parser/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nterm API Help Dialog
|
|
3
|
+
|
|
4
|
+
Shows API usage, examples, and workflows for the scripting interface.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from PyQt6.QtWidgets import (
|
|
8
|
+
QDialog, QVBoxLayout, QHBoxLayout, QTabWidget,
|
|
9
|
+
QTextEdit, QPushButton, QLabel, QWidget, QMessageBox
|
|
10
|
+
)
|
|
11
|
+
from PyQt6.QtCore import Qt
|
|
12
|
+
from PyQt6.QtGui import QFont
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class APIHelpDialog(QDialog):
|
|
16
|
+
"""Dialog showing nterm API documentation and examples."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, parent=None):
|
|
19
|
+
super().__init__(parent)
|
|
20
|
+
self.setWindowTitle("nterm API Help")
|
|
21
|
+
self.setMinimumSize(900, 700)
|
|
22
|
+
self.setup_ui()
|
|
23
|
+
|
|
24
|
+
def setup_ui(self):
|
|
25
|
+
layout = QVBoxLayout(self)
|
|
26
|
+
|
|
27
|
+
# Header
|
|
28
|
+
header = QLabel("nterm Scripting API")
|
|
29
|
+
header_font = QFont()
|
|
30
|
+
header_font.setPointSize(16)
|
|
31
|
+
header_font.setBold(True)
|
|
32
|
+
header.setFont(header_font)
|
|
33
|
+
layout.addWidget(header)
|
|
34
|
+
|
|
35
|
+
subtitle = QLabel("Access your network devices programmatically from IPython")
|
|
36
|
+
layout.addWidget(subtitle)
|
|
37
|
+
|
|
38
|
+
layout.addSpacing(10)
|
|
39
|
+
|
|
40
|
+
# Tabs
|
|
41
|
+
tabs = QTabWidget()
|
|
42
|
+
tabs.addTab(self._create_overview_tab(), "Overview")
|
|
43
|
+
tabs.addTab(self._create_quickstart_tab(), "Quick Start")
|
|
44
|
+
tabs.addTab(self._create_reference_tab(), "API Reference")
|
|
45
|
+
tabs.addTab(self._create_examples_tab(), "Examples")
|
|
46
|
+
tabs.addTab(self._create_troubleshooting_tab(), "Troubleshooting")
|
|
47
|
+
layout.addWidget(tabs)
|
|
48
|
+
|
|
49
|
+
# Buttons
|
|
50
|
+
btn_layout = QHBoxLayout()
|
|
51
|
+
btn_layout.addStretch()
|
|
52
|
+
|
|
53
|
+
copy_btn = QPushButton("Copy Sample Code")
|
|
54
|
+
copy_btn.clicked.connect(self._copy_sample_code)
|
|
55
|
+
btn_layout.addWidget(copy_btn)
|
|
56
|
+
|
|
57
|
+
close_btn = QPushButton("Close")
|
|
58
|
+
close_btn.setDefault(True)
|
|
59
|
+
close_btn.clicked.connect(self.accept)
|
|
60
|
+
btn_layout.addWidget(close_btn)
|
|
61
|
+
|
|
62
|
+
layout.addLayout(btn_layout)
|
|
63
|
+
|
|
64
|
+
def _create_overview_tab(self) -> QWidget:
|
|
65
|
+
"""Create overview tab."""
|
|
66
|
+
widget = QWidget()
|
|
67
|
+
layout = QVBoxLayout(widget)
|
|
68
|
+
|
|
69
|
+
text = QTextEdit()
|
|
70
|
+
text.setReadOnly(True)
|
|
71
|
+
text.setMarkdown("""
|
|
72
|
+
# nterm Scripting API
|
|
73
|
+
|
|
74
|
+
The nterm API provides programmatic access to your network devices from IPython.
|
|
75
|
+
|
|
76
|
+
## Features
|
|
77
|
+
|
|
78
|
+
**Device Management**
|
|
79
|
+
- Query saved devices and folders
|
|
80
|
+
- Search by name, hostname, or tags
|
|
81
|
+
- Access device metadata and connection history
|
|
82
|
+
|
|
83
|
+
**Secure Connections**
|
|
84
|
+
- Encrypted credential vault with pattern matching
|
|
85
|
+
- Auto-platform detection (Cisco, Arista, Juniper, etc.)
|
|
86
|
+
- Legacy device support (RSA SHA-1 fallback)
|
|
87
|
+
- Jump host support built-in
|
|
88
|
+
|
|
89
|
+
**Command Execution**
|
|
90
|
+
- Execute commands on network devices
|
|
91
|
+
- Automatic TextFSM parsing for structured data
|
|
92
|
+
- Field normalization across vendors
|
|
93
|
+
- Fallback to raw output if parsing fails
|
|
94
|
+
|
|
95
|
+
**Developer Tools**
|
|
96
|
+
- Rich data types with tab completion
|
|
97
|
+
- Debugging tools for parsing issues
|
|
98
|
+
- Database diagnostics
|
|
99
|
+
- Connection tracking
|
|
100
|
+
|
|
101
|
+
## Accessing the API
|
|
102
|
+
|
|
103
|
+
The API is pre-loaded in IPython sessions:
|
|
104
|
+
|
|
105
|
+
1. **Dev → IPython → Open in Tab**
|
|
106
|
+
2. The `api` object is automatically available
|
|
107
|
+
3. Use `api.help()` to see all commands
|
|
108
|
+
|
|
109
|
+
## Prerequisites
|
|
110
|
+
|
|
111
|
+
**TextFSM Template Database**
|
|
112
|
+
|
|
113
|
+
The API requires the TextFSM template database for parsing command output.
|
|
114
|
+
|
|
115
|
+
- Download via: **Dev → Download NTC Templates...**
|
|
116
|
+
- Select platforms you need (cisco_ios, arista_eos, etc.)
|
|
117
|
+
- Database stored as `tfsm_templates.db`
|
|
118
|
+
|
|
119
|
+
**Credential Vault**
|
|
120
|
+
|
|
121
|
+
Store device credentials securely:
|
|
122
|
+
|
|
123
|
+
- **Edit → Credential Manager...**
|
|
124
|
+
- Create credentials with pattern matching
|
|
125
|
+
- Unlock vault: `api.unlock("password")`
|
|
126
|
+
""")
|
|
127
|
+
layout.addWidget(text)
|
|
128
|
+
return widget
|
|
129
|
+
|
|
130
|
+
def _create_quickstart_tab(self) -> QWidget:
|
|
131
|
+
"""Create quick start tab."""
|
|
132
|
+
widget = QWidget()
|
|
133
|
+
layout = QVBoxLayout(widget)
|
|
134
|
+
|
|
135
|
+
text = QTextEdit()
|
|
136
|
+
text.setReadOnly(True)
|
|
137
|
+
text.setFont(QFont("Courier", 10))
|
|
138
|
+
text.setText("""# Quick Start - First Connection
|
|
139
|
+
|
|
140
|
+
# 1. Unlock vault
|
|
141
|
+
api.unlock("vault-password")
|
|
142
|
+
|
|
143
|
+
# 2. List your devices
|
|
144
|
+
api.devices()
|
|
145
|
+
# [Device(wan-core-1, 172.16.128.1:22, cred=home_lab), ...]
|
|
146
|
+
|
|
147
|
+
# 3. Connect to a device
|
|
148
|
+
session = api.connect("wan-core-1")
|
|
149
|
+
# <ActiveSession wan-core-1@172.16.128.1:22 connected, platform=cisco_ios>
|
|
150
|
+
|
|
151
|
+
# 4. Execute a command
|
|
152
|
+
result = api.send(session, "show version")
|
|
153
|
+
|
|
154
|
+
# 5. Access parsed data
|
|
155
|
+
print(result.parsed_data)
|
|
156
|
+
# [{'VERSION': '15.2(4)M11', 'HOSTNAME': 'wan-core-1', ...}]
|
|
157
|
+
|
|
158
|
+
# 6. Disconnect
|
|
159
|
+
api.disconnect(session)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# Common Workflows
|
|
163
|
+
|
|
164
|
+
# Search devices
|
|
165
|
+
devices = api.search("leaf")
|
|
166
|
+
devices = api.devices("eng-*")
|
|
167
|
+
devices = api.devices(folder="Lab-ENG")
|
|
168
|
+
|
|
169
|
+
# Connect with specific credential
|
|
170
|
+
session = api.connect("device", credential="lab-admin")
|
|
171
|
+
|
|
172
|
+
# Check connection status
|
|
173
|
+
if session.is_connected():
|
|
174
|
+
result = api.send(session, "show ip route")
|
|
175
|
+
|
|
176
|
+
# Disable parsing for raw output
|
|
177
|
+
result = api.send(session, "show run", parse=False)
|
|
178
|
+
print(result.raw_output)
|
|
179
|
+
|
|
180
|
+
# Multi-device workflow
|
|
181
|
+
for device in api.devices("spine-*"):
|
|
182
|
+
session = api.connect(device.name)
|
|
183
|
+
result = api.send(session, "show version")
|
|
184
|
+
if result.parsed_data:
|
|
185
|
+
version = result.parsed_data[0]['VERSION']
|
|
186
|
+
print(f"{device.name}: {version}")
|
|
187
|
+
api.disconnect(session)
|
|
188
|
+
""")
|
|
189
|
+
layout.addWidget(text)
|
|
190
|
+
return widget
|
|
191
|
+
|
|
192
|
+
def _create_reference_tab(self) -> QWidget:
|
|
193
|
+
"""Create API reference tab."""
|
|
194
|
+
widget = QWidget()
|
|
195
|
+
layout = QVBoxLayout(widget)
|
|
196
|
+
|
|
197
|
+
text = QTextEdit()
|
|
198
|
+
text.setReadOnly(True)
|
|
199
|
+
text.setFont(QFont("Courier", 9))
|
|
200
|
+
text.setText("""# API Reference
|
|
201
|
+
|
|
202
|
+
## Device Operations
|
|
203
|
+
|
|
204
|
+
api.devices() # List all devices
|
|
205
|
+
api.devices("pattern*") # Filter by glob pattern
|
|
206
|
+
api.devices(folder="Lab-ENG") # Filter by folder
|
|
207
|
+
api.search("query") # Search by name/hostname
|
|
208
|
+
api.device("name") # Get specific device
|
|
209
|
+
api.folders() # List all folders
|
|
210
|
+
|
|
211
|
+
## Credential Operations (requires unlocked vault)
|
|
212
|
+
|
|
213
|
+
api.unlock("password") # Unlock vault
|
|
214
|
+
api.lock() # Lock vault
|
|
215
|
+
api.credentials() # List all credentials
|
|
216
|
+
api.credentials("*admin*") # Filter by pattern
|
|
217
|
+
api.credential("name") # Get specific credential
|
|
218
|
+
api.resolve_credential("host") # Find matching credential
|
|
219
|
+
|
|
220
|
+
## Connection Operations
|
|
221
|
+
|
|
222
|
+
session = api.connect("device") # Connect and auto-detect
|
|
223
|
+
session = api.connect("device", "cred") # Connect with credential
|
|
224
|
+
api.disconnect(session) # Close connection
|
|
225
|
+
api.active_sessions() # List open connections
|
|
226
|
+
|
|
227
|
+
## Command Execution
|
|
228
|
+
|
|
229
|
+
result = api.send(session, "show version") # Execute and parse
|
|
230
|
+
result = api.send(session, "cmd", parse=False) # Raw output only
|
|
231
|
+
result = api.send(session, "cmd", timeout=60) # Custom timeout
|
|
232
|
+
result = api.send(session, "cmd", normalize=False) # Don't normalize fields
|
|
233
|
+
|
|
234
|
+
## Result Access
|
|
235
|
+
|
|
236
|
+
result.raw_output # Raw text from device
|
|
237
|
+
result.parsed_data # Parsed List[Dict] or None
|
|
238
|
+
result.platform # Detected platform
|
|
239
|
+
result.parse_success # Whether parsing worked
|
|
240
|
+
result.parse_template # Template used
|
|
241
|
+
result.to_dict() # Export as dictionary
|
|
242
|
+
|
|
243
|
+
## Session Attributes
|
|
244
|
+
|
|
245
|
+
session.device_name # Device name
|
|
246
|
+
session.hostname # IP/hostname
|
|
247
|
+
session.port # SSH port
|
|
248
|
+
session.platform # Detected platform
|
|
249
|
+
session.prompt # Device prompt
|
|
250
|
+
session.is_connected() # Check if active
|
|
251
|
+
session.connected_at # Connection timestamp
|
|
252
|
+
|
|
253
|
+
## Debugging
|
|
254
|
+
|
|
255
|
+
api.debug_parse(cmd, output, platform) # Debug parsing issues
|
|
256
|
+
api.db_info() # Database diagnostics
|
|
257
|
+
api.status() # API summary
|
|
258
|
+
|
|
259
|
+
## Status Properties
|
|
260
|
+
|
|
261
|
+
api.vault_unlocked # Vault status (bool)
|
|
262
|
+
api.vault_initialized # Vault exists (bool)
|
|
263
|
+
api._tfsm_engine # Parser instance
|
|
264
|
+
""")
|
|
265
|
+
layout.addWidget(text)
|
|
266
|
+
return widget
|
|
267
|
+
|
|
268
|
+
def _create_examples_tab(self) -> QWidget:
|
|
269
|
+
"""Create examples tab."""
|
|
270
|
+
widget = QWidget()
|
|
271
|
+
layout = QVBoxLayout(widget)
|
|
272
|
+
|
|
273
|
+
text = QTextEdit()
|
|
274
|
+
text.setReadOnly(True)
|
|
275
|
+
text.setFont(QFont("Courier", 9))
|
|
276
|
+
text.setText("""# Examples
|
|
277
|
+
|
|
278
|
+
# Example 1: Collect Software Versions
|
|
279
|
+
api.unlock("password")
|
|
280
|
+
versions = {}
|
|
281
|
+
|
|
282
|
+
for device in api.devices():
|
|
283
|
+
try:
|
|
284
|
+
session = api.connect(device.name)
|
|
285
|
+
result = api.send(session, "show version")
|
|
286
|
+
|
|
287
|
+
if result.parsed_data:
|
|
288
|
+
ver = result.parsed_data[0].get('VERSION', 'unknown')
|
|
289
|
+
versions[device.name] = ver
|
|
290
|
+
|
|
291
|
+
api.disconnect(session)
|
|
292
|
+
except Exception as e:
|
|
293
|
+
print(f"Failed on {device.name}: {e}")
|
|
294
|
+
|
|
295
|
+
# Print results
|
|
296
|
+
for name, ver in sorted(versions.items()):
|
|
297
|
+
print(f"{name:20} {ver}")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# Example 2: Find Interfaces with Errors
|
|
301
|
+
session = api.connect("core-switch")
|
|
302
|
+
result = api.send(session, "show interfaces")
|
|
303
|
+
|
|
304
|
+
if result.parsed_data:
|
|
305
|
+
for intf in result.parsed_data:
|
|
306
|
+
in_errors = int(intf.get('in_errors', 0))
|
|
307
|
+
out_errors = int(intf.get('out_errors', 0))
|
|
308
|
+
|
|
309
|
+
if in_errors > 0 or out_errors > 0:
|
|
310
|
+
print(f"{intf['interface']}: in={in_errors}, out={out_errors}")
|
|
311
|
+
|
|
312
|
+
api.disconnect(session)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# Example 3: Configuration Backup
|
|
316
|
+
import json
|
|
317
|
+
from datetime import datetime
|
|
318
|
+
|
|
319
|
+
backups = {}
|
|
320
|
+
|
|
321
|
+
for device in api.devices(folder="Production"):
|
|
322
|
+
session = api.connect(device.name)
|
|
323
|
+
result = api.send(session, "show running-config", parse=False)
|
|
324
|
+
|
|
325
|
+
backups[device.name] = {
|
|
326
|
+
'hostname': device.hostname,
|
|
327
|
+
'config': result.raw_output,
|
|
328
|
+
'timestamp': datetime.now().isoformat(),
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
api.disconnect(session)
|
|
332
|
+
|
|
333
|
+
# Save to file
|
|
334
|
+
with open('backups.json', 'w') as f:
|
|
335
|
+
json.dump(backups, f, indent=2)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# Example 4: Audit Device Reachability
|
|
339
|
+
api.unlock("password")
|
|
340
|
+
|
|
341
|
+
print("Testing device connectivity...")
|
|
342
|
+
print(f"{'Device':<20} {'Status':<10} {'Platform':<15}")
|
|
343
|
+
print("-" * 45)
|
|
344
|
+
|
|
345
|
+
for device in api.devices():
|
|
346
|
+
try:
|
|
347
|
+
session = api.connect(device.name)
|
|
348
|
+
platform = session.platform or 'unknown'
|
|
349
|
+
print(f"{device.name:<20} {'UP':<10} {platform:<15}")
|
|
350
|
+
api.disconnect(session)
|
|
351
|
+
except Exception as e:
|
|
352
|
+
print(f"{device.name:<20} {'DOWN':<10} {str(e)[:15]}")
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# Example 5: Compare Configurations
|
|
356
|
+
session1 = api.connect("spine-1")
|
|
357
|
+
session2 = api.connect("spine-2")
|
|
358
|
+
|
|
359
|
+
config1 = api.send(session1, "show run", parse=False).raw_output
|
|
360
|
+
config2 = api.send(session2, "show run", parse=False).raw_output
|
|
361
|
+
|
|
362
|
+
# Find differences
|
|
363
|
+
lines1 = set(config1.split('\\n'))
|
|
364
|
+
lines2 = set(config2.split('\\n'))
|
|
365
|
+
|
|
366
|
+
print("Only in spine-1:")
|
|
367
|
+
for line in sorted(lines1 - lines2)[:10]:
|
|
368
|
+
print(f" {line}")
|
|
369
|
+
|
|
370
|
+
print("\\nOnly in spine-2:")
|
|
371
|
+
for line in sorted(lines2 - lines1)[:10]:
|
|
372
|
+
print(f" {line}")
|
|
373
|
+
|
|
374
|
+
api.disconnect(session1)
|
|
375
|
+
api.disconnect(session2)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# Example 6: Device Inventory Report
|
|
379
|
+
import csv
|
|
380
|
+
|
|
381
|
+
api.unlock("password")
|
|
382
|
+
|
|
383
|
+
with open('inventory.csv', 'w', newline='') as f:
|
|
384
|
+
writer = csv.writer(f)
|
|
385
|
+
writer.writerow(['Device', 'IP', 'Platform', 'Version', 'Serial', 'Uptime'])
|
|
386
|
+
|
|
387
|
+
for device in api.devices():
|
|
388
|
+
try:
|
|
389
|
+
session = api.connect(device.name)
|
|
390
|
+
result = api.send(session, "show version")
|
|
391
|
+
|
|
392
|
+
if result.parsed_data:
|
|
393
|
+
data = result.parsed_data[0]
|
|
394
|
+
writer.writerow([
|
|
395
|
+
device.name,
|
|
396
|
+
device.hostname,
|
|
397
|
+
session.platform,
|
|
398
|
+
data.get('VERSION', ''),
|
|
399
|
+
data.get('SERIAL', [''])[0],
|
|
400
|
+
data.get('UPTIME', ''),
|
|
401
|
+
])
|
|
402
|
+
|
|
403
|
+
api.disconnect(session)
|
|
404
|
+
except Exception as e:
|
|
405
|
+
writer.writerow([device.name, device.hostname, 'ERROR', str(e), '', ''])
|
|
406
|
+
|
|
407
|
+
print("Inventory saved to inventory.csv")
|
|
408
|
+
""")
|
|
409
|
+
layout.addWidget(text)
|
|
410
|
+
return widget
|
|
411
|
+
|
|
412
|
+
def _create_troubleshooting_tab(self) -> QWidget:
|
|
413
|
+
"""Create troubleshooting tab."""
|
|
414
|
+
widget = QWidget()
|
|
415
|
+
layout = QVBoxLayout(widget)
|
|
416
|
+
|
|
417
|
+
text = QTextEdit()
|
|
418
|
+
text.setReadOnly(True)
|
|
419
|
+
text.setMarkdown("""
|
|
420
|
+
# Troubleshooting
|
|
421
|
+
|
|
422
|
+
## Database Not Found
|
|
423
|
+
|
|
424
|
+
**Error:** `RuntimeError: Failed to initialize TextFSM engine`
|
|
425
|
+
|
|
426
|
+
**Solution:**
|
|
427
|
+
1. Go to **Dev → Download NTC Templates...**
|
|
428
|
+
2. Click **Fetch Available Platforms**
|
|
429
|
+
3. Select platforms you need
|
|
430
|
+
4. Click **Download Selected**
|
|
431
|
+
5. Restart IPython session
|
|
432
|
+
|
|
433
|
+
**Check status:**
|
|
434
|
+
```python
|
|
435
|
+
api.db_info()
|
|
436
|
+
# Shows: db_path, db_exists, db_size
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Vault Locked
|
|
440
|
+
|
|
441
|
+
**Error:** `RuntimeError: Vault is locked`
|
|
442
|
+
|
|
443
|
+
**Solution:**
|
|
444
|
+
```python
|
|
445
|
+
api.unlock("your-vault-password")
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
**Check status:**
|
|
449
|
+
```python
|
|
450
|
+
api.vault_unlocked # Returns True/False
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
## No Credentials
|
|
454
|
+
|
|
455
|
+
**Error:** `ValueError: No credentials available for hostname`
|
|
456
|
+
|
|
457
|
+
**Solution:**
|
|
458
|
+
1. Go to **Edit → Credential Manager...**
|
|
459
|
+
2. Add credential with host pattern matching
|
|
460
|
+
3. Unlock vault and try again
|
|
461
|
+
|
|
462
|
+
**Debug:**
|
|
463
|
+
```python
|
|
464
|
+
api.credentials() # List all credentials
|
|
465
|
+
api.resolve_credential("192.168.1.1") # Check which would match
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Connection Failed
|
|
469
|
+
|
|
470
|
+
**Error:** Connection timeout or authentication failure
|
|
471
|
+
|
|
472
|
+
**Debug:**
|
|
473
|
+
```python
|
|
474
|
+
# Check device info
|
|
475
|
+
device = api.device("device-name")
|
|
476
|
+
print(device)
|
|
477
|
+
|
|
478
|
+
# Try with different credential
|
|
479
|
+
session = api.connect("device", credential="other-cred")
|
|
480
|
+
|
|
481
|
+
# Check if device is reachable
|
|
482
|
+
# (try from terminal first)
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
## Parsing Failed
|
|
486
|
+
|
|
487
|
+
**Symptom:** `result.parsed_data` is None but command succeeded
|
|
488
|
+
|
|
489
|
+
**Debug:**
|
|
490
|
+
```python
|
|
491
|
+
result = api.send(session, "show version")
|
|
492
|
+
|
|
493
|
+
# Check raw output
|
|
494
|
+
print(result.raw_output[:200])
|
|
495
|
+
|
|
496
|
+
# Debug parsing
|
|
497
|
+
debug = api.debug_parse(
|
|
498
|
+
command="show version",
|
|
499
|
+
output=result.raw_output,
|
|
500
|
+
platform=session.platform
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
print(debug)
|
|
504
|
+
# Shows: template_used, best_score, all_scores, error
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
**Solutions:**
|
|
508
|
+
- Template might not exist for this command/platform
|
|
509
|
+
- Output format might be non-standard
|
|
510
|
+
- Use `parse=False` to get raw output
|
|
511
|
+
- Download more templates if platform is missing
|
|
512
|
+
|
|
513
|
+
## Platform Not Detected
|
|
514
|
+
|
|
515
|
+
**Symptom:** `session.platform` is None
|
|
516
|
+
|
|
517
|
+
**Debug:**
|
|
518
|
+
```python
|
|
519
|
+
session = api.connect("device")
|
|
520
|
+
print(session.platform) # None
|
|
521
|
+
|
|
522
|
+
# Check show version output
|
|
523
|
+
result = api.send(session, "show version", parse=False)
|
|
524
|
+
print(result.raw_output[:500])
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
**Solution:**
|
|
528
|
+
Platform detection looks for keywords in show version:
|
|
529
|
+
- Cisco: "Cisco IOS Software"
|
|
530
|
+
- Arista: "Arista"
|
|
531
|
+
- Juniper: "JUNOS"
|
|
532
|
+
|
|
533
|
+
If not detected, parsing won't work. Set manually if needed (not currently supported, but you can modify the session object).
|
|
534
|
+
|
|
535
|
+
## Session Stuck
|
|
536
|
+
|
|
537
|
+
**Symptom:** Command hangs or times out
|
|
538
|
+
|
|
539
|
+
**Solutions:**
|
|
540
|
+
```python
|
|
541
|
+
# Increase timeout
|
|
542
|
+
result = api.send(session, "show tech-support", timeout=120)
|
|
543
|
+
|
|
544
|
+
# Check if session still active
|
|
545
|
+
session.is_connected() # Returns 1 or 0
|
|
546
|
+
|
|
547
|
+
# Disconnect and reconnect
|
|
548
|
+
api.disconnect(session)
|
|
549
|
+
session = api.connect("device")
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
## Getting Help
|
|
553
|
+
|
|
554
|
+
**In IPython:**
|
|
555
|
+
```python
|
|
556
|
+
api.help() # Show all commands
|
|
557
|
+
api.status() # API status summary
|
|
558
|
+
api.db_info() # Database diagnostics
|
|
559
|
+
|
|
560
|
+
# Object inspection
|
|
561
|
+
session? # Show ActiveSession help
|
|
562
|
+
result? # Show CommandResult help
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**From GUI:**
|
|
566
|
+
- **Dev → API Help...** (this dialog)
|
|
567
|
+
- **Dev → Download NTC Templates...**
|
|
568
|
+
- **Edit → Credential Manager...**
|
|
569
|
+
""")
|
|
570
|
+
layout.addWidget(text)
|
|
571
|
+
return widget
|
|
572
|
+
|
|
573
|
+
def _copy_sample_code(self):
|
|
574
|
+
"""Copy sample code to clipboard."""
|
|
575
|
+
sample = """# nterm API Quick Start
|
|
576
|
+
from nterm.scripting import api
|
|
577
|
+
|
|
578
|
+
# Unlock vault
|
|
579
|
+
api.unlock("vault-password")
|
|
580
|
+
|
|
581
|
+
# List devices
|
|
582
|
+
devices = api.devices()
|
|
583
|
+
|
|
584
|
+
# Connect to device
|
|
585
|
+
session = api.connect("device-name")
|
|
586
|
+
|
|
587
|
+
# Execute command
|
|
588
|
+
result = api.send(session, "show version")
|
|
589
|
+
|
|
590
|
+
# Access parsed data
|
|
591
|
+
if result.parsed_data:
|
|
592
|
+
print(result.parsed_data[0])
|
|
593
|
+
else:
|
|
594
|
+
print(result.raw_output)
|
|
595
|
+
|
|
596
|
+
# Disconnect
|
|
597
|
+
api.disconnect(session)
|
|
598
|
+
"""
|
|
599
|
+
from PyQt6.QtWidgets import QApplication
|
|
600
|
+
clipboard = QApplication.clipboard()
|
|
601
|
+
clipboard.setText(sample)
|
|
602
|
+
|
|
603
|
+
QMessageBox.information(
|
|
604
|
+
self,
|
|
605
|
+
"Copied",
|
|
606
|
+
"Sample code copied to clipboard!\n\nPaste into IPython to get started."
|
|
607
|
+
)
|