Merged network addresses into `NetworkInterface` model.

This commit is contained in:
Fabio Manganiello 2023-04-22 14:39:02 +02:00
parent ebe79ac29a
commit 977b55dea9
Signed by: blacklight
GPG Key ID: D90FBA7F76362774
6 changed files with 134 additions and 94 deletions

View File

@ -90,6 +90,51 @@
<div class="name" v-text="value.drop_out" /> <div class="name" v-text="value.drop_out" />
</div> </div>
</div> </div>
<div class="child head" @click.stop="areAddressesCollapsed = !areAddressesCollapsed">
<div class="col-11 label">Addresses</div>
<div class="col-1 collapse-toggler pull-right">
<i class="fas"
:class="{'fa-chevron-down': areAddressesCollapsed, 'fa-chevron-up': !areAddressesCollapsed}" />
</div>
</div>
<div class="body children attributes fade-in addresses"
v-if="value.addresses?.length && !areAddressesCollapsed">
<div class="address-container"
v-for="address in (value.addresses || [])"
:key="address.address"
>
<div class="child head"
@click.stop="displayedAddresses[address.address] = !displayedAddresses[address.address]"
>
<div class="col-11 label" v-text="address.address" />
<div class="col-1 collapse-toggler pull-right">
<i class="fas"
:class="{
'fa-chevron-down': !displayedAddresses[address.address],
'fa-chevron-up': displayedAddresses[address.address]
}"
/>
</div>
</div>
<div class="body children attributes fade-in address-details"
v-if="displayedAddresses[address.address]">
<div class="child" v-if="address.family">
<div class="label">Family</div>
<div class="value" v-text="address.family" />
</div>
<div class="child" v-if="address.netmask">
<div class="label">Netmask</div>
<div class="value" v-text="address.netmask" />
</div>
<div class="child" v-if="address.broadcast">
<div class="label">Broadcast</div>
<div class="value" v-text="address.broadcast" />
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -106,6 +151,8 @@ export default {
data() { data() {
return { return {
isCollapsed: true, isCollapsed: true,
areAddressesCollapsed: true,
displayedAddresses: {},
} }
}, },
} }
@ -131,6 +178,12 @@ export default {
min-height: 3em; min-height: 3em;
cursor: pointer; cursor: pointer;
@include from($tablet) {
@include until($desktop) {
margin-left: 3.25em;
}
}
&:hover { &:hover {
color: $default-hover-fg; color: $default-hover-fg;
} }

View File

@ -198,6 +198,7 @@ if 'network_interface' not in Base.metadata:
errors_out = Column(Integer) errors_out = Column(Integer)
drop_in = Column(Integer) drop_in = Column(Integer)
drop_out = Column(Integer) drop_out = Column(Integer)
addresses = Column(JSON)
__mapper_args__ = { __mapper_args__ = {
'polymorphic_identity': __tablename__, 'polymorphic_identity': __tablename__,

View File

@ -28,40 +28,6 @@ class SensorResponse(SystemResponse):
pass pass
class NetworkAddressResponse(NetworkResponse):
def __init__(
self,
nic: str,
ipv4_address: Optional[str] = None,
ipv4_netmask: Optional[str] = None,
ipv4_broadcast: Optional[str] = None,
ipv6_address: Optional[str] = None,
ipv6_netmask: Optional[str] = None,
ipv6_broadcast: Optional[str] = None,
mac_address: Optional[str] = None,
mac_broadcast: Optional[str] = None,
ptp: Optional[str] = None,
*args,
**kwargs
):
super().__init__(
*args,
output={
'nic': nic,
'ipv4_address': ipv4_address,
'ipv4_netmask': ipv4_netmask,
'ipv4_broadcast': ipv4_broadcast,
'ipv6_address': ipv6_address,
'ipv6_netmask': ipv6_netmask,
'ipv6_broadcast': ipv6_broadcast,
'mac_address': mac_address,
'mac_broadcast': mac_broadcast,
'ptp': ptp,
},
**kwargs
)
class NetworkInterfaceStatsResponse(NetworkResponse): class NetworkInterfaceStatsResponse(NetworkResponse):
def __init__( def __init__(
self, nic: str, is_up: bool, duplex: str, speed: int, mtu: int, *args, **kwargs self, nic: str, is_up: bool, duplex: str, speed: int, mtu: int, *args, **kwargs

View File

@ -1,5 +1,4 @@
import os import os
import socket
from datetime import datetime from datetime import datetime
from typing import Tuple, Union, List, Optional, Dict from typing import Tuple, Union, List, Optional, Dict
@ -23,7 +22,6 @@ from platypush.entities.system import (
) )
from platypush.message.response.system import ( from platypush.message.response.system import (
NetworkResponseList, NetworkResponseList,
NetworkAddressResponse,
NetworkInterfaceStatsResponse, NetworkInterfaceStatsResponse,
SensorTemperatureResponse, SensorTemperatureResponse,
SensorResponseList, SensorResponseList,
@ -257,9 +255,14 @@ class SystemPlugin(SensorPlugin, EntityManager):
return DiskSchema().dump(self._disk_info(), many=True) return DiskSchema().dump(self._disk_info(), many=True)
def _net_io_counters(self) -> List[NetworkInterface]: def _net_io_counters(self) -> List[NetworkInterface]:
addrs = psutil.net_if_addrs()
return NetworkInterfaceSchema().load( # type: ignore return NetworkInterfaceSchema().load( # type: ignore
[ [
{'interface': interface, **stats._asdict()} {
'interface': interface,
'addresses': addrs.get(interface, []),
**stats._asdict(),
}
for interface, stats in psutil.net_io_counters(pernic=True).items() for interface, stats in psutil.net_io_counters(pernic=True).items()
if any(bool(val) for val in stats._asdict().values()) if any(bool(val) for val in stats._asdict().values())
], ],
@ -328,62 +331,6 @@ class SystemPlugin(SensorPlugin, EntityManager):
many=True, many=True,
) )
@action
def net_addresses(
self, nic: Optional[str] = None
) -> Union[NetworkAddressResponse, NetworkResponseList]:
"""
Get address info associated to the network interfaces.
:param nic: Select the stats for a specific network device (e.g. 'eth0'). Default: get stats for all NICs.
:return: :class:`platypush.message.response.system.NetworkAddressResponse` or list of
:class:`platypush.message.response.system.NetworkAddressResponse`.
"""
addrs = psutil.net_if_addrs()
def _expand_addresses(_nic, _addrs):
args = {'nic': _nic}
for addr in _addrs:
if addr.family == socket.AddressFamily.AF_INET:
args.update(
{
'ipv4_address': addr.address,
'ipv4_netmask': addr.netmask,
'ipv4_broadcast': addr.broadcast,
}
)
elif addr.family == socket.AddressFamily.AF_INET6:
args.update(
{
'ipv6_address': addr.address,
'ipv6_netmask': addr.netmask,
'ipv6_broadcast': addr.broadcast,
}
)
elif addr.family == socket.AddressFamily.AF_PACKET:
args.update(
{
'mac_address': addr.address,
'mac_broadcast': addr.broadcast,
}
)
if addr.ptp and not args.get('ptp'):
args['ptp'] = addr.ptp
return NetworkAddressResponse(**args)
if nic:
addrs = [addr for name, addr in addrs.items() if name == nic]
assert addrs, 'No such network interface: {}'.format(nic)
addr = addrs[0]
return _expand_addresses(nic, addr)
return NetworkResponseList(
[_expand_addresses(nic, addr) for nic, addr in addrs.items()]
)
@action @action
def net_stats( def net_stats(
self, nic: Optional[str] = None self, nic: Optional[str] = None

View File

@ -1,3 +1,5 @@
from socket import AddressFamily
from marshmallow import pre_load from marshmallow import pre_load
from .._base import SystemBaseSchema from .._base import SystemBaseSchema
@ -11,6 +13,8 @@ class NetworkInterfaceBaseSchema(SystemBaseSchema):
@pre_load @pre_load
def pre_load(self, data: dict, **_) -> dict: def pre_load(self, data: dict, **_) -> dict:
data = super().pre_load(data) data = super().pre_load(data)
# Custom attribute mappings
for in_attr, out_attr in { for in_attr, out_attr in {
'errin': 'errors_in', 'errin': 'errors_in',
'errout': 'errors_out', 'errout': 'errors_out',
@ -20,4 +24,12 @@ class NetworkInterfaceBaseSchema(SystemBaseSchema):
if in_attr in data: if in_attr in data:
data[out_attr] = data.pop(in_attr) data[out_attr] = data.pop(in_attr)
# Serialize enum values
for i, addr in enumerate(data.get('addresses', [])):
if hasattr(addr, '_asdict'):
addr = addr._asdict()
if isinstance(addr.get('family'), AddressFamily):
addr['family'] = addr['family'].name
data['addresses'][i] = addr
return data return data

View File

@ -1,5 +1,6 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional from socket import AddressFamily
from typing import List, Optional
@dataclass @dataclass
@ -80,3 +81,63 @@ class NetworkInterface:
}, },
} }
) )
addresses: List['NetworkInterfaceAddress'] = field(
default_factory=list,
metadata={
'metadata': {
'description': 'List of addresses associated to the interface',
'example': [
{
'family': AddressFamily.AF_INET.name,
'address': '192.168.1.2',
'netmask': '255.255.255.0',
'broadcast': '192.168.1.255',
}
],
},
},
)
@dataclass
class NetworkInterfaceAddress:
"""
Network interface address data class.
"""
family: AddressFamily = field(
metadata={
'metadata': {
'description': 'Address family',
'example': AddressFamily.AF_INET.name,
}
}
)
address: Optional[str] = field(
metadata={
'metadata': {
'description': 'IPv4 or IPv6 address of the interface',
'example': '192.168.1.2',
}
}
)
netmask: Optional[str] = field(
metadata={
'metadata': {
'description': 'Netmask for the interface address',
'example': '255.255.255.0',
}
}
)
broadcast: Optional[str] = field(
metadata={
'metadata': {
'description': 'Broadcast address for the interface',
'example': '192.168.1.255',
}
}
)