diff --git a/jc/parsers/iftop.py b/jc/parsers/iftop.py new file mode 100644 index 00000000..d1e3ceb6 --- /dev/null +++ b/jc/parsers/iftop.py @@ -0,0 +1,623 @@ +"""jc - JSON Convert `iftop` command output parser + +Some of `iftop` options are supported. + + +Usage (cli): + + $ iftop -i -t -P -s 1 | jc --iftop + $ iftop -i -t -B -s1 | jc --iftop + +Usage (module): + + import jc + result = jc.parse('iftop', iftop_command_output) + +Schema: + + [ + { + "device": string, + "ip_address": string, + "mac_address": string, + "clients": [ + { + "index": integer, + "connections": [ + { + "host_name": string, + "host_port": string, # can be service or missing + "last_2s": string, + "last_10s": string, + "last_40s": string, + "cumulative": string, + "direction": string + } + ] + } + ] + "total_send_rate": { + "last_2s": string, + "last_10s": string, + "last_40s": string + } + "total_receive_rate": { + "last_2s": string, + "last_10s": string, + "last_40s": string + } + "total_send_and_receive_rate": { + "last_2s": string, + "last_10s": string, + "last_40s": string + } + "peak_rate": { + "last_2s": string, + "last_10s": string, + "last_40s": string + } + "cumulative_rate": { + "last_2s": string, + "last_10s": string, + "last_40s": string + } + + +Examples: + + $ iftop -i eno0 -t -P -s 1 | jc --iftop -p -r + [ + { + "device": "enp0s3", + "ip_address": "10.10.15.129", + "mac_address": "11:22:33:44:55:66", + "clients": [ + { + "index": 1, + "connections": [ + { + "host_name": "ubuntu-2004-clean-01", + "host_port": "ssh", + "last_2s": "448b", + "last_10s": "448b", + "last_40s": "448b", + "cumulative": "112B", + "direction": "send" + }, + { + "host_name": "10.10.15.72", + "host_port": "40876", + "last_2s": "208b", + "last_10s": "208b", + "last_40s": "208b", + "cumulative": "52B", + "direction": "receive" + } + ] + } + ], + "total_send_rate": { + "last_2s": "448b", + "last_10s": "448b", + "last_40s": "448b" + }, + "total_receive_rate": { + "last_2s": "208b", + "last_10s": "208b", + "last_40s": "208b" + }, + "total_send_and_receive_rate": { + "last_2s": "656b", + "last_10s": "656b", + "last_40s": "656b" + }, + "peak_rate": { + "last_2s": "448b", + "last_10s": "208b", + "last_40s": "656b" + }, + "cumulative_rate": { + "last_2s": "112B", + "last_10s": "52B", + "last_40s": "164B" + } + } + ] + +""" +import re +from typing import List, Dict +from jc.jc_types import JSONDictType +import jc.utils +from collections import namedtuple +from numbers import Number + + +class info: + """Provides parser metadata (version, author, etc.)""" + + version = "0.1" + description = "`iftop` command parser" + author = "Ron Green" + author_email = "11993626+georgettica@users.noreply.github.com" + compatible = ["linux"] + tags = ["command"] + + +__version__ = info.version + + +def _process(proc_data: List[JSONDictType], quiet: bool = False) -> List[JSONDictType]: + """ + Final processing to conform to the schema. + + Parameters: + + proc_data: (List of Dictionaries) raw structured data to process + + Returns: + + List of Dictionaries. Structured to conform to the schema. + """ + string_to_bytes_fields = ["last_2s", "last_10s", "last_40s", "cumulative"] + one_nesting = [ + "total_send_rate", + "total_receive_rate", + "total_send_and_receive_rate", + "peak_rate", + "cumulative_rate", + ] + if not proc_data: + return proc_data + for entry in proc_data: + # print(f"{entry=}") + for entry_key in entry: + # print(f"{entry_key=}") + if entry_key in one_nesting: + # print(f"{entry[entry_key]=}") + for one_nesting_item_key in entry[entry_key]: + # print(f"{one_nesting_item_key=}") + if one_nesting_item_key in string_to_bytes_fields: + entry[entry_key][one_nesting_item_key] = humanfriendly_parse_size(entry[entry_key][one_nesting_item_key]) + elif entry_key == "clients": + for client in entry[entry_key]: + # print(f"{client=}") + if "connections" not in client: + continue + for connection in client["connections"]: + # print(f"{connection=}") + for connection_key in connection: + # print(f"{connection_key=}") + if connection_key in string_to_bytes_fields: + connection[connection_key] = humanfriendly_parse_size(connection[connection_key]) + return proc_data + + +# Named tuples to define units of size. +SizeUnit = namedtuple('SizeUnit', 'divider, symbol, name') +CombinedUnit = namedtuple('CombinedUnit', 'decimal, binary') + +# Differences between Python 2 and 3. +try: + # Python 2. + basestring = basestring +except (ImportError, NameError): + # Python 3. + basestring = str + +def humanfriendly_is_string(value): + """ + Check if a value is a :func:`python2:basestring` (in Python 2) or :class:`python3:str` (in Python 3) object. + + :param value: The value to check. + :returns: :data:`True` if the value is a string, :data:`False` otherwise. + """ + return isinstance(value, basestring) + +# Common disk size units in binary (base-2) and decimal (base-10) multiples. +disk_size_units = ( + CombinedUnit(SizeUnit(1000**1, 'KB', 'kilobyte'), SizeUnit(1024**1, 'KiB', 'kibibyte')), + CombinedUnit(SizeUnit(1000**2, 'MB', 'megabyte'), SizeUnit(1024**2, 'MiB', 'mebibyte')), + CombinedUnit(SizeUnit(1000**3, 'GB', 'gigabyte'), SizeUnit(1024**3, 'GiB', 'gibibyte')), + CombinedUnit(SizeUnit(1000**4, 'TB', 'terabyte'), SizeUnit(1024**4, 'TiB', 'tebibyte')), + CombinedUnit(SizeUnit(1000**5, 'PB', 'petabyte'), SizeUnit(1024**5, 'PiB', 'pebibyte')), + CombinedUnit(SizeUnit(1000**6, 'EB', 'exabyte'), SizeUnit(1024**6, 'EiB', 'exbibyte')), + CombinedUnit(SizeUnit(1000**7, 'ZB', 'zettabyte'), SizeUnit(1024**7, 'ZiB', 'zebibyte')), + CombinedUnit(SizeUnit(1000**8, 'YB', 'yottabyte'), SizeUnit(1024**8, 'YiB', 'yobibyte')), +) + +class HumanfriendlyInvalidSize(Exception): + pass + +def humanfriendly_parse_size(size, binary=False): + """ + Parse a human readable data size and return the number of bytes. + + :param size: The human readable file size to parse (a string). + :param binary: :data:`True` to use binary multiples of bytes (base-2) for + ambiguous unit symbols and names, :data:`False` to use + decimal multiples of bytes (base-10). + :returns: The corresponding size in bytes (an integer). + :raises: :exc:`InvalidSize` when the input can't be parsed. + + This function knows how to parse sizes in bytes, kilobytes, megabytes, + gigabytes, terabytes and petabytes. Some examples: + + >>> from humanfriendly import parse_size + >>> parse_size('42') + 42 + >>> parse_size('13b') + 13 + >>> parse_size('5 bytes') + 5 + >>> parse_size('1 KB') + 1000 + >>> parse_size('1 kilobyte') + 1000 + >>> parse_size('1 KiB') + 1024 + >>> parse_size('1 KB', binary=True) + 1024 + >>> parse_size('1.5 GB') + 1500000000 + >>> parse_size('1.5 GB', binary=True) + 1610612736 + """ + tokens = humanfriendly_tokenize(size) + if tokens and isinstance(tokens[0], Number): + # Get the normalized unit (if any) from the tokenized input. + normalized_unit = tokens[1].lower() if len(tokens) == 2 and humanfriendly_is_string(tokens[1]) else '' + # If the input contains only a number, it's assumed to be the number of + # bytes. The second token can also explicitly reference the unit bytes. + if len(tokens) == 1 or normalized_unit.startswith('b'): + return int(tokens[0]) + # Otherwise we expect two tokens: A number and a unit. + if normalized_unit: + # Convert plural units to singular units, for details: + # https://github.com/xolox/python-humanfriendly/issues/26 + normalized_unit = normalized_unit.rstrip('s') + for unit in disk_size_units: + # First we check for unambiguous symbols (KiB, MiB, GiB, etc) + # and names (kibibyte, mebibyte, gibibyte, etc) because their + # handling is always the same. + if normalized_unit in (unit.binary.symbol.lower(), unit.binary.name.lower()): + return int(tokens[0] * unit.binary.divider) + # Now we will deal with ambiguous prefixes (K, M, G, etc), + # symbols (KB, MB, GB, etc) and names (kilobyte, megabyte, + # gigabyte, etc) according to the caller's preference. + if (normalized_unit in (unit.decimal.symbol.lower(), unit.decimal.name.lower()) or + normalized_unit.startswith(unit.decimal.symbol[0].lower())): + return int(tokens[0] * (unit.binary.divider if binary else unit.decimal.divider)) + # We failed to parse the size specification. + msg = "Failed to parse size! (input %r was tokenized as %r)" + raise HumanfriendlyInvalidSize(format(msg, size, tokens)) + + +# taken from https://github.com/xolox/python-humanfriendly/blob/master/humanfriendly/text.py#L402 +# so there are no dependencies on the humanfriendly package +def humanfriendly_tokenize(text): + tokenized_input = [] + for token in re.split(r'(\d+(?:\.\d+)?)', text): + token = token.strip() + if re.match(r'\d+\.\d+', token): + tokenized_input.append(float(token)) + elif token.isdigit(): + tokenized_input.append(int(token)) + elif token: + tokenized_input.append(token) + return tokenized_input + + +def parse(data: str, raw: bool = False, quiet: bool = False) -> List[JSONDictType]: + """ + Main text parsing function + + Parameters: + + data: (string) text data to parse + raw: (boolean) unprocessed output if True + quiet: (boolean) suppress warning messages if True + + Returns: + + List of Dictionaries. Raw or processed structured data. + """ + jc.utils.compatibility(__name__, info.compatible, quiet) + jc.utils.input_type_check(data) + + raw_output: List[Dict] = [] + + interface_item: Dict = {} + + clients: List = [] + + before_arrow = r"\s+(?P\d+)\s+(?P[^\s]+):(?P[^\s]+)\s+" + before_arrow_no_port = r"\s+(?P\d+)\s+(?P[^\s]+)\s+" + after_arrow_before_newline = r"\s+(?P[^\s]+)\s+(?P[^\s]+)\s+(?P[^\s]+)\s+(?P[^\s]+)" + newline_before_arrow = r"\s+(?P.+):(?P\w+)\s+" + newline_before_arrow_no_port = r"\s+(?P.+)\s+" + after_arrow_till_end = r"\s+(?P[^\s]+)\s+(?P[^\s]+)\s+(?P[^\s]+)\s+(?P[^\s]+)" + re_linux_clients_before_newline = re.compile( + rf"{before_arrow}=>{after_arrow_before_newline}" + ) + re_linux_clients_before_newline_no_port = re.compile( + rf"{before_arrow_no_port}=>{after_arrow_before_newline}" + ) + re_linux_clients_after_newline_no_port = re.compile( + rf"{newline_before_arrow_no_port}<={after_arrow_till_end}" + ) + + re_linux_clients_after_newline = re.compile( + rf"{newline_before_arrow}<={after_arrow_till_end}" + ) + + re_total_send_rate = re.compile( + r"Total send rate:\s+(?P[^\s]+)\s+(?P[^\s]+)\s+(?P[^\s]+)" + ) + re_total_receive_rate = re.compile( + r"Total receive rate:\s+(?P[^\s]+)\s+(?P[^\s]+)\s+(?P[^\s]+)" + ) + re_total_send_and_receive_rate = re.compile( + r"Total send and receive rate:\s+(?P[^\s]+)\s+(?P[^\s]+)\s+(?P[^\s]+)" + ) + re_peak_rate = re.compile( + r"Peak rate \(sent/received/total\):\s+(?P[^\s]+)\s+(?P[^\s]+)\s+(?P[^\s]+)" + ) + re_cumulative_rate = re.compile( + r"Cumulative \(sent/received/total\):\s+(?P[^\s]+)\s+(?P[^\s]+)\s+(?P[^\s]+)" + ) + + jc.utils.compatibility(__name__, info.compatible, quiet) + jc.utils.input_type_check(data) + + raw_output: List[Dict] = [] + current_client: Dict = {} + + if not jc.utils.has_data(data): + return raw_output if raw else _process(raw_output, quiet=quiet) + + is_previous_line_interface = False + saw_already_host_line = False + for line in filter(None, data.splitlines()): + if line.startswith("interface:"): + # Example: + # interface: enp0s3 + + interface_item["device"] = line.split(":")[1].strip() + elif line.startswith("IP address is:"): + # Example: + # IP address is: 10.10.15.129 + + interface_item["ip_address"] = line.split(":")[1].strip() + elif line.startswith("MAC address is:"): + # Example: + # MAC address is: 08:00:27:c0:4a:4f + + # strip off the "MAC address is: " part + data_without_front = line.split(":")[1:] + # join the remaining parts back together + data_without_front = ":".join(data_without_front) + interface_item["mac_address"] = data_without_front.strip() + + elif line.startswith("Listening on"): + # Example: + # Listening on enp0s3 + pass + elif ( + line.startswith("# Host name (port/service if enabled)") + and not saw_already_host_line + ): + saw_already_host_line = True + # Example: + # # Host name (port/service if enabled) last 2s last 10s last 40s cumulative + pass + elif ( + line.startswith("# Host name (port/service if enabled)") + and saw_already_host_line + ): + old_interface_item, interface_item = interface_item, {} + interface_item.update( + { + "device": old_interface_item["device"], + "ip_address": old_interface_item["ip_address"], + "mac_address": old_interface_item["mac_address"], + } + ) + + elif "=>" in line and is_previous_line_interface and ":" in line: + # should not happen + pass + elif "=>" in line and not is_previous_line_interface and ":" in line: + # Example: + # 1 ubuntu-2004-clean-01:ssh => 448b 448b 448b 112B + + is_previous_line_interface = True + match_raw = re_linux_clients_before_newline.match(line) + if not match_raw: + # this is a bug in iftop + # + continue + match_dict = match_raw.groupdict() + current_client = {} + current_client["index"] = int(match_dict["index"]) + current_client["connections"] = [] + current_client_send = { + "host_name": match_dict["host_name"], + "host_port": match_dict["host_port"], + "last_2s": match_dict["send_last_2s"], + "last_10s": match_dict["send_last_10s"], + "last_40s": match_dict["send_last_40s"], + "cumulative": match_dict["send_cumulative"], + "direction": "send", + } + current_client["connections"].append(current_client_send) + # not adding yet as the receive part is not yet parsed + elif "=>" in line and not is_previous_line_interface and ":" not in line: + # should not happen + pass + elif "=>" in line and is_previous_line_interface and ":" not in line: + is_previous_line_interface = True + match_raw = re_linux_clients_before_newline_no_port.match(line) + if not match_raw: + # this is a bug in iftop + # + continue + match_dict = match_raw.groupdict() + current_client = {} + current_client["index"] = int(match_dict["index"]) + current_client["connections"] = [] + current_client_send = { + "host_name": match_dict["host_name"], + "last_2s": match_dict["send_last_2s"], + "last_10s": match_dict["send_last_10s"], + "last_40s": match_dict["send_last_40s"], + "cumulative": match_dict["send_cumulative"], + "direction": "send", + } + current_client["connections"].append(current_client_send) + # not adding yet as the receive part is not yet parsed + elif "<=" in line and not is_previous_line_interface and ":" in line: + # should not happen + pass + elif "<=" in line and is_previous_line_interface and ":" in line: + # Example: + # 10.10.15.72:40876 <= 208b 208b 208b 52B + + is_previous_line_interface = False + + match_raw = re_linux_clients_after_newline.match(line) + if not match_raw: + # this is a bug in iftop + # + continue + match_dict = match_raw.groupdict() + current_client_receive = { + "host_name": match_dict["receive_ip"], + "host_port": match_dict["receive_port"], + "last_2s": match_dict["receive_last_2s"], + "last_10s": match_dict["receive_last_10s"], + "last_40s": match_dict["receive_last_40s"], + "cumulative": match_dict["receive_cumulative"], + "direction": "receive", + } + + current_client["connections"].append(current_client_receive) + clients.append(current_client) + elif "<=" in line and not is_previous_line_interface and ":" not in line: + # should not happen + pass + elif "<=" in line and is_previous_line_interface and ":" not in line: + # Example: + # 10.10.15.72:40876 <= 208b 208b 208b 52B + + is_previous_line_interface = False + + match_raw = re_linux_clients_after_newline_no_port.match(line) + if not match_raw: + # this is a bug in iftop + # + continue + match_dict = match_raw.groupdict() + current_client_receive = { + "host_name": match_dict["receive_ip"], + "last_2s": match_dict["receive_last_2s"], + "last_10s": match_dict["receive_last_10s"], + "last_40s": match_dict["receive_last_40s"], + "cumulative": match_dict["receive_cumulative"], + "direction": "receive", + } + + current_client["connections"].append(current_client_receive) + clients.append(current_client) + # check if all of the characters are dashes or equal signs + elif all(c == "-" for c in line): + pass + elif line.startswith("Total send rate"): + # Example: + # Total send rate: 448b 448b 448b + match_raw = re_total_send_rate.match(line) + if not match_raw: + # this is a bug in iftop + # + continue + match_dict = match_raw.groupdict() + interface_item["total_send_rate"] = {} + interface_item["total_send_rate"].update( + { + "last_2s": match_dict["total_send_rate_last_2s"], + "last_10s": match_dict["total_send_rate_last_10s"], + "last_40s": match_dict["total_send_rate_last_40s"], + } + ) + elif line.startswith("Total receive rate"): + # Example: + # Total receive rate: 208b 208b 208b + match_raw = re_total_receive_rate.match(line) + if not match_raw: + # this is a bug in iftop + # + continue + match_dict = match_raw.groupdict() + interface_item["total_receive_rate"] = {} + interface_item["total_receive_rate"].update( + { + "last_2s": match_dict["total_receive_rate_last_2s"], + "last_10s": match_dict["total_receive_rate_last_10s"], + "last_40s": match_dict["total_receive_rate_last_40s"], + } + ) + elif line.startswith("Total send and receive rate"): + # Example: + # Total send and receive rate: 656b 656b 656b + match_raw = re_total_send_and_receive_rate.match(line) + if not match_raw: + # this is a bug in iftop + # + continue + match_dict = match_raw.groupdict() + interface_item["total_send_and_receive_rate"] = {} + interface_item["total_send_and_receive_rate"].update( + { + "last_2s": match_dict["total_send_and_receive_rate_last_2s"], + "last_10s": match_dict["total_send_and_receive_rate_last_10s"], + "last_40s": match_dict["total_send_and_receive_rate_last_40s"], + } + ) + elif line.startswith("Peak rate"): + match_raw = re_peak_rate.match(line) + if not match_raw: + # this is a bug in iftop + # + continue + match_dict = match_raw.groupdict() + interface_item["peak_rate"] = {} + interface_item["peak_rate"].update( + { + "last_2s": match_dict["peak_rate_sent"], + "last_10s": match_dict["peak_rate_received"], + "last_40s": match_dict["peak_rate_total"], + } + ) + elif line.startswith("Cumulative"): + match_raw = re_cumulative_rate.match(line) + if not match_raw: + # this is a bug in iftop + # + continue + match_dict = match_raw.groupdict() + interface_item["cumulative_rate"] = {} + interface_item["cumulative_rate"].update( + { + "last_2s": match_dict["cumulative_rate_sent"], + "last_10s": match_dict["cumulative_rate_received"], + "last_40s": match_dict["cumulative_rate_total"], + } + ) + elif all(c == "=" for c in line): + interface_item["clients"] = clients + clients = [] + raw_output.append(interface_item.copy()) # keep the copy here as without it keeps the objects linked + else: + pass + + return raw_output if raw else _process(raw_output, quiet=quiet) diff --git a/tests/fixtures/ubuntu-20.10/iftop-b-n1-noport.json b/tests/fixtures/ubuntu-20.10/iftop-b-n1-noport.json new file mode 100644 index 00000000..f68e7b45 --- /dev/null +++ b/tests/fixtures/ubuntu-20.10/iftop-b-n1-noport.json @@ -0,0 +1,33 @@ +[ + { + "device": "enp0s3", + "ip_address": "10.10.15.129", + "mac_address": "08:00:27:c0:4a:4f", + "total_send_rate": { + "last_2s": 4820, + "last_10s": 4820, + "last_40s": 4820 + }, + "total_receive_rate": { + "last_2s": 16600, + "last_10s": 16600, + "last_40s": 16600 + }, + "total_send_and_receive_rate": { + "last_2s": 21400, + "last_10s": 21400, + "last_40s": 21400 + }, + "peak_rate": { + "last_2s": 4820, + "last_10s": 16600, + "last_40s": 21400 + }, + "cumulative_rate": { + "last_2s": 9630, + "last_10s": 33100, + "last_40s": 42800 + }, + "clients": [] + } +] \ No newline at end of file diff --git a/tests/fixtures/ubuntu-20.10/iftop-b-n1-noport.out b/tests/fixtures/ubuntu-20.10/iftop-b-n1-noport.out new file mode 100644 index 00000000..b0de111c --- /dev/null +++ b/tests/fixtures/ubuntu-20.10/iftop-b-n1-noport.out @@ -0,0 +1,18 @@ +interface: enp0s3 +IP address is: 10.10.15.129 +MAC address is: 08:00:27:c0:4a:4f +Listening on enp0s3 + # Host name (port/service if enabled) last 2s last 10s last 40s cumulative +-------------------------------------------------------------------------------------------- + 1 ubuntu-2004-clean-01 => 4.82KB 4.82KB 4.82KB 9.63KB + 10.10.15.72 <= 14.5KB 14.5KB 14.5KB 29.1KB + 2 ubuntu-2004-clean-02 => 0B 0B 0B 0B + 10.10.15.72 <= 2.02KB 2.02KB 2.02KB 4.04KB +-------------------------------------------------------------------------------------------- +Total send rate: 4.82KB 4.82KB 4.82KB +Total receive rate: 16.6KB 16.6KB 16.6KB +Total send and receive rate: 21.4KB 21.4KB 21.4KB +-------------------------------------------------------------------------------------------- +Peak rate (sent/received/total): 4.82KB 16.6KB 21.4KB +Cumulative (sent/received/total): 9.63KB 33.1KB 42.8KB +============================================================================================ \ No newline at end of file diff --git a/tests/fixtures/ubuntu-20.10/iftop-b-n1.json b/tests/fixtures/ubuntu-20.10/iftop-b-n1.json new file mode 100644 index 00000000..76c30b7b --- /dev/null +++ b/tests/fixtures/ubuntu-20.10/iftop-b-n1.json @@ -0,0 +1,57 @@ +[ + { + "device": "enp0s3", + "ip_address": "10.10.15.129", + "mac_address": "08:00:27:c0:4a:4f", + "clients": [ + { + "index": 1, + "connections": [ + { + "host_name": "ubuntu-2004-clean-01", + "host_port": "ssh", + "last_2s": 448, + "last_10s": 448, + "last_40s": 448, + "cumulative": 112, + "direction": "send" + }, + { + "host_name": "10.10.15.72", + "host_port": "40876", + "last_2s": 208, + "last_10s": 208, + "last_40s": 208, + "cumulative": 52, + "direction": "receive" + } + ] + } + ], + "total_send_rate": { + "last_2s": 448, + "last_10s": 448, + "last_40s": 448 + }, + "total_receive_rate": { + "last_2s": 208, + "last_10s": 208, + "last_40s": 208 + }, + "total_send_and_receive_rate": { + "last_2s": 656, + "last_10s": 656, + "last_40s": 656 + }, + "peak_rate": { + "last_2s": 448, + "last_10s": 208, + "last_40s": 656 + }, + "cumulative_rate": { + "last_2s": 112, + "last_10s": 52, + "last_40s": 164 + } + } +] \ No newline at end of file diff --git a/tests/fixtures/ubuntu-20.10/iftop-b-n1.out b/tests/fixtures/ubuntu-20.10/iftop-b-n1.out new file mode 100644 index 00000000..8e242ac9 --- /dev/null +++ b/tests/fixtures/ubuntu-20.10/iftop-b-n1.out @@ -0,0 +1,16 @@ +interface: enp0s3 +IP address is: 10.10.15.129 +MAC address is: 08:00:27:c0:4a:4f +Listening on enp0s3 + # Host name (port/service if enabled) last 2s last 10s last 40s cumulative +-------------------------------------------------------------------------------------------- + 1 ubuntu-2004-clean-01:ssh => 448b 448b 448b 112B + 10.10.15.72:40876 <= 208b 208b 208b 52B +-------------------------------------------------------------------------------------------- +Total send rate: 448b 448b 448b +Total receive rate: 208b 208b 208b +Total send and receive rate: 656b 656b 656b +-------------------------------------------------------------------------------------------- +Peak rate (sent/received/total): 448b 208b 656b +Cumulative (sent/received/total): 112B 52B 164B +============================================================================================ \ No newline at end of file diff --git a/tests/fixtures/ubuntu-20.10/iftop-b-n3.json b/tests/fixtures/ubuntu-20.10/iftop-b-n3.json new file mode 100644 index 00000000..30e53bb9 --- /dev/null +++ b/tests/fixtures/ubuntu-20.10/iftop-b-n3.json @@ -0,0 +1,236 @@ +[ + { + "device": "enp0s3", + "ip_address": "10.10.15.129", + "mac_address": "08:00:27:c0:4a:4f", + "total_send_rate": { + "last_2s": 23200000, + "last_10s": 23200000, + "last_40s": 23200000 + }, + "total_receive_rate": { + "last_2s": 5650000, + "last_10s": 5650000, + "last_40s": 5650000 + }, + "total_send_and_receive_rate": { + "last_2s": 28800000, + "last_10s": 28800000, + "last_40s": 28800000 + }, + "peak_rate": { + "last_2s": 23200000, + "last_10s": 5650000, + "last_40s": 28800000 + }, + "cumulative_rate": { + "last_2s": 5790000, + "last_10s": 1410000, + "last_40s": 7200000 + }, + "clients": [ + { + "index": 1, + "connections": [ + { + "host_name": "ubuntu-2004-clean-01", + "host_port": "33222", + "last_2s": 4720, + "last_10s": 4720, + "last_40s": 4720, + "cumulative": 1180, + "direction": "send" + }, + { + "host_name": "10.10.15.72", + "host_port": "https", + "last_2s": 1990000, + "last_10s": 1990000, + "last_40s": 1990000, + "cumulative": 508000, + "direction": "receive" + } + ] + }, + { + "index": 2, + "connections": [ + { + "host_name": "ubuntu-2004-clean-01", + "host_port": "https", + "last_2s": 1980000, + "last_10s": 1980000, + "last_40s": 1980000, + "cumulative": 507000, + "direction": "send" + }, + { + "host_name": "10.10.15.73", + "host_port": "34562", + "last_2s": 3170, + "last_10s": 3170, + "last_40s": 3170, + "cumulative": 811, + "direction": "receive" + } + ] + } + ] + }, + { + "device": "enp0s3", + "ip_address": "10.10.15.129", + "mac_address": "08:00:27:c0:4a:4f", + "total_send_rate": { + "last_2s": 23200000, + "last_10s": 23200000, + "last_40s": 23200000 + }, + "total_receive_rate": { + "last_2s": 5650000, + "last_10s": 5650000, + "last_40s": 5650000 + }, + "total_send_and_receive_rate": { + "last_2s": 28800000, + "last_10s": 28800000, + "last_40s": 28800000 + }, + "peak_rate": { + "last_2s": 23200000, + "last_10s": 5650000, + "last_40s": 28800000 + }, + "cumulative_rate": { + "last_2s": 5790000, + "last_10s": 1410000, + "last_40s": 7200000 + }, + "clients": [ + { + "index": 1, + "connections": [ + { + "host_name": "ubuntu-2004-clean-01", + "host_port": "33222", + "last_2s": 4720, + "last_10s": 4720, + "last_40s": 4720, + "cumulative": 1180, + "direction": "send" + }, + { + "host_name": "10.10.15.72", + "host_port": "https", + "last_2s": 1990000, + "last_10s": 1990000, + "last_40s": 1990000, + "cumulative": 508000, + "direction": "receive" + } + ] + }, + { + "index": 2, + "connections": [ + { + "host_name": "ubuntu-2004-clean-01", + "host_port": "https", + "last_2s": 1980000, + "last_10s": 1980000, + "last_40s": 1980000, + "cumulative": 507000, + "direction": "send" + }, + { + "host_name": "10.10.15.73", + "host_port": "34562", + "last_2s": 3170, + "last_10s": 3170, + "last_40s": 3170, + "cumulative": 811, + "direction": "receive" + } + ] + } + ] + }, + { + "device": "enp0s3", + "ip_address": "10.10.15.129", + "mac_address": "08:00:27:c0:4a:4f", + "total_send_rate": { + "last_2s": 23200000, + "last_10s": 23200000, + "last_40s": 23200000 + }, + "total_receive_rate": { + "last_2s": 5650000, + "last_10s": 5650000, + "last_40s": 5650000 + }, + "total_send_and_receive_rate": { + "last_2s": 28800000, + "last_10s": 28800000, + "last_40s": 28800000 + }, + "peak_rate": { + "last_2s": 23200000, + "last_10s": 5650000, + "last_40s": 28800000 + }, + "cumulative_rate": { + "last_2s": 5790000, + "last_10s": 1410000, + "last_40s": 7200000 + }, + "clients": [ + { + "index": 1, + "connections": [ + { + "host_name": "ubuntu-2004-clean-01", + "host_port": "33222", + "last_2s": 4720, + "last_10s": 4720, + "last_40s": 4720, + "cumulative": 1180, + "direction": "send" + }, + { + "host_name": "10.10.15.72", + "host_port": "https", + "last_2s": 1990000, + "last_10s": 1990000, + "last_40s": 1990000, + "cumulative": 508000, + "direction": "receive" + } + ] + }, + { + "index": 2, + "connections": [ + { + "host_name": "ubuntu-2004-clean-01", + "host_port": "https", + "last_2s": 1980000, + "last_10s": 1980000, + "last_40s": 1980000, + "cumulative": 507000, + "direction": "send" + }, + { + "host_name": "10.10.15.73", + "host_port": "34562", + "last_2s": 3170, + "last_10s": 3170, + "last_40s": 3170, + "cumulative": 811, + "direction": "receive" + } + ] + } + ] + } +] \ No newline at end of file diff --git a/tests/fixtures/ubuntu-20.10/iftop-b-n3.out b/tests/fixtures/ubuntu-20.10/iftop-b-n3.out new file mode 100644 index 00000000..9a9d568d --- /dev/null +++ b/tests/fixtures/ubuntu-20.10/iftop-b-n3.out @@ -0,0 +1,48 @@ +interface: enp0s3 +IP address is: 10.10.15.129 +MAC address is: 08:00:27:c0:4a:4f +Listening on enp0s3 + # Host name (port/service if enabled) last 2s last 10s last 40s cumulative +-------------------------------------------------------------------------------------------- + 1 ubuntu-2004-clean-01:33222 => 4.72Kb 4.72Kb 4.72Kb 1.18KB + 10.10.15.72:https <= 1.99Mb 1.99Mb 1.99Mb 508KB + 2 ubuntu-2004-clean-01:https => 1.98Mb 1.98Mb 1.98Mb 507KB + 10.10.15.73:34562 <= 3.17Kb 3.17Kb 3.17Kb 811B +-------------------------------------------------------------------------------------------- +Total send rate: 23.2Mb 23.2Mb 23.2Mb +Total receive rate: 5.65Mb 5.65Mb 5.65Mb +Total send and receive rate: 28.8Mb 28.8Mb 28.8Mb +-------------------------------------------------------------------------------------------- +Peak rate (sent/received/total): 23.2Mb 5.65Mb 28.8Mb +Cumulative (sent/received/total): 5.79MB 1.41MB 7.20MB +============================================================================================ + + # Host name (port/service if enabled) last 2s last 10s last 40s cumulative +-------------------------------------------------------------------------------------------- + 1 ubuntu-2004-clean-01:33222 => 4.72Kb 4.72Kb 4.72Kb 1.18KB + 10.10.15.72:https <= 1.99Mb 1.99Mb 1.99Mb 508KB + 2 ubuntu-2004-clean-01:https => 1.98Mb 1.98Mb 1.98Mb 507KB + 10.10.15.73:34562 <= 3.17Kb 3.17Kb 3.17Kb 811B +-------------------------------------------------------------------------------------------- +Total send rate: 23.2Mb 23.2Mb 23.2Mb +Total receive rate: 5.65Mb 5.65Mb 5.65Mb +Total send and receive rate: 28.8Mb 28.8Mb 28.8Mb +-------------------------------------------------------------------------------------------- +Peak rate (sent/received/total): 23.2Mb 5.65Mb 28.8Mb +Cumulative (sent/received/total): 5.79MB 1.41MB 7.20MB +============================================================================================ + + # Host name (port/service if enabled) last 2s last 10s last 40s cumulative +-------------------------------------------------------------------------------------------- + 1 ubuntu-2004-clean-01:33222 => 4.72Kb 4.72Kb 4.72Kb 1.18KB + 10.10.15.72:https <= 1.99Mb 1.99Mb 1.99Mb 508KB + 2 ubuntu-2004-clean-01:https => 1.98Mb 1.98Mb 1.98Mb 507KB + 10.10.15.73:34562 <= 3.17Kb 3.17Kb 3.17Kb 811B +-------------------------------------------------------------------------------------------- +Total send rate: 23.2Mb 23.2Mb 23.2Mb +Total receive rate: 5.65Mb 5.65Mb 5.65Mb +Total send and receive rate: 28.8Mb 28.8Mb 28.8Mb +-------------------------------------------------------------------------------------------- +Peak rate (sent/received/total): 23.2Mb 5.65Mb 28.8Mb +Cumulative (sent/received/total): 5.79MB 1.41MB 7.20MB +============================================================================================ \ No newline at end of file diff --git a/tests/test_iftop.py b/tests/test_iftop.py new file mode 100644 index 00000000..e8385f3a --- /dev/null +++ b/tests/test_iftop.py @@ -0,0 +1,57 @@ +import os +import unittest +import json +import jc.parsers.iftop + +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class MyTests(unittest.TestCase): + + # input + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/ubuntu-20.10/iftop-b-n1.out'), 'r', encoding='utf-8') as f: + ubuntu_20_10_iftop_b_n1 = f.read() + + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/ubuntu-20.10/iftop-b-n3.out'), 'r', encoding='utf-8') as f: + ubuntu_20_10_iftop_b_n3 = f.read() + + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/ubuntu-20.10/iftop-b-n1-noport.out'), 'r', encoding='utf-8') as f: + ubuntu_20_10_iftop_b_n1_noport = f.read() + + # output + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/ubuntu-20.10/iftop-b-n1.json'), 'r', encoding='utf-8') as f: + ubuntu_20_10_iftop_b_n1_json = json.loads(f.read()) + + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/ubuntu-20.10/iftop-b-n3.json'), 'r', encoding='utf-8') as f: + ubuntu_20_10_iftop_b_n3_json = json.loads(f.read()) + + with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/ubuntu-20.10/iftop-b-n1-noport.json'), 'r', encoding='utf-8') as f: + ubuntu_20_10_iftop_b_n1_noport_json = json.loads(f.read()) + + + def test_iftop_nodata(self): + """ + Test 'iftop -b' with no data + """ + self.assertEqual(jc.parsers.iftop.parse('', quiet=True), []) + + def test_iftop_ubuntu_20_10(self): + """ + Test 'iftop -i -t -P -s 1' with units as MiB on Ubuntu 20.10 + """ + self.assertEqual(jc.parsers.iftop.parse(self.ubuntu_20_10_iftop_b_n1, quiet=True), self.ubuntu_20_10_iftop_b_n1_json) + + def test_iftop_multiple_runs_ubuntu_20_10(self): + """ + Test 'iftop -i -t -P -s 1' with units as MiB on Ubuntu 20.10 + """ + self.assertEqual(jc.parsers.iftop.parse(self.ubuntu_20_10_iftop_b_n3, quiet=True), self.ubuntu_20_10_iftop_b_n3_json) + + def test_iftop_ubuntu_20_10_no_port(self): + """ + Test 'iftop -i -t -B -s 1' with units as MiB on Ubuntu 20.10 + """ + self.assertEqual(jc.parsers.iftop.parse(self.ubuntu_20_10_iftop_b_n1_noport, quiet=True), self.ubuntu_20_10_iftop_b_n1_noport_json) + +if __name__ == '__main__': + unittest.main()