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,19 +95,27 @@ class HostManager(CoreSysAttributes):
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."""
await self.info.update()
if self.sys_dbus.systemd.is_connected:
if services and self.sys_dbus.systemd.is_connected:
await self.services.update()
if self.sys_dbus.network.is_connected:
if network and self.sys_dbus.network.is_connected:
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()
if audio:
with suppress(PulseAudioError):
await self.sound.update()
@ -117,7 +125,8 @@ class HostManager(CoreSysAttributes):
async def load(self):
"""Load host information."""
with suppress(HassioError):
await self.reload()
await self.reload(network=False)
await self.network.load()
# Register for events
self.sys_bus.register_event(BusEvent.HARDWARE_NEW_DEVICE, self._hardware_events)

View File

@ -2,11 +2,15 @@
from __future__ import annotations
import asyncio
from contextlib import suppress
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface
import logging
import attr
from supervisor.jobs.const import JobCondition
from supervisor.jobs.decorator import Job
from ..const import ATTR_HOST_INTERNET
from ..coresys import CoreSys, CoreSysAttributes
from ..dbus.const import (
@ -79,7 +83,6 @@ class NetworkManager(CoreSysAttributes):
async def check_connectivity(self):
"""Check the internet connection."""
if not self.sys_dbus.network.connectivity_enabled:
return
@ -100,6 +103,25 @@ class NetworkManager(CoreSysAttributes):
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):
"""Update properties over dbus."""
_LOGGER.info("Updating local network information")
@ -114,7 +136,9 @@ class NetworkManager(CoreSysAttributes):
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."""
inet = self.sys_dbus.network.interfaces.get(interface.name)
con: NetworkConnection = None
@ -147,6 +171,13 @@ class NetworkManager(CoreSysAttributes):
f"Can't update config on {interface.name}: {err}", _LOGGER.error
) 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
elif inet and interface.enabled:
_LOGGER.debug("Create new configuration for %s", interface.name)

View File

@ -19,6 +19,7 @@ class JobCondition(str, Enum):
RUNNING = "running"
HAOS = "haos"
OS_AGENT = "os_agent"
HOST_NETWORK = "host_network"
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"
)
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:
"""Process exection limits."""
if self.limit not in (

View File

@ -18,8 +18,13 @@ from supervisor.api import RestAPI
from supervisor.bootstrap import initialize_coresys
from supervisor.const import REQUEST_FROM
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.hostname import Hostname
from supervisor.dbus.interface import DBusInterface
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.store.addon import AddonStore
from supervisor.store.repository import Repository
@ -147,6 +152,37 @@ async def network_manager(dbus) -> NetworkManager:
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
async def coresys(loop, docker, network_manager, aiohttp_client, run_dir) -> CoreSys:
"""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",
)