Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug in host network info collector #2779

Closed
eparla774 opened this issue Apr 3, 2021 · 1 comment · Fixed by #2821
Closed

Bug in host network info collector #2779

eparla774 opened this issue Apr 3, 2021 · 1 comment · Fixed by #2821
Labels

Comments

@eparla774
Copy link
Contributor

eparla774 commented Apr 3, 2021

Describe the issue you are experiencing

If host computer is connected to open wifi network, network.py script fails, /network/info page returns http code 500 and supervisor control panel cannot load

What is the used version of the Supervisor?

supervisor-2021.03.9

What type of installation are you running?

Home Assistant Supervised

Which operating system are you running on?

Other (e.g., Raspbian/Raspberry Pi OS/Fedora)

What is the version of your installed operating system?

Ubuntu 20.10 aarch64 for Raspberry PI 4

What version of Home Assistant Core is installed?

core-2021.3.4

Steps to reproduce the issue

  1. Connect to open wifi network
  2. Try to open supervisor panel

Anything in the Supervisor logs that might be useful for us?

Core logs:

2021-03-08 21:35:17 ERROR (MainThread) [aiohttp.server] Error handling request
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/aiohttp/web_protocol.py", line 314, in data_received
    messages, upgraded, tail = self._request_parser.feed_data(data)
  File "aiohttp/_http_parser.pyx", line 546, in aiohttp._http_parser.HttpParser.feed_data
aiohttp.http_exceptions.BadStatusLine: 400, message="Bad status line 'invalid HTTP method'"
2021-03-08 22:02:33 ERROR (MainThread) [aiohttp.server] Error handling request
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/aiohttp/web_protocol.py", line 314, in data_received
    messages, upgraded, tail = self._request_parser.feed_data(data)
  File "aiohttp/_http_parser.pyx", line 546, in aiohttp._http_parser.HttpParser.feed_data
aiohttp.http_exceptions.BadStatusLine: 400, message="Bad status line 'invalid HTTP method'"
2021-03-08 22:59:03 ERROR (MainThread) [homeassistant.components.hassio.handler] /network/info return code 500
2021-03-08 22:59:03 ERROR (MainThread) [homeassistant.components.hassio] Failed to to call /network/info -
2021-03-08 23:01:51 ERROR (MainThread) [aiohttp.server] Error handling request
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/aiohttp/web_protocol.py", line 314, in data_received
    messages, upgraded, tail = self._request_parser.feed_data(data)
  File "aiohttp/_http_parser.pyx", line 546, in aiohttp._http_parser.HttpParser.feed_data
aiohttp.http_exceptions.BadStatusLine: 400, message="Bad status line 'invalid HTTP method'"
2021-03-08 23:21:10 ERROR (MainThread) [homeassistant.components.hassio.handler] /network/info return code 500
2021-03-08 23:21:10 ERROR (MainThread) [homeassistant.components.hassio] Failed to to call /network/info -
2021-03-08 23:49:49 ERROR (MainThread) [aiohttp.server] Error handling request
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/aiohttp/web_protocol.py", line 314, in data_received
    messages, upgraded, tail = self._request_parser.feed_data(data)
  File "aiohttp/_http_parser.pyx", line 546, in aiohttp._http_parser.HttpParser.feed_data
aiohttp.http_exceptions.BadStatusLine: 400, message="Bad status line 'invalid HTTP method'"
2021-03-08 23:58:10 ERROR (MainThread) [aiohttp.server] Error handling request
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/aiohttp/web_protocol.py", line 314, in data_received
    messages, upgraded, tail = self._request_parser.feed_data(data)
  File "aiohttp/_http_parser.pyx", line 546, in aiohttp._http_parser.HttpParser.feed_data
aiohttp.http_exceptions.BadStatusLine: 400, message="Bad status line 'invalid HTTP method'"
2021-03-09 00:01:52 ERROR (MainThread) [homeassistant.components.hassio.handler] /network/info return code 500
2021-03-09 00:01:52 ERROR (MainThread) [homeassistant.components.hassio] Failed to to call /network/info -
2021-03-09 00:21:24 ERROR (MainThread) [homeassistant.components.hassio.handler] /network/info return code 500
2021-03-09 00:21:24 ERROR (MainThread) [homeassistant.components.hassio] Failed to to call /network/info -

Supervisor logs:

21-03-31 14:44:34 ERROR (MainThread) [aiohttp.server] Error handling request
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/aiohttp/web_protocol.py", line 422, in _handle_request
    resp = await self._request_handler(request)
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/integrations/aiohttp.py", line 123, in sentry_app_handle
    reraise(*_capture_exception(hub))
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/_compat.py", line 54, in reraise
    raise value
  File "/usr/local/lib/python3.8/site-packages/sentry_sdk/integrations/aiohttp.py", line 113, in sentry_app_handle
    response = await old_handle(self, request)
  File "/usr/local/lib/python3.8/site-packages/aiohttp/web_app.py", line 499, in _handle
    resp = await handler(request)
  File "/usr/local/lib/python3.8/site-packages/aiohttp/web_middlewares.py", line 119, in impl
    return await handler(request)
  File "/usr/src/supervisor/supervisor/api/security.py", line 135, in system_validation
    return await handler(request)
  File "/usr/src/supervisor/supervisor/api/security.py", line 197, in token_validation
    return await handler(request)
  File "/usr/src/supervisor/supervisor/api/utils.py", line 65, in wrap_api
    answer = await method(api, *args, **kwargs)
  File "/usr/src/supervisor/supervisor/api/network.py", line 166, in info
    for interface in self.sys_host.network.interfaces
  File "/usr/src/supervisor/supervisor/host/network.py", line 65, in interfaces
    interfaces.append(Interface.from_dbus_interface(inet))
  File "/usr/src/supervisor/supervisor/host/network.py", line 307, in from_dbus_interface
    Interface._map_nm_wifi(inet),
  File "/usr/src/supervisor/supervisor/host/network.py", line 352, in _map_nm_wifi
    if inet.settings.wireless_security.key_mgmt == "none":
AttributeError: 'NoneType' object has no attribute 'key_mgmt'

I fixed the bug and here is my fixed network.py:

"""Info control for host."""
from __future__ import annotations

import asyncio
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface
import logging
from typing import List, Optional, Union

import attr

from ..const import ATTR_HOST_INTERNET
from ..coresys import CoreSys, CoreSysAttributes
from ..dbus.const import (
    DBUS_NAME_NM_CONNECTION_ACTIVE_CHANGED,
    ConnectionStateType,
    DeviceType,
    InterfaceMethod as NMInterfaceMethod,
    WirelessMethodType,
)
from ..dbus.network.accesspoint import NetworkWirelessAP
from ..dbus.network.connection import NetworkConnection
from ..dbus.network.interface import NetworkInterface
from ..dbus.payloads.generate import interface_update_payload
from ..exceptions import (
    DBusError,
    DBusNotConnectedError,
    DBusProgramError,
    HostNetworkError,
    HostNetworkNotFound,
    HostNotSupportedError,
)
from .const import AuthMethod, InterfaceMethod, InterfaceType, WifiMode

_LOGGER: logging.Logger = logging.getLogger(__name__)


