r"""jc - JSON Convert openvpn-status.log file parser The `*_epoch` calculated timestamp fields are naive. (i.e. based on the local time of the system the parser is run on) Usage (cli): $ cat openvpn-status.log | jc --openvpn Usage (module): import jc result = jc.parse('openvpn', openvpn_status_log_file_output) Schema: { "clients": [ { "common_name": string, "real_address": string, "real_address_prefix": integer, # [0] "real_address_port": integer, # [0] "bytes_received": integer, "bytes_sent": integer, "connected_since": string, "connected_since_epoch": integer, "updated": string, "updated_epoch": integer, } ], "routing_table": [ { "virtual_address": string, "virtual_address_prefix": integer, # [0] "virtual_address_port": integer, # [0] "common_name": string, "real_address": string, "real_address_prefix": integer, # [0] "real_address_port": integer, # [0] "last_reference": string, "last_reference_epoch": integer, } ], "global_stats": { "max_bcast_mcast_queue_len": integer } } [0] null/None if not found Examples: $ cat openvpn-status.log | jc --openvpn -p { "clients": [ { "common_name": "foo@example.com", "real_address": "10.10.10.10", "bytes_received": 334948, "bytes_sent": 1973012, "connected_since": "Thu Jun 18 04:23:03 2015", "updated": "Thu Jun 18 08:12:15 2015", "real_address_prefix": null, "real_address_port": 49502, "connected_since_epoch": 1434626583, "updated_epoch": 1434640335 }, { "common_name": "foo@example.com", "real_address": "10.10.10.10", "bytes_received": 334948, "bytes_sent": 1973012, "connected_since": "Thu Jun 18 04:23:03 2015", "updated": "Thu Jun 18 08:12:15 2015", "real_address_prefix": null, "real_address_port": 49503, "connected_since_epoch": 1434626583, "updated_epoch": 1434640335 } ], "routing_table": [ { "virtual_address": "192.168.255.118", "common_name": "baz@example.com", "real_address": "10.10.10.10", "last_reference": "Thu Jun 18 08:12:09 2015", "virtual_address_prefix": null, "virtual_address_port": null, "real_address_prefix": null, "real_address_port": 63414, "last_reference_epoch": 1434640329 }, { "virtual_address": "10.200.0.0", "common_name": "baz@example.com", "real_address": "10.10.10.10", "last_reference": "Thu Jun 18 08:12:09 2015", "virtual_address_prefix": 16, "virtual_address_port": null, "real_address_prefix": null, "real_address_port": 63414, "last_reference_epoch": 1434640329 } ], "global_stats": { "max_bcast_mcast_queue_len": 0 } } $ cat openvpn-status.log | jc --openvpn -p -r { "clients": [ { "common_name": "foo@example.com", "real_address": "10.10.10.10:49502", "bytes_received": "334948", "bytes_sent": "1973012", "connected_since": "Thu Jun 18 04:23:03 2015", "updated": "Thu Jun 18 08:12:15 2015" }, { "common_name": "foo@example.com", "real_address": "10.10.10.10:49503", "bytes_received": "334948", "bytes_sent": "1973012", "connected_since": "Thu Jun 18 04:23:03 2015", "updated": "Thu Jun 18 08:12:15 2015" } ], "routing_table": [ { "virtual_address": "192.168.255.118", "common_name": "baz@example.com", "real_address": "10.10.10.10:63414", "last_reference": "Thu Jun 18 08:12:09 2015" }, { "virtual_address": "10.200.0.0/16", "common_name": "baz@example.com", "real_address": "10.10.10.10:63414", "last_reference": "Thu Jun 18 08:12:09 2015" } ], "global_stats": { "max_bcast_mcast_queue_len": "0" } } """ import re import ipaddress from typing import List, Dict, Tuple from jc.jc_types import JSONDictType import jc.utils class info(): """Provides parser metadata (version, author, etc.)""" version = '1.0' description = 'openvpn-status.log file parser' author = 'Kelly Brazil' author_email = 'kellyjonbrazil@gmail.com' compatible = ['linux', 'darwin', 'cygwin', 'win32', 'aix', 'freebsd'] tags = ['file'] __version__ = info.version def _split_addr(addr_str: str) -> Tuple: """Check the type of address (v4, v6, mac) and split out the address, prefix, and port. Values are None if they don't exist.""" address = possible_addr = prefix = port = possible_port = None try: address, prefix = addr_str.rsplit('/', maxsplit=1) except Exception: address = addr_str # is this a mac address? then stop if re.match(r'(?:\S\S\:){5}\S\S', address): return address, prefix, port # is it an ipv4 with port or just ipv6? if ':' in address: try: possible_addr, possible_port = address.rsplit(':', maxsplit=1) _ = ipaddress.IPv4Address(possible_addr) address = possible_addr port = possible_port # assume it was an IPv6 address except Exception: pass return address, prefix, port def _process(proc_data: JSONDictType) -> JSONDictType: """ Final processing to conform to the schema. Parameters: proc_data: (Dictionary) raw structured data to process Returns: Dictionary. Structured to conform to the schema. """ int_list = {'bytes_received', 'bytes_sent', 'max_bcast_mcast_queue_len'} date_fields = {'connected_since', 'updated', 'last_reference'} addr_fields = {'real_address', 'virtual_address'} if 'clients' in proc_data: for item in proc_data['clients']: for k, v in item.copy().items(): if k in int_list: item[k] = jc.utils.convert_to_int(v) if k in date_fields: dt = jc.utils.timestamp(item[k], format_hint=(1000,)) item[k + '_epoch'] = dt.naive if k in addr_fields: addr, prefix, port = _split_addr(v) item[k] = addr item[k + '_prefix'] = jc.utils.convert_to_int(prefix) item[k + '_port'] = jc.utils.convert_to_int(port) if 'routing_table' in proc_data: for item in proc_data['routing_table']: for k, v in item.copy(). items(): if k in date_fields: dt = jc.utils.timestamp(item[k], format_hint=(1000,)) item[k + '_epoch'] = dt.naive if k in addr_fields: addr, prefix, port = _split_addr(v) item[k] = addr item[k + '_prefix'] = jc.utils.convert_to_int(prefix) item[k + '_port'] = jc.utils.convert_to_int(port) if 'global_stats' in proc_data: for k, v in proc_data['global_stats'].items(): if k in int_list: if k in int_list: proc_data['global_stats'][k] = jc.utils.convert_to_int(v) return proc_data def parse( data: str, raw: bool = False, quiet: bool = False ) -> 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: Dictionary. Raw or processed structured data. """ jc.utils.compatibility(__name__, info.compatible, quiet) jc.utils.input_type_check(data) raw_output: Dict = {} clients: List[Dict] = [] routing_table: List[Dict] = [] global_stats: Dict = {} section: str = '' # clients, routing, stats updated: str = '' if jc.utils.has_data(data): for line in filter(None, data.splitlines()): if line.startswith('OpenVPN CLIENT LIST'): section = 'clients' continue if line.startswith('ROUTING TABLE'): section = 'routing' continue if line.startswith('GLOBAL STATS'): section = 'stats' continue if line.startswith('END'): break if section == 'clients' and line.startswith('Updated,'): _, updated = line.split(',', maxsplit=1) continue if section == 'clients' and line.startswith('Common Name,Real Address,'): continue if section == 'clients': c_name, real_addr, r_bytes, s_bytes, connected = line.split(',', maxsplit=5) clients.append( { 'common_name': c_name, 'real_address': real_addr, 'bytes_received': r_bytes, 'bytes_sent': s_bytes, 'connected_since': connected, 'updated': updated } ) continue if section == 'routing' and line.startswith('Virtual Address,Common Name,'): continue if section == 'routing': # Virtual Address,Common Name,Real Address,Last Ref # 192.168.255.118,baz@example.com,10.10.10.10:63414,Thu Jun 18 08:12:09 2015 virt_addr, c_name, real_addr, last_ref = line.split(',', maxsplit=4) route = { 'virtual_address': virt_addr, 'common_name': c_name, 'real_address': real_addr, 'last_reference': last_ref } # fixup for virtual addresses ending in "C" if 'virtual_address' in route: if route['virtual_address'].endswith('C'): route['virtual_address'] = route['virtual_address'][:-1] routing_table.append(route) continue if section == "stats": if line.startswith('Max bcast/mcast queue length'): global_stats['max_bcast_mcast_queue_len'] = line.split(',', maxsplit=1)[1] continue raw_output['clients'] = clients raw_output['routing_table'] = routing_table raw_output['global_stats'] = {} raw_output['global_stats'].update(global_stats) return raw_output if raw else _process(raw_output)