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.
@@ -32,7 +32,7 @@ class APIHelpDialog(QDialog):
32
32
  header.setFont(header_font)
33
33
  layout.addWidget(header)
34
34
 
35
- subtitle = QLabel("Access your network devices programmatically from IPython")
35
+ subtitle = QLabel("Programmatic network automation from IPython, Python scripts, or as the foundation for MCP tools")
36
36
  layout.addWidget(subtitle)
37
37
 
38
38
  layout.addSpacing(10)
@@ -42,6 +42,7 @@ class APIHelpDialog(QDialog):
42
42
  tabs.addTab(self._create_overview_tab(), "Overview")
43
43
  tabs.addTab(self._create_quickstart_tab(), "Quick Start")
44
44
  tabs.addTab(self._create_reference_tab(), "API Reference")
45
+ tabs.addTab(self._create_platform_tab(), "Platform Commands")
45
46
  tabs.addTab(self._create_examples_tab(), "Examples")
46
47
  tabs.addTab(self._create_troubleshooting_tab(), "Troubleshooting")
47
48
  layout.addWidget(tabs)
@@ -71,32 +72,33 @@ class APIHelpDialog(QDialog):
71
72
  text.setMarkdown("""
72
73
  # nterm Scripting API
73
74
 
74
- The nterm API provides programmatic access to your network devices from IPython.
75
+ Programmatic network automation from IPython, Python scripts, or as the foundation for MCP tools and agentic workflows.
75
76
 
76
- ## Features
77
+ Connect to devices, execute commands, and get structured data back - all using your existing nterm sessions and encrypted credentials.
77
78
 
78
- **Device Management**
79
- - Query saved devices and folders
80
- - Search by name, hostname, or tags
81
- - Access device metadata and connection history
79
+ ## Features
82
80
 
83
- **Secure Connections**
84
- - Encrypted credential vault with pattern matching
85
- - Auto-platform detection (Cisco, Arista, Juniper, etc.)
81
+ **Connection Management**
82
+ - Auto-credential resolution from encrypted vault
83
+ - Platform auto-detection (Cisco IOS/NX-OS/IOS-XE/XR, Arista EOS, Juniper)
84
+ - **Context manager** for automatic cleanup (`with api.session()`)
86
85
  - Legacy device support (RSA SHA-1 fallback)
87
86
  - Jump host support built-in
87
+ - Connection pooling and tracking
88
88
 
89
- **Command Execution**
90
- - Execute commands on network devices
91
- - Automatic TextFSM parsing for structured data
89
+ **Structured Data**
90
+ - **961 TextFSM templates** from networktocode/ntc-templates
91
+ - Automatic command parsing - raw text → List[Dict]
92
+ - **Platform-aware commands** - one call, correct syntax
92
93
  - Field normalization across vendors
93
94
  - Fallback to raw output if parsing fails
94
95
 
95
96
  **Developer Tools**
96
- - Rich data types with tab completion
97
- - Debugging tools for parsing issues
98
- - Database diagnostics
99
- - Connection tracking
97
+ - Rich dataclasses with tab completion
98
+ - `debug_parse()` for troubleshooting parsing
99
+ - `db_info()` for database diagnostics
100
+ - `disconnect_all()` for cleanup
101
+ - Comprehensive help system (F1 in GUI)
100
102
 
101
103
  ## Accessing the API
102
104
 
@@ -110,11 +112,12 @@ The API is pre-loaded in IPython sessions:
110
112
 
111
113
  **TextFSM Template Database**
112
114
 
113
- The API requires the TextFSM template database for parsing command output.
115
+ Required for command parsing. Download via GUI:
114
116
 
115
- - Download via: **Dev → Download NTC Templates...**
116
- - Select platforms you need (cisco_ios, arista_eos, etc.)
117
- - Database stored as `tfsm_templates.db`
117
+ - **Dev → Download NTC Templates...**
118
+ - Click **Fetch Available Platforms**
119
+ - Select platforms (cisco_ios, arista_eos, etc.)
120
+ - Click **Download Selected**
118
121
 
119
122
  **Credential Vault**
120
123
 
@@ -135,7 +138,7 @@ Store device credentials securely:
135
138
  text = QTextEdit()
136
139
  text.setReadOnly(True)
137
140
  text.setFont(QFont("Courier", 10))
138
- text.setText("""# Quick Start - First Connection
141
+ text.setText("""# Quick Start - Context Manager (Recommended)
139
142
 
140
143
  # 1. Unlock vault
141
144
  api.unlock("vault-password")
@@ -144,47 +147,63 @@ api.unlock("vault-password")
144
147
  api.devices()
145
148
  # [Device(wan-core-1, 172.16.128.1:22, cred=home_lab), ...]
146
149
 
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
+ # 3. Connect with context manager (auto-disconnects)
151
+ with api.session("wan-core-1") as session:
152
+ result = api.send(session, "show version")
153
+ print(result.parsed_data)
154
+ # [{'VERSION': '15.2(4)M11', 'HOSTNAME': 'wan-core-1', ...}]
155
+ # Session automatically closed here
150
156
 
151
- # 4. Execute a command
152
- result = api.send(session, "show version")
153
157
 
154
- # 5. Access parsed data
155
- print(result.parsed_data)
156
- # [{'VERSION': '15.2(4)M11', 'HOSTNAME': 'wan-core-1', ...}]
158
+ # Platform-Aware Commands
157
159
 
158
- # 6. Disconnect
159
- api.disconnect(session)
160
+ with api.session("wan-core-1") as s:
161
+ # Automatically uses correct syntax for platform
162
+ result = api.send_platform_command(s, 'config', parse=False)
163
+ print(f"Config: {len(result.raw_output)} bytes")
164
+
165
+ # Get version info
166
+ result = api.send_platform_command(s, 'version')
167
+
168
+ # Get BGP summary (works on Cisco, Arista, Juniper)
169
+ result = api.send_platform_command(s, 'bgp_summary')
160
170
 
161
171
 
162
- # Common Workflows
172
+ # Try Multiple Commands (CDP/LLDP discovery)
163
173
 
164
- # Search devices
165
- devices = api.search("leaf")
166
- devices = api.devices("eng-*")
167
- devices = api.devices(folder="Lab-ENG")
174
+ with api.session("wan-core-1") as s:
175
+ # Try CDP first, fall back to LLDP
176
+ result = api.send_first(s, [
177
+ "show cdp neighbors detail",
178
+ "show lldp neighbors detail",
179
+ ])
180
+
181
+ if result and result.parsed_data:
182
+ for neighbor in result.parsed_data:
183
+ print(f"{neighbor.get('NEIGHBOR', 'unknown')}")
168
184
 
169
- # Connect with specific credential
170
- session = api.connect("device", credential="lab-admin")
171
185
 
172
- # Check connection status
173
- if session.is_connected():
174
- result = api.send(session, "show ip route")
186
+ # Multi-Device Workflow
175
187
 
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
188
  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)
189
+ with api.session(device.name) as s:
190
+ result = api.send(s, "show version")
191
+
192
+ if result.parsed_data:
193
+ version = result.parsed_data[0]['VERSION']
194
+ print(f"{device.name}: {version}")
195
+ # Sessions auto-disconnect when exiting context
196
+
197
+
198
+ # Manual Connection (requires explicit disconnect)
199
+
200
+ session = api.connect("device-name")
201
+ result = api.send(session, "show version")
202
+ api.disconnect(session)
203
+
204
+ # Cleanup all sessions
205
+ count = api.disconnect_all()
206
+ print(f"Disconnected {count} session(s)")
188
207
  """)
189
208
  layout.addWidget(text)
190
209
  return widget
@@ -204,207 +223,358 @@ for device in api.devices("spine-*"):
204
223
  api.devices() # List all devices
205
224
  api.devices("pattern*") # Filter by glob pattern
206
225
  api.devices(folder="Lab-ENG") # Filter by folder
207
- api.search("query") # Search by name/hostname
226
+ api.search("query") # Search by name/hostname/description
208
227
  api.device("name") # Get specific device
209
228
  api.folders() # List all folders
210
229
 
230
+ DeviceInfo fields: name, hostname, port, folder, credential,
231
+ last_connected, connect_count
232
+
211
233
  ## Credential Operations (requires unlocked vault)
212
234
 
213
235
  api.unlock("password") # Unlock vault
214
236
  api.lock() # Lock vault
215
- api.credentials() # List all credentials
237
+ api.credentials() # List credentials (no secrets)
216
238
  api.credentials("*admin*") # Filter by pattern
217
239
  api.credential("name") # Get specific credential
218
240
  api.resolve_credential("host") # Find matching credential
219
241
 
242
+ Properties:
243
+ api.vault_initialized # Vault exists
244
+ api.vault_unlocked # Vault is unlocked
245
+
246
+ CredentialInfo fields: name, username, has_password, has_key,
247
+ match_hosts, match_tags, is_default
248
+
220
249
  ## Connection Operations
221
250
 
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
251
+ # Context manager (recommended) - auto-disconnects on exit
252
+ with api.session("device-name") as session:
253
+ result = api.send(session, "show version")
254
+ # Session automatically closed here
255
+
256
+ # Manual connection (requires explicit disconnect)
257
+ session = api.connect("device-name")
258
+ session = api.connect("device", credential="cred-name")
259
+ session = api.connect("192.168.1.1", debug=True)
260
+
261
+ # Session attributes
262
+ session.device_name # Device name
263
+ session.hostname # IP/hostname
264
+ session.platform # 'cisco_ios', 'arista_eos', etc.
265
+ session.prompt # Device prompt
266
+ session.is_connected() # Check if active
267
+ session.connected_at # Timestamp
268
+
269
+ # Disconnect
270
+ api.disconnect(session) # Single session
271
+ api.disconnect_all() # All active sessions
272
+ api.active_sessions() # List open connections
226
273
 
227
274
  ## Command Execution
228
275
 
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
276
+ # Execute with parsing (default)
277
+ result = api.send(session, "show version")
278
+ result = api.send(session, "show interfaces")
233
279
 
234
- ## Result Access
280
+ # Options
281
+ result = api.send(session, "cmd", parse=False) # Raw output only
282
+ result = api.send(session, "cmd", timeout=60) # Custom timeout
283
+ result = api.send(session, "cmd", normalize=False) # Keep vendor field names
235
284
 
285
+ # Result attributes
286
+ result.command # Command that was run
236
287
  result.raw_output # Raw text from device
237
- result.parsed_data # Parsed List[Dict] or None
288
+ result.parsed_data # List[Dict] or None
238
289
  result.platform # Detected platform
239
290
  result.parse_success # Whether parsing worked
240
291
  result.parse_template # Template used
292
+ result.timestamp # When command was run
241
293
  result.to_dict() # Export as dictionary
242
294
 
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
295
+ ## Platform-Aware Commands (NEW)
296
+
297
+ # Automatically uses correct syntax for detected platform
298
+ result = api.send_platform_command(session, 'config', parse=False)
299
+ result = api.send_platform_command(session, 'version')
300
+ result = api.send_platform_command(session, 'interfaces_status')
301
+ result = api.send_platform_command(session, 'interface_detail', name='Gi0/1')
302
+ result = api.send_platform_command(session, 'bgp_summary')
303
+ result = api.send_platform_command(session, 'routing_table')
304
+
305
+ # See "Platform Commands" tab for full list
306
+
307
+ ## Try Multiple Commands (NEW)
308
+
309
+ # Try commands in order until one succeeds
310
+ result = api.send_first(session, [
311
+ "show cdp neighbors detail",
312
+ "show lldp neighbors detail",
313
+ ])
314
+
315
+ # Options
316
+ result = api.send_first(
317
+ session,
318
+ commands,
319
+ parse=True, # Attempt parsing
320
+ timeout=30, # Per-command timeout
321
+ require_parsed=True, # Only succeed if parsed_data is non-empty
322
+ )
252
323
 
253
324
  ## Debugging
254
325
 
255
326
  api.debug_parse(cmd, output, platform) # Debug parsing issues
256
327
  api.db_info() # Database diagnostics
257
328
  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
329
+ api.help() # Show all commands
264
330
  """)
265
331
  layout.addWidget(text)
266
332
  return widget
267
333
 
268
- def _create_examples_tab(self) -> QWidget:
269
- """Create examples tab."""
334
+ def _create_platform_tab(self) -> QWidget:
335
+ """Create platform commands tab."""
270
336
  widget = QWidget()
271
337
  layout = QVBoxLayout(widget)
272
338
 
273
339
  text = QTextEdit()
274
340
  text.setReadOnly(True)
275
- text.setFont(QFont("Courier", 9))
276
- text.setText("""# Examples
341
+ text.setMarkdown("""
342
+ # Platform-Aware Commands
277
343
 
278
- # Example 1: Collect Software Versions
279
- api.unlock("password")
280
- versions = {}
344
+ The `send_platform_command()` method automatically uses the correct syntax for the detected platform.
281
345
 
282
- for device in api.devices():
283
- try:
284
- session = api.connect(device.name)
285
- result = api.send(session, "show version")
346
+ ## Supported Platforms
286
347
 
287
- if result.parsed_data:
288
- ver = result.parsed_data[0].get('VERSION', 'unknown')
289
- versions[device.name] = ver
348
+ Auto-detected from `show version` output:
290
349
 
291
- api.disconnect(session)
292
- except Exception as e:
293
- print(f"Failed on {device.name}: {e}")
350
+ - `cisco_ios` - Cisco IOS
351
+ - `cisco_nxos` - Cisco Nexus NX-OS
352
+ - `cisco_iosxe` - Cisco IOS-XE
353
+ - `cisco_iosxr` - Cisco IOS-XR
354
+ - `arista_eos` - Arista EOS
355
+ - `juniper_junos` - Juniper Junos
294
356
 
295
- # Print results
296
- for name, ver in sorted(versions.items()):
297
- print(f"{name:20} {ver}")
357
+ ## Available Command Types
298
358
 
359
+ | Command Type | Cisco IOS | Arista EOS | Juniper |
360
+ |--------------|-----------|------------|---------|
361
+ | `config` | show running-config | show running-config | show configuration |
362
+ | `version` | show version | show version | show version |
363
+ | `interfaces` | show interfaces | show interfaces | show interfaces |
364
+ | `interfaces_status` | show interfaces status | show interfaces status | show interfaces terse |
365
+ | `interface_detail` | show interfaces {name} | show interfaces {name} | show interfaces {name} |
366
+ | `neighbors_cdp` | show cdp neighbors detail | show lldp neighbors detail | - |
367
+ | `neighbors_lldp` | show lldp neighbors detail | show lldp neighbors detail | show lldp neighbors |
368
+ | `neighbors` | show cdp neighbors detail | show lldp neighbors detail | show lldp neighbors |
369
+ | `routing_table` | show ip route | show ip route | show route |
370
+ | `bgp_summary` | show ip bgp summary | show ip bgp summary | show bgp summary |
371
+ | `bgp_neighbors` | show ip bgp neighbors | show ip bgp neighbors | show bgp neighbor |
299
372
 
300
- # Example 2: Find Interfaces with Errors
301
- session = api.connect("core-switch")
302
- result = api.send(session, "show interfaces")
373
+ ## Usage Examples
374
+
375
+ ```python
376
+ # Get running config (works on any platform)
377
+ result = api.send_platform_command(session, 'config', parse=False)
303
378
 
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))
379
+ # Get version info
380
+ result = api.send_platform_command(session, 'version')
308
381
 
309
- if in_errors > 0 or out_errors > 0:
310
- print(f"{intf['interface']}: in={in_errors}, out={out_errors}")
382
+ # Get interface status
383
+ result = api.send_platform_command(session, 'interfaces_status')
311
384
 
312
- api.disconnect(session)
385
+ # Get specific interface
386
+ result = api.send_platform_command(session, 'interface_detail', name='Gi0/1')
313
387
 
388
+ # Get BGP summary
389
+ result = api.send_platform_command(session, 'bgp_summary')
314
390
 
315
- # Example 3: Configuration Backup
316
- import json
317
- from datetime import datetime
391
+ # Get routing table
392
+ result = api.send_platform_command(session, 'routing_table')
393
+ ```
318
394
 
319
- backups = {}
395
+ ## Try Multiple Commands
320
396
 
321
- for device in api.devices(folder="Production"):
322
- session = api.connect(device.name)
323
- result = api.send(session, "show running-config", parse=False)
397
+ For discovery across platforms with different capabilities:
324
398
 
325
- backups[device.name] = {
326
- 'hostname': device.hostname,
327
- 'config': result.raw_output,
328
- 'timestamp': datetime.now().isoformat(),
329
- }
399
+ ```python
400
+ # Try CDP first, fall back to LLDP
401
+ result = api.send_first(session, [
402
+ "show cdp neighbors detail",
403
+ "show lldp neighbors detail",
404
+ ])
405
+
406
+ # Platform-agnostic config fetch
407
+ result = api.send_first(session, [
408
+ "show running-config",
409
+ "show configuration",
410
+ ], parse=False, require_parsed=False)
411
+ ```
330
412
 
331
- api.disconnect(session)
413
+ ## Template Coverage
332
414
 
333
- # Save to file
334
- with open('backups.json', 'w') as f:
335
- json.dump(backups, f, indent=2)
415
+ **961 TextFSM templates** available for 69 platforms via ntc-templates.
336
416
 
417
+ Download additional templates via:
418
+ **Dev → Download NTC Templates...**
419
+ """)
420
+ layout.addWidget(text)
421
+ return widget
337
422
 
338
- # Example 4: Audit Device Reachability
339
- api.unlock("password")
423
+ def _create_examples_tab(self) -> QWidget:
424
+ """Create examples tab."""
425
+ widget = QWidget()
426
+ layout = QVBoxLayout(widget)
340
427
 
341
- print("Testing device connectivity...")
342
- print(f"{'Device':<20} {'Status':<10} {'Platform':<15}")
343
- print("-" * 45)
428
+ text = QTextEdit()
429
+ text.setReadOnly(True)
430
+ text.setFont(QFont("Courier", 9))
431
+ text.setText("""# Examples
432
+
433
+ # Example 1: Collect Software Versions (Context Manager)
434
+ api.unlock("password")
435
+ versions = {}
344
436
 
345
437
  for device in api.devices():
346
438
  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)
439
+ with api.session(device.name) as s:
440
+ result = api.send(s, "show version")
441
+
442
+ if result.parsed_data:
443
+ ver = result.parsed_data[0].get('VERSION', 'unknown')
444
+ versions[device.name] = ver
351
445
  except Exception as e:
352
- print(f"{device.name:<20} {'DOWN':<10} {str(e)[:15]}")
446
+ print(f"Failed on {device.name}: {e}")
353
447
 
448
+ # Print results
449
+ for name, ver in sorted(versions.items()):
450
+ print(f"{name:20} {ver}")
354
451
 
355
- # Example 5: Compare Configurations
356
- session1 = api.connect("spine-1")
357
- session2 = api.connect("spine-2")
358
452
 
359
- config1 = api.send(session1, "show run", parse=False).raw_output
360
- config2 = api.send(session2, "show run", parse=False).raw_output
453
+ # Example 2: CDP/LLDP Neighbor Discovery
454
+ with api.session("wan-core-1") as s:
455
+ # Automatically tries CDP then LLDP
456
+ result = api.send_first(s, [
457
+ "show cdp neighbors detail",
458
+ "show lldp neighbors detail",
459
+ ])
460
+
461
+ if result and result.parsed_data:
462
+ for neighbor in result.parsed_data:
463
+ local_intf = neighbor.get('LOCAL_INTERFACE', 'unknown')
464
+ remote = neighbor.get('NEIGHBOR', 'unknown')
465
+ print(f"{remote:30} via {local_intf}")
361
466
 
362
- # Find differences
363
- lines1 = set(config1.split('\\n'))
364
- lines2 = set(config2.split('\\n'))
365
467
 
366
- print("Only in spine-1:")
367
- for line in sorted(lines1 - lines2)[:10]:
368
- print(f" {line}")
468
+ # Example 3: Platform-Aware Config Backup
469
+ from pathlib import Path
470
+ from datetime import datetime
369
471
 
370
- print("\\nOnly in spine-2:")
371
- for line in sorted(lines2 - lines1)[:10]:
372
- print(f" {line}")
472
+ api.unlock("password")
473
+ backup_dir = Path("config_backups")
474
+ backup_dir.mkdir(exist_ok=True)
373
475
 
374
- api.disconnect(session1)
375
- api.disconnect(session2)
476
+ for device in api.devices(folder="Production"):
477
+ try:
478
+ with api.session(device.name) as s:
479
+ # Works on Cisco, Arista, Juniper - picks correct command
480
+ result = api.send_platform_command(s, 'config', parse=False)
481
+
482
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
483
+ filename = backup_dir / f"{device.name}_{timestamp}.cfg"
484
+ filename.write_text(result.raw_output)
485
+
486
+ print(f"✓ {device.name}: {len(result.raw_output)} bytes")
487
+ except Exception as e:
488
+ print(f"✗ {device.name}: {e}")
489
+
490
+ print(f"\\nBackups saved to: {backup_dir}")
376
491
 
377
492
 
378
- # Example 6: Device Inventory Report
493
+ # Example 4: Interface Error Report
494
+ with api.session("distribution-1") as s:
495
+ result = api.send(s, "show interfaces")
496
+
497
+ if result.parsed_data:
498
+ errors_found = False
499
+ for intf in result.parsed_data:
500
+ in_errors = int(intf.get('in_errors', 0) or 0)
501
+ out_errors = int(intf.get('out_errors', 0) or 0)
502
+
503
+ if in_errors > 0 or out_errors > 0:
504
+ errors_found = True
505
+ print(f"{intf['interface']:15} IN: {in_errors:>8} OUT: {out_errors:>8}")
506
+
507
+ if not errors_found:
508
+ print("No interface errors found")
509
+
510
+
511
+ # Example 5: Device Inventory CSV
379
512
  import csv
513
+ from nterm.scripting.platform_utils import extract_version_info
380
514
 
381
515
  api.unlock("password")
382
516
 
383
517
  with open('inventory.csv', 'w', newline='') as f:
384
518
  writer = csv.writer(f)
385
519
  writer.writerow(['Device', 'IP', 'Platform', 'Version', 'Serial', 'Uptime'])
386
-
520
+
387
521
  for device in api.devices():
388
522
  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)
523
+ with api.session(device.name) as s:
524
+ result = api.send_platform_command(s, 'version')
525
+
526
+ if result and result.parsed_data:
527
+ info = extract_version_info(result.parsed_data, s.platform)
528
+ writer.writerow([
529
+ device.name,
530
+ device.hostname,
531
+ s.platform,
532
+ info.get('version', ''),
533
+ info.get('serial', ''),
534
+ info.get('uptime', ''),
535
+ ])
536
+ else:
537
+ writer.writerow([device.name, device.hostname, s.platform, '', '', ''])
404
538
  except Exception as e:
405
539
  writer.writerow([device.name, device.hostname, 'ERROR', str(e), '', ''])
406
540
 
407
541
  print("Inventory saved to inventory.csv")
542
+
543
+
544
+ # Example 6: Multi-Device BGP Summary
545
+ api.unlock("password")
546
+ bgp_report = {}
547
+
548
+ for device in api.devices("*spine*"):
549
+ try:
550
+ with api.session(device.name) as s:
551
+ result = api.send_platform_command(s, 'bgp_summary')
552
+
553
+ if result and result.parsed_data:
554
+ neighbor_count = len(result.parsed_data)
555
+ established = sum(1 for n in result.parsed_data
556
+ if str(n.get('STATE_PFXRCD', '')).isdigit())
557
+ bgp_report[device.name] = {
558
+ 'total': neighbor_count,
559
+ 'established': established,
560
+ }
561
+ except Exception as e:
562
+ bgp_report[device.name] = {'error': str(e)}
563
+
564
+ # Print report
565
+ print(f"{'Device':<25} {'Total':<10} {'Established':<10}")
566
+ print("-" * 45)
567
+ for device, info in sorted(bgp_report.items()):
568
+ if 'error' in info:
569
+ print(f"{device:<25} ERROR: {info['error']}")
570
+ else:
571
+ print(f"{device:<25} {info['total']:<10} {info['established']:<10}")
572
+
573
+
574
+ # Example 7: Cleanup All Sessions
575
+ # At end of script or in finally block
576
+ count = api.disconnect_all()
577
+ print(f"Disconnected {count} session(s)")
408
578
  """)
409
579
  layout.addWidget(text)
410
580
  return widget
@@ -430,10 +600,10 @@ print("Inventory saved to inventory.csv")
430
600
  4. Click **Download Selected**
431
601
  5. Restart IPython session
432
602
 
433
- **Check status:**
603
+ **Verify:**
434
604
  ```python
435
605
  api.db_info()
436
- # Shows: db_path, db_exists, db_size
606
+ # {'db_exists': True, 'db_size_mb': 0.3, ...}
437
607
  ```
438
608
 
439
609
  ## Vault Locked
@@ -465,54 +635,82 @@ api.credentials() # List all credentials
465
635
  api.resolve_credential("192.168.1.1") # Check which would match
466
636
  ```
467
637
 
468
- ## Connection Failed
638
+ ## Parsing Failed
469
639
 
470
- **Error:** Connection timeout or authentication failure
640
+ **Symptom:** `result.parsed_data` is `None`
471
641
 
472
642
  **Debug:**
473
643
  ```python
474
- # Check device info
475
- device = api.device("device-name")
476
- print(device)
644
+ result = api.send(session, "show version")
477
645
 
478
- # Try with different credential
479
- session = api.connect("device", credential="other-cred")
646
+ # Check raw output
647
+ print(result.raw_output[:200])
480
648
 
481
- # Check if device is reachable
482
- # (try from terminal first)
649
+ # Debug parsing
650
+ debug = api.debug_parse("show version", result.raw_output, session.platform)
651
+ print(debug)
652
+ # Shows: template_used, best_score, all_scores, error
483
653
  ```
484
654
 
485
- ## Parsing Failed
655
+ **Common causes:**
656
+ - Template doesn't exist for this platform/command
657
+ - Platform not detected (check `session.platform`)
658
+ - Output format non-standard
659
+ - Database missing templates (download more)
660
+
661
+ **Workaround:**
662
+ ```python
663
+ result = api.send(session, "show version", parse=False)
664
+ print(result.raw_output)
665
+ ```
486
666
 
487
- **Symptom:** `result.parsed_data` is None but command succeeded
667
+ ## Paging Not Disabled
668
+
669
+ **Error:** `PagingNotDisabledError: Paging prompt '--More--' detected`
670
+
671
+ **Cause:** Terminal paging wasn't disabled before command execution.
672
+
673
+ This indicates a problem with platform detection or session setup. The API automatically sends `terminal length 0` (or equivalent) after connecting.
488
674
 
489
675
  **Debug:**
490
676
  ```python
491
- result = api.send(session, "show version")
677
+ # Check platform was detected
678
+ print(session.platform) # Should not be None
492
679
 
493
- # Check raw output
494
- print(result.raw_output[:200])
680
+ # Try with debug enabled
681
+ session = api.connect("device", debug=True)
682
+ ```
495
683
 
496
- # Debug parsing
497
- debug = api.debug_parse(
498
- command="show version",
499
- output=result.raw_output,
500
- platform=session.platform
501
- )
684
+ ## Connection Failed
502
685
 
503
- print(debug)
504
- # Shows: template_used, best_score, all_scores, error
505
- ```
686
+ **Debug:**
687
+ ```python
688
+ # Check device info
689
+ device = api.device("device-name")
690
+ print(device)
506
691
 
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
692
+ # Try with different credential
693
+ session = api.connect("device", credential="other-cred")
694
+
695
+ # Check credentials
696
+ api.credentials()
697
+ api.resolve_credential("192.168.1.1")
698
+
699
+ # Enable debug mode
700
+ session = api.connect("device", debug=True)
701
+ ```
512
702
 
513
703
  ## Platform Not Detected
514
704
 
515
- **Symptom:** `session.platform` is None
705
+ **Symptom:** `session.platform` is `None`
706
+
707
+ Platform detection looks for keywords in `show version` output:
708
+ - Cisco IOS: "Cisco IOS Software"
709
+ - Cisco NX-OS: "Cisco Nexus Operating System"
710
+ - Arista: "Arista", "vEOS"
711
+ - Juniper: "JUNOS"
712
+
713
+ If not detected, parsing won't work automatically.
516
714
 
517
715
  **Debug:**
518
716
  ```python
@@ -524,14 +722,6 @@ result = api.send(session, "show version", parse=False)
524
722
  print(result.raw_output[:500])
525
723
  ```
526
724
 
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
725
  ## Session Stuck
536
726
 
537
727
  **Symptom:** Command hangs or times out
@@ -542,11 +732,14 @@ If not detected, parsing won't work. Set manually if needed (not currently suppo
542
732
  result = api.send(session, "show tech-support", timeout=120)
543
733
 
544
734
  # Check if session still active
545
- session.is_connected() # Returns 1 or 0
735
+ session.is_connected()
546
736
 
547
737
  # Disconnect and reconnect
548
738
  api.disconnect(session)
549
739
  session = api.connect("device")
740
+
741
+ # Or cleanup all
742
+ api.disconnect_all()
550
743
  ```
551
744
 
552
745
  ## Getting Help
@@ -566,6 +759,7 @@ result? # Show CommandResult help
566
759
  - **Dev → API Help...** (this dialog)
567
760
  - **Dev → Download NTC Templates...**
568
761
  - **Edit → Credential Manager...**
762
+ - Press **F1** for comprehensive help
569
763
  """)
570
764
  layout.addWidget(text)
571
765
  return widget
@@ -573,7 +767,6 @@ result? # Show CommandResult help
573
767
  def _copy_sample_code(self):
574
768
  """Copy sample code to clipboard."""
575
769
  sample = """# nterm API Quick Start
576
- from nterm.scripting import api
577
770
 
578
771
  # Unlock vault
579
772
  api.unlock("vault-password")
@@ -581,20 +774,30 @@ api.unlock("vault-password")
581
774
  # List devices
582
775
  devices = api.devices()
583
776
 
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
777
+ # Connect with context manager (recommended)
778
+ with api.session("device-name") as s:
779
+ # Execute command with parsing
780
+ result = api.send(s, "show version")
781
+
782
+ # Access parsed data
783
+ if result.parsed_data:
784
+ print(result.parsed_data[0])
785
+ else:
786
+ print(result.raw_output)
787
+
788
+ # Platform-aware command
789
+ config = api.send_platform_command(s, 'config', parse=False)
790
+ print(f"Config size: {len(config.raw_output)} bytes")
791
+
792
+ # Session auto-disconnects when exiting 'with' block
793
+
794
+ # Manual connection (if needed)
795
+ session = api.connect("other-device")
796
+ result = api.send(session, "show interfaces")
597
797
  api.disconnect(session)
798
+
799
+ # Cleanup all sessions
800
+ api.disconnect_all()
598
801
  """
599
802
  from PyQt6.QtWidgets import QApplication
600
803
  clipboard = QApplication.clipboard()