diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/Disk.vue b/platypush/backend/http/webapp/src/components/panels/Entities/Disk.vue new file mode 100644 index 000000000..58c0eca02 --- /dev/null +++ b/platypush/backend/http/webapp/src/components/panels/Entities/Disk.vue @@ -0,0 +1,225 @@ + + + + + diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/meta.json b/platypush/backend/http/webapp/src/components/panels/Entities/meta.json index 49e45a605..436a88520 100644 --- a/platypush/backend/http/webapp/src/components/panels/Entities/meta.json +++ b/platypush/backend/http/webapp/src/components/panels/Entities/meta.json @@ -63,6 +63,14 @@ } }, + "disk": { + "name": "System", + "name_plural": "System", + "icon": { + "class": "fas fa-hard-drive" + } + }, + "current_sensor": { "name": "Sensor", "name_plural": "Sensors", diff --git a/platypush/entities/system.py b/platypush/entities/system.py index cb9fbc1ff..dd9d4040b 100644 --- a/platypush/entities/system.py +++ b/platypush/entities/system.py @@ -142,3 +142,36 @@ if 'swap_stats' not in Base.metadata: __mapper_args__ = { 'polymorphic_identity': __tablename__, } + + +if 'disk' not in Base.metadata: + + class Disk(Entity): + """ + ``Disk`` ORM model. + """ + + __tablename__ = 'disk' + + id = Column( + Integer, ForeignKey(Entity.id, ondelete='CASCADE'), primary_key=True + ) + + mountpoint = Column(String) + fstype = Column(String) + opts = Column(String) + total = Column(Integer) + used = Column(Integer) + free = Column(Integer) + percent = Column(Float) + read_count = Column(Integer) + write_count = Column(Integer) + read_bytes = Column(Integer) + write_bytes = Column(Integer) + read_time = Column(Float) + write_time = Column(Float) + busy_time = Column(Float) + + __mapper_args__ = { + 'polymorphic_identity': __tablename__, + } diff --git a/platypush/message/response/system/__init__.py b/platypush/message/response/system/__init__.py index ac9b4157a..b3fe3538d 100644 --- a/platypush/message/response/system/__init__.py +++ b/platypush/message/response/system/__init__.py @@ -28,86 +28,6 @@ class SensorResponse(SystemResponse): pass -class DiskPartitionResponse(DiskResponse): - def __init__( - self, - device: str, - mount_point: str, - fstype: Optional[str] = None, - opts: Optional[str] = None, - *args, - **kwargs - ): - super().__init__( - *args, - output={ - 'device': device, - 'mount_point': mount_point, - 'fstype': fstype, - 'opts': opts, - }, - **kwargs - ) - - -class DiskUsageResponse(DiskResponse): - def __init__( - self, - path: str, - total: int, - used: int, - free: int, - percent: float, - *args, - **kwargs - ): - super().__init__( - *args, - output={ - 'path': path, - 'total': total, - 'used': used, - 'free': free, - 'percent': percent, - }, - **kwargs - ) - - -class DiskIoCountersResponse(DiskResponse): - def __init__( - self, - read_count: int, - write_count: int, - read_bytes: int, - write_bytes: int, - read_time: int, - write_time: int, - read_merged_count: int, - write_merged_count: int, - busy_time: int, - disk: Optional[str] = None, - *args, - **kwargs - ): - super().__init__( - *args, - output={ - 'read_count': read_count, - 'write_count': write_count, - 'read_bytes': read_bytes, - 'write_bytes': write_bytes, - 'read_time': read_time, - 'write_time': write_time, - 'read_merged_count': read_merged_count, - 'write_merged_count': write_merged_count, - 'busy_time': busy_time, - 'disk': disk, - }, - **kwargs - ) - - class NetworkIoCountersResponse(NetworkResponse): def __init__( self, @@ -363,11 +283,6 @@ class SystemResponseList(SystemResponse): super().__init__(output=[r.output for r in responses], *args, **kwargs) -class DiskResponseList(DiskResponse, SystemResponseList): - def __init__(self, responses: List[DiskResponse], *args, **kwargs): - super().__init__(responses=responses, *args, **kwargs) - - class NetworkResponseList(NetworkResponse, SystemResponseList): def __init__(self, responses: List[NetworkResponse], *args, **kwargs): super().__init__(responses=responses, *args, **kwargs) diff --git a/platypush/plugins/system/__init__.py b/platypush/plugins/system/__init__.py index 1031a6332..3e0d2b4d1 100644 --- a/platypush/plugins/system/__init__.py +++ b/platypush/plugins/system/__init__.py @@ -1,9 +1,12 @@ +import os import socket from datetime import datetime from typing import Tuple, Union, List, Optional, Dict from typing_extensions import override +import psutil + from platypush.entities import Entity from platypush.entities.devices import Device from platypush.entities.managers import EntityManager @@ -13,14 +16,11 @@ from platypush.entities.system import ( CpuInfo as CpuInfoModel, CpuStats as CpuStatsModel, CpuTimes as CpuTimesModel, + Disk as DiskModel, MemoryStats as MemoryStatsModel, SwapStats as SwapStatsModel, ) from platypush.message.response.system import ( - DiskResponseList, - DiskPartitionResponse, - DiskUsageResponse, - DiskIoCountersResponse, NetworkIoCountersResponse, NetworkResponseList, NetworkConnectionResponse, @@ -46,6 +46,8 @@ from platypush.schemas.system import ( CpuStatsSchema, CpuTimes, CpuTimesSchema, + Disk, + DiskSchema, MemoryStats, MemoryStatsSchema, SwapStats, @@ -95,16 +97,12 @@ class SystemPlugin(SensorPlugin, EntityManager): @classmethod def _cpu_times_avg(cls, percent=True) -> CpuTimes: - import psutil - method = psutil.cpu_times_percent if percent else psutil.cpu_times times = method(percpu=False) return cls._load_cpu_times(times, many=False) # type: ignore @classmethod def _cpu_times_per_cpu(cls, percent=True) -> List[CpuTimes]: - import psutil - method = psutil.cpu_times_percent if percent else psutil.cpu_times times = method(percpu=True) return cls._load_cpu_times(times, many=True) # type: ignore @@ -143,8 +141,6 @@ class SystemPlugin(SensorPlugin, EntityManager): function be called with at least 0.1 seconds between calls. :return: float if ``per_cpu=False``, ``list[float]`` otherwise. """ - import psutil - percent = psutil.cpu_percent(percpu=per_cpu, interval=interval) if per_cpu: @@ -152,8 +148,6 @@ class SystemPlugin(SensorPlugin, EntityManager): return percent def _cpu_stats(self) -> CpuStats: - import psutil - stats = psutil.cpu_stats() return CpuStatsSchema().load(stats._asdict()) # type: ignore @@ -167,14 +161,10 @@ class SystemPlugin(SensorPlugin, EntityManager): return CpuStatsSchema().dump(self._cpu_stats()) # type: ignore def _cpu_frequency_avg(self) -> CpuFrequency: - import psutil - freq = psutil.cpu_freq(percpu=False) return CpuFrequencySchema().load(freq._asdict()) # type: ignore def _cpu_frequency_per_cpu(self) -> List[CpuFrequency]: - import psutil - freq = psutil.cpu_freq(percpu=True) return CpuFrequencySchema().load(freq._asdict(), many=True) # type: ignore @@ -203,13 +193,9 @@ class SystemPlugin(SensorPlugin, EntityManager): """ Get the average load as a vector that represents the load within the last 1, 5 and 15 minutes. """ - import psutil - return psutil.getloadavg() def _mem_virtual(self) -> MemoryStats: - import psutil - return MemoryStatsSchema().load( psutil.virtual_memory()._asdict() ) # type: ignore @@ -224,8 +210,6 @@ class SystemPlugin(SensorPlugin, EntityManager): return MemoryStatsSchema().dump(self._mem_virtual()) # type: ignore def _mem_swap(self) -> SwapStats: - import psutil - return SwapStatsSchema().load(psutil.swap_memory()._asdict()) # type: ignore @action @@ -237,111 +221,41 @@ class SystemPlugin(SensorPlugin, EntityManager): """ return SwapStatsSchema().dump(self._mem_swap()) # type: ignore - @action - def disk_partitions(self) -> DiskResponseList: - """ - Get the list of partitions mounted on the system. - :return: list of :class:`platypush.message.response.system.DiskPartitionResponse` - """ - import psutil + def _disk_info(self) -> List[Disk]: + parts = {part.device: part._asdict() for part in psutil.disk_partitions()} + basename_parts = {os.path.basename(part): part for part in parts} - parts = psutil.disk_partitions() - return DiskResponseList( + io_stats = { + basename_parts[disk]: stats._asdict() + for disk, stats in psutil.disk_io_counters(perdisk=True).items() + if disk in basename_parts + } + + usage = { + disk: psutil.disk_usage(info['mountpoint'])._asdict() + for disk, info in parts.items() + } + + return DiskSchema().load( # type: ignore [ - DiskPartitionResponse( - device=p.device, - mount_point=p.mountpoint, - fstype=p.fstype, - opts=p.opts, - ) - for p in parts - ] + { + **info, + **io_stats[part], + **usage[part], + } + for part, info in parts.items() + ], + many=True, ) @action - def disk_usage( - self, path: Optional[str] = None - ) -> Union[DiskUsageResponse, DiskResponseList]: + def disk_info(self): """ - Get the usage of a mounted disk. + Get information about the detected disks and partitions. - :param path: Path where the device is mounted (default: get stats for all mounted devices). - :return: :class:`platypush.message.response.system.DiskUsageResponse` or list of - :class:`platypush.message.response.system.DiskUsageResponse`. + :return: .. schema:: system.DiskSchema(many=True) """ - import psutil - - if path: - usage = psutil.disk_usage(path) - return DiskUsageResponse( - path=path, - total=usage.total, - used=usage.used, - free=usage.free, - percent=usage.percent, - ) - else: - disks = { - p.mountpoint: psutil.disk_usage(p.mountpoint) - for p in psutil.disk_partitions() - } - - return DiskResponseList( - [ - DiskUsageResponse( - path=path, - total=disk.total, - used=disk.used, - free=disk.free, - percent=disk.percent, - ) - for path, disk in disks.items() - ] - ) - - @action - def disk_io_counters( - self, disk: Optional[str] = None, per_disk: bool = False - ) -> Union[DiskIoCountersResponse, DiskResponseList]: - """ - Get the I/O counter stats for the mounted disks. - - :param disk: Select the stats for a specific disk (e.g. 'sda1'). Default: get stats for all mounted disks. - :param per_disk: Return the stats per disk (default: False). - :return: :class:`platypush.message.response.system.DiskIoCountersResponse` or list of - :class:`platypush.message.response.system.DiskIoCountersResponse`. - """ - import psutil - - def _expand_response(_disk, _stats): - return DiskIoCountersResponse( - read_count=_stats.read_count, - write_count=_stats.write_count, - read_bytes=_stats.read_bytes, - write_bytes=_stats.write_bytes, - read_time=_stats.read_time, - write_time=_stats.write_time, - read_merged_count=_stats.read_merged_count, - write_merged_count=_stats.write_merged_count, - busy_time=_stats.busy_time, - disk=_disk, - ) - - if disk: - per_disk = True - - io = psutil.disk_io_counters(perdisk=per_disk) - if disk: - stats = [d for name, d in io.items() if name == disk] - assert stats, 'No such disk: {}'.format(disk) - return _expand_response(disk, stats[0]) - - if not per_disk: - return _expand_response(None, io) - - return DiskResponseList( - [_expand_response(disk, stats) for disk, stats in io.items()] - ) + return DiskSchema().dump(self._disk_info(), many=True) @action def net_io_counters( @@ -355,7 +269,6 @@ class SystemPlugin(SensorPlugin, EntityManager): :return: :class:`platypush.message.response.system.NetIoCountersResponse` or list of :class:`platypush.message.response.system.NetIoCountersResponse`. """ - import psutil def _expand_response(_nic, _stats): return NetworkIoCountersResponse( @@ -414,8 +327,6 @@ class SystemPlugin(SensorPlugin, EntityManager): :return: List of :class:`platypush.message.response.system.NetworkConnectionResponse`. """ - import psutil - conns = psutil.net_connections(kind=type) return NetworkResponseList( @@ -446,8 +357,6 @@ class SystemPlugin(SensorPlugin, EntityManager): :return: :class:`platypush.message.response.system.NetworkAddressResponse` or list of :class:`platypush.message.response.system.NetworkAddressResponse`. """ - import psutil - addrs = psutil.net_if_addrs() def _expand_addresses(_nic, _addrs): @@ -504,8 +413,6 @@ class SystemPlugin(SensorPlugin, EntityManager): :return: :class:`platypush.message.response.system.NetworkInterfaceStatsResponse` or list of :class:`platypush.message.response.system.NetworkInterfaceStatsResponse`. """ - import psutil - stats = psutil.net_if_stats() def _expand_stats(_nic, _stats): @@ -540,8 +447,6 @@ class SystemPlugin(SensorPlugin, EntityManager): :param sensor: Select the sensor name. :param fahrenheit: Return the temperature in Fahrenheit (default: Celsius). """ - import psutil - stats = psutil.sensors_temperatures(fahrenheit=fahrenheit) if sensor: @@ -596,8 +501,6 @@ class SystemPlugin(SensorPlugin, EntityManager): :param sensor: Select the sensor name. :return: List of :class:`platypush.message.response.system.SensorFanResponse`. """ - import psutil - stats = psutil.sensors_fans() def _expand_stats(name, _stats): @@ -627,8 +530,6 @@ class SystemPlugin(SensorPlugin, EntityManager): Get stats from the battery sensor. :return: List of :class:`platypush.message.response.system.SensorFanResponse`. """ - import psutil - stats = psutil.sensors_battery() return SensorBatteryResponse( @@ -643,8 +544,6 @@ class SystemPlugin(SensorPlugin, EntityManager): Get the list of connected users. :return: List of :class:`platypush.message.response.system.ConnectUserResponse`. """ - import psutil - users = psutil.users() return ConnectedUserResponseList( @@ -668,8 +567,6 @@ class SystemPlugin(SensorPlugin, EntityManager): :param filter: Filter the list by name. :return: List of :class:`platypush.message.response.system.ProcessResponse`. """ - import psutil - processes = [psutil.Process(pid) for pid in psutil.pids()] p_list = [] @@ -720,8 +617,6 @@ class SystemPlugin(SensorPlugin, EntityManager): @staticmethod def _get_process(pid: int): - import psutil - return psutil.Process(pid) @action @@ -730,8 +625,6 @@ class SystemPlugin(SensorPlugin, EntityManager): :param pid: Process PID. :return: ``True`` if the process exists, ``False`` otherwise. """ - import psutil - return psutil.pid_exists(pid) @action @@ -794,6 +687,7 @@ class SystemPlugin(SensorPlugin, EntityManager): }, 'memory': self._mem_virtual(), 'swap': self._mem_swap(), + 'disks': self._disk_info(), } ) @@ -870,6 +764,14 @@ class SystemPlugin(SensorPlugin, EntityManager): name='Swap', **entities['swap'], ), + *[ + DiskModel( + id=f'system:disk:{disk["device"]}', + name=disk.pop('device'), + **disk, + ) + for disk in entities['disks'] + ], ] diff --git a/platypush/schemas/system.py b/platypush/schemas/system.py index 927272c10..adcd05777 100644 --- a/platypush/schemas/system.py +++ b/platypush/schemas/system.py @@ -68,6 +68,25 @@ class CpuTimesBaseSchema(DataClassSchema): } +class DiskBaseSchema(DataClassSchema): + """ + Base schema for disk stats. + """ + + @pre_load + def pre_load(self, data: dict, **_) -> dict: + # Convert read/write/busy times from milliseconds to seconds + for attr in ['read_time', 'write_time', 'busy_time']: + if data.get(attr) is not None: + data[attr] /= 1000 + + # Normalize the percentage between 0 and 1 + if data.get('percent') is not None: + data['percent'] /= 100 + + return data + + @dataclass class CpuInfo: """ @@ -356,6 +375,131 @@ class SwapStats: percent: float = percent_field() +@dataclass +class Disk: + """ + Disk data class. + """ + + device: str = field( + metadata={ + 'metadata': { + 'description': 'Path/identifier of the disk/partition', + 'example': '/dev/sda1', + } + } + ) + + mountpoint: Optional[str] = field( + metadata={ + 'metadata': { + 'description': 'Where the disk is mounted', + 'example': '/home', + } + } + ) + + fstype: Optional[str] = field( + metadata={ + 'metadata': { + 'description': 'Filesystem type', + 'example': 'ext4', + } + } + ) + + opts: Optional[str] = field( + metadata={ + 'metadata': { + 'description': 'Extra mount options passed to the partition', + 'example': 'rw,relatime,fmask=0022,dmask=0022,utf8', + } + } + ) + + total: int = field( + metadata={ + 'metadata': { + 'description': 'Total available space, in bytes', + } + } + ) + + used: int = field( + metadata={ + 'metadata': { + 'description': 'Used disk space, in bytes', + } + } + ) + + free: int = field( + metadata={ + 'metadata': { + 'description': 'Free disk space, in bytes', + } + } + ) + + read_count: int = field( + metadata={ + 'metadata': { + 'description': 'Number of recorded read operations', + } + } + ) + + write_count: int = field( + metadata={ + 'metadata': { + 'description': 'Number of recorded write operations', + } + } + ) + + read_bytes: int = field( + metadata={ + 'metadata': { + 'description': 'Number of read bytes', + } + } + ) + + write_bytes: int = field( + metadata={ + 'metadata': { + 'description': 'Number of written bytes', + } + } + ) + + read_time: float = field( + metadata={ + 'metadata': { + 'description': 'Time spent reading, in seconds', + } + } + ) + + write_time: float = field( + metadata={ + 'metadata': { + 'description': 'Time spent writing, in seconds', + } + } + ) + + busy_time: float = field( + metadata={ + 'metadata': { + 'description': 'Total disk busy time, in seconds', + } + } + ) + + percent: float = percent_field() + + @dataclass class SystemInfo: """ @@ -365,12 +509,14 @@ class SystemInfo: cpu: CpuData memory: MemoryStats swap: SwapStats + disks: List[Disk] CpuFrequencySchema = class_schema(CpuFrequency, base_schema=DataClassSchema) CpuInfoSchema = class_schema(CpuInfo, base_schema=CpuInfoBaseSchema) CpuTimesSchema = class_schema(CpuTimes, base_schema=CpuTimesBaseSchema) CpuStatsSchema = class_schema(CpuStats, base_schema=DataClassSchema) +DiskSchema = class_schema(Disk, base_schema=DiskBaseSchema) MemoryStatsSchema = class_schema(MemoryStats, base_schema=MemoryStatsBaseSchema) SwapStatsSchema = class_schema(SwapStats, base_schema=MemoryStatsBaseSchema) SystemInfoSchema = class_schema(SystemInfo, base_schema=DataClassSchema)