forked from platypush/platypush
parent
3bb7c02572
commit
d048752184
5 changed files with 194 additions and 23 deletions
|
@ -71,6 +71,9 @@
|
|||
"music.spotify": {
|
||||
"class": "fab fa-spotify"
|
||||
},
|
||||
"ping": {
|
||||
"class": "fas fa-server"
|
||||
},
|
||||
"torrent": {
|
||||
"class": "fa fa-magnet"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<div class="entity ping-host-container">
|
||||
<div class="head">
|
||||
<div class="col-1 icon-container">
|
||||
<span class="icon" :class="iconClass" />
|
||||
</div>
|
||||
|
||||
<div class="col-10 name" @click.stop="isCollapsed = !isCollapsed">
|
||||
<div class="name" v-text="value.name" />
|
||||
</div>
|
||||
|
||||
<div class="col-1 collapse-toggler" @click.stop="isCollapsed = !isCollapsed">
|
||||
<i class="fas" :class="{'fa-chevron-down': isCollapsed, 'fa-chevron-up': !isCollapsed}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="body children attributes fade-in" v-if="!isCollapsed">
|
||||
<div class="child">
|
||||
<div class="col-s-12 col-m-6 label">
|
||||
<div class="name">Host</div>
|
||||
</div>
|
||||
<div class="value">
|
||||
<div class="name" v-text="value.name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="child" v-if="value.reachable">
|
||||
<div class="col-s-12 col-m-6 label">
|
||||
<div class="name">Ping</div>
|
||||
</div>
|
||||
<div class="value">
|
||||
<span v-text="value.avg" /> ms
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EntityMixin from "./EntityMixin"
|
||||
|
||||
export default {
|
||||
mixins: [EntityMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
isCollapsed: true,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
iconClass() {
|
||||
return this.value.reachable ? "reachable" : "unreachable"
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "common";
|
||||
|
||||
.ping-host-container {
|
||||
.head .icon-container {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2em;
|
||||
margin-right: 0.5em;
|
||||
padding-right: 0;
|
||||
|
||||
.icon {
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
border-radius: 50%;
|
||||
|
||||
&.reachable {
|
||||
background-color: $ok-fg;
|
||||
}
|
||||
|
||||
&.unreachable {
|
||||
background-color: $error-fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -327,6 +327,14 @@
|
|||
}
|
||||
},
|
||||
|
||||
"ping_host": {
|
||||
"name": "Host",
|
||||
"name_plural": "Hosts",
|
||||
"icon": {
|
||||
"class": "fas fa-server"
|
||||
}
|
||||
},
|
||||
|
||||
"time_duration_sensor": {
|
||||
"name": "Sensor",
|
||||
"name_plural": "Sensors",
|
||||
|
|
34
platypush/entities/ping.py
Normal file
34
platypush/entities/ping.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from sqlalchemy import (
|
||||
Column,
|
||||
ForeignKey,
|
||||
Float,
|
||||
Integer,
|
||||
)
|
||||
|
||||
from platypush.common.db import is_defined
|
||||
|
||||
from .devices import Device
|
||||
|
||||
|
||||
if not is_defined('ping_host'):
|
||||
|
||||
class PingHost(Device):
|
||||
"""
|
||||
Entity that maps a generic host that can be pinged.
|
||||
"""
|
||||
|
||||
__tablename__ = 'ping_host'
|
||||
|
||||
id = Column(
|
||||
Integer, ForeignKey(Device.id, ondelete='CASCADE'), primary_key=True
|
||||
)
|
||||
|
||||
min = Column(Float)
|
||||
max = Column(Float)
|
||||
avg = Column(Float)
|
||||
mdev = Column(Float)
|
||||
|
||||
__table_args__ = {'extend_existing': True}
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': __tablename__,
|
||||
}
|
|
@ -3,8 +3,11 @@ import re
|
|||
import subprocess
|
||||
import sys
|
||||
from concurrent.futures import ProcessPoolExecutor
|
||||
from typing import Dict, Optional, List
|
||||
from concurrent.futures.process import BrokenProcessPool
|
||||
from typing import Collection, Dict, Iterable, Optional, List
|
||||
|
||||
from platypush.entities import EntityManager
|
||||
from platypush.entities.ping import PingHost
|
||||
from platypush.message.event.ping import HostUpEvent, HostDownEvent, PingResponseEvent
|
||||
from platypush.plugins import RunnablePlugin, action
|
||||
from platypush.schemas.ping import PingResponseSchema
|
||||
|
@ -19,6 +22,8 @@ WIN32_PING_MATCHER = re.compile(r"(?P<min>\d+)ms.+(?P<max>\d+)ms.+(?P<avg>\d+)ms
|
|||
|
||||
|
||||
def ping(host: str, ping_cmd: List[str], logger: logging.Logger) -> dict:
|
||||
out = None
|
||||
pinger = None
|
||||
err_response = dict(
|
||||
PingResponseSchema().dump(
|
||||
{
|
||||
|
@ -28,12 +33,12 @@ def ping(host: str, ping_cmd: List[str], logger: logging.Logger) -> dict:
|
|||
)
|
||||
)
|
||||
|
||||
try:
|
||||
with subprocess.Popen(
|
||||
ping_cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
) as pinger:
|
||||
try:
|
||||
out = pinger.communicate()
|
||||
if sys.platform == "win32":
|
||||
match = WIN32_PING_MATCHER.search(str(out).rsplit("\n", maxsplit=1)[-1])
|
||||
|
@ -43,12 +48,12 @@ def ping(host: str, ping_cmd: List[str], logger: logging.Logger) -> dict:
|
|||
match = PING_MATCHER_BUSYBOX.search(
|
||||
str(out).rsplit("\n", maxsplit=1)[-1]
|
||||
)
|
||||
assert match is not None, f"No match found in {out}"
|
||||
assert match is not None, out
|
||||
min_val, avg_val, max_val = match.groups()
|
||||
mdev_val = None
|
||||
else:
|
||||
match = PING_MATCHER.search(str(out).rsplit("\n", maxsplit=1)[-1])
|
||||
assert match is not None, f"No match found in {out}"
|
||||
assert match is not None, out
|
||||
min_val, avg_val, max_val, mdev_val = match.groups()
|
||||
|
||||
return dict(
|
||||
|
@ -64,16 +69,21 @@ def ping(host: str, ping_cmd: List[str], logger: logging.Logger) -> dict:
|
|||
)
|
||||
)
|
||||
except Exception as e:
|
||||
if not isinstance(e, (subprocess.CalledProcessError, KeyboardInterrupt)):
|
||||
logger.warning("Error while pinging host %s: %s", host, e)
|
||||
logger.exception(e)
|
||||
err = (
|
||||
'\n'.join(line.decode().strip() for line in out)
|
||||
if isinstance(out, (tuple, list))
|
||||
else str(e)
|
||||
)
|
||||
|
||||
logger.warning("Error while pinging host %s: %s", host, err)
|
||||
if pinger and pinger.poll() is None:
|
||||
pinger.kill()
|
||||
pinger.wait()
|
||||
|
||||
return err_response
|
||||
|
||||
|
||||
class PingPlugin(RunnablePlugin):
|
||||
class PingPlugin(RunnablePlugin, EntityManager):
|
||||
"""
|
||||
This integration allows you to:
|
||||
|
||||
|
@ -88,7 +98,7 @@ class PingPlugin(RunnablePlugin):
|
|||
count: int = 1,
|
||||
timeout: float = 5.0,
|
||||
hosts: Optional[List[str]] = None,
|
||||
poll_interval: float = 10.0,
|
||||
poll_interval: float = 20.0,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
|
@ -103,7 +113,7 @@ class PingPlugin(RunnablePlugin):
|
|||
self.executable = executable
|
||||
self.count = count
|
||||
self.timeout = timeout
|
||||
self.hosts: Dict[str, Optional[bool]] = {h: None for h in (hosts or [])}
|
||||
self.hosts: Dict[str, Optional[dict]] = {h: None for h in (hosts or [])}
|
||||
|
||||
def _get_ping_cmd(self, host: str, count: int, timeout: float) -> List[str]:
|
||||
if sys.platform == 'win32':
|
||||
|
@ -162,17 +172,46 @@ class PingPlugin(RunnablePlugin):
|
|||
if success is None:
|
||||
return
|
||||
|
||||
prev_result = self.hosts.get(host)
|
||||
if success:
|
||||
if self.hosts.get(host) in (False, None):
|
||||
if not (prev_result and prev_result.get("success")):
|
||||
self._bus.post(HostUpEvent(host=host))
|
||||
|
||||
result.pop("success", None)
|
||||
self._bus.post(PingResponseEvent(**result))
|
||||
self.hosts[host] = True
|
||||
self.hosts[host] = result
|
||||
else:
|
||||
if self.hosts.get(host) in (True, None):
|
||||
if not prev_result or prev_result.get("success"):
|
||||
self._bus.post(HostDownEvent(host=host))
|
||||
self.hosts[host] = False
|
||||
self.hosts[host] = result
|
||||
|
||||
@action
|
||||
def status(self) -> Collection[PingHost]:
|
||||
"""
|
||||
Get the status of the monitored hosts.
|
||||
:return: Dictionary of monitored hosts and their status.
|
||||
"""
|
||||
return self.publish_entities()
|
||||
|
||||
def publish_entities(self, *_, **__) -> Collection[PingHost]:
|
||||
return super().publish_entities(self.hosts.values())
|
||||
|
||||
def transform_entities(
|
||||
self, entities: Collection[Optional[dict]], **_
|
||||
) -> Iterable[PingHost]:
|
||||
return super().transform_entities(
|
||||
[
|
||||
PingHost(
|
||||
id=status.get("host"),
|
||||
name=status.get("host"),
|
||||
reachable=status.get("success"),
|
||||
min=status.get("min"),
|
||||
max=status.get("max"),
|
||||
avg=status.get("avg"),
|
||||
)
|
||||
for status in entities
|
||||
if status
|
||||
]
|
||||
)
|
||||
|
||||
def main(self):
|
||||
# Don't start the thread if no monitored hosts are configured
|
||||
|
@ -193,12 +232,13 @@ class PingPlugin(RunnablePlugin):
|
|||
[self.logger] * len(self.hosts),
|
||||
):
|
||||
self._process_ping_result(result)
|
||||
except KeyboardInterrupt:
|
||||
except (KeyboardInterrupt, BrokenProcessPool):
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.warning("Error while pinging hosts: %s", e)
|
||||
self.logger.exception(e)
|
||||
finally:
|
||||
self.publish_entities()
|
||||
self.wait_stop(self.poll_interval)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue