Refactoring the system plugin to support entities.

This commit is contained in:
Fabio Manganiello 2023-04-15 01:25:04 +02:00
parent 3e3c48d779
commit b43017ef01
Signed by: blacklight
GPG key ID: D90FBA7F76362774

View file

@ -2,18 +2,47 @@ import socket
from datetime import datetime from datetime import datetime
from typing import Union, List, Optional, Dict from typing import Union, List, Optional, Dict
from typing_extensions import override
from platypush.message.response.system import CpuInfoResponse, CpuTimesResponse, CpuResponseList, CpuStatsResponse, \ from platypush.entities import Entity
CpuFrequencyResponse, VirtualMemoryUsageResponse, SwapMemoryUsageResponse, DiskResponseList, \ from platypush.entities.managers import EntityManager
DiskPartitionResponse, DiskUsageResponse, DiskIoCountersResponse, NetworkIoCountersResponse, NetworkResponseList, \ from platypush.entities.system import CpuInfo as CpuInfoModel
NetworkConnectionResponse, NetworkAddressResponse, NetworkInterfaceStatsResponse, SensorTemperatureResponse, \ from platypush.message.response.system import (
SensorResponseList, SensorFanResponse, SensorBatteryResponse, ConnectedUserResponseList, ConnectUserResponse, \ CpuTimesResponse,
ProcessResponseList, ProcessResponse CpuResponseList,
CpuStatsResponse,
from platypush.plugins import Plugin, action CpuFrequencyResponse,
VirtualMemoryUsageResponse,
SwapMemoryUsageResponse,
DiskResponseList,
DiskPartitionResponse,
DiskUsageResponse,
DiskIoCountersResponse,
NetworkIoCountersResponse,
NetworkResponseList,
NetworkConnectionResponse,
NetworkAddressResponse,
NetworkInterfaceStatsResponse,
SensorTemperatureResponse,
SensorResponseList,
SensorFanResponse,
SensorBatteryResponse,
ConnectedUserResponseList,
ConnectUserResponse,
ProcessResponseList,
ProcessResponse,
)
from platypush.plugins import action
from platypush.plugins.sensor import SensorPlugin
from platypush.schemas.system import (
CpuInfo,
CpuInfoSchema,
SystemInfoSchema,
)
class SystemPlugin(Plugin): # pylint: disable=too-many-ancestors
class SystemPlugin(SensorPlugin, EntityManager):
""" """
Plugin to get system info. Plugin to get system info.
@ -24,35 +53,40 @@ class SystemPlugin(Plugin):
""" """
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__cpu_info: Optional[CpuInfo] = None
@staticmethod
def _entity_args(name: str) -> Dict[str, str]:
return {
'id': f'system:{name}',
'name': name,
}
@property
def _cpu_info(self) -> CpuInfo:
from cpuinfo import get_cpu_info
if not self.__cpu_info:
# The CPU information won't change while the process is running, so
# it makes sense to cache it only once.
self.__cpu_info = CpuInfoSchema().load(get_cpu_info()) # type: ignore
return self.__cpu_info # type: ignore
@action @action
def cpu_info(self) -> CpuInfoResponse: def cpu_info(self):
""" """
Get CPU info. Get CPU info.
:return: :class:`platypush.message.response.system.CpuInfoResponse` :return: .. schema:: system.CpuInfoSchema
""" """
from cpuinfo import get_cpu_info return CpuInfoSchema().dump(self._cpu_info)
info = get_cpu_info()
return CpuInfoResponse(
arch=info.get('raw_arch_string'),
bits=info.get('bits'),
count=info.get('count'),
vendor_id=info.get('vendor_id'),
brand=info.get('brand'),
hz_advertised=info.get('hz_advertised_raw')[0],
hz_actual=info.get('hz_actual_raw')[0],
stepping=info.get('stepping'),
model=info.get('model'),
family=info.get('family'),
flags=info.get('flags'),
l1_instruction_cache_size=info.get('l1_instruction_cache_size'),
l1_data_cache_size=info.get('l1_data_cache_size'),
l2_cache_size=info.get('l2_cache_size'),
l3_cache_size=info.get('l3_cache_size'),
)
@action @action
def cpu_times(self, per_cpu=False, percent=False) -> Union[CpuTimesResponse, CpuResponseList]: def cpu_times(
self, per_cpu=False, percent=False
) -> Union[CpuTimesResponse, CpuResponseList]:
""" """
Get the CPU times stats. Get the CPU times stats.
@ -62,25 +96,30 @@ class SystemPlugin(Plugin):
""" """
import psutil import psutil
times = psutil.cpu_times_percent(percpu=per_cpu) if percent else \ times = (
psutil.cpu_times(percpu=per_cpu) psutil.cpu_times_percent(percpu=per_cpu)
if percent
else psutil.cpu_times(percpu=per_cpu)
)
if per_cpu: if per_cpu:
return CpuResponseList([ return CpuResponseList(
CpuTimesResponse( [
user=t.user, CpuTimesResponse(
nice=t.nice, user=t.user,
system=t.system, nice=t.nice,
idle=t.idle, system=t.system,
iowait=t.iowait, idle=t.idle,
irq=t.irq, iowait=t.iowait,
softirq=t.softirq, irq=t.irq,
steal=t.steal, softirq=t.softirq,
guest=t.guest, steal=t.steal,
guest_nice=t.guest_nice, guest=t.guest,
) guest_nice=t.guest_nice,
for t in times )
]) for t in times
]
)
return CpuTimesResponse( return CpuTimesResponse(
user=times.user, user=times.user,
@ -96,7 +135,9 @@ class SystemPlugin(Plugin):
) )
@action @action
def cpu_percent(self, per_cpu: bool = False, interval: Optional[float] = None) -> Union[float, List[float]]: def cpu_percent(
self, per_cpu: bool = False, interval: Optional[float] = None
) -> Union[float, List[float]]:
""" """
Get the CPU load percentage. Get the CPU load percentage.
@ -108,10 +149,11 @@ class SystemPlugin(Plugin):
:return: float if ``per_cpu=False``, ``list[float]`` otherwise. :return: float if ``per_cpu=False``, ``list[float]`` otherwise.
""" """
import psutil import psutil
percent = psutil.cpu_percent(percpu=per_cpu, interval=interval) percent = psutil.cpu_percent(percpu=per_cpu, interval=interval)
if per_cpu: if per_cpu:
return [p for p in percent] return list(percent) # type: ignore
return percent return percent
@action @action
@ -121,6 +163,7 @@ class SystemPlugin(Plugin):
:return: :class:`platypush.message.response.system.CpuStatsResponse` :return: :class:`platypush.message.response.system.CpuStatsResponse`
""" """
import psutil import psutil
stats = psutil.cpu_stats() stats = psutil.cpu_stats()
return CpuStatsResponse( return CpuStatsResponse(
@ -131,7 +174,9 @@ class SystemPlugin(Plugin):
) )
@action @action
def cpu_frequency(self, per_cpu: bool = False) -> Union[CpuFrequencyResponse, CpuResponseList]: def cpu_frequency(
self, per_cpu: bool = False
) -> Union[CpuFrequencyResponse, CpuResponseList]:
""" """
Get CPU stats. Get CPU stats.
@ -139,17 +184,20 @@ class SystemPlugin(Plugin):
:return: :class:`platypush.message.response.system.CpuFrequencyResponse` :return: :class:`platypush.message.response.system.CpuFrequencyResponse`
""" """
import psutil import psutil
freq = psutil.cpu_freq(percpu=per_cpu) freq = psutil.cpu_freq(percpu=per_cpu)
if per_cpu: if per_cpu:
return CpuResponseList([ return CpuResponseList(
CpuFrequencyResponse( [
min=f.min, CpuFrequencyResponse(
max=f.max, min=f.min,
current=f.current, max=f.max,
) current=f.current,
for f in freq )
]) for f in freq
]
)
return CpuFrequencyResponse( return CpuFrequencyResponse(
min=freq.min, min=freq.min,
@ -163,6 +211,7 @@ class SystemPlugin(Plugin):
Get the average load as a vector that represents the load within the last 1, 5 and 15 minutes. Get the average load as a vector that represents the load within the last 1, 5 and 15 minutes.
""" """
import psutil import psutil
return psutil.getloadavg() return psutil.getloadavg()
@action @action
@ -172,6 +221,7 @@ class SystemPlugin(Plugin):
:return: list of :class:`platypush.message.response.system.VirtualMemoryUsageResponse` :return: list of :class:`platypush.message.response.system.VirtualMemoryUsageResponse`
""" """
import psutil import psutil
mem = psutil.virtual_memory() mem = psutil.virtual_memory()
return VirtualMemoryUsageResponse( return VirtualMemoryUsageResponse(
total=mem.total, total=mem.total,
@ -193,6 +243,7 @@ class SystemPlugin(Plugin):
:return: list of :class:`platypush.message.response.system.SwapMemoryUsageResponse` :return: list of :class:`platypush.message.response.system.SwapMemoryUsageResponse`
""" """
import psutil import psutil
mem = psutil.swap_memory() mem = psutil.swap_memory()
return SwapMemoryUsageResponse( return SwapMemoryUsageResponse(
total=mem.total, total=mem.total,
@ -210,18 +261,24 @@ class SystemPlugin(Plugin):
:return: list of :class:`platypush.message.response.system.DiskPartitionResponse` :return: list of :class:`platypush.message.response.system.DiskPartitionResponse`
""" """
import psutil import psutil
parts = psutil.disk_partitions() parts = psutil.disk_partitions()
return DiskResponseList([ return DiskResponseList(
DiskPartitionResponse( [
device=p.device, DiskPartitionResponse(
mount_point=p.mountpoint, device=p.device,
fstype=p.fstype, mount_point=p.mountpoint,
opts=p.opts, fstype=p.fstype,
) for p in parts opts=p.opts,
]) )
for p in parts
]
)
@action @action
def disk_usage(self, path: Optional[str] = None) -> Union[DiskUsageResponse, DiskResponseList]: def disk_usage(
self, path: Optional[str] = None
) -> Union[DiskUsageResponse, DiskResponseList]:
""" """
Get the usage of a mounted disk. Get the usage of a mounted disk.
@ -234,29 +291,35 @@ class SystemPlugin(Plugin):
if path: if path:
usage = psutil.disk_usage(path) usage = psutil.disk_usage(path)
return DiskUsageResponse( return DiskUsageResponse(
path=path, path=path,
total=usage.total, total=usage.total,
used=usage.used, used=usage.used,
free=usage.free, free=usage.free,
percent=usage.percent, percent=usage.percent,
) )
else: else:
disks = {p.mountpoint: psutil.disk_usage(p.mountpoint) disks = {
for p in psutil.disk_partitions()} p.mountpoint: psutil.disk_usage(p.mountpoint)
for p in psutil.disk_partitions()
}
return DiskResponseList([ return DiskResponseList(
DiskUsageResponse( [
path=path, DiskUsageResponse(
total=disk.total, path=path,
used=disk.used, total=disk.total,
free=disk.free, used=disk.used,
percent=disk.percent, free=disk.free,
) for path, disk in disks.items() percent=disk.percent,
]) )
for path, disk in disks.items()
]
)
@action @action
def disk_io_counters(self, disk: Optional[str] = None, per_disk: bool = False) -> \ def disk_io_counters(
Union[DiskIoCountersResponse, DiskResponseList]: self, disk: Optional[str] = None, per_disk: bool = False
) -> Union[DiskIoCountersResponse, DiskResponseList]:
""" """
Get the I/O counter stats for the mounted disks. Get the I/O counter stats for the mounted disks.
@ -293,14 +356,14 @@ class SystemPlugin(Plugin):
if not per_disk: if not per_disk:
return _expand_response(None, io) return _expand_response(None, io)
return DiskResponseList([ return DiskResponseList(
_expand_response(disk, stats) [_expand_response(disk, stats) for disk, stats in io.items()]
for disk, stats in io.items() )
])
@action @action
def net_io_counters(self, nic: Optional[str] = None, per_nic: bool = False) -> \ def net_io_counters(
Union[NetworkIoCountersResponse, NetworkResponseList]: self, nic: Optional[str] = None, per_nic: bool = False
) -> Union[NetworkIoCountersResponse, NetworkResponseList]:
""" """
Get the I/O counters stats for the network interfaces. Get the I/O counters stats for the network interfaces.
@ -336,14 +399,14 @@ class SystemPlugin(Plugin):
if not per_nic: if not per_nic:
return _expand_response(nic, io) return _expand_response(nic, io)
return NetworkResponseList([ return NetworkResponseList(
_expand_response(nic, stats) [_expand_response(nic, stats) for nic, stats in io.items()]
for nic, stats in io.items() )
])
# noinspection PyShadowingBuiltins
@action @action
def net_connections(self, type: Optional[str] = None) -> Union[NetworkConnectionResponse, NetworkResponseList]: def net_connections(
self, type: Optional[str] = None
) -> Union[NetworkConnectionResponse, NetworkResponseList]:
""" """
Get the list of active network connections. Get the list of active network connections.
On macOS this function requires root privileges. On macOS this function requires root privileges.
@ -369,24 +432,30 @@ class SystemPlugin(Plugin):
:return: List of :class:`platypush.message.response.system.NetworkConnectionResponse`. :return: List of :class:`platypush.message.response.system.NetworkConnectionResponse`.
""" """
import psutil import psutil
conns = psutil.net_connections(kind=type) conns = psutil.net_connections(kind=type)
return NetworkResponseList([ return NetworkResponseList(
NetworkConnectionResponse( [
fd=conn.fd, NetworkConnectionResponse(
family=conn.family.name, fd=conn.fd,
type=conn.type.name, family=conn.family.name,
local_address=conn.laddr[0] if conn.laddr else None, type=conn.type.name,
local_port=conn.laddr[1] if len(conn.laddr) > 1 else None, local_address=conn.laddr[0] if conn.laddr else None,
remote_address=conn.raddr[0] if conn.raddr else None, local_port=conn.laddr[1] if len(conn.laddr) > 1 else None,
remote_port=conn.raddr[1] if len(conn.raddr) > 1 else None, remote_address=conn.raddr[0] if conn.raddr else None,
status=conn.status, remote_port=conn.raddr[1] if len(conn.raddr) > 1 else None,
pid=conn.pid, status=conn.status,
) for conn in conns pid=conn.pid,
]) )
for conn in conns
]
)
@action @action
def net_addresses(self, nic: Optional[str] = None) -> Union[NetworkAddressResponse, NetworkResponseList]: def net_addresses(
self, nic: Optional[str] = None
) -> Union[NetworkAddressResponse, NetworkResponseList]:
""" """
Get address info associated to the network interfaces. Get address info associated to the network interfaces.
@ -395,6 +464,7 @@ class SystemPlugin(Plugin):
:class:`platypush.message.response.system.NetworkAddressResponse`. :class:`platypush.message.response.system.NetworkAddressResponse`.
""" """
import psutil import psutil
addrs = psutil.net_if_addrs() addrs = psutil.net_if_addrs()
def _expand_addresses(_nic, _addrs): def _expand_addresses(_nic, _addrs):
@ -402,22 +472,28 @@ class SystemPlugin(Plugin):
for addr in _addrs: for addr in _addrs:
if addr.family == socket.AddressFamily.AF_INET: if addr.family == socket.AddressFamily.AF_INET:
args.update({ args.update(
'ipv4_address': addr.address, {
'ipv4_netmask': addr.netmask, 'ipv4_address': addr.address,
'ipv4_broadcast': addr.broadcast, 'ipv4_netmask': addr.netmask,
}) 'ipv4_broadcast': addr.broadcast,
}
)
elif addr.family == socket.AddressFamily.AF_INET6: elif addr.family == socket.AddressFamily.AF_INET6:
args.update({ args.update(
'ipv6_address': addr.address, {
'ipv6_netmask': addr.netmask, 'ipv6_address': addr.address,
'ipv6_broadcast': addr.broadcast, 'ipv6_netmask': addr.netmask,
}) 'ipv6_broadcast': addr.broadcast,
}
)
elif addr.family == socket.AddressFamily.AF_PACKET: elif addr.family == socket.AddressFamily.AF_PACKET:
args.update({ args.update(
'mac_address': addr.address, {
'mac_broadcast': addr.broadcast, 'mac_address': addr.address,
}) 'mac_broadcast': addr.broadcast,
}
)
if addr.ptp and not args.get('ptp'): if addr.ptp and not args.get('ptp'):
args['ptp'] = addr.ptp args['ptp'] = addr.ptp
@ -430,13 +506,14 @@ class SystemPlugin(Plugin):
addr = addrs[0] addr = addrs[0]
return _expand_addresses(nic, addr) return _expand_addresses(nic, addr)
return NetworkResponseList([ return NetworkResponseList(
_expand_addresses(nic, addr) [_expand_addresses(nic, addr) for nic, addr in addrs.items()]
for nic, addr in addrs.items() )
])
@action @action
def net_stats(self, nic: Optional[str] = None) -> Union[NetworkInterfaceStatsResponse, NetworkResponseList]: def net_stats(
self, nic: Optional[str] = None
) -> Union[NetworkInterfaceStatsResponse, NetworkResponseList]:
""" """
Get stats about the network interfaces. Get stats about the network interfaces.
@ -445,6 +522,7 @@ class SystemPlugin(Plugin):
:class:`platypush.message.response.system.NetworkInterfaceStatsResponse`. :class:`platypush.message.response.system.NetworkInterfaceStatsResponse`.
""" """
import psutil import psutil
stats = psutil.net_if_stats() stats = psutil.net_if_stats()
def _expand_stats(_nic, _stats): def _expand_stats(_nic, _stats):
@ -461,16 +539,18 @@ class SystemPlugin(Plugin):
assert stats, 'No such network interface: {}'.format(nic) assert stats, 'No such network interface: {}'.format(nic)
return _expand_stats(nic, stats[0]) return _expand_stats(nic, stats[0])
return NetworkResponseList([ return NetworkResponseList(
_expand_stats(nic, addr) [_expand_stats(nic, addr) for nic, addr in stats.items()]
for nic, addr in stats.items() )
])
# noinspection DuplicatedCode
@action @action
def sensors_temperature(self, sensor: Optional[str] = None, fahrenheit: bool = False) \ def sensors_temperature(
-> Union[SensorTemperatureResponse, List[SensorTemperatureResponse], self, sensor: Optional[str] = None, fahrenheit: bool = False
Dict[str, Union[SensorTemperatureResponse, List[SensorTemperatureResponse]]]]: ) -> Union[
SensorTemperatureResponse,
List[SensorTemperatureResponse],
Dict[str, Union[SensorTemperatureResponse, List[SensorTemperatureResponse]]],
]:
""" """
Get stats from the temperature sensors. Get stats from the temperature sensors.
@ -478,6 +558,7 @@ class SystemPlugin(Plugin):
:param fahrenheit: Return the temperature in Fahrenheit (default: Celsius). :param fahrenheit: Return the temperature in Fahrenheit (default: Celsius).
""" """
import psutil import psutil
stats = psutil.sensors_temperatures(fahrenheit=fahrenheit) stats = psutil.sensors_temperatures(fahrenheit=fahrenheit)
if sensor: if sensor:
@ -524,7 +605,6 @@ class SystemPlugin(Plugin):
return ret return ret
# noinspection DuplicatedCode
@action @action
def sensors_fan(self, sensor: Optional[str] = None) -> SensorResponseList: def sensors_fan(self, sensor: Optional[str] = None) -> SensorResponseList:
""" """
@ -534,27 +614,29 @@ class SystemPlugin(Plugin):
:return: List of :class:`platypush.message.response.system.SensorFanResponse`. :return: List of :class:`platypush.message.response.system.SensorFanResponse`.
""" """
import psutil import psutil
stats = psutil.sensors_fans() stats = psutil.sensors_fans()
def _expand_stats(name, _stats): def _expand_stats(name, _stats):
return SensorResponseList([ return SensorResponseList(
SensorFanResponse( [
name=name, SensorFanResponse(
current=s.current, name=name,
label=s.label, current=s.current,
) label=s.label,
for s in _stats )
]) for s in _stats
]
)
if sensor: if sensor:
stats = [addr for name, addr in stats.items() if name == sensor] stats = [addr for name, addr in stats.items() if name == sensor]
assert stats, 'No such sensor name: {}'.format(sensor) assert stats, 'No such sensor name: {}'.format(sensor)
return _expand_stats(sensor, stats[0]) return _expand_stats(sensor, stats[0])
return SensorResponseList([ return SensorResponseList(
_expand_stats(name, stat) [_expand_stats(name, stat) for name, stat in stats.items()]
for name, stat in stats.items() )
])
@action @action
def sensors_battery(self) -> SensorBatteryResponse: def sensors_battery(self) -> SensorBatteryResponse:
@ -563,6 +645,7 @@ class SystemPlugin(Plugin):
:return: List of :class:`platypush.message.response.system.SensorFanResponse`. :return: List of :class:`platypush.message.response.system.SensorFanResponse`.
""" """
import psutil import psutil
stats = psutil.sensors_battery() stats = psutil.sensors_battery()
return SensorBatteryResponse( return SensorBatteryResponse(
@ -578,20 +661,22 @@ class SystemPlugin(Plugin):
:return: List of :class:`platypush.message.response.system.ConnectUserResponse`. :return: List of :class:`platypush.message.response.system.ConnectUserResponse`.
""" """
import psutil import psutil
users = psutil.users() users = psutil.users()
return ConnectedUserResponseList([ return ConnectedUserResponseList(
ConnectUserResponse( [
name=u.name, ConnectUserResponse(
terminal=u.terminal, name=u.name,
host=u.host, terminal=u.terminal,
started=datetime.fromtimestamp(u.started), host=u.host,
pid=u.pid, started=datetime.fromtimestamp(u.started),
) pid=u.pid,
for u in users )
]) for u in users
]
)
# noinspection PyShadowingBuiltins
@action @action
def processes(self, filter: Optional[str] = '') -> ProcessResponseList: def processes(self, filter: Optional[str] = '') -> ProcessResponseList:
""" """
@ -601,6 +686,7 @@ class SystemPlugin(Plugin):
:return: List of :class:`platypush.message.response.system.ProcessResponse`. :return: List of :class:`platypush.message.response.system.ProcessResponse`.
""" """
import psutil import psutil
processes = [psutil.Process(pid) for pid in psutil.pids()] processes = [psutil.Process(pid) for pid in psutil.pids()]
p_list = [] p_list = []
@ -652,6 +738,7 @@ class SystemPlugin(Plugin):
@staticmethod @staticmethod
def _get_process(pid: int): def _get_process(pid: int):
import psutil import psutil
return psutil.Process(pid) return psutil.Process(pid)
@action @action
@ -661,6 +748,7 @@ class SystemPlugin(Plugin):
:return: ``True`` if the process exists, ``False`` otherwise. :return: ``True`` if the process exists, ``False`` otherwise.
""" """
import psutil import psutil
return psutil.pid_exists(pid) return psutil.pid_exists(pid)
@action @action
@ -696,7 +784,7 @@ class SystemPlugin(Plugin):
self._get_process(pid).kill() self._get_process(pid).kill()
@action @action
def wait(self, pid: int, timeout: int = None): def wait(self, pid: int, timeout: Optional[int] = None):
""" """
Wait for a process to terminate. Wait for a process to terminate.
@ -705,5 +793,28 @@ class SystemPlugin(Plugin):
""" """
self._get_process(pid).wait(timeout) self._get_process(pid).wait(timeout)
@override
@action
def get_measurement(self, *_, **__):
"""
:return: .. schema:: system.SystemInfoSchema
"""
ret = SystemInfoSchema().dump(
{
'cpu_info': self._cpu_info,
}
)
return ret
@override
def transform_entities(self, entities: dict) -> List[Entity]:
return [
CpuInfoModel(
**self._entity_args('cpu_info'),
**entities['cpu_info'],
),
]
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et: