Added ping plugin and backend

This commit is contained in:
Fabio Manganiello 2019-12-27 23:26:39 +01:00
parent ce2b3ae849
commit 663be43f06
6 changed files with 220 additions and 17 deletions

68
platypush/backend/ping.py Normal file
View File

@ -0,0 +1,68 @@
import time
from typing import List, Tuple
from platypush.backend import Backend
from platypush.context import get_plugin
from platypush.message.event.ping import HostUpEvent, HostDownEvent
from platypush.utils.workers import Worker, Workers
class PingBackend(Backend):
"""
This backend allows you to ping multiple remote hosts at regular intervals.
Triggers:
- :class:`platypush.message.ping.HostDownEvent` if a host stops responding ping requests
- :class:`platypush.message.ping.HostUpEvent` if a host starts responding ping requests
"""
class Pinger(Worker):
def __init__(self, *args, **kwargs):
self.timeout = kwargs.pop('timeout')
super().__init__(*args, **kwargs)
def process(self, host: str) -> Tuple[str, bool]:
pinger = get_plugin('ping')
response = pinger.ping(host, timeout=self.timeout, count=1).output
return host, response['success'] is True
def __init__(self, hosts: List[str], timeout: float = 5.0, interval: float = 60.0, *args, **kwargs):
"""
:param hosts: List of IP addresses or host names to monitor.
:param timeout: Ping timeout.
:param interval: Interval between two scans.
"""
super().__init__(*args, **kwargs)
self.hosts = {h: None for h in hosts}
self.timeout = timeout
self.interval = interval
def run(self):
super().run()
self.logger.info('Starting ping backend with {} hosts to monitor'.format(len(self.hosts)))
while not self.should_stop():
workers = Workers(min(len(self.hosts), 10), self.Pinger, timeout=self.timeout)
with workers:
for host in self.hosts.keys():
workers.put(host)
for response in workers.responses:
host, is_up = response
if is_up != self.hosts[host]:
if is_up:
self.bus.post(HostUpEvent(host))
else:
self.bus.post(HostDownEvent(host))
self.hosts[host] = is_up
time.sleep(self.interval)
# vim:sw=4:ts=4:et:

View File

@ -10,6 +10,15 @@ logger = logging.getLogger(__name__)
class Message(object):
""" Message generic class """
class Encoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime.datetime) or \
isinstance(obj, datetime.date) or \
isinstance(obj, datetime.time):
return obj.isoformat()
return super().default(obj)
def __init__(self, timestamp=None, *args, **kwargs):
self.timestamp = timestamp or time.time()
@ -24,7 +33,7 @@ class Message(object):
for attr in self.__dir__()
if (attr != '_timestamp' or not attr.startswith('_'))
and not inspect.ismethod(getattr(self, attr))
}, cls=MessageEncoder).replace('\n', ' ')
}, cls=self.Encoder).replace('\n', ' ')
def __bytes__(self):
"""
@ -133,14 +142,4 @@ class Mapping(dict):
return str(self.__dict__)
class MessageEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime.datetime) or \
isinstance(obj, datetime.date) or \
isinstance(obj, datetime.time):
return obj.isoformat()
return super().default(obj)
# vim:sw=4:ts=4:et:

View File

@ -1,4 +1,4 @@
from platypush.message.event import Event, EventMatchResult
from platypush.message.event import Event
class PingEvent(Event):
@ -13,5 +13,20 @@ class PingEvent(Event):
super().__init__(message=message, *args, **kwargs)
# vim:sw=4:ts=4:et:
class HostDownEvent(Event):
"""
Event triggered when a remote host stops responding ping requests.
"""
def __init__(self, host: str, *args, **kwargs):
super().__init__(host=host, *args, **kwargs)
class HostUpEvent(Event):
"""
Event triggered when a remote host starts responding ping requests.
"""
def __init__(self, host: str, *args, **kwargs):
super().__init__(host=host, *args, **kwargs)
# vim:sw=4:ts=4:et:

View File

@ -1,7 +1,7 @@
import json
import time
from platypush.message import Message, MessageEncoder
from platypush.message import Message
class Response(Message):
@ -66,9 +66,8 @@ class Response(Message):
Overrides the str() operator and converts
the message into a UTF-8 JSON string
"""
output = self.output if self.output is not None and self.output != {} else {
'status': 'ok' if not self.errors else 'error'
'success': True if not self.errors else False
}
response_dict = {
@ -86,7 +85,7 @@ class Response(Message):
if self.disable_logging:
response_dict['_disable_logging'] = self.disable_logging
return json.dumps(response_dict, cls=MessageEncoder)
return json.dumps(response_dict, cls=self.Encoder)
# vim:sw=4:ts=4:et:

View File

@ -0,0 +1,27 @@
from typing import Optional
from platypush.message.response import Response
class PingResponse(Response):
# noinspection PyShadowingBuiltins
def __init__(self,
host: str,
success: bool,
*args,
min: Optional[float] = None,
max: Optional[float] = None,
avg: Optional[float] = None,
mdev: Optional[float] = None,
**kwargs):
super().__init__(*args, output={
'host': host,
'success': success,
'min': min,
'max': max,
'avg': avg,
'mdev': mdev,
}, **kwargs)
# vim:sw=4:ts=4:et:

95
platypush/plugins/ping.py Normal file
View File

@ -0,0 +1,95 @@
import re
import subprocess
import sys
from typing import Optional, List
from platypush.message.response.ping import PingResponse
from platypush.plugins import Plugin, action
PING_MATCHER = re.compile(
r"(?P<min>\d+.\d+)/(?P<avg>\d+.\d+)/(?P<max>\d+.\d+)/(?P<mdev>\d+.\d+)"
)
PING_MATCHER_BUSYBOX = re.compile(
r"(?P<min>\d+.\d+)/(?P<avg>\d+.\d+)/(?P<max>\d+.\d+)"
)
WIN32_PING_MATCHER = re.compile(r"(?P<min>\d+)ms.+(?P<max>\d+)ms.+(?P<avg>\d+)ms")
class PingPlugin(Plugin):
"""
Perform ICMP network ping on remote hosts.
"""
def __init__(self, executable: str = 'ping', count: int = 1, timeout: float = 5.0, **kwargs):
"""
:param executable: Path to the ``ping`` executable. Default: the first ``ping`` executable found in PATH.
:param count: Default number of packets that should be sent (default: 1).
:param timeout: Default timeout before failing a ping request (default: 5 seconds).
"""
super().__init__(**kwargs)
self.executable = executable
self.count = count
self.timeout = timeout
def _get_ping_cmd(self, host: str, count: int, timeout: float) -> List[str]:
if sys.platform == 'win32':
return [
self.executable,
'-n',
str(count or self.count),
'-w',
str((timeout or self.timeout) * 1000),
host,
]
return [
self.executable,
'-n',
'-q',
'-c',
str(count or self.count),
'-W',
str(timeout or self.timeout),
host,
]
@action
def ping(self, host: str, count: Optional[int] = None, timeout: Optional[float] = None) -> PingResponse:
"""
Ping a remote host.
:param host: Remote host IP or name
:param count: Number of packets that should be sent (default: 1).
:param timeout: Timeout before failing a ping request (default: 5 seconds).
"""
count = count or self.count
timeout = timeout or self.timeout
pinger = subprocess.Popen(
self._get_ping_cmd(host, count=count, timeout=timeout),
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
out = pinger.communicate()
if sys.platform == "win32":
match = WIN32_PING_MATCHER.search(str(out).split("\n")[-1])
min_val, avg_val, max_val = match.groups()
mdev_val = None
elif "max/" not in str(out):
match = PING_MATCHER_BUSYBOX.search(str(out).split("\n")[-1])
min_val, avg_val, max_val = match.groups()
mdev_val = None
else:
match = PING_MATCHER.search(str(out).split("\n")[-1])
min_val, avg_val, max_val, mdev_val = match.groups()
return PingResponse(host=host, success=True, min=min_val, max=max_val, avg=avg_val, mdev=mdev_val)
except (subprocess.CalledProcessError, AttributeError):
return PingResponse(host=host, success=False)
# vim:sw=4:ts=4:et: