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.
@@ -0,0 +1,5 @@
1
+ from .parser import ParsingError
2
+ from .shortcuts import parse_status
3
+ from .version import __version__
4
+
5
+ __all__ = ["parse_status", "ParsingError", "__version__"]
@@ -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
@@ -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
+ )
@@ -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()
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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