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 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)
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
+ )