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
nterm/parser/api_help_dialog.py
CHANGED
|
@@ -32,7 +32,7 @@ class APIHelpDialog(QDialog):
|
|
|
32
32
|
header.setFont(header_font)
|
|
33
33
|
layout.addWidget(header)
|
|
34
34
|
|
|
35
|
-
subtitle = QLabel("
|
|
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
|
-
|
|
75
|
+
Programmatic network automation from IPython, Python scripts, or as the foundation for MCP tools and agentic workflows.
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
Connect to devices, execute commands, and get structured data back - all using your existing nterm sessions and encrypted credentials.
|
|
77
78
|
|
|
78
|
-
|
|
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
|
-
**
|
|
84
|
-
-
|
|
85
|
-
-
|
|
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
|
-
**
|
|
90
|
-
-
|
|
91
|
-
- Automatic
|
|
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
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
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
|
-
|
|
115
|
+
Required for command parsing. Download via GUI:
|
|
114
116
|
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
-
|
|
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 -
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
#
|
|
155
|
-
print(result.parsed_data)
|
|
156
|
-
# [{'VERSION': '15.2(4)M11', 'HOSTNAME': 'wan-core-1', ...}]
|
|
158
|
+
# Platform-Aware Commands
|
|
157
159
|
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
#
|
|
172
|
+
# Try Multiple Commands (CDP/LLDP discovery)
|
|
163
173
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
api.
|
|
225
|
-
|
|
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
|
-
|
|
230
|
-
result = api.send(session, "
|
|
231
|
-
result = api.send(session, "
|
|
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
|
-
|
|
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 #
|
|
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
|
-
##
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
session
|
|
247
|
-
session
|
|
248
|
-
session
|
|
249
|
-
session
|
|
250
|
-
|
|
251
|
-
session
|
|
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
|
|
269
|
-
"""Create
|
|
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.
|
|
276
|
-
|
|
341
|
+
text.setMarkdown("""
|
|
342
|
+
# Platform-Aware Commands
|
|
277
343
|
|
|
278
|
-
|
|
279
|
-
api.unlock("password")
|
|
280
|
-
versions = {}
|
|
344
|
+
The `send_platform_command()` method automatically uses the correct syntax for the detected platform.
|
|
281
345
|
|
|
282
|
-
|
|
283
|
-
try:
|
|
284
|
-
session = api.connect(device.name)
|
|
285
|
-
result = api.send(session, "show version")
|
|
346
|
+
## Supported Platforms
|
|
286
347
|
|
|
287
|
-
|
|
288
|
-
ver = result.parsed_data[0].get('VERSION', 'unknown')
|
|
289
|
-
versions[device.name] = ver
|
|
348
|
+
Auto-detected from `show version` output:
|
|
290
349
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
305
|
-
|
|
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
|
-
|
|
310
|
-
|
|
382
|
+
# Get interface status
|
|
383
|
+
result = api.send_platform_command(session, 'interfaces_status')
|
|
311
384
|
|
|
312
|
-
|
|
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
|
-
#
|
|
316
|
-
|
|
317
|
-
|
|
391
|
+
# Get routing table
|
|
392
|
+
result = api.send_platform_command(session, 'routing_table')
|
|
393
|
+
```
|
|
318
394
|
|
|
319
|
-
|
|
395
|
+
## Try Multiple Commands
|
|
320
396
|
|
|
321
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
413
|
+
## Template Coverage
|
|
332
414
|
|
|
333
|
-
|
|
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
|
-
|
|
339
|
-
|
|
423
|
+
def _create_examples_tab(self) -> QWidget:
|
|
424
|
+
"""Create examples tab."""
|
|
425
|
+
widget = QWidget()
|
|
426
|
+
layout = QVBoxLayout(widget)
|
|
340
427
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
468
|
+
# Example 3: Platform-Aware Config Backup
|
|
469
|
+
from pathlib import Path
|
|
470
|
+
from datetime import datetime
|
|
369
471
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
472
|
+
api.unlock("password")
|
|
473
|
+
backup_dir = Path("config_backups")
|
|
474
|
+
backup_dir.mkdir(exist_ok=True)
|
|
373
475
|
|
|
374
|
-
api.
|
|
375
|
-
|
|
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
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
**
|
|
603
|
+
**Verify:**
|
|
434
604
|
```python
|
|
435
605
|
api.db_info()
|
|
436
|
-
#
|
|
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
|
-
##
|
|
638
|
+
## Parsing Failed
|
|
469
639
|
|
|
470
|
-
**
|
|
640
|
+
**Symptom:** `result.parsed_data` is `None`
|
|
471
641
|
|
|
472
642
|
**Debug:**
|
|
473
643
|
```python
|
|
474
|
-
|
|
475
|
-
device = api.device("device-name")
|
|
476
|
-
print(device)
|
|
644
|
+
result = api.send(session, "show version")
|
|
477
645
|
|
|
478
|
-
#
|
|
479
|
-
|
|
646
|
+
# Check raw output
|
|
647
|
+
print(result.raw_output[:200])
|
|
480
648
|
|
|
481
|
-
#
|
|
482
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
677
|
+
# Check platform was detected
|
|
678
|
+
print(session.platform) # Should not be None
|
|
492
679
|
|
|
493
|
-
#
|
|
494
|
-
|
|
680
|
+
# Try with debug enabled
|
|
681
|
+
session = api.connect("device", debug=True)
|
|
682
|
+
```
|
|
495
683
|
|
|
496
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
686
|
+
**Debug:**
|
|
687
|
+
```python
|
|
688
|
+
# Check device info
|
|
689
|
+
device = api.device("device-name")
|
|
690
|
+
print(device)
|
|
506
691
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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()
|
|
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
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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()
|