Skip to content

Commit 07ef285

Browse files
authored
Add traceroute streaming parser - traceroute-s (#669)
* test: split out test fixtures for long ipv6 traceroute for consistency * refactor(jc/parsers/traceroute): remove duplicate ParseError class * refactor(jc/parsers/traceroute): pre-process data in _loads() for easy-to-reuse * refactor(jc/parsers/traceroute): split hop serialization into separate function to reuse * refactor(jc/parsers/traceroute): simplify numeric conversion and make it reusable for traceroute_s * fix(jc/parsers/traceroute): stricter regex to match traceroute headers only * feat(jc/parsers/traceroute_s): v1.0 implementation * fix(jc/parsers/traceroute): revert "_" prefix in function and class names * fixup! fix(jc/parsers/traceroute): revert "_" prefix in function and class names * chore(jc/parsers/traceroute): update the author information
1 parent 467ad60 commit 07ef285

29 files changed

+5181
-70
lines changed

jc/lib.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@
214214
'top-s',
215215
'tracepath',
216216
'traceroute',
217+
'traceroute-s',
217218
'tune2fs',
218219
'udevadm',
219220
'ufw',

jc/parsers/traceroute.py

Lines changed: 52 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
from decimal import Decimal
120120
import jc.utils
121121
from copy import deepcopy
122+
from jc.exceptions import ParseError
122123

123124

124125
class info():
@@ -164,7 +165,7 @@ class info():
164165
SOFTWARE.
165166
'''
166167

167-
RE_HEADER = re.compile(r'(\S+)\s+\((\d+\.\d+\.\d+\.\d+|[0-9a-fA-F:]+)\)')
168+
RE_HEADER = re.compile(r'traceroute6? to (\S+)\s+\((\d+\.\d+\.\d+\.\d+|[0-9a-fA-F:]+)\)')
168169
RE_PROBE_NAME_IP = re.compile(r'(\S+)\s+\((\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|[0-9a-fA-F:]+)\)+')
169170
RE_PROBE_IP_ONLY = re.compile(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+([^\(])')
170171
RE_PROBE_IPV6_ONLY = re.compile(r'(([a-f0-9]*:)+[a-f0-9]+)')
@@ -291,8 +292,23 @@ def _get_probes(hop_string: str):
291292
return probes
292293

293294

294-
def _loads(data):
295-
lines = data.splitlines()
295+
def _loads(data: str, quiet: bool):
296+
lines = []
297+
298+
# remove any warning lines
299+
for data_line in data.splitlines():
300+
if 'traceroute: Warning: ' not in data_line and 'traceroute6: Warning: ' not in data_line:
301+
lines.append(data_line)
302+
else:
303+
continue
304+
305+
# check if header row exists, otherwise add a dummy header
306+
if not lines[0].startswith('traceroute to ') and not lines[0].startswith('traceroute6 to '):
307+
lines[:0] = ['traceroute to <<_>> (<<_>>), 30 hops max, 60 byte packets']
308+
309+
# print warning to STDERR
310+
if not quiet:
311+
jc.utils.warning_message(['No header row found. For destination info redirect STDERR to STDOUT'])
296312

297313
# Get headers
298314
match_dest = RE_HEADER.search(lines[0])
@@ -330,11 +346,28 @@ def _loads(data):
330346
return traceroute
331347

332348

333-
class ParseError(Exception):
334-
pass
349+
########################################################################################
335350

336351

337-
########################################################################################
352+
def _serialize_hop(hop: _Hop):
353+
hop_obj = {}
354+
hop_obj['hop'] = str(hop.idx)
355+
probe_list = []
356+
357+
if hop.probes:
358+
for probe in hop.probes:
359+
probe_obj = {
360+
'annotation': probe.annotation,
361+
'asn': None if probe.asn is None else str(probe.asn),
362+
'ip': probe.ip,
363+
'name': probe.name,
364+
'rtt': None if probe.rtt is None else str(probe.rtt)
365+
}
366+
probe_list.append(probe_obj)
367+
368+
hop_obj['probes'] = probe_list
369+
370+
return hop_obj
338371

339372

340373
def _process(proc_data):
@@ -349,26 +382,20 @@ def _process(proc_data):
349382
350383
Dictionary. Structured to conform to the schema.
351384
"""
352-
int_list = {'hop', 'asn'}
385+
int_list = {'hop', 'asn', 'max_hops', 'data_bytes'}
353386
float_list = {'rtt'}
354387

355-
if 'hops' in proc_data:
356-
for entry in proc_data['hops']:
357-
for key in entry:
358-
if key in int_list:
359-
entry[key] = jc.utils.convert_to_int(entry[key])
360-
361-
if key in float_list:
362-
entry[key] = jc.utils.convert_to_float(entry[key])
388+
for entry in proc_data.get('hops', []):
389+
_process(entry)
390+
for entry in proc_data.get('probes', []):
391+
_process(entry)
363392

364-
if 'probes' in entry:
365-
for item in entry['probes']:
366-
for key in item:
367-
if key in int_list:
368-
item[key] = jc.utils.convert_to_int(item[key])
393+
for key in proc_data:
394+
if key in int_list:
395+
proc_data[key] = jc.utils.convert_to_int(proc_data[key])
369396

370-
if key in float_list:
371-
item[key] = jc.utils.convert_to_float(item[key])
397+
if key in float_list:
398+
proc_data[key] = jc.utils.convert_to_float(proc_data[key])
372399

373400
return proc_data
374401

@@ -393,48 +420,11 @@ def parse(data, raw=False, quiet=False):
393420
raw_output = {}
394421

395422
if jc.utils.has_data(data):
396-
397-
# remove any warning lines
398-
new_data = []
399-
for data_line in data.splitlines():
400-
if 'traceroute: Warning: ' not in data_line and 'traceroute6: Warning: ' not in data_line:
401-
new_data.append(data_line)
402-
else:
403-
continue
404-
405-
# check if header row exists, otherwise add a dummy header
406-
if not new_data[0].startswith('traceroute to ') and not new_data[0].startswith('traceroute6 to '):
407-
new_data[:0] = ['traceroute to <<_>> (<<_>>), 30 hops max, 60 byte packets']
408-
409-
# print warning to STDERR
410-
if not quiet:
411-
jc.utils.warning_message(['No header row found. For destination info redirect STDERR to STDOUT'])
412-
413-
data = '\n'.join(new_data)
414-
415-
tr = _loads(data)
416-
hops = tr.hops
423+
tr = _loads(data, quiet)
417424
hops_list = []
418425

419-
if hops:
420-
for hop in hops:
421-
hop_obj = {}
422-
hop_obj['hop'] = str(hop.idx)
423-
probe_list = []
424-
425-
if hop.probes:
426-
for probe in hop.probes:
427-
probe_obj = {
428-
'annotation': probe.annotation,
429-
'asn': None if probe.asn is None else str(probe.asn),
430-
'ip': probe.ip,
431-
'name': probe.name,
432-
'rtt': None if probe.rtt is None else str(probe.rtt)
433-
}
434-
probe_list.append(probe_obj)
435-
436-
hop_obj['probes'] = probe_list
437-
hops_list.append(hop_obj)
426+
for hop in tr.hops:
427+
hops_list.append(_serialize_hop(hop))
438428

439429
raw_output = {
440430
'destination_ip': tr.dest_ip,

0 commit comments

Comments
 (0)