class NetworkManager(CoreSysAttributes):
    """Handle local network setup."""

    def __init__(self, coresys: CoreSys):
        """Initialize system center handling."""
        self.coresys: CoreSys = coresys
        self._connectivity: Optional[bool] = None

    @property
    def connectivity(self) -> Optional[bool]:
        """Return true current connectivity state."""
        return self._connectivity

    @connectivity.setter
    def connectivity(self, state: bool) -> None:
        """Set host connectivity state."""
        if self._connectivity == state:
            return
        self._connectivity = state
        self.sys_homeassistant.websocket.supervisor_update_event(
            "network", {ATTR_HOST_INTERNET: state}
        )

    @property
    def interfaces(self) -> List[Interface]:
        """Return a dictionary of active interfaces."""
        interfaces: List[Interface] = []
        for inet in self.sys_dbus.network.interfaces.values():
            interfaces.append(Interface.from_dbus_interface(inet))

        return interfaces

    @property
    def dns_servers(self) -> List[str]:
        """Return a list of local DNS servers."""
        # Read all local dns servers
        servers: List[str] = []
        for config in self.sys_dbus.network.dns.configuration:
            if config.vpn or not config.nameservers:
                continue
            servers.extend(config.nameservers)

        return list(dict.fromkeys(servers))

    async def check_connectivity(self):
        """Check the internet connection.

        ConnectionState 4 == FULL (has internet)
        https://developer.gnome.org/NetworkManager/stable/nm-dbus-types.html#NMConnectivityState
        """
        if not self.sys_dbus.network.connectivity_enabled:
            return

        # Check connectivity
        try:
            state = await self.sys_dbus.network.check_connectivity()
            self.connectivity = state[0] == 4
        except DBusError as err:
            _LOGGER.warning("Can't update connectivity information: %s", err)
            self.connectivity = False

    def get(self, inet_name: str) -> Interface:
        """Return interface from interface name."""
        if inet_name not in self.sys_dbus.network.interfaces:
            raise HostNetworkNotFound()

        return Interface.from_dbus_interface(
            self.sys_dbus.network.interfaces[inet_name]
        )

    async def update(self):
        """Update properties over dbus."""
        _LOGGER.info("Updating local network information")
        try:
            await self.sys_dbus.network.update()
        except DBusError:
            _LOGGER.warning("Can't update network information!")
        except DBusNotConnectedError as err:
            _LOGGER.error("No network D-Bus connection available")
            raise HostNotSupportedError() from err

        await self.check_connectivity()

    async def apply_changes(self, interface: Interface) -> None:
        """Apply Interface changes to host."""
        inet = self.sys_dbus.network.interfaces.get(interface.name)

        # Update exist configuration
        if (
            inet
            and inet.settings
            and inet.settings.connection.interface_name == interface.name
            and interface.enabled
        ):
            settings = interface_update_payload(
                interface,
                name=inet.settings.connection.id,
                uuid=inet.settings.connection.uuid,
            )

            try:
                await inet.settings.update(settings)
                await self.sys_dbus.network.activate_connection(
                    inet.settings.object_path, inet.object_path
                )
            except DBusError as err:
                _LOGGER.error("Can't update config on %s: %s", interface.name, err)
                raise HostNetworkError() from err

        # Create new configuration and activate interface
        elif inet and interface.enabled:
            settings = interface_update_payload(interface)

            try:
                await self.sys_dbus.network.add_and_activate_connection(
                    settings, inet.object_path
                )
            except DBusError as err:
                _LOGGER.error(
                    "Can't create config and activate %s: %s", interface.name, err
                )
                raise HostNetworkError() from err

        # Remove config from interface
        elif inet and inet.settings and not interface.enabled:
            try:
                await inet.settings.delete()
            except DBusError as err:
                _LOGGER.error("Can't disable interface %s: %s", interface.name, err)
                raise HostNetworkError() from err

        # Create new interface (like vlan)
        elif not inet:
            settings = interface_update_payload(interface)

            try:
                await self.sys_dbus.network.settings.add_connection(settings)
            except DBusError as err:
                _LOGGER.error("Can't create new interface: %s", err)
                raise HostNetworkError() from err
        else:
            _LOGGER.warning("Requested Network interface update is not possible")
            raise HostNetworkError()

        await self.sys_dbus.network.dbus.wait_signal(
            DBUS_NAME_NM_CONNECTION_ACTIVE_CHANGED
        )
        await self.update()

    async def scan_wifi(self, interface: Interface) -> List[AccessPoint]:
        """Scan on Interface for AccessPoint."""
        inet = self.sys_dbus.network.interfaces.get(interface.name)

        if inet.type != DeviceType.WIRELESS:
            _LOGGER.error("Can only scan with wireless card - %s", interface.name)
            raise HostNotSupportedError()

        # Request Scan
        try:
            await inet.wireless.request_scan()
        except DBusProgramError as err:
            _LOGGER.debug("Can't request a new scan: %s", err)
        except DBusError as err:
            raise HostNetworkError() from err
        else:
            await asyncio.sleep(5)

        # Process AP
        accesspoints: List[AccessPoint] = []
        for ap_object in (await inet.wireless.get_all_accesspoints())[0]:
            accesspoint = NetworkWirelessAP(ap_object)

            try:
                await accesspoint.connect()
            except DBusError as err:
                _LOGGER.warning("Can't process an AP: %s", err)
                continue
            else:
                accesspoints.append(
                    AccessPoint(
                        WifiMode[WirelessMethodType(accesspoint.mode).name],
                        accesspoint.ssid,
                        accesspoint.mac,
                        accesspoint.frequency,
                        accesspoint.strength,
                    )
                )

        return accesspoints


@attr.s(slots=True)
class AccessPoint:
    """Represent a wifi configuration."""

    mode: WifiMode = attr.ib()
    ssid: str = attr.ib()
    mac: str = attr.ib()
    frequency: int = attr.ib()
    signal: int = attr.ib()


@attr.s(slots=True)
class IpConfig:
    """Represent a IP configuration."""

    method: InterfaceMethod = attr.ib()
    address: List[Union[IPv4Interface, IPv6Interface]] = attr.ib()
    gateway: Optional[Union[IPv4Address, IPv6Address]] = attr.ib()
    nameservers: List[Union[IPv4Address, IPv6Address]] = attr.ib()


@attr.s(slots=True)
class WifiConfig:
    """Represent a wifi configuration."""

    mode: WifiMode = attr.ib()
    ssid: str = attr.ib()
    auth: AuthMethod = attr.ib()
    psk: Optional[str] = attr.ib()
    signal: Optional[int] = attr.ib()


@attr.s(slots=True)
class VlanConfig:
    """Represent a vlan configuration."""

    id: int = attr.ib()
    interface: str = attr.ib()


