Mark system as unhealthy on OSError Bad message errors (#4750)
* Bad message error marks system as unhealthy * Finish adding test cases for changes * Rename test file for uniqueness * bad_message to oserror_bad_message * Omit some checks and check for network mounts
This commit is contained in:
parent
b7ddfba71d
commit
3cc6bd19ad
|
@ -3,6 +3,7 @@ import asyncio
|
|||
from collections.abc import Awaitable
|
||||
from contextlib import suppress
|
||||
from copy import deepcopy
|
||||
import errno
|
||||
from ipaddress import IPv4Address
|
||||
import logging
|
||||
from pathlib import Path, PurePath
|
||||
|
@ -72,6 +73,7 @@ from ..hardware.data import Device
|
|||
from ..homeassistant.const import WSEvent, WSType
|
||||
from ..jobs.const import JobExecutionLimit
|
||||
from ..jobs.decorator import Job
|
||||
from ..resolution.const import UnhealthyReason
|
||||
from ..store.addon import AddonStore
|
||||
from ..utils import check_port
|
||||
from ..utils.apparmor import adjust_profile
|
||||
|
@ -793,6 +795,8 @@ class Addon(AddonModel):
|
|||
try:
|
||||
self.path_pulse.write_text(pulse_config, encoding="utf-8")
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
_LOGGER.error(
|
||||
"Add-on %s can't write pulse/client.config: %s", self.slug, err
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Backups RESTful API."""
|
||||
import asyncio
|
||||
import errno
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
@ -36,6 +37,7 @@ from ..const import (
|
|||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import APIError
|
||||
from ..mounts.const import MountUsage
|
||||
from ..resolution.const import UnhealthyReason
|
||||
from .const import CONTENT_TYPE_TAR
|
||||
from .utils import api_process, api_validate
|
||||
|
||||
|
@ -288,6 +290,8 @@ class APIBackups(CoreSysAttributes):
|
|||
backup.write(chunk)
|
||||
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
_LOGGER.error("Can't write new backup file: %s", err)
|
||||
return False
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Iterable
|
||||
import errno
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
|
@ -19,6 +20,7 @@ from ..jobs.const import JOB_GROUP_BACKUP_MANAGER, JobCondition, JobExecutionLim
|
|||
from ..jobs.decorator import Job
|
||||
from ..jobs.job_group import JobGroup
|
||||
from ..mounts.mount import Mount
|
||||
from ..resolution.const import UnhealthyReason
|
||||
from ..utils.common import FileConfiguration
|
||||
from ..utils.dt import utcnow
|
||||
from ..utils.sentinel import DEFAULT
|
||||
|
@ -31,18 +33,6 @@ from .validate import ALL_FOLDERS, SCHEMA_BACKUPS_CONFIG
|
|||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _list_backup_files(path: Path) -> Iterable[Path]:
|
||||
"""Return iterable of backup files, suppress and log OSError for network mounts."""
|
||||
try:
|
||||
# is_dir does a stat syscall which raises if the mount is down
|
||||
if path.is_dir():
|
||||
return path.glob("*.tar")
|
||||
except OSError as err:
|
||||
_LOGGER.error("Could not list backups from %s: %s", path.as_posix(), err)
|
||||
|
||||
return []
|
||||
|
||||
|
||||
class BackupManager(FileConfiguration, JobGroup):
|
||||
"""Manage backups."""
|
||||
|
||||
|
@ -119,6 +109,19 @@ class BackupManager(FileConfiguration, JobGroup):
|
|||
)
|
||||
self.sys_jobs.current.stage = stage
|
||||
|
||||
def _list_backup_files(self, path: Path) -> Iterable[Path]:
|
||||
"""Return iterable of backup files, suppress and log OSError for network mounts."""
|
||||
try:
|
||||
# is_dir does a stat syscall which raises if the mount is down
|
||||
if path.is_dir():
|
||||
return path.glob("*.tar")
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG and path == self.sys_config.path_backup:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
_LOGGER.error("Could not list backups from %s: %s", path.as_posix(), err)
|
||||
|
||||
return []
|
||||
|
||||
def _create_backup(
|
||||
self,
|
||||
name: str,
|
||||
|
@ -169,7 +172,7 @@ class BackupManager(FileConfiguration, JobGroup):
|
|||
tasks = [
|
||||
self.sys_create_task(_load_backup(tar_file))
|
||||
for path in self.backup_locations
|
||||
for tar_file in _list_backup_files(path)
|
||||
for tar_file in self._list_backup_files(path)
|
||||
]
|
||||
|
||||
_LOGGER.info("Found %d backup files", len(tasks))
|
||||
|
@ -184,6 +187,11 @@ class BackupManager(FileConfiguration, JobGroup):
|
|||
_LOGGER.info("Removed backup file %s", backup.slug)
|
||||
|
||||
except OSError as err:
|
||||
if (
|
||||
err.errno == errno.EBADMSG
|
||||
and backup.tarfile.parent == self.sys_config.path_backup
|
||||
):
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
_LOGGER.error("Can't remove backup %s: %s", backup.slug, err)
|
||||
return False
|
||||
|
||||
|
@ -208,6 +216,8 @@ class BackupManager(FileConfiguration, JobGroup):
|
|||
backup.tarfile.rename(tar_origin)
|
||||
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
_LOGGER.error("Can't move backup file to storage: %s", err)
|
||||
return None
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""Home Assistant control object."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import errno
|
||||
from ipaddress import IPv4Address
|
||||
import logging
|
||||
from pathlib import Path, PurePath
|
||||
|
@ -42,6 +43,7 @@ from ..exceptions import (
|
|||
from ..hardware.const import PolicyGroup
|
||||
from ..hardware.data import Device
|
||||
from ..jobs.decorator import Job, JobExecutionLimit
|
||||
from ..resolution.const import UnhealthyReason
|
||||
from ..utils import remove_folder
|
||||
from ..utils.common import FileConfiguration
|
||||
from ..utils.json import read_json_file, write_json_file
|
||||
|
@ -300,6 +302,8 @@ class HomeAssistant(FileConfiguration, CoreSysAttributes):
|
|||
try:
|
||||
self.path_pulse.write_text(pulse_config, encoding="utf-8")
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
_LOGGER.error("Home Assistant can't write pulse/client.config: %s", err)
|
||||
else:
|
||||
_LOGGER.info("Update pulse/client.config: %s", self.path_pulse)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""AppArmor control for host."""
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
@ -9,7 +10,7 @@ from awesomeversion import AwesomeVersion
|
|||
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..exceptions import DBusError, HostAppArmorError
|
||||
from ..resolution.const import UnsupportedReason
|
||||
from ..resolution.const import UnhealthyReason, UnsupportedReason
|
||||
from ..utils.apparmor import validate_profile
|
||||
from .const import HostFeature
|
||||
|
||||
|
@ -80,6 +81,8 @@ class AppArmorControl(CoreSysAttributes):
|
|||
try:
|
||||
await self.sys_run_in_executor(shutil.copyfile, profile_file, dest_profile)
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
raise HostAppArmorError(
|
||||
f"Can't copy {profile_file}: {err}", _LOGGER.error
|
||||
) from err
|
||||
|
@ -103,6 +106,8 @@ class AppArmorControl(CoreSysAttributes):
|
|||
try:
|
||||
await self.sys_run_in_executor(profile_file.unlink)
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
raise HostAppArmorError(
|
||||
f"Can't remove profile: {err}", _LOGGER.error
|
||||
) from err
|
||||
|
@ -117,6 +122,8 @@ class AppArmorControl(CoreSysAttributes):
|
|||
try:
|
||||
await self.sys_run_in_executor(shutil.copy, profile_file, backup_file)
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
raise HostAppArmorError(
|
||||
f"Can't backup profile {profile_name}: {err}", _LOGGER.error
|
||||
) from err
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""OS support on supervisor."""
|
||||
from collections.abc import Awaitable
|
||||
import errno
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
|
@ -13,6 +14,7 @@ from ..dbus.rauc import RaucState
|
|||
from ..exceptions import DBusError, HassOSJobError, HassOSUpdateError
|
||||
from ..jobs.const import JobCondition, JobExecutionLimit
|
||||
from ..jobs.decorator import Job
|
||||
from ..resolution.const import UnhealthyReason
|
||||
from .data_disk import DataDisk
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
@ -120,6 +122,8 @@ class OSManager(CoreSysAttributes):
|
|||
) from err
|
||||
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
raise HassOSUpdateError(
|
||||
f"Can't write OTA file: {err!s}", _LOGGER.error
|
||||
) from err
|
||||
|
|
|
@ -4,6 +4,7 @@ Code: https://github.com/home-assistant/plugin-audio
|
|||
"""
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
import errno
|
||||
import logging
|
||||
from pathlib import Path, PurePath
|
||||
import shutil
|
||||
|
@ -25,6 +26,7 @@ from ..exceptions import (
|
|||
)
|
||||
from ..jobs.const import JobExecutionLimit
|
||||
from ..jobs.decorator import Job
|
||||
from ..resolution.const import UnhealthyReason
|
||||
from ..utils.json import write_json_file
|
||||
from ..utils.sentry import capture_exception
|
||||
from .base import PluginBase
|
||||
|
@ -83,6 +85,9 @@ class PluginAudio(PluginBase):
|
|||
PULSE_CLIENT_TMPL.read_text(encoding="utf-8")
|
||||
)
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
|
||||
_LOGGER.error("Can't read pulse-client.tmpl: %s", err)
|
||||
|
||||
await super().load()
|
||||
|
@ -93,6 +98,8 @@ class PluginAudio(PluginBase):
|
|||
try:
|
||||
shutil.copy(ASOUND_TMPL, asound)
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
_LOGGER.error("Can't create default asound: %s", err)
|
||||
|
||||
async def install(self) -> None:
|
||||
|
|
|
@ -4,6 +4,7 @@ Code: https://github.com/home-assistant/plugin-dns
|
|||
"""
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
import errno
|
||||
from ipaddress import IPv4Address
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
@ -29,7 +30,7 @@ from ..exceptions import (
|
|||
)
|
||||
from ..jobs.const import JobExecutionLimit
|
||||
from ..jobs.decorator import Job
|
||||
from ..resolution.const import ContextType, IssueType, SuggestionType
|
||||
from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason
|
||||
from ..utils.json import write_json_file
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..validate import dns_url
|
||||
|
@ -146,12 +147,16 @@ class PluginDns(PluginBase):
|
|||
RESOLV_TMPL.read_text(encoding="utf-8")
|
||||
)
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
_LOGGER.error("Can't read resolve.tmpl: %s", err)
|
||||
try:
|
||||
self.hosts_template = jinja2.Template(
|
||||
HOSTS_TMPL.read_text(encoding="utf-8")
|
||||
)
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
_LOGGER.error("Can't read hosts.tmpl: %s", err)
|
||||
|
||||
await self._init_hosts()
|
||||
|
@ -364,6 +369,8 @@ class PluginDns(PluginBase):
|
|||
self.hosts.write_text, data, encoding="utf-8"
|
||||
)
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
raise CoreDNSError(f"Can't update hosts: {err}", _LOGGER.error) from err
|
||||
|
||||
async def add_host(
|
||||
|
@ -436,6 +443,12 @@ class PluginDns(PluginBase):
|
|||
|
||||
def _write_resolv(self, resolv_conf: Path) -> None:
|
||||
"""Update/Write resolv.conf file."""
|
||||
if not self.resolv_template:
|
||||
_LOGGER.warning(
|
||||
"Resolv template is missing, cannot write/update %s", resolv_conf
|
||||
)
|
||||
return
|
||||
|
||||
nameservers = [str(self.sys_docker.network.dns), "127.0.0.11"]
|
||||
|
||||
# Read resolv config
|
||||
|
@ -445,6 +458,8 @@ class PluginDns(PluginBase):
|
|||
try:
|
||||
resolv_conf.write_text(data)
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
_LOGGER.warning("Can't write/update %s: %s", resolv_conf, err)
|
||||
return
|
||||
|
||||
|
|
|
@ -59,9 +59,10 @@ class UnhealthyReason(StrEnum):
|
|||
"""Reasons for unsupported status."""
|
||||
|
||||
DOCKER = "docker"
|
||||
OSERROR_BAD_MESSAGE = "oserror_bad_message"
|
||||
PRIVILEGED = "privileged"
|
||||
SUPERVISOR = "supervisor"
|
||||
SETUP = "setup"
|
||||
PRIVILEGED = "privileged"
|
||||
UNTRUSTED = "untrusted"
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Evaluation class for Content Trust."""
|
||||
import errno
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
|
@ -6,7 +7,7 @@ from ...const import CoreState
|
|||
from ...coresys import CoreSys
|
||||
from ...exceptions import CodeNotaryError, CodeNotaryUntrusted
|
||||
from ...utils.codenotary import calc_checksum_path_sourcecode
|
||||
from ..const import ContextType, IssueType, UnsupportedReason
|
||||
from ..const import ContextType, IssueType, UnhealthyReason, UnsupportedReason
|
||||
from .base import EvaluateBase
|
||||
|
||||
_SUPERVISOR_SOURCE = Path("/usr/src/supervisor/supervisor")
|
||||
|
@ -48,6 +49,9 @@ class EvaluateSourceMods(EvaluateBase):
|
|||
calc_checksum_path_sourcecode, _SUPERVISOR_SOURCE
|
||||
)
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
|
||||
self.sys_resolution.create_issue(
|
||||
IssueType.CORRUPT_FILESYSTEM, ContextType.SYSTEM
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Init file for Supervisor add-on data."""
|
||||
from dataclasses import dataclass
|
||||
import errno
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
@ -19,7 +20,7 @@ from ..const import (
|
|||
)
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..exceptions import ConfigurationFileError
|
||||
from ..resolution.const import ContextType, IssueType, SuggestionType
|
||||
from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason
|
||||
from ..utils.common import find_one_filetype, read_json_or_yaml_file
|
||||
from ..utils.json import read_json_file
|
||||
from .const import StoreType
|
||||
|
@ -157,7 +158,9 @@ class StoreData(CoreSysAttributes):
|
|||
addon_list = await self.sys_run_in_executor(_get_addons_list)
|
||||
except OSError as err:
|
||||
suggestion = None
|
||||
if path.stem != StoreType.LOCAL:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
elif path.stem != StoreType.LOCAL:
|
||||
suggestion = [SuggestionType.EXECUTE_RESET]
|
||||
self.sys_resolution.create_issue(
|
||||
IssueType.CORRUPT_REPOSITORY,
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from collections.abc import Awaitable
|
||||
from contextlib import suppress
|
||||
from datetime import timedelta
|
||||
import errno
|
||||
from ipaddress import IPv4Address
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
@ -27,7 +28,7 @@ from .exceptions import (
|
|||
)
|
||||
from .jobs.const import JobCondition, JobExecutionLimit
|
||||
from .jobs.decorator import Job
|
||||
from .resolution.const import ContextType, IssueType
|
||||
from .resolution.const import ContextType, IssueType, UnhealthyReason
|
||||
from .utils.codenotary import calc_checksum
|
||||
from .utils.sentry import capture_exception
|
||||
|
||||
|
@ -155,6 +156,8 @@ class Supervisor(CoreSysAttributes):
|
|||
try:
|
||||
profile_file.write_text(data, encoding="utf-8")
|
||||
except OSError as err:
|
||||
if err.errno == errno.EBADMSG:
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||
raise SupervisorAppArmorError(
|
||||
f"Can't write temporary profile: {err!s}", _LOGGER.error
|
||||
) from err
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import errno
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
|
@ -696,3 +697,27 @@ async def test_local_example_ingress_port_set(
|
|||
await install_addon_example.load()
|
||||
|
||||
assert install_addon_example.ingress_port != 0
|
||||
|
||||
|
||||
def test_addon_pulse_error(
|
||||
coresys: CoreSys,
|
||||
install_addon_example: Addon,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
tmp_supervisor_data,
|
||||
):
|
||||
"""Test error writing pulse config for addon."""
|
||||
with patch(
|
||||
"supervisor.addons.addon.Path.write_text", side_effect=(err := OSError())
|
||||
):
|
||||
err.errno = errno.EBUSY
|
||||
install_addon_example.write_pulse()
|
||||
|
||||
assert "can't write pulse/client.config" in caplog.text
|
||||
assert coresys.core.healthy is True
|
||||
|
||||
caplog.clear()
|
||||
err.errno = errno.EBADMSG
|
||||
install_addon_example.write_pulse()
|
||||
|
||||
assert "can't write pulse/client.config" in caplog.text
|
||||
assert coresys.core.healthy is False
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""Test BackupManager class."""
|
||||
|
||||
import asyncio
|
||||
import errno
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock, Mock, PropertyMock, patch
|
||||
|
||||
|
@ -1555,3 +1557,82 @@ async def test_skip_homeassistant_database(
|
|||
assert read_json_file(test_db) == {"hello": "world"}
|
||||
assert read_json_file(test_db_wal) == {"hello": "world"}
|
||||
assert not test_db_shm.exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tar_parent,healthy_expected",
|
||||
[
|
||||
(Path("/data/mounts/test"), True),
|
||||
(Path("/data/backup"), False),
|
||||
],
|
||||
)
|
||||
def test_backup_remove_error(
|
||||
coresys: CoreSys,
|
||||
full_backup_mock: Backup,
|
||||
tar_parent: Path,
|
||||
healthy_expected: bool,
|
||||
):
|
||||
"""Test removing a backup error."""
|
||||
full_backup_mock.tarfile.unlink.side_effect = (err := OSError())
|
||||
full_backup_mock.tarfile.parent = tar_parent
|
||||
|
||||
err.errno = errno.EBUSY
|
||||
assert coresys.backups.remove(full_backup_mock) is False
|
||||
assert coresys.core.healthy is True
|
||||
|
||||
err.errno = errno.EBADMSG
|
||||
assert coresys.backups.remove(full_backup_mock) is False
|
||||
assert coresys.core.healthy is healthy_expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error_path,healthy_expected",
|
||||
[(Path("/data/backup"), False), (Path("/data/mounts/backup_test"), True)],
|
||||
)
|
||||
async def test_reload_error(
|
||||
coresys: CoreSys,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
error_path: Path,
|
||||
healthy_expected: bool,
|
||||
path_extern,
|
||||
mount_propagation,
|
||||
):
|
||||
"""Test error during reload."""
|
||||
err = OSError()
|
||||
|
||||
def mock_is_dir(path: Path) -> bool:
|
||||
"""Mock of is_dir."""
|
||||
if path == error_path:
|
||||
raise err
|
||||
return True
|
||||
|
||||
# Add a backup mount
|
||||
await coresys.mounts.load()
|
||||
await coresys.mounts.create_mount(
|
||||
Mount.from_dict(
|
||||
coresys,
|
||||
{
|
||||
"name": "backup_test",
|
||||
"usage": "backup",
|
||||
"type": "cifs",
|
||||
"server": "test.local",
|
||||
"share": "test",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
with patch("supervisor.backups.manager.Path.is_dir", new=mock_is_dir), patch(
|
||||
"supervisor.backups.manager.Path.glob", return_value=[]
|
||||
):
|
||||
err.errno = errno.EBUSY
|
||||
await coresys.backups.reload()
|
||||
|
||||
assert "Could not list backups" in caplog.text
|
||||
assert coresys.core.healthy is True
|
||||
|
||||
caplog.clear()
|
||||
err.errno = errno.EBADMSG
|
||||
await coresys.backups.reload()
|
||||
|
||||
assert "Could not list backups" in caplog.text
|
||||
assert coresys.core.healthy is healthy_expected
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
"""Test hardware utils."""
|
||||
# pylint: disable=protected-access
|
||||
import errno
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from pytest import LogCaptureFixture
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.hardware.data import Device
|
||||
|
||||
|
||||
def test_have_audio(coresys):
|
||||
def test_have_audio(coresys: CoreSys):
|
||||
"""Test usb device filter."""
|
||||
assert not coresys.hardware.helper.support_audio
|
||||
|
||||
|
@ -26,7 +29,7 @@ def test_have_audio(coresys):
|
|||
assert coresys.hardware.helper.support_audio
|
||||
|
||||
|
||||
def test_have_usb(coresys):
|
||||
def test_have_usb(coresys: CoreSys):
|
||||
"""Test usb device filter."""
|
||||
assert not coresys.hardware.helper.support_usb
|
||||
|
||||
|
@ -46,7 +49,7 @@ def test_have_usb(coresys):
|
|||
assert coresys.hardware.helper.support_usb
|
||||
|
||||
|
||||
def test_have_gpio(coresys):
|
||||
def test_have_gpio(coresys: CoreSys):
|
||||
"""Test usb device filter."""
|
||||
assert not coresys.hardware.helper.support_gpio
|
||||
|
||||
|
@ -66,7 +69,7 @@ def test_have_gpio(coresys):
|
|||
assert coresys.hardware.helper.support_gpio
|
||||
|
||||
|
||||
def test_hide_virtual_device(coresys):
|
||||
def test_hide_virtual_device(coresys: CoreSys):
|
||||
"""Test hidding virtual devices."""
|
||||
udev_device = MagicMock()
|
||||
|
||||
|
@ -81,3 +84,15 @@ def test_hide_virtual_device(coresys):
|
|||
|
||||
udev_device.sys_path = "/sys/devices/virtual/vc/vcs1"
|
||||
assert coresys.hardware.helper.hide_virtual_device(udev_device)
|
||||
|
||||
|
||||
def test_last_boot_error(coresys: CoreSys, caplog: LogCaptureFixture):
|
||||
"""Test error reading last boot."""
|
||||
with patch(
|
||||
"supervisor.hardware.helper.Path.read_text", side_effect=(err := OSError())
|
||||
):
|
||||
err.errno = errno.EBADMSG
|
||||
assert coresys.hardware.helper.last_boot is None
|
||||
|
||||
assert coresys.core.healthy is True
|
||||
assert "Can't read stat data" in caplog.text
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
"""Test Homeassistant module."""
|
||||
|
||||
import asyncio
|
||||
import errno
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from pytest import LogCaptureFixture
|
||||
|
||||
from supervisor.const import CoreState
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.docker.interface import DockerInterface
|
||||
|
@ -44,3 +47,23 @@ async def test_get_users_none(coresys: CoreSys, ha_ws_client: AsyncMock):
|
|||
assert [] == await coresys.homeassistant.get_users.__wrapped__(
|
||||
coresys.homeassistant
|
||||
)
|
||||
|
||||
|
||||
def test_write_pulse_error(coresys: CoreSys, caplog: LogCaptureFixture):
|
||||
"""Test errors writing pulse config."""
|
||||
with patch(
|
||||
"supervisor.homeassistant.module.Path.write_text",
|
||||
side_effect=(err := OSError()),
|
||||
):
|
||||
err.errno = errno.EBUSY
|
||||
coresys.homeassistant.write_pulse()
|
||||
|
||||
assert "can't write pulse/client.config" in caplog.text
|
||||
assert coresys.core.healthy is True
|
||||
|
||||
caplog.clear()
|
||||
err.errno = errno.EBADMSG
|
||||
coresys.homeassistant.write_pulse()
|
||||
|
||||
assert "can't write pulse/client.config" in caplog.text
|
||||
assert coresys.core.healthy is False
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
"""Test host apparmor control."""
|
||||
|
||||
import errno
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from pytest import raises
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.exceptions import HostAppArmorError
|
||||
|
||||
|
||||
async def test_load_profile_error(coresys: CoreSys):
|
||||
"""Test error loading apparmor profile."""
|
||||
test_path = Path("test")
|
||||
with patch("supervisor.host.apparmor.validate_profile"), patch(
|
||||
"supervisor.host.apparmor.shutil.copyfile", side_effect=(err := OSError())
|
||||
):
|
||||
err.errno = errno.EBUSY
|
||||
with raises(HostAppArmorError):
|
||||
await coresys.host.apparmor.load_profile("test", test_path)
|
||||
assert coresys.core.healthy is True
|
||||
|
||||
err.errno = errno.EBADMSG
|
||||
with raises(HostAppArmorError):
|
||||
await coresys.host.apparmor.load_profile("test", test_path)
|
||||
assert coresys.core.healthy is False
|
||||
|
||||
|
||||
async def test_remove_profile_error(coresys: CoreSys, path_extern):
|
||||
"""Test error removing apparmor profile."""
|
||||
coresys.host.apparmor._profiles.add("test") # pylint: disable=protected-access
|
||||
with patch("supervisor.host.apparmor.Path.unlink", side_effect=(err := OSError())):
|
||||
err.errno = errno.EBUSY
|
||||
with raises(HostAppArmorError):
|
||||
await coresys.host.apparmor.remove_profile("test")
|
||||
assert coresys.core.healthy is True
|
||||
|
||||
err.errno = errno.EBADMSG
|
||||
with raises(HostAppArmorError):
|
||||
await coresys.host.apparmor.remove_profile("test")
|
||||
assert coresys.core.healthy is False
|
||||
|
||||
|
||||
async def test_backup_profile_error(coresys: CoreSys, path_extern):
|
||||
"""Test error while backing up apparmor profile."""
|
||||
test_path = Path("test")
|
||||
coresys.host.apparmor._profiles.add("test") # pylint: disable=protected-access
|
||||
with patch(
|
||||
"supervisor.host.apparmor.shutil.copyfile", side_effect=(err := OSError())
|
||||
):
|
||||
err.errno = errno.EBUSY
|
||||
with raises(HostAppArmorError):
|
||||
await coresys.host.apparmor.backup_profile("test", test_path)
|
||||
assert coresys.core.healthy is True
|
||||
|
||||
err.errno = errno.EBADMSG
|
||||
with raises(HostAppArmorError):
|
||||
await coresys.host.apparmor.backup_profile("test", test_path)
|
||||
assert coresys.core.healthy is False
|
|
@ -1,4 +1,5 @@
|
|||
"""Test audio plugin."""
|
||||
import errno
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
|
@ -56,3 +57,26 @@ async def test_config_write(
|
|||
"debug": True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_load_error(
|
||||
coresys: CoreSys, caplog: pytest.LogCaptureFixture, container
|
||||
):
|
||||
"""Test error reading config file during load."""
|
||||
with patch(
|
||||
"supervisor.plugins.audio.Path.read_text", side_effect=(err := OSError())
|
||||
), patch("supervisor.plugins.audio.shutil.copy", side_effect=err):
|
||||
err.errno = errno.EBUSY
|
||||
await coresys.plugins.audio.load()
|
||||
|
||||
assert "Can't read pulse-client.tmpl" in caplog.text
|
||||
assert "Can't create default asound" in caplog.text
|
||||
assert coresys.core.healthy is True
|
||||
|
||||
caplog.clear()
|
||||
err.errno = errno.EBADMSG
|
||||
await coresys.plugins.audio.load()
|
||||
|
||||
assert "Can't read pulse-client.tmpl" in caplog.text
|
||||
assert "Can't create default asound" in caplog.text
|
||||
assert coresys.core.healthy is False
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Test DNS plugin."""
|
||||
import asyncio
|
||||
import errno
|
||||
from ipaddress import IPv4Address
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
@ -183,3 +184,49 @@ async def test_loop_detection_on_failure(coresys: CoreSys):
|
|||
Suggestion(SuggestionType.EXECUTE_RESET, ContextType.PLUGIN, "dns")
|
||||
]
|
||||
rebuild.assert_called_once()
|
||||
|
||||
|
||||
async def test_load_error(
|
||||
coresys: CoreSys, caplog: pytest.LogCaptureFixture, container
|
||||
):
|
||||
"""Test error reading config files during load."""
|
||||
with patch(
|
||||
"supervisor.plugins.dns.Path.read_text", side_effect=(err := OSError())
|
||||
), patch("supervisor.plugins.dns.Path.write_text", side_effect=err):
|
||||
err.errno = errno.EBUSY
|
||||
await coresys.plugins.dns.load()
|
||||
|
||||
assert "Can't read resolve.tmpl" in caplog.text
|
||||
assert "Can't read hosts.tmpl" in caplog.text
|
||||
assert "Resolv template is missing" in caplog.text
|
||||
assert coresys.core.healthy is True
|
||||
|
||||
caplog.clear()
|
||||
err.errno = errno.EBADMSG
|
||||
await coresys.plugins.dns.load()
|
||||
|
||||
assert "Can't read resolve.tmpl" in caplog.text
|
||||
assert "Can't read hosts.tmpl" in caplog.text
|
||||
assert "Resolv template is missing" in caplog.text
|
||||
assert coresys.core.healthy is False
|
||||
|
||||
|
||||
async def test_load_error_writing_resolv(
|
||||
coresys: CoreSys, caplog: pytest.LogCaptureFixture, container
|
||||
):
|
||||
"""Test error writing resolv during load."""
|
||||
with patch(
|
||||
"supervisor.plugins.dns.Path.write_text", side_effect=(err := OSError())
|
||||
):
|
||||
err.errno = errno.EBUSY
|
||||
await coresys.plugins.dns.load()
|
||||
|
||||
assert "Can't write/update /etc/resolv.conf" in caplog.text
|
||||
assert coresys.core.healthy is True
|
||||
|
||||
caplog.clear()
|
||||
err.errno = errno.EBADMSG
|
||||
await coresys.plugins.dns.load()
|
||||
|
||||
assert "Can't write/update /etc/resolv.conf" in caplog.text
|
||||
assert coresys.core.healthy is False
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Test evaluation base."""
|
||||
# pylint: disable=import-error,protected-access
|
||||
import errno
|
||||
from unittest.mock import patch
|
||||
|
||||
from supervisor.const import CoreState
|
||||
|
@ -50,3 +51,20 @@ async def test_did_run(coresys: CoreSys):
|
|||
await apparmor()
|
||||
evaluate.assert_not_called()
|
||||
evaluate.reset_mock()
|
||||
|
||||
|
||||
async def test_evaluation_error(coresys: CoreSys):
|
||||
"""Test error reading file during evaluation."""
|
||||
apparmor = EvaluateAppArmor(coresys)
|
||||
coresys.core.state = CoreState.INITIALIZE
|
||||
|
||||
assert apparmor.reason not in coresys.resolution.unsupported
|
||||
|
||||
with patch(
|
||||
"supervisor.resolution.evaluations.apparmor.Path.read_text",
|
||||
side_effect=(err := OSError()),
|
||||
):
|
||||
err.errno = errno.EBADMSG
|
||||
await apparmor()
|
||||
assert apparmor.reason in coresys.resolution.unsupported
|
||||
assert coresys.core.healthy is True
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Test evaluation base."""
|
||||
# pylint: disable=import-error,protected-access
|
||||
import errno
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
@ -7,6 +8,8 @@ from unittest.mock import AsyncMock, patch
|
|||
from supervisor.const import CoreState
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.exceptions import CodeNotaryError, CodeNotaryUntrusted
|
||||
from supervisor.resolution.const import ContextType, IssueType
|
||||
from supervisor.resolution.data import Issue
|
||||
from supervisor.resolution.evaluations.source_mods import EvaluateSourceMods
|
||||
|
||||
|
||||
|
@ -56,3 +59,30 @@ async def test_did_run(coresys: CoreSys):
|
|||
await sourcemods()
|
||||
evaluate.assert_not_called()
|
||||
evaluate.reset_mock()
|
||||
|
||||
|
||||
async def test_evaluation_error(coresys: CoreSys):
|
||||
"""Test error reading file during evaluation."""
|
||||
sourcemods = EvaluateSourceMods(coresys)
|
||||
coresys.core.state = CoreState.RUNNING
|
||||
corrupt_fs = Issue(IssueType.CORRUPT_FILESYSTEM, ContextType.SYSTEM)
|
||||
|
||||
assert sourcemods.reason not in coresys.resolution.unsupported
|
||||
assert corrupt_fs not in coresys.resolution.issues
|
||||
|
||||
with patch(
|
||||
"supervisor.utils.codenotary.dirhash",
|
||||
side_effect=(err := OSError()),
|
||||
):
|
||||
err.errno = errno.EBUSY
|
||||
await sourcemods()
|
||||
assert sourcemods.reason not in coresys.resolution.unsupported
|
||||
assert corrupt_fs in coresys.resolution.issues
|
||||
assert coresys.core.healthy is True
|
||||
|
||||
coresys.resolution.dismiss_issue(corrupt_fs)
|
||||
err.errno = errno.EBADMSG
|
||||
await sourcemods()
|
||||
assert sourcemods.reason not in coresys.resolution.unsupported
|
||||
assert corrupt_fs in coresys.resolution.issues
|
||||
assert coresys.core.healthy is False
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
"""Test that we are reading add-on files correctly."""
|
||||
import errno
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
||||
from supervisor.resolution.data import Issue, Suggestion
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
|
||||
async def test_read_addon_files(coresys: CoreSys):
|
||||
|
@ -23,3 +28,23 @@ async def test_read_addon_files(coresys: CoreSys):
|
|||
|
||||
assert len(addon_list) == 1
|
||||
assert str(addon_list[0]) == "addon/config.yml"
|
||||
|
||||
|
||||
async def test_reading_addon_files_error(coresys: CoreSys):
|
||||
"""Test error trying to read addon files."""
|
||||
corrupt_repo = Issue(IssueType.CORRUPT_REPOSITORY, ContextType.STORE, "test")
|
||||
reset_repo = Suggestion(SuggestionType.EXECUTE_RESET, ContextType.STORE, "test")
|
||||
|
||||
with patch("pathlib.Path.glob", side_effect=(err := OSError())):
|
||||
err.errno = errno.EBUSY
|
||||
assert (await coresys.store.data._find_addons(Path("test"), {})) is None
|
||||
assert corrupt_repo in coresys.resolution.issues
|
||||
assert reset_repo in coresys.resolution.suggestions
|
||||
assert coresys.core.healthy is True
|
||||
|
||||
coresys.resolution.dismiss_issue(corrupt_repo)
|
||||
err.errno = errno.EBADMSG
|
||||
assert (await coresys.store.data._find_addons(Path("test"), {})) is None
|
||||
assert corrupt_repo in coresys.resolution.issues
|
||||
assert reset_repo not in coresys.resolution.suggestions
|
||||
assert coresys.core.healthy is False
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
"""Testing handling with CoreState."""
|
||||
# pylint: disable=W0212
|
||||
import datetime
|
||||
import errno
|
||||
from unittest.mock import AsyncMock, PropertyMock, patch
|
||||
|
||||
from pytest import LogCaptureFixture
|
||||
|
||||
from supervisor.const import CoreState
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.exceptions import WhoamiSSLError
|
||||
|
@ -14,7 +17,6 @@ from supervisor.utils.whoami import WhoamiData
|
|||
|
||||
def test_write_state(run_dir, coresys: CoreSys):
|
||||
"""Test write corestate to /run/supervisor."""
|
||||
|
||||
coresys.core.state = CoreState.RUNNING
|
||||
|
||||
assert run_dir.read_text() == CoreState.RUNNING
|
||||
|
@ -77,3 +79,16 @@ async def test_adjust_system_datetime_if_time_behind(coresys: CoreSys):
|
|||
mock_retrieve_whoami.assert_called_once()
|
||||
mock_set_datetime.assert_called_once()
|
||||
mock_check_connectivity.assert_called_once()
|
||||
|
||||
|
||||
def test_write_state_failure(run_dir, coresys: CoreSys, caplog: LogCaptureFixture):
|
||||
"""Test failure to write corestate to /run/supervisor."""
|
||||
with patch(
|
||||
"supervisor.core.RUN_SUPERVISOR_STATE.write_text",
|
||||
side_effect=(err := OSError()),
|
||||
):
|
||||
err.errno = errno.EBADMSG
|
||||
coresys.core.state = CoreState.RUNNING
|
||||
|
||||
assert "Can't update the Supervisor state" in caplog.text
|
||||
assert coresys.core.healthy is True
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""Test supervisor object."""
|
||||
|
||||
from datetime import timedelta
|
||||
import errno
|
||||
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
|
||||
|
||||
from aiohttp import ClientTimeout
|
||||
|
@ -11,7 +12,11 @@ import pytest
|
|||
from supervisor.const import UpdateChannel
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.docker.supervisor import DockerSupervisor
|
||||
from supervisor.exceptions import DockerError, SupervisorUpdateError
|
||||
from supervisor.exceptions import (
|
||||
DockerError,
|
||||
SupervisorAppArmorError,
|
||||
SupervisorUpdateError,
|
||||
)
|
||||
from supervisor.host.apparmor import AppArmorControl
|
||||
from supervisor.resolution.const import ContextType, IssueType
|
||||
from supervisor.resolution.data import Issue
|
||||
|
@ -108,3 +113,22 @@ async def test_update_apparmor(
|
|||
timeout=ClientTimeout(total=10),
|
||||
)
|
||||
load_profile.assert_called_once()
|
||||
|
||||
|
||||
async def test_update_apparmor_error(coresys: CoreSys, tmp_supervisor_data):
|
||||
"""Test error updating apparmor profile."""
|
||||
with patch("supervisor.coresys.aiohttp.ClientSession.get") as get, patch.object(
|
||||
AppArmorControl, "load_profile"
|
||||
), patch("supervisor.supervisor.Path.write_text", side_effect=(err := OSError())):
|
||||
get.return_value.__aenter__.return_value.status = 200
|
||||
get.return_value.__aenter__.return_value.text = AsyncMock(return_value="")
|
||||
|
||||
err.errno = errno.EBUSY
|
||||
with pytest.raises(SupervisorAppArmorError):
|
||||
await coresys.supervisor.update_apparmor()
|
||||
assert coresys.core.healthy is True
|
||||
|
||||
err.errno = errno.EBADMSG
|
||||
with pytest.raises(SupervisorAppArmorError):
|
||||
await coresys.supervisor.update_apparmor()
|
||||
assert coresys.core.healthy is False
|
||||
|
|
Loading…
Reference in New Issue