openvpn-status-reader 0.3.0__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.
- openvpn_status/__init__.py +5 -0
- openvpn_status/descriptors.py +51 -0
- openvpn_status/models.py +44 -0
- openvpn_status/parser.py +144 -0
- openvpn_status/shortcuts.py +16 -0
- openvpn_status/utils.py +82 -0
- openvpn_status/version.py +16 -0
- openvpn_status_reader-0.3.0.dist-info/METADATA +149 -0
- openvpn_status_reader-0.3.0.dist-info/RECORD +13 -0
- openvpn_status_reader-0.3.0.dist-info/WHEEL +5 -0
- openvpn_status_reader-0.3.0.dist-info/licenses/AUTHORS.rst +1 -0
- openvpn_status_reader-0.3.0.dist-info/licenses/LICENSE +20 -0
- openvpn_status_reader-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
class LabelProperty:
|
|
2
|
+
"""The property with label name."""
|
|
3
|
+
|
|
4
|
+
def __init__(self, label, default=None, input_type=None):
|
|
5
|
+
self.label = label
|
|
6
|
+
self.default = default
|
|
7
|
+
self.input_type = input_type
|
|
8
|
+
self.__name__ = None
|
|
9
|
+
|
|
10
|
+
def __get__(self, instance, owner):
|
|
11
|
+
if instance is None:
|
|
12
|
+
return self
|
|
13
|
+
if self.__name__ in instance.__dict__:
|
|
14
|
+
return instance.__dict__[self.__name__]
|
|
15
|
+
elif self.default is not None:
|
|
16
|
+
value = self.default()
|
|
17
|
+
instance.__dict__[self.__name__] = value
|
|
18
|
+
return value
|
|
19
|
+
else:
|
|
20
|
+
names = (owner.__name__, self.__name__)
|
|
21
|
+
raise AttributeError("%r object has no attribute %r" % names)
|
|
22
|
+
|
|
23
|
+
def __set__(self, instance, value):
|
|
24
|
+
if self.input_type is not None:
|
|
25
|
+
try:
|
|
26
|
+
value = self.input_type(value)
|
|
27
|
+
except ValueError as e:
|
|
28
|
+
raise AssignmentValueError(e)
|
|
29
|
+
instance.__dict__[self.__name__] = value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_missing = object()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def name_descriptors(cls):
|
|
36
|
+
for name, value in iter_descriptors(cls):
|
|
37
|
+
if getattr(value, "__name__", _missing) is None:
|
|
38
|
+
value.__name__ = name
|
|
39
|
+
return cls
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def iter_descriptors(cls):
|
|
43
|
+
for name, value in cls.__dict__.items():
|
|
44
|
+
if name.startswith("__"):
|
|
45
|
+
continue
|
|
46
|
+
if getattr(value, "__get__", None) or getattr(value, "__set__", None):
|
|
47
|
+
yield name, value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AssignmentValueError(ValueError):
|
|
51
|
+
pass
|
openvpn_status/models.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from collections import OrderedDict
|
|
2
|
+
|
|
3
|
+
from .descriptors import LabelProperty, name_descriptors
|
|
4
|
+
from .utils import parse_filesize, parse_peer, parse_time, parse_vaddr
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@name_descriptors
|
|
8
|
+
class Status:
|
|
9
|
+
"""The OpenVPN status model."""
|
|
10
|
+
|
|
11
|
+
client_list = LabelProperty("OpenVPN CLIENT LIST", OrderedDict)
|
|
12
|
+
routing_table = LabelProperty("ROUTING TABLE", OrderedDict)
|
|
13
|
+
global_stats = LabelProperty("GLOBAL STATS")
|
|
14
|
+
updated_at = LabelProperty("Updated", input_type=parse_time)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@name_descriptors
|
|
18
|
+
class Client:
|
|
19
|
+
"""The OpenVPN client model."""
|
|
20
|
+
|
|
21
|
+
common_name = LabelProperty("Common Name")
|
|
22
|
+
real_address = LabelProperty("Real Address", input_type=parse_peer)
|
|
23
|
+
bytes_received = LabelProperty("Bytes Received", input_type=parse_filesize)
|
|
24
|
+
bytes_sent = LabelProperty("Bytes Sent", input_type=parse_filesize)
|
|
25
|
+
connected_since = LabelProperty("Connected Since", input_type=parse_time)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@name_descriptors
|
|
29
|
+
class Routing:
|
|
30
|
+
"""The OpenVPN routing model."""
|
|
31
|
+
|
|
32
|
+
virtual_address = LabelProperty("Virtual Address", input_type=parse_vaddr)
|
|
33
|
+
common_name = LabelProperty("Common Name")
|
|
34
|
+
real_address = LabelProperty("Real Address", input_type=parse_peer)
|
|
35
|
+
last_ref = LabelProperty("Last Ref", input_type=parse_time)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@name_descriptors
|
|
39
|
+
class GlobalStats:
|
|
40
|
+
"""The OpenVPN global stats model."""
|
|
41
|
+
|
|
42
|
+
max_bcast_mcast_queue_len = LabelProperty(
|
|
43
|
+
"Max bcast/mcast queue length", input_type=int
|
|
44
|
+
)
|
openvpn_status/parser.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
|
|
3
|
+
from .descriptors import AssignmentValueError, iter_descriptors
|
|
4
|
+
from .models import Client, GlobalStats, Routing, Status
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LogParser(Iterator):
|
|
8
|
+
"""The parser for parsing OpenVPN status log.
|
|
9
|
+
|
|
10
|
+
This kind of parser is stateful. So the :meth:`LogParser.parse` could be
|
|
11
|
+
called once in the same instance of parser.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
list_separator = ","
|
|
15
|
+
line_separator = "\n"
|
|
16
|
+
terminator = "END"
|
|
17
|
+
|
|
18
|
+
def __init__(self, lines):
|
|
19
|
+
self.lines = iter(lines)
|
|
20
|
+
self._last_line = None
|
|
21
|
+
self._rollback = False
|
|
22
|
+
|
|
23
|
+
def __next__(self):
|
|
24
|
+
if self._rollback:
|
|
25
|
+
self._rollback = False
|
|
26
|
+
return self._last_line
|
|
27
|
+
while True:
|
|
28
|
+
line = next(self.lines).strip()
|
|
29
|
+
if line:
|
|
30
|
+
self._last_line = line
|
|
31
|
+
return line
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def fromstring(cls, content):
|
|
35
|
+
"""Creates a parser from content of log.
|
|
36
|
+
|
|
37
|
+
:param str content: The log content.
|
|
38
|
+
:return: The parser instance.
|
|
39
|
+
"""
|
|
40
|
+
return cls(content.strip().split(cls.line_separator))
|
|
41
|
+
|
|
42
|
+
def rollback(self):
|
|
43
|
+
self._rollback = True
|
|
44
|
+
|
|
45
|
+
def expect_line(self, content):
|
|
46
|
+
try:
|
|
47
|
+
line = next(self)
|
|
48
|
+
except StopIteration:
|
|
49
|
+
raise ParsingError("expected %r but got end of input" % content)
|
|
50
|
+
if line != content:
|
|
51
|
+
raise ParsingError("expected %r but got %r" % (content, line))
|
|
52
|
+
|
|
53
|
+
def expect_list(self):
|
|
54
|
+
try:
|
|
55
|
+
line = next(self)
|
|
56
|
+
except StopIteration:
|
|
57
|
+
raise ParsingError("expected list but got end of input")
|
|
58
|
+
splited = line.split(self.list_separator)
|
|
59
|
+
if len(splited) == 1:
|
|
60
|
+
raise ParsingError("expected list but got %r" % line)
|
|
61
|
+
return splited
|
|
62
|
+
|
|
63
|
+
def expect_tuple(self, name):
|
|
64
|
+
try:
|
|
65
|
+
line = next(self)
|
|
66
|
+
except StopIteration:
|
|
67
|
+
raise ParsingError("expected 2-tuple but got end of input")
|
|
68
|
+
splited = line.split(self.list_separator)
|
|
69
|
+
if len(splited) != 2:
|
|
70
|
+
raise ParsingError("expected 2-tuple but got %r" % line)
|
|
71
|
+
if splited[0] != name:
|
|
72
|
+
raise ParsingError("expected 2-tuple starting with %r" % name)
|
|
73
|
+
return splited[1]
|
|
74
|
+
|
|
75
|
+
def parse(self):
|
|
76
|
+
"""Parses the status log.
|
|
77
|
+
|
|
78
|
+
:raises ParsingError: if syntax error found in the log.
|
|
79
|
+
:return: The :class:`.models.Status` with filled data.
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
return self._parse()
|
|
83
|
+
except AssignmentValueError as e:
|
|
84
|
+
msg = str(e).encode("ascii", "backslashreplace").decode("ascii")
|
|
85
|
+
raise ParsingError("expected valid format: %s" % msg)
|
|
86
|
+
|
|
87
|
+
def _parse(self):
|
|
88
|
+
status = Status()
|
|
89
|
+
self.expect_line(Status.client_list.label)
|
|
90
|
+
|
|
91
|
+
status.updated_at = self.expect_tuple(Status.updated_at.label)
|
|
92
|
+
status.client_list.update(
|
|
93
|
+
{
|
|
94
|
+
str(c.real_address): c
|
|
95
|
+
for c in self._parse_fields(Client, Status.routing_table.label)
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
status.routing_table.update(
|
|
99
|
+
{
|
|
100
|
+
str(r.virtual_address): r
|
|
101
|
+
for r in self._parse_fields(Routing, Status.global_stats.label)
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
status.global_stats = GlobalStats()
|
|
105
|
+
status.global_stats.max_bcast_mcast_queue_len = self.expect_tuple(
|
|
106
|
+
GlobalStats.max_bcast_mcast_queue_len.label
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self.expect_line(self.terminator)
|
|
110
|
+
return status
|
|
111
|
+
|
|
112
|
+
def _parse_fields(self, cls, next_line):
|
|
113
|
+
labels = self.expect_list()
|
|
114
|
+
descriptors = iter_descriptors(cls)
|
|
115
|
+
label_to_name = {descriptor.label: name for name, descriptor in descriptors}
|
|
116
|
+
if len(labels) != len(label_to_name) or set(labels) != set(label_to_name):
|
|
117
|
+
raise ParsingError("invalid fields for %s" % cls.__name__)
|
|
118
|
+
index_to_name = {
|
|
119
|
+
index: label_to_name[label] for index, label in enumerate(labels)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
while True:
|
|
123
|
+
try:
|
|
124
|
+
values = self.expect_list()
|
|
125
|
+
except ParsingError as list_error:
|
|
126
|
+
try:
|
|
127
|
+
self.rollback()
|
|
128
|
+
self.expect_line(next_line)
|
|
129
|
+
except ParsingError as line_error:
|
|
130
|
+
raise ParsingError(*(list_error.args + line_error.args))
|
|
131
|
+
else:
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
if len(values) != len(index_to_name):
|
|
135
|
+
raise ParsingError("invalid fields for %s" % cls.__name__)
|
|
136
|
+
instance = cls()
|
|
137
|
+
for index, value in enumerate(values):
|
|
138
|
+
name = index_to_name[index]
|
|
139
|
+
setattr(instance, name, value)
|
|
140
|
+
yield instance
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ParsingError(Exception):
|
|
144
|
+
pass
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .parser import LogParser
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def parse_status(status_log, encoding="utf-8"):
|
|
5
|
+
"""Parses the status log of OpenVPN.
|
|
6
|
+
|
|
7
|
+
:param status_log: The content of status log.
|
|
8
|
+
:type status_log: :class:`str`
|
|
9
|
+
:param encoding: Optional. The encoding of status log.
|
|
10
|
+
:type encoding: :class:`str`
|
|
11
|
+
:return: The instance of :class:`.models.Status`
|
|
12
|
+
"""
|
|
13
|
+
if isinstance(status_log, bytes):
|
|
14
|
+
status_log = status_log.decode(encoding)
|
|
15
|
+
parser = LogParser.fromstring(status_log)
|
|
16
|
+
return parser.parse()
|
openvpn_status/utils.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import datetime
|
|
3
|
+
import ipaddress
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from humanize.filesize import naturalsize
|
|
7
|
+
from netaddr import EUI, mac_unix
|
|
8
|
+
|
|
9
|
+
DATETIME_FORMAT_OPENVPN_V2_5 = "%Y-%m-%d %H:%M:%S"
|
|
10
|
+
DATETIME_FORMAT_OPENVPN_V2_4 = "%a %b %d %H:%M:%S %Y"
|
|
11
|
+
RE_VIRTUAL_ADDR_MAC = re.compile(r"^(?:[a-f0-9]{2}:){5}[a-f0-9]{2}(?:@\d+)?$", re.I)
|
|
12
|
+
RE_VIRTUAL_ADDR_NETWORK = re.compile("/(\\d{1,3})$")
|
|
13
|
+
RE_VIRTUAL_ADDR_CLIENT = re.compile("C$")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_time(time):
|
|
17
|
+
"""Parses date and time from input string in OpenVPN logging format."""
|
|
18
|
+
if isinstance(time, datetime.datetime):
|
|
19
|
+
return time
|
|
20
|
+
try:
|
|
21
|
+
# Try the current numeric timestamp before the legacy format.
|
|
22
|
+
return datetime.datetime.strptime(time, DATETIME_FORMAT_OPENVPN_V2_5)
|
|
23
|
+
except ValueError:
|
|
24
|
+
return datetime.datetime.strptime(time, DATETIME_FORMAT_OPENVPN_V2_4)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_peer(peer):
|
|
28
|
+
if isinstance(peer, PeerAddress):
|
|
29
|
+
return peer
|
|
30
|
+
host, port = peer.rsplit(":", 1)
|
|
31
|
+
return PeerAddress(ipaddress.ip_address(host), int(port))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_vaddr(virtual_addr):
|
|
35
|
+
match = RE_VIRTUAL_ADDR_MAC.search(virtual_addr)
|
|
36
|
+
if match:
|
|
37
|
+
return EUI(virtual_addr.split("@")[0], dialect=mac_unix)
|
|
38
|
+
|
|
39
|
+
match = RE_VIRTUAL_ADDR_NETWORK.search(virtual_addr)
|
|
40
|
+
if match and 0 < int(match.group(1)) <= 128:
|
|
41
|
+
return ipaddress.ip_network(virtual_addr)
|
|
42
|
+
|
|
43
|
+
match = RE_VIRTUAL_ADDR_CLIENT.search(virtual_addr)
|
|
44
|
+
if match:
|
|
45
|
+
return ipaddress.ip_address(RE_VIRTUAL_ADDR_CLIENT.sub("", virtual_addr))
|
|
46
|
+
|
|
47
|
+
return ipaddress.ip_address(virtual_addr)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_filesize(size):
|
|
51
|
+
if isinstance(size, FileSize):
|
|
52
|
+
return size
|
|
53
|
+
return FileSize(size)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PeerAddress(collections.namedtuple("PeerAddress", "host port")):
|
|
57
|
+
"""The address of peer entity.
|
|
58
|
+
|
|
59
|
+
:param host: The host address of peer entity.
|
|
60
|
+
:type host: :class:`~ipaddress.IPv4Address` or
|
|
61
|
+
:class:`~ipaddress.IPv6Address`
|
|
62
|
+
:param port: The port of peer entity.
|
|
63
|
+
:type port: :class:`int`
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __str__(self):
|
|
67
|
+
return "{0}:{1}".format(self.host, self.port)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class FileSize(int):
|
|
71
|
+
"""The size of bytes."""
|
|
72
|
+
|
|
73
|
+
def __str__(self):
|
|
74
|
+
return self.humanize()
|
|
75
|
+
|
|
76
|
+
def humanize(self, **kwargs):
|
|
77
|
+
"""Gets the human-friendly representation of file size.
|
|
78
|
+
|
|
79
|
+
:param kwargs: All keyword arguments will be passed to
|
|
80
|
+
:func:`humanize.filesize.naturalsize`.
|
|
81
|
+
"""
|
|
82
|
+
return naturalsize(self, **kwargs)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
VERSION = (0, 3, 0, "final")
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_version():
|
|
5
|
+
version = "%s.%s.%s" % VERSION[:3]
|
|
6
|
+
if VERSION[3:] == ("alpha", 0):
|
|
7
|
+
return "%s pre-alpha" % version
|
|
8
|
+
if VERSION[3].startswith("post"):
|
|
9
|
+
return "%s.%s" % (version, VERSION[3])
|
|
10
|
+
if VERSION[3] != "final":
|
|
11
|
+
revision = VERSION[4] if len(VERSION) > 4 else 0
|
|
12
|
+
return "%s%s%s" % (version, VERSION[3][0], revision)
|
|
13
|
+
return version
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__version__ = get_version()
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openvpn-status-reader
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Parse OpenVPN status logs in Python
|
|
5
|
+
Home-page: https://github.com/openwisp/openvpn-status-reader
|
|
6
|
+
Author: OpenWISP
|
|
7
|
+
Author-email: support@openwisp.org
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: openvpn,status,log
|
|
10
|
+
Platform: Any
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
23
|
+
Classifier: Topic :: Internet :: Log Analysis
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Topic :: System :: Logging
|
|
26
|
+
Classifier: Topic :: Text Processing
|
|
27
|
+
Classifier: Topic :: Utilities
|
|
28
|
+
Requires-Python: >=3.11
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
License-File: AUTHORS.rst
|
|
31
|
+
Requires-Dist: humanize<5,>=4.16
|
|
32
|
+
Requires-Dist: netaddr<2,>=1.3
|
|
33
|
+
Dynamic: author
|
|
34
|
+
Dynamic: author-email
|
|
35
|
+
Dynamic: classifier
|
|
36
|
+
Dynamic: description
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: keywords
|
|
39
|
+
Dynamic: license
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
Dynamic: platform
|
|
42
|
+
Dynamic: requires-dist
|
|
43
|
+
Dynamic: requires-python
|
|
44
|
+
Dynamic: summary
|
|
45
|
+
|
|
46
|
+
OpenVPN Status Reader
|
|
47
|
+
=====================
|
|
48
|
+
|
|
49
|
+
.. image:: https://github.com/openwisp/openvpn-status-reader/actions/workflows/ci.yml/badge.svg?branch=master
|
|
50
|
+
:target: https://github.com/openwisp/openvpn-status-reader/actions/workflows/ci.yml
|
|
51
|
+
:alt: CI build status
|
|
52
|
+
|
|
53
|
+
.. image:: https://coveralls.io/repos/openwisp/openvpn-status-reader/badge.svg
|
|
54
|
+
:target: https://coveralls.io/r/openwisp/openvpn-status-reader
|
|
55
|
+
|
|
56
|
+
.. image:: https://img.shields.io/librariesio/release/github/openwisp/openvpn-status-reader
|
|
57
|
+
:target: https://libraries.io/github/openwisp/openvpn-status-reader#repository_dependencies
|
|
58
|
+
:alt: Dependency monitoring
|
|
59
|
+
|
|
60
|
+
.. image:: https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=flat-square
|
|
61
|
+
:target: https://gitter.im/openwisp/general
|
|
62
|
+
|
|
63
|
+
.. image:: https://badge.fury.io/py/openvpn-status-reader.svg
|
|
64
|
+
:target: https://badge.fury.io/py/openvpn-status-reader
|
|
65
|
+
|
|
66
|
+
.. image:: https://pepy.tech/badge/openvpn-status-reader
|
|
67
|
+
:target: https://pepy.tech/project/openvpn-status-reader
|
|
68
|
+
:alt: downloads
|
|
69
|
+
|
|
70
|
+
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
|
71
|
+
:target: https://pypi.org/project/black/
|
|
72
|
+
:alt: code style: black
|
|
73
|
+
|
|
74
|
+
.. summary-begin
|
|
75
|
+
|
|
76
|
+
**openvpn-status-reader** is an OpenWISP-maintained Python library for
|
|
77
|
+
parsing OpenVPN status-version-1 logs into Python data structures. It
|
|
78
|
+
supports both legacy and current numeric OpenVPN timestamps.
|
|
79
|
+
|
|
80
|
+
.. summary-end
|
|
81
|
+
|
|
82
|
+
This project is a fork of the original openvpn-status_ repository. It
|
|
83
|
+
retains the ``openvpn_status`` import namespace for compatibility.
|
|
84
|
+
|
|
85
|
+
It supports Python 3.11 and newer.
|
|
86
|
+
|
|
87
|
+
Installation
|
|
88
|
+
------------
|
|
89
|
+
|
|
90
|
+
.. code-block:: bash
|
|
91
|
+
|
|
92
|
+
pip install openvpn-status-reader
|
|
93
|
+
|
|
94
|
+
Don't forget to put it in ``setup.py`` / ``requirements.txt``.
|
|
95
|
+
|
|
96
|
+
Getting Started
|
|
97
|
+
---------------
|
|
98
|
+
|
|
99
|
+
You could configure your OpenVPN server to log for client status. In usual
|
|
100
|
+
it could be achieved by adding ``status /path/to/openvpn-status.log`` line
|
|
101
|
+
to ``/etc/openvpn/openvpn.conf``. For example:
|
|
102
|
+
|
|
103
|
+
::
|
|
104
|
+
|
|
105
|
+
proto udp
|
|
106
|
+
port 1194
|
|
107
|
+
dev tun0
|
|
108
|
+
status /var/run/openvpn-status.log
|
|
109
|
+
|
|
110
|
+
Once OpenVPN server running, the log file will be created and written. It
|
|
111
|
+
looks like:
|
|
112
|
+
|
|
113
|
+
::
|
|
114
|
+
|
|
115
|
+
OpenVPN CLIENT LIST
|
|
116
|
+
Updated,Thu Jun 18 08:12:15 2015
|
|
117
|
+
Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since
|
|
118
|
+
foo@example.com,10.10.10.10:49502,334948,1973012,Thu Jun 18 04:23:03 2015
|
|
119
|
+
bar@example.com,10.10.10.10:64169,1817262,28981224,Thu Jun 18 04:08:39 2015
|
|
120
|
+
ROUTING TABLE
|
|
121
|
+
Virtual Address,Common Name,Real Address,Last Ref
|
|
122
|
+
192.168.255.134,foo@example.com,10.10.10.10:49502,Thu Jun 18 08:12:09 2015
|
|
123
|
+
192.168.255.126,bar@example.com,10.10.10.10:64169,Thu Jun 18 08:11:55 2015
|
|
124
|
+
GLOBAL STATS
|
|
125
|
+
Max bcast/mcast queue length,0
|
|
126
|
+
END
|
|
127
|
+
|
|
128
|
+
Now we could parse log file with this library:
|
|
129
|
+
|
|
130
|
+
.. code-block:: python
|
|
131
|
+
|
|
132
|
+
from openvpn_status import parse_status
|
|
133
|
+
|
|
134
|
+
with open("/var/run/openvpn-status.log") as logfile:
|
|
135
|
+
status = parse_status(logfile.read())
|
|
136
|
+
|
|
137
|
+
print(status.updated_at) # datetime.datetime(2015, 6, 18, 8, 12, 15)
|
|
138
|
+
|
|
139
|
+
foo_client = status.client_list["169.254.0.1"]
|
|
140
|
+
print(foo_client.common_name) # foo@example.com
|
|
141
|
+
print(foo_client.bytes_received) # 334.9 kB
|
|
142
|
+
print(foo_client.bytes_sent) # 2.0 MB
|
|
143
|
+
print(int(foo_client.bytes_sent)) # 2097152
|
|
144
|
+
|
|
145
|
+
More details are in the `API reference`_.
|
|
146
|
+
|
|
147
|
+
.. _api reference: https://openvpn-status-reader.readthedocs.io/en/latest/api.html
|
|
148
|
+
|
|
149
|
+
.. _openvpn-status: https://github.com/sgares/openvpn-status
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
openvpn_status/__init__.py,sha256=65Mw3mwHExVRbIDQnCRuEUn9VFvV5G-WWz_6cUaaVHE,161
|
|
2
|
+
openvpn_status/descriptors.py,sha256=mdIdjJwcsQwqYV2Mp5dFQcHg23Gd8kCR4WKBR9bjUkQ,1501
|
|
3
|
+
openvpn_status/models.py,sha256=Nq4zQrkmjUuctAgWQwR04tW7OTZeJvF4NCk6neHzcbU,1427
|
|
4
|
+
openvpn_status/parser.py,sha256=SZjysxPLkjV6xHQ8ZNLf4lh2PZrnP_bdeApYssUmMQw,4745
|
|
5
|
+
openvpn_status/shortcuts.py,sha256=RPzeOIOYqoVqcYbL_6-ri4RNdpDWLyuqdtj2O9xVfRg,519
|
|
6
|
+
openvpn_status/utils.py,sha256=51L8qhL2-NIShLyF_gJHmU8pYtUTKgXH_mvAFx0vM4I,2461
|
|
7
|
+
openvpn_status/version.py,sha256=9ooItZ1qzN0D6zvu82nrxKtjTC_TS1xAJ9OAYjlpbk4,447
|
|
8
|
+
openvpn_status_reader-0.3.0.dist-info/licenses/AUTHORS.rst,sha256=_yZAhGNJkmhOmpjvSEz7nCEz8q4j1AE93Ng8avDyEjU,37
|
|
9
|
+
openvpn_status_reader-0.3.0.dist-info/licenses/LICENSE,sha256=Ik-e1SSP7v1kYtaYghyMy3PoAkLQNFkSMCEmIlKtkjk,1074
|
|
10
|
+
openvpn_status_reader-0.3.0.dist-info/METADATA,sha256=9zFhdGe8Ev-yaAwtRDrnlAKNLMhfkIFD0OpeI5WBZfM,4966
|
|
11
|
+
openvpn_status_reader-0.3.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
12
|
+
openvpn_status_reader-0.3.0.dist-info/top_level.txt,sha256=KEaBstJM0_80gRTuMmOulOCKaLi3LAsU59T5XTbmO5o,15
|
|
13
|
+
openvpn_status_reader-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- Jiangge Zhang <tonyseek@gmail.com>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright (c) Jiangge Zhang
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
18
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
19
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
|
20
|
+
OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
openvpn_status
|