forked from platypush/platypush
parent
3bb7c02572
commit
d048752184
5 changed files with 194 additions and 23 deletions
|
@ -71,6 +71,9 @@
|
||||||
"music.spotify": {
|
"music.spotify": {
|
||||||
"class": "fab fa-spotify"
|
"class": "fab fa-spotify"
|
||||||
},
|
},
|
||||||
|
"ping": {
|
||||||
|
"class": "fas fa-server"
|
||||||
|
},
|
||||||
"torrent": {
|
"torrent": {
|
||||||
"class": "fa fa-magnet"
|
"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": {
|
"time_duration_sensor": {
|
||||||
"name": "Sensor",
|
"name": "Sensor",
|
||||||
"name_plural": "Sensors",
|
"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 subprocess
|
||||||
import sys
|
import sys
|
||||||
from concurrent.futures import ProcessPoolExecutor
|
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.message.event.ping import HostUpEvent, HostDownEvent, PingResponseEvent
|
||||||
from platypush.plugins import RunnablePlugin, action
|
from platypush.plugins import RunnablePlugin, action
|
||||||
from platypush.schemas.ping import PingResponseSchema
|
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:
|
def ping(host: str, ping_cmd: List[str], logger: logging.Logger) -> dict:
|
||||||
|
out = None
|
||||||
|
pinger = None
|
||||||
err_response = dict(
|
err_response = dict(
|
||||||
PingResponseSchema().dump(
|
PingResponseSchema().dump(
|
||||||
{
|
{
|
||||||
|
@ -28,12 +33,12 @@ def ping(host: str, ping_cmd: List[str], logger: logging.Logger) -> dict:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
with subprocess.Popen(
|
with subprocess.Popen(
|
||||||
ping_cmd,
|
ping_cmd,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
) as pinger:
|
) as pinger:
|
||||||
try:
|
|
||||||
out = pinger.communicate()
|
out = pinger.communicate()
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
match = WIN32_PING_MATCHER.search(str(out).rsplit("\n", maxsplit=1)[-1])
|
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(
|
match = PING_MATCHER_BUSYBOX.search(
|
||||||
str(out).rsplit("\n", maxsplit=1)[-1]
|
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()
|
min_val, avg_val, max_val = match.groups()
|
||||||
mdev_val = None
|
mdev_val = None
|
||||||
else:
|
else:
|
||||||
match = PING_MATCHER.search(str(out).rsplit("\n", maxsplit=1)[-1])
|
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()
|
min_val, avg_val, max_val, mdev_val = match.groups()
|
||||||
|
|
||||||
return dict(
|
return dict(
|
||||||
|
@ -64,16 +69,21 @@ def ping(host: str, ping_cmd: List[str], logger: logging.Logger) -> dict:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if not isinstance(e, (subprocess.CalledProcessError, KeyboardInterrupt)):
|
err = (
|
||||||
logger.warning("Error while pinging host %s: %s", host, e)
|
'\n'.join(line.decode().strip() for line in out)
|
||||||
logger.exception(e)
|
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.kill()
|
||||||
pinger.wait()
|
pinger.wait()
|
||||||
|
|
||||||
return err_response
|
return err_response
|
||||||
|
|
||||||
|
|
||||||
class PingPlugin(RunnablePlugin):
|
class PingPlugin(RunnablePlugin, EntityManager):
|
||||||
"""
|
"""
|
||||||
This integration allows you to:
|
This integration allows you to:
|
||||||
|
|
||||||
|
@ -88,7 +98,7 @@ class PingPlugin(RunnablePlugin):
|
||||||
count: int = 1,
|
count: int = 1,
|
||||||
timeout: float = 5.0,
|
timeout: float = 5.0,
|
||||||
hosts: Optional[List[str]] = None,
|
hosts: Optional[List[str]] = None,
|
||||||
poll_interval: float = 10.0,
|
poll_interval: float = 20.0,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
@ -103,7 +113,7 @@ class PingPlugin(RunnablePlugin):
|
||||||
self.executable = executable
|
self.executable = executable
|
||||||
self.count = count
|
self.count = count
|
||||||
self.timeout = timeout
|
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]:
|
def _get_ping_cmd(self, host: str, count: int, timeout: float) -> List[str]:
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
|
@ -162,17 +172,46 @@ class PingPlugin(RunnablePlugin):
|
||||||
if success is None:
|
if success is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
prev_result = self.hosts.get(host)
|
||||||
if success:
|
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))
|
self._bus.post(HostUpEvent(host=host))
|
||||||
|
|
||||||
result.pop("success", None)
|
|
||||||
self._bus.post(PingResponseEvent(**result))
|
self._bus.post(PingResponseEvent(**result))
|
||||||
self.hosts[host] = True
|
self.hosts[host] = result
|
||||||
else:
|
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._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):
|
def main(self):
|
||||||
# Don't start the thread if no monitored hosts are configured
|
# Don't start the thread if no monitored hosts are configured
|
||||||
|
@ -193,12 +232,13 @@ class PingPlugin(RunnablePlugin):
|
||||||
[self.logger] * len(self.hosts),
|
[self.logger] * len(self.hosts),
|
||||||
):
|
):
|
||||||
self._process_ping_result(result)
|
self._process_ping_result(result)
|
||||||
except KeyboardInterrupt:
|
except (KeyboardInterrupt, BrokenProcessPool):
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning("Error while pinging hosts: %s", e)
|
self.logger.warning("Error while pinging hosts: %s", e)
|
||||||
self.logger.exception(e)
|
self.logger.exception(e)
|
||||||
finally:
|
finally:
|
||||||
|
self.publish_entities()
|
||||||
self.wait_stop(self.poll_interval)
|
self.wait_stop(self.poll_interval)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue