Apply network settings on start to fix defaults (#3528)

* Apply network settings on start to fix defaults

* Use job check and add tests
This commit is contained in:
Mike Degatano 2022-03-31 06:14:40 -04:00 committed by GitHub
parent bfe1cb073c
commit e2ac5042d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 419 additions and 11 deletions

View File

@ -95,21 +95,29 @@ class HostManager(CoreSysAttributes):
return features return features
async def reload(self): async def reload(
self,
*,
services: bool = True,
network: bool = True,
agent: bool = True,
audio: bool = True,
):
"""Reload host functions.""" """Reload host functions."""
await self.info.update() await self.info.update()
if self.sys_dbus.systemd.is_connected: if services and self.sys_dbus.systemd.is_connected:
await self.services.update() await self.services.update()
if self.sys_dbus.network.is_connected: if network and self.sys_dbus.network.is_connected:
await self.network.update() await self.network.update()
if self.sys_dbus.agent.is_connected: if agent and self.sys_dbus.agent.is_connected:
await self.sys_dbus.agent.update() await self.sys_dbus.agent.update()
with suppress(PulseAudioError): if audio:
await self.sound.update() with suppress(PulseAudioError):
await self.sound.update()
_LOGGER.info("Host information reload completed") _LOGGER.info("Host information reload completed")
self.supported_features.cache_clear() # pylint: disable=no-member self.supported_features.cache_clear() # pylint: disable=no-member
@ -117,7 +125,8 @@ class HostManager(CoreSysAttributes):
async def load(self): async def load(self):
"""Load host information.""" """Load host information."""
with suppress(HassioError): with suppress(HassioError):
await self.reload() await self.reload(network=False)
await self.network.load()
# Register for events # Register for events
self.sys_bus.register_event(BusEvent.HARDWARE_NEW_DEVICE, self._hardware_events) self.sys_bus.register_event(BusEvent.HARDWARE_NEW_DEVICE, self._hardware_events)

View File

@ -2,11 +2,15 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from contextlib import suppress
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface
import logging import logging
import attr import attr
from supervisor.jobs.const import JobCondition
from supervisor.jobs.decorator import Job
from ..const import ATTR_HOST_INTERNET from ..const import ATTR_HOST_INTERNET
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..dbus.const import ( from ..dbus.const import (
@ -79,7 +83,6 @@ class NetworkManager(CoreSysAttributes):
async def check_connectivity(self): async def check_connectivity(self):
"""Check the internet connection.""" """Check the internet connection."""
if not self.sys_dbus.network.connectivity_enabled: if not self.sys_dbus.network.connectivity_enabled:
return return
@ -100,6 +103,25 @@ class NetworkManager(CoreSysAttributes):
self.sys_dbus.network.interfaces[inet_name] self.sys_dbus.network.interfaces[inet_name]
) )
@Job(conditions=JobCondition.HOST_NETWORK)
async def load(self):
"""Load network information and reapply defaults over dbus."""
await self.update()
# Apply current settings on each interface so OS can update any out of date defaults
interfaces = [
Interface.from_dbus_interface(self.sys_dbus.network.interfaces[i])
for i in self.sys_dbus.network.interfaces
]
with suppress(HostNetworkNotFound):
await asyncio.gather(
*[
self.apply_changes(interface, update_only=True)
for interface in interfaces
if interface.enabled
]
)
async def update(self): async def update(self):
"""Update properties over dbus.""" """Update properties over dbus."""
_LOGGER.info("Updating local network information") _LOGGER.info("Updating local network information")
@ -114,7 +136,9 @@ class NetworkManager(CoreSysAttributes):
await self.check_connectivity() await self.check_connectivity()
async def apply_changes(self, interface: Interface) -> None: async def apply_changes(
self, interface: Interface, *, update_only: bool = False
) -> None:
"""Apply Interface changes to host.""" """Apply Interface changes to host."""
inet = self.sys_dbus.network.interfaces.get(interface.name) inet = self.sys_dbus.network.interfaces.get(interface.name)
con: NetworkConnection = None con: NetworkConnection = None
@ -147,6 +171,13 @@ class NetworkManager(CoreSysAttributes):
f"Can't update config on {interface.name}: {err}", _LOGGER.error f"Can't update config on {interface.name}: {err}", _LOGGER.error
) from err ) from err
# Stop if only updates are allowed as other paths create/delete interfaces
elif update_only:
raise HostNetworkNotFound(
f"Requested to update interface {interface.name} which does not exist or is disabled.",
_LOGGER.warning,
)
# Create new configuration and activate interface # Create new configuration and activate interface
elif inet and interface.enabled: elif inet and interface.enabled:
_LOGGER.debug("Create new configuration for %s", interface.name) _LOGGER.debug("Create new configuration for %s", interface.name)

View File

@ -19,6 +19,7 @@ class JobCondition(str, Enum):
RUNNING = "running" RUNNING = "running"
HAOS = "haos" HAOS = "haos"
OS_AGENT = "os_agent" OS_AGENT = "os_agent"
HOST_NETWORK = "host_network"
class JobExecutionLimit(str, Enum): class JobExecutionLimit(str, Enum):

View File

@ -181,6 +181,14 @@ class Job(CoreSysAttributes):
f"'{self._method.__qualname__}' blocked from execution, no Home Assistant OS-Agent available" f"'{self._method.__qualname__}' blocked from execution, no Home Assistant OS-Agent available"
) )
if (
JobCondition.HOST_NETWORK in self.conditions
and not self.sys_dbus.network.is_connected
):
raise JobConditionException(
f"'{self._method.__qualname__}' blocked from execution, host Network Manager not available"
)
async def _acquire_exection_limit(self) -> None: async def _acquire_exection_limit(self) -> None:
"""Process exection limits.""" """Process exection limits."""
if self.limit not in ( if self.limit not in (

View File

@ -18,8 +18,13 @@ from supervisor.api import RestAPI
from supervisor.bootstrap import initialize_coresys from supervisor.bootstrap import initialize_coresys
from supervisor.const import REQUEST_FROM from supervisor.const import REQUEST_FROM
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.dbus.agent import OSAgent
from supervisor.dbus.const import DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED from supervisor.dbus.const import DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED
from supervisor.dbus.hostname import Hostname
from supervisor.dbus.interface import DBusInterface
from supervisor.dbus.network import NetworkManager from supervisor.dbus.network import NetworkManager
from supervisor.dbus.systemd import Systemd
from supervisor.dbus.timedate import TimeDate
from supervisor.docker import DockerAPI from supervisor.docker import DockerAPI
from supervisor.store.addon import AddonStore from supervisor.store.addon import AddonStore
from supervisor.store.repository import Repository from supervisor.store.repository import Repository
@ -147,6 +152,37 @@ async def network_manager(dbus) -> NetworkManager:
yield nm_obj yield nm_obj
async def mock_dbus_interface(dbus: DBus, instance: DBusInterface) -> DBusInterface:
"""Mock dbus for a DBusInterface instance."""
instance.dbus = dbus
await instance.connect()
return instance
@pytest.fixture
async def hostname(dbus: DBus) -> Hostname:
"""Mock Hostname."""
yield await mock_dbus_interface(dbus, Hostname())
@pytest.fixture
async def timedate(dbus: DBus) -> TimeDate:
"""Mock Timedate."""
yield await mock_dbus_interface(dbus, TimeDate())
@pytest.fixture
async def systemd(dbus: DBus) -> Systemd:
"""Mock Systemd."""
yield await mock_dbus_interface(dbus, Systemd())
@pytest.fixture
async def os_agent(dbus: DBus) -> Systemd:
"""Mock OSAgent."""
yield await mock_dbus_interface(dbus, OSAgent())
@pytest.fixture @pytest.fixture
async def coresys(loop, docker, network_manager, aiohttp_client, run_dir) -> CoreSys: async def coresys(loop, docker, network_manager, aiohttp_client, run_dir) -> CoreSys:
"""Create a CoreSys Mock.""" """Create a CoreSys Mock."""

View File

@ -1 +1 @@
[] ["/org/freedesktop/NetworkManager/ActiveConnection/1"]

View File

@ -1 +1,38 @@
[{"connection": {"id": "Wired connection 1", "permissions": [], "timestamp": 1598125548, "type": "802-3-ethernet", "uuid": "0c23631e-2118-355c-bbb0-8943229cb0d6"}, "ipv4": {"address-data": [{"address": "192.168.2.148", "prefix": 24}], "addresses": [[2483202240, 24, 16951488]], "dns": [16951488], "dns-search": [], "gateway": "192.168.2.1", "method": "auto", "route-data": [], "routes": []}, "ipv6": {"address-data": [], "addresses": [], "dns": [], "dns-search": [], "method": "auto", "route-data": [], "routes": []}, "proxy": {}, "802-3-ethernet": {"auto-negotiate": false, "mac-address-blacklist": [], "s390-options": {}}, "802-11-wireless": {"ssid": [78, 69, 84, 84]}}] [
{
"connection": {
"id": "Wired connection 1",
"interface-name": "eth0",
"permissions": [],
"timestamp": 1598125548,
"type": "802-3-ethernet",
"uuid": "0c23631e-2118-355c-bbb0-8943229cb0d6"
},
"ipv4": {
"address-data": [{ "address": "192.168.2.148", "prefix": 24 }],
"addresses": [[2483202240, 24, 16951488]],
"dns": [16951488],
"dns-search": [],
"gateway": "192.168.2.1",
"method": "auto",
"route-data": [],
"routes": []
},
"ipv6": {
"address-data": [],
"addresses": [],
"dns": [],
"dns-search": [],
"method": "auto",
"route-data": [],
"routes": []
},
"proxy": {},
"802-3-ethernet": {
"auto-negotiate": false,
"mac-address-blacklist": [],
"s390-options": {}
},
"802-11-wireless": { "ssid": [78, 69, 84, 84] }
}
]

View File

@ -0,0 +1,50 @@
[
[
"etc-machine\\x2did.mount",
"/etc/machine-id",
"loaded",
"active",
"mounted",
"",
"/org/freedesktop/systemd1/unit/etc_2dmachine_5cx2did_2emount",
0,
"",
"/"
],
[
"firewalld.service",
"firewalld.service",
"not-found",
"inactive",
"dead",
"",
"/org/freedesktop/systemd1/unit/firewalld_2eservice",
0,
"",
"/"
],
[
"sys-devices-virtual-tty-ttypd.device",
"/sys/devices/virtual/tty/ttypd",
"loaded",
"active",
"plugged",
"",
"/org/freedesktop/systemd1/unit/sys_2ddevices_2dvirtual_2dtty_2dttypd_2edevice",
0,
"",
"/"
],
[
"zram-swap.service",
"HassOS ZRAM swap",
"loaded",
"active",
"exited",
"",
"/org/freedesktop/systemd1/unit/zram_2dswap_2eservice",
0,
"",
"/"
]
]

View File

@ -0,0 +1,127 @@
{
"Version": "249",
"Features": "+PAM -AUDIT -SELINUX +APPARMOR -IMA -SMACK -SECCOMP +GCRYPT +GNUTLS +OPENSSL -ACL +BLKID +CURL -ELFUTILS -FIDO2 -IDN2 -IDN +IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 -PWQUALITY -P11KIT -QRENCODE -BZIP2 -LZ4 -XZ +ZLIB -ZSTD -XKBCOMMON -UTMP -SYSVINIT default-hierarchy=hybrid",
"Virtualization": "",
"Architecture": "arm64",
"Tainted": "cgroupsv1",
"FirmwareTimestamp": 0,
"FirmwareTimestampMonotonic": 0,
"LoaderTimestamp": 0,
"LoaderTimestampMonotonic": 0,
"KernelTimestamp": 1646197924245019,
"KernelTimestampMonotonic": 0,
"InitRDTimestamp": 0,
"InitRDTimestampMonotonic": 0,
"UserspaceTimestamp": 1646197926126937,
"UserspaceTimestampMonotonic": 1881921,
"FinishTimestamp": 1646197962613554,
"FinishTimestampMonotonic": 38368540,
"SecurityStartTimestamp": 1646197926137295,
"SecurityStartTimestampMonotonic": 1892280,
"SecurityFinishTimestamp": 1646197926139253,
"SecurityFinishTimestampMonotonic": 1894237,
"GeneratorsStartTimestamp": 1646197926235939,
"GeneratorsStartTimestampMonotonic": 1990923,
"GeneratorsFinishTimestamp": 1646197926260378,
"GeneratorsFinishTimestampMonotonic": 2015363,
"UnitsLoadStartTimestamp": 1646197926260388,
"UnitsLoadStartTimestampMonotonic": 2015371,
"UnitsLoadFinishTimestamp": 1646197926339294,
"UnitsLoadFinishTimestampMonotonic": 2094278,
"InitRDSecurityStartTimestamp": 0,
"InitRDSecurityStartTimestampMonotonic": 0,
"InitRDSecurityFinishTimestamp": 0,
"InitRDSecurityFinishTimestampMonotonic": 0,
"InitRDGeneratorsStartTimestamp": 0,
"InitRDGeneratorsStartTimestampMonotonic": 0,
"InitRDGeneratorsFinishTimestamp": 0,
"InitRDGeneratorsFinishTimestampMonotonic": 0,
"InitRDUnitsLoadStartTimestamp": 0,
"InitRDUnitsLoadStartTimestampMonotonic": 0,
"InitRDUnitsLoadFinishTimestamp": 0,
"InitRDUnitsLoadFinishTimestampMonotonic": 0,
"LogLevel": "info",
"LogTarget": "journal - or - kmsg",
"NNames": 377,
"NFailedUnits": 0,
"NJobs": 0,
"NInstalledJobs": 798,
"NFailedJobs": 0,
"Progress": 1.0,
"Environment": [
"LANG = C.UTF - 8",
"PATH = /usr/local / sbin: /usr/local / bin: /usr/sbin: /usr/bin"
],
"ConfirmSpawn": false,
"ShowStatus": true,
"UnitPath": [
" / etc / systemd / system.control",
" / run / systemd / system.control",
" / run / systemd / transient",
" / run / systemd / generator.early",
" / etc / systemd / system",
" / etc / systemd / system.attached",
" / run / systemd / system",
" / run / systemd / system.attached",
" / run / systemd / generator",
" / usr / local / lib / systemd / system",
" / usr / lib / systemd / system",
" / run / systemd / generator.late"
],
"DefaultStandardOutput": "journal",
"DefaultStandardError": "inherit",
"RuntimeWatchdogUSec": 0,
"RebootWatchdogUSec": 600000000,
"KExecWatchdogUSec": 0,
"ServiceWatchdogs": true,
"ControlGroup": "",
"SystemState": "running",
"ExitCode": [0, 0, 0, 0],
"DefaultTimerAccuracyUSec": 60000000,
"DefaultTimeoutStartUSec": 90000000,
"DefaultTimeoutStopUSec": 90000000,
"DefaultTimeoutAbortUSec": 90000000,
"DefaultRestartUSec": 100000,
"DefaultStartLimitIntervalUSec": 10000000,
"DefaultStartLimitBurst": 5,
"DefaultCPUAccounting": false,
"DefaultBlockIOAccounting": false,
"DefaultMemoryAccounting": true,
"DefaultTasksAccounting": true,
"DefaultLimitCPU": 18446744073709551615,
"DefaultLimitCPUSoft": 18446744073709551615,
"DefaultLimitFSIZE": 18446744073709551615,
"DefaultLimitFSIZESoft": 18446744073709551615,
"DefaultLimitDATA": 18446744073709551615,
"DefaultLimitDATASoft": 18446744073709551615,
"DefaultLimitSTACK": 18446744073709551615,
"DefaultLimitSTACKSoft": 8388608,
"DefaultLimitCORE": 18446744073709551615,
"DefaultLimitCORESoft": 18446744073709551615,
"DefaultLimitRSS": 18446744073709551615,
"DefaultLimitRSSSoft": 18446744073709551615,
"DefaultLimitNOFILE": 524288,
"DefaultLimitNOFILESoft": 1024,
"DefaultLimitAS": 18446744073709551615,
"DefaultLimitASSoft": 18446744073709551615,
"DefaultLimitNPROC": 14236,
"DefaultLimitNPROCSoft": 14236,
"DefaultLimitMEMLOCK": 65536,
"DefaultLimitMEMLOCKSoft": 65536,
"DefaultLimitLOCKS": 18446744073709551615,
"DefaultLimitLOCKSSoft": 18446744073709551615,
"DefaultLimitSIGPENDING": 14236,
"DefaultLimitSIGPENDINGSoft": 14236,
"DefaultLimitMSGQUEUE": 819200,
"DefaultLimitMSGQUEUESoft": 819200,
"DefaultLimitNICE": 0,
"DefaultLimitNICESoft": 0,
"DefaultLimitRTPRIO": 0,
"DefaultLimitRTPRIOSoft": 0,
"DefaultLimitRTTIME": 18446744073709551615,
"DefaultLimitRTTIMESoft": 18446744073709551615,
"DefaultTasksMax": 4270,
"TimerSlackNSec": 50000,
"DefaultOOMPolicy": "stop",
"CtrlAltDelBurstAction": "reboot - force"
}

View File

@ -0,0 +1,78 @@
"""Test host manager."""
from unittest.mock import AsyncMock, PropertyMock, patch
from supervisor.coresys import CoreSys
from supervisor.dbus.agent import OSAgent
from supervisor.dbus.hostname import Hostname
from supervisor.dbus.systemd import Systemd
from supervisor.dbus.timedate import TimeDate
async def test_reload(coresys: CoreSys):
"""Test manager reload."""
with patch.object(coresys.host.info, "update") as info_update, patch.object(
coresys.host.services, "update"
) as services_update, patch.object(
coresys.host.network, "update"
) as network_update, patch.object(
coresys.host.sys_dbus.agent, "update", new=AsyncMock()
) as agent_update, patch.object(
coresys.host.sound, "update"
) as sound_update:
await coresys.host.reload()
info_update.assert_called_once()
services_update.assert_called_once()
network_update.assert_called_once()
agent_update.assert_called_once()
sound_update.assert_called_once()
info_update.reset_mock()
services_update.reset_mock()
network_update.reset_mock()
agent_update.reset_mock()
sound_update.reset_mock()
await coresys.host.reload(
services=False, network=False, agent=False, audio=False
)
info_update.assert_called_once()
services_update.assert_not_called()
network_update.assert_not_called()
agent_update.assert_not_called()
sound_update.assert_not_called()
async def test_load(
coresys: CoreSys,
hostname: Hostname,
systemd: Systemd,
timedate: TimeDate,
os_agent: OSAgent,
):
"""Test manager load."""
type(coresys.dbus).hostname = PropertyMock(return_value=hostname)
type(coresys.dbus).systemd = PropertyMock(return_value=systemd)
type(coresys.dbus).timedate = PropertyMock(return_value=timedate)
type(coresys.dbus).agent = PropertyMock(return_value=os_agent)
with patch.object(coresys.host.sound, "update") as sound_update, patch.object(
coresys.host.apparmor, "load"
) as apparmor_load:
# Network is updated on connect for a version check so its not None already
assert coresys.dbus.hostname.hostname is None
assert coresys.dbus.systemd.boot_timestamp is None
assert coresys.dbus.timedate.timezone is None
assert coresys.dbus.agent.diagnostics is None
await coresys.host.load()
assert coresys.dbus.hostname.hostname == "homeassistant-n2"
assert coresys.dbus.systemd.boot_timestamp == 1646197962613554
assert coresys.dbus.timedate.timezone == "Etc/UTC"
assert coresys.dbus.agent.diagnostics is True
assert coresys.dbus.network.connectivity_enabled is True
sound_update.assert_called_once()
apparmor_load.assert_called_once()

View File

@ -0,0 +1,31 @@
"""Test network manager."""
from unittest.mock import Mock, patch
from supervisor.coresys import CoreSys
async def test_load(coresys: CoreSys):
"""Test network manager load."""
with patch.object(
coresys.host.sys_dbus.network,
"activate_connection",
new=Mock(wraps=coresys.host.sys_dbus.network.activate_connection),
) as activate_connection:
await coresys.host.network.load()
assert coresys.host.network.connectivity is True
assert len(coresys.host.network.dns_servers) == 1
assert str(coresys.host.network.dns_servers[0]) == "192.168.30.1"
assert len(coresys.host.network.interfaces) == 2
assert coresys.host.network.interfaces[0].name == "eth0"
assert coresys.host.network.interfaces[0].enabled is True
assert coresys.host.network.interfaces[1].name == "wlan0"
assert coresys.host.network.interfaces[1].enabled is False
assert activate_connection.call_count == 1
assert activate_connection.call_args.args == (
"/org/freedesktop/NetworkManager/Settings/1",
"/org/freedesktop/NetworkManager/Devices/1",
)