mirror of
https://github.com/kellyjonbrazil/jc.git
synced 2025-06-19 00:17:51 +02:00
353 lines
11 KiB
Python
353 lines
11 KiB
Python
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)
|