ntermqt 0.1.4__py3-none-any.whl → 0.1.5__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/api.py +926 -19
- {ntermqt-0.1.4.dist-info → ntermqt-0.1.5.dist-info}/METADATA +4 -5
- {ntermqt-0.1.4.dist-info → ntermqt-0.1.5.dist-info}/RECORD +13 -8
- nterm/examples/basic_terminal.py +0 -415
- {ntermqt-0.1.4.dist-info → ntermqt-0.1.5.dist-info}/WHEEL +0 -0
- {ntermqt-0.1.4.dist-info → ntermqt-0.1.5.dist-info}/entry_points.txt +0 -0
- {ntermqt-0.1.4.dist-info → ntermqt-0.1.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
import textfsm
|
|
3
|
+
from typing import Dict, List, Tuple, Optional
|
|
4
|
+
import io
|
|
5
|
+
import time
|
|
6
|
+
import click
|
|
7
|
+
from multiprocessing import Process, Queue
|
|
8
|
+
import multiprocessing
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ThreadSafeConnection:
|
|
15
|
+
"""Thread-local storage for SQLite connections"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, db_path: str, verbose: bool = False):
|
|
18
|
+
self.db_path = db_path
|
|
19
|
+
self.verbose = verbose
|
|
20
|
+
self._local = threading.local()
|
|
21
|
+
|
|
22
|
+
@contextmanager
|
|
23
|
+
def get_connection(self):
|
|
24
|
+
"""Get a thread-local connection"""
|
|
25
|
+
if not hasattr(self._local, 'connection'):
|
|
26
|
+
self._local.connection = sqlite3.connect(self.db_path)
|
|
27
|
+
self._local.connection.row_factory = sqlite3.Row
|
|
28
|
+
if self.verbose:
|
|
29
|
+
click.echo(f"Created new connection in thread {threading.get_ident()}")
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
yield self._local.connection
|
|
33
|
+
except Exception as e:
|
|
34
|
+
if hasattr(self._local, 'connection'):
|
|
35
|
+
self._local.connection.close()
|
|
36
|
+
delattr(self._local, 'connection')
|
|
37
|
+
raise e
|
|
38
|
+
|
|
39
|
+
def close_all(self):
|
|
40
|
+
"""Close connection if it exists for current thread"""
|
|
41
|
+
if hasattr(self._local, 'connection'):
|
|
42
|
+
self._local.connection.close()
|
|
43
|
+
delattr(self._local, 'connection')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TextFSMAutoEngine:
|
|
47
|
+
def __init__(self, db_path: str, verbose: bool = False):
|
|
48
|
+
self.db_path = db_path
|
|
49
|
+
self.verbose = verbose
|
|
50
|
+
self.connection_manager = ThreadSafeConnection(db_path, verbose)
|
|
51
|
+
|
|
52
|
+
def _calculate_template_score(
|
|
53
|
+
self,
|
|
54
|
+
parsed_data: List[Dict],
|
|
55
|
+
template: sqlite3.Row,
|
|
56
|
+
raw_output: str
|
|
57
|
+
) -> float:
|
|
58
|
+
"""
|
|
59
|
+
Score template match quality (0-100 scale).
|
|
60
|
+
|
|
61
|
+
Factors:
|
|
62
|
+
- Record count (0-30 pts): Did the template find data?
|
|
63
|
+
- Field richness (0-30 pts): How many fields per record?
|
|
64
|
+
- Population rate (0-25 pts): Are fields actually filled?
|
|
65
|
+
- Consistency (0-15 pts): Uniform data across records?
|
|
66
|
+
"""
|
|
67
|
+
if not parsed_data:
|
|
68
|
+
return 0.0
|
|
69
|
+
|
|
70
|
+
num_records = len(parsed_data)
|
|
71
|
+
num_fields = len(parsed_data[0].keys()) if parsed_data else 0
|
|
72
|
+
is_version_cmd = 'version' in template['cli_command'].lower()
|
|
73
|
+
|
|
74
|
+
# === Factor 1: Record Count (0-30 points) ===
|
|
75
|
+
if is_version_cmd:
|
|
76
|
+
# Version commands: expect exactly 1 record
|
|
77
|
+
record_score = 30.0 if num_records == 1 else max(0, 15 - (num_records - 1) * 5)
|
|
78
|
+
else:
|
|
79
|
+
# Diminishing returns: log scale capped at 30
|
|
80
|
+
# 1 rec = 10, 3 rec = 20, 10+ rec = 30
|
|
81
|
+
if num_records >= 10:
|
|
82
|
+
record_score = 30.0
|
|
83
|
+
elif num_records >= 3:
|
|
84
|
+
record_score = 20.0 + (num_records - 3) * (10.0 / 7.0)
|
|
85
|
+
else:
|
|
86
|
+
record_score = num_records * 10.0
|
|
87
|
+
|
|
88
|
+
# === Factor 2: Field Richness (0-30 points) ===
|
|
89
|
+
# More fields = richer data extraction
|
|
90
|
+
# 1-2 fields = weak, 3-5 = decent, 6-10 = good, 10+ = excellent
|
|
91
|
+
if num_fields >= 10:
|
|
92
|
+
field_score = 30.0
|
|
93
|
+
elif num_fields >= 6:
|
|
94
|
+
field_score = 20.0 + (num_fields - 6) * 2.5
|
|
95
|
+
elif num_fields >= 3:
|
|
96
|
+
field_score = 10.0 + (num_fields - 3) * (10.0 / 3.0)
|
|
97
|
+
else:
|
|
98
|
+
field_score = num_fields * 5.0
|
|
99
|
+
|
|
100
|
+
# === Factor 3: Population Rate (0-25 points) ===
|
|
101
|
+
# What percentage of cells have actual data?
|
|
102
|
+
total_cells = num_records * num_fields
|
|
103
|
+
populated_cells = 0
|
|
104
|
+
|
|
105
|
+
for record in parsed_data:
|
|
106
|
+
for value in record.values():
|
|
107
|
+
if value is not None and str(value).strip():
|
|
108
|
+
populated_cells += 1
|
|
109
|
+
|
|
110
|
+
population_rate = populated_cells / total_cells if total_cells > 0 else 0
|
|
111
|
+
population_score = population_rate * 25.0
|
|
112
|
+
|
|
113
|
+
# === Factor 4: Consistency (0-15 points) ===
|
|
114
|
+
# Are the same fields populated across all records?
|
|
115
|
+
if num_records > 1:
|
|
116
|
+
# Check which fields are populated in each record
|
|
117
|
+
field_fill_counts = {key: 0 for key in parsed_data[0].keys()}
|
|
118
|
+
|
|
119
|
+
for record in parsed_data:
|
|
120
|
+
for key, value in record.items():
|
|
121
|
+
if value is not None and str(value).strip():
|
|
122
|
+
field_fill_counts[key] += 1
|
|
123
|
+
|
|
124
|
+
# Consistency = fields that are either always filled or never filled
|
|
125
|
+
consistent_fields = sum(
|
|
126
|
+
1 for count in field_fill_counts.values()
|
|
127
|
+
if count == 0 or count == num_records
|
|
128
|
+
)
|
|
129
|
+
consistency_rate = consistent_fields / num_fields if num_fields > 0 else 0
|
|
130
|
+
consistency_score = consistency_rate * 15.0
|
|
131
|
+
else:
|
|
132
|
+
# Single record = perfectly consistent
|
|
133
|
+
consistency_score = 15.0
|
|
134
|
+
|
|
135
|
+
total_score = record_score + field_score + population_score + consistency_score
|
|
136
|
+
|
|
137
|
+
if self.verbose:
|
|
138
|
+
click.echo(f" Scoring: records={record_score:.1f}, fields={field_score:.1f}, "
|
|
139
|
+
f"population={population_score:.1f}, consistency={consistency_score:.1f} "
|
|
140
|
+
f"-> {total_score:.1f}")
|
|
141
|
+
|
|
142
|
+
return total_score
|
|
143
|
+
|
|
144
|
+
def find_best_template(self, device_output: str, filter_string: Optional[str] = None) -> Tuple[
|
|
145
|
+
Optional[str], Optional[List[Dict]], float, List[Tuple[str, float, int]]]:
|
|
146
|
+
"""Try filtered templates against the output and return the best match plus all non-zero scores."""
|
|
147
|
+
best_template = None
|
|
148
|
+
best_parsed_output = None
|
|
149
|
+
best_score = 0
|
|
150
|
+
all_scores = [] # List of (template_name, score, record_count)
|
|
151
|
+
|
|
152
|
+
with self.connection_manager.get_connection() as conn:
|
|
153
|
+
templates = self.get_filtered_templates(conn, filter_string)
|
|
154
|
+
total_templates = len(templates)
|
|
155
|
+
|
|
156
|
+
if self.verbose:
|
|
157
|
+
click.echo(f"Found {total_templates} matching templates for filter: {filter_string}")
|
|
158
|
+
|
|
159
|
+
for idx, template in enumerate(templates, 1):
|
|
160
|
+
if self.verbose:
|
|
161
|
+
percentage = (idx / total_templates) * 100
|
|
162
|
+
click.echo(f"\nTemplate {idx}/{total_templates} ({percentage:.1f}%): {template['cli_command']}")
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
textfsm_template = textfsm.TextFSM(io.StringIO(template['textfsm_content']))
|
|
166
|
+
parsed = textfsm_template.ParseText(device_output)
|
|
167
|
+
parsed_dicts = [dict(zip(textfsm_template.header, row)) for row in parsed]
|
|
168
|
+
score = self._calculate_template_score(parsed_dicts, template, device_output)
|
|
169
|
+
|
|
170
|
+
if self.verbose:
|
|
171
|
+
click.echo(f" -> Score={score:.2f}, Records={len(parsed_dicts)}")
|
|
172
|
+
|
|
173
|
+
# Track all non-zero scores
|
|
174
|
+
if score > 0:
|
|
175
|
+
all_scores.append((template['cli_command'], score, len(parsed_dicts)))
|
|
176
|
+
|
|
177
|
+
if score > best_score:
|
|
178
|
+
best_score = score
|
|
179
|
+
best_template = template['cli_command']
|
|
180
|
+
best_parsed_output = parsed_dicts
|
|
181
|
+
if self.verbose:
|
|
182
|
+
click.echo(click.style(" New best match!", fg='green'))
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
if self.verbose:
|
|
186
|
+
click.echo(f" -> Failed to parse: {str(e)}")
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
# Sort all_scores by score descending
|
|
190
|
+
all_scores.sort(key=lambda x: x[1], reverse=True)
|
|
191
|
+
|
|
192
|
+
return best_template, best_parsed_output, best_score, all_scores
|
|
193
|
+
|
|
194
|
+
def get_filtered_templates(self, connection: sqlite3.Connection, filter_string: Optional[str] = None):
|
|
195
|
+
"""Get filtered templates from database using provided connection."""
|
|
196
|
+
cursor = connection.cursor()
|
|
197
|
+
if filter_string:
|
|
198
|
+
filter_terms = filter_string.replace('-', '_').split('_')
|
|
199
|
+
query = "SELECT * FROM templates WHERE 1=1"
|
|
200
|
+
params = []
|
|
201
|
+
for term in filter_terms:
|
|
202
|
+
if term and len(term) > 2:
|
|
203
|
+
query += " AND cli_command LIKE ?"
|
|
204
|
+
params.append(f"%{term}%")
|
|
205
|
+
cursor.execute(query, params)
|
|
206
|
+
else:
|
|
207
|
+
cursor.execute("SELECT * FROM templates")
|
|
208
|
+
return cursor.fetchall()
|
|
209
|
+
|
|
210
|
+
def __del__(self):
|
|
211
|
+
"""Clean up connections on deletion"""
|
|
212
|
+
self.connection_manager.close_all()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# Example usage
|
|
216
|
+
if __name__ == '__main__':
|
|
217
|
+
multiprocessing.freeze_support()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# Example of using the engine in multiple threads
|
|
221
|
+
def worker(engine, output, filter_str):
|
|
222
|
+
result = engine.find_best_template(output, filter_str)
|
|
223
|
+
print(f"Thread {threading.get_ident()}: Found template: {result[0]}")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
engine = TextFSMAutoEngine("./secure_cartography/tfsm_templates.db", verbose=True)
|
|
227
|
+
threads = []
|
|
228
|
+
|
|
229
|
+
# Create multiple threads
|
|
230
|
+
for i in range(3):
|
|
231
|
+
t = threading.Thread(target=worker, args=(engine, "sample output", "show version"))
|
|
232
|
+
threads.append(t)
|
|
233
|
+
t.start()
|
|
234
|
+
|
|
235
|
+
# Wait for all threads to complete
|
|
236
|
+
for t in threads:
|
|
237
|
+
t.join()
|