@attr.s(slots=True)
class Interface:
    """Represent a host network interface."""

    name: str = attr.ib()
    enabled: bool = attr.ib()
    connected: bool = attr.ib()
    primary: bool = attr.ib()
    type: InterfaceType = attr.ib()
    ipv4: Optional[IpConfig] = attr.ib()
    ipv6: Optional[IpConfig] = attr.ib()
    wifi: Optional[WifiConfig] = attr.ib()
    vlan: Optional[VlanConfig] = attr.ib()

    @staticmethod
    def from_dbus_interface(inet: NetworkInterface) -> Interface:
        """Concert a dbus interface into normal Interface."""
        return Interface(
            inet.name,
            inet.settings is not None,
            Interface._map_nm_connected(inet.connection),
            inet.primary,
            Interface._map_nm_type(inet.type),
            IpConfig(
                Interface._map_nm_method(inet.settings.ipv4.method),
                inet.connection.ipv4.address,
                inet.connection.ipv4.gateway,
                inet.connection.ipv4.nameservers,
            )
            if inet.connection and inet.connection.ipv4
            else IpConfig(InterfaceMethod.DISABLED, [], None, []),
            IpConfig(
                Interface._map_nm_method(inet.settings.ipv6.method),
                inet.connection.ipv6.address,
                inet.connection.ipv6.gateway,
                inet.connection.ipv6.nameservers,
            )
            if inet.connection and inet.connection.ipv6
            else IpConfig(InterfaceMethod.DISABLED, [], None, []),
            Interface._map_nm_wifi(inet),
            Interface._map_nm_vlan(inet),
        )

    @staticmethod
    def _map_nm_method(method: str) -> InterfaceMethod:
        """Map IP interface method."""
        mapping = {
            NMInterfaceMethod.AUTO: InterfaceMethod.AUTO,
            NMInterfaceMethod.DISABLED: InterfaceMethod.DISABLED,
            NMInterfaceMethod.MANUAL: InterfaceMethod.STATIC,
        }

        return mapping.get(method, InterfaceMethod.DISABLED)

    @staticmethod
    def _map_nm_connected(connection: Optional[NetworkConnection]) -> bool:
        """Map connectivity state."""
        if not connection:
            return False

        return connection.state in (
            ConnectionStateType.ACTIVATED,
            ConnectionStateType.ACTIVATING,
        )

    @staticmethod
    def _map_nm_type(device_type: int) -> InterfaceType:
        mapping = {
            DeviceType.ETHERNET: InterfaceType.ETHERNET,
            DeviceType.WIRELESS: InterfaceType.WIRELESS,
            DeviceType.VLAN: InterfaceType.VLAN,
        }
        return mapping[device_type]

    @staticmethod
    def _map_nm_wifi(inet: NetworkInterface) -> Optional[WifiConfig]:
        """Create mapping to nm wifi property."""
        if inet.type != DeviceType.WIRELESS or not inet.settings:
            return None

        # Authentication
        auth = None
        if not inet.settings.wireless_security:
            auth = AuthMethod.OPEN
        elif inet.settings.wireless_security.key_mgmt == "none":
                auth = AuthMethod.WEP
        elif inet.settings.wireless_security.key_mgmt == "wpa-psk":
                auth = AuthMethod.WPA_PSK

        # WifiMode
        mode = WifiMode.INFRASTRUCTURE
        if inet.settings.wireless.mode:
            mode = WifiMode(inet.settings.wireless.mode)

        # Signal
        if inet.wireless:
            signal = inet.wireless.active.strength
        else:
            signal = None

        if not inet.settings.wireless_security:
            passwd = None
        else:
            passwd = inet.settings.wireless_security.psk

        return WifiConfig(
            mode,
            inet.settings.wireless.ssid,
            auth,
            passwd,
            signal,
        )

    @staticmethod
    def _map_nm_vlan(inet: NetworkInterface) -> Optional[WifiConfig]:
        """Create mapping to nm vlan property."""
        if inet.type != DeviceType.VLAN or not inet.settings:
            return None

        return VlanConfig(inet.settings.vlan.id, inet.settings.vlan.parent)
@eparla774 eparla774 added the bug label Apr 3, 2021
@pvizeli
Copy link
Member

pvizeli commented Apr 6, 2021

Ubuntu 20.10 aarch64 for Raspberry PI 4 is not supported. Feel free to provide an PR to fix the issue, but we give no support on that

@pvizeli pvizeli closed this as completed Apr 6, 2021
eparla774 added a commit to eparla774/supervisor that referenced this issue Apr 18, 2021
Fixed bug in host network info collector (Issue home-assistant#2779)
pvizeli pushed a commit that referenced this issue Apr 26, 2021
* Fix for bug in host network info collector

Fixed bug in host network info collector (Issue #2779)

* Added psk to authentification

* Fixed error

* Fixed error

* Update network.py
@github-actions github-actions bot locked and limited conversation to collaborators May 7, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants