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 collections.abc import Awaitable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
import errno
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
|
@ -72,6 +73,7 @@ from ..hardware.data import Device
|
||||||
from ..homeassistant.const import WSEvent, WSType
|
from ..homeassistant.const import WSEvent, WSType
|
||||||
from ..jobs.const import JobExecutionLimit
|
from ..jobs.const import JobExecutionLimit
|
||||||
from ..jobs.decorator import Job
|
from ..jobs.decorator import Job
|
||||||
|
from ..resolution.const import UnhealthyReason
|
||||||
from ..store.addon import AddonStore
|
from ..store.addon import AddonStore
|
||||||
from ..utils import check_port
|
from ..utils import check_port
|
||||||
from ..utils.apparmor import adjust_profile
|
from ..utils.apparmor import adjust_profile
|
||||||
|
@ -793,6 +795,8 @@ class Addon(AddonModel):
|
||||||
try:
|
try:
|
||||||
self.path_pulse.write_text(pulse_config, encoding="utf-8")
|
self.path_pulse.write_text(pulse_config, encoding="utf-8")
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
|
if err.errno == errno.EBADMSG:
|
||||||
|
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Add-on %s can't write pulse/client.config: %s", self.slug, err
|
"Add-on %s can't write pulse/client.config: %s", self.slug, err
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""Backups RESTful API."""
|
"""Backups RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import errno
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
@ -36,6 +37,7 @@ from ..const import (
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import APIError
|
from ..exceptions import APIError
|
||||||
from ..mounts.const import MountUsage
|
from ..mounts.const import MountUsage
|
||||||
|
from ..resolution.const import UnhealthyReason
|
||||||
from .const import CONTENT_TYPE_TAR
|
from .const import CONTENT_TYPE_TAR
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
|
@ -288,6 +290,8 @@ class APIBackups(CoreSysAttributes):
|
||||||
backup.write(chunk)
|
backup.write(chunk)
|
||||||
|
|
||||||
except OSError as err:
|
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)
|
_LOGGER.error("Can't write new backup file: %s", err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable, Iterable
|
from collections.abc import Awaitable, Iterable
|
||||||
|
import errno
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
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.decorator import Job
|
||||||
from ..jobs.job_group import JobGroup
|
from ..jobs.job_group import JobGroup
|
||||||
from ..mounts.mount import Mount
|
from ..mounts.mount import Mount
|
||||||
|
from ..resolution.const import UnhealthyReason
|
||||||
from ..utils.common import FileConfiguration
|
from ..utils.common import FileConfiguration
|
||||||
from ..utils.dt import utcnow
|
from ..utils.dt import utcnow
|
||||||
from ..utils.sentinel import DEFAULT
|
from ..utils.sentinel import DEFAULT
|
||||||
|
@ -31,18 +33,6 @@ from .validate import ALL_FOLDERS, SCHEMA_BACKUPS_CONFIG
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_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):
|
class BackupManager(FileConfiguration, JobGroup):
|
||||||
"""Manage backups."""
|
"""Manage backups."""
|
||||||
|
|
||||||
|
@ -119,6 +109,19 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||||
)
|
)
|
||||||
self.sys_jobs.current.stage = stage
|
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(
|
def _create_backup(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
|
@ -169,7 +172,7 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||||
tasks = [
|
tasks = [
|
||||||
self.sys_create_task(_load_backup(tar_file))
|
self.sys_create_task(_load_backup(tar_file))
|
||||||
for path in self.backup_locations
|
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))
|
_LOGGER.info("Found %d backup files", len(tasks))
|
||||||
|
@ -184,6 +187,11 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||||
_LOGGER.info("Removed backup file %s", backup.slug)
|
_LOGGER.info("Removed backup file %s", backup.slug)
|
||||||
|
|
||||||
except OSError as err:
|
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)
|
_LOGGER.error("Can't remove backup %s: %s", backup.slug, err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -208,6 +216,8 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||||
backup.tarfile.rename(tar_origin)
|
backup.tarfile.rename(tar_origin)
|
||||||
|
|
||||||
except OSError as err:
|
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)
|
_LOGGER.error("Can't move backup file to storage: %s", err)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""Home Assistant control object."""
|
"""Home Assistant control object."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import errno
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
|
@ -42,6 +43,7 @@ from ..exceptions import (
|
||||||
from ..hardware.const import PolicyGroup
|
from ..hardware.const import PolicyGroup
|
||||||
from ..hardware.data import Device
|
from ..hardware.data import Device
|
||||||
from ..jobs.decorator import Job, JobExecutionLimit
|
from ..jobs.decorator import Job, JobExecutionLimit
|
||||||
|
from ..resolution.const import UnhealthyReason
|
||||||
from ..utils import remove_folder
|
from ..utils import remove_folder
|
||||||
from ..utils.common import FileConfiguration
|
from ..utils.common import FileConfiguration
|
||||||
from ..utils.json import read_json_file, write_json_file
|
from ..utils.json import read_json_file, write_json_file
|
||||||
|
@ -300,6 +302,8 @@ class HomeAssistant(FileConfiguration, CoreSysAttributes):
|
||||||
try:
|
try:
|
||||||
self.path_pulse.write_text(pulse_config, encoding="utf-8")
|
self.path_pulse.write_text(pulse_config, encoding="utf-8")
|
||||||
except OSError as err:
|
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)
|
_LOGGER.error("Home Assistant can't write pulse/client.config: %s", err)
|
||||||
else:
|
else:
|
||||||
_LOGGER.info("Update pulse/client.config: %s", self.path_pulse)
|
_LOGGER.info("Update pulse/client.config: %s", self.path_pulse)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""AppArmor control for host."""
|
"""AppArmor control for host."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import errno
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
|
@ -9,7 +10,7 @@ from awesomeversion import AwesomeVersion
|
||||||
|
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..exceptions import DBusError, HostAppArmorError
|
from ..exceptions import DBusError, HostAppArmorError
|
||||||
from ..resolution.const import UnsupportedReason
|
from ..resolution.const import UnhealthyReason, UnsupportedReason
|
||||||
from ..utils.apparmor import validate_profile
|
from ..utils.apparmor import validate_profile
|
||||||
from .const import HostFeature
|
from .const import HostFeature
|
||||||
|
|
||||||
|
@ -80,6 +81,8 @@ class AppArmorControl(CoreSysAttributes):
|
||||||
try:
|
try:
|
||||||
await self.sys_run_in_executor(shutil.copyfile, profile_file, dest_profile)
|
await self.sys_run_in_executor(shutil.copyfile, profile_file, dest_profile)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
|
if err.errno == errno.EBADMSG:
|
||||||
|
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||||
raise HostAppArmorError(
|
raise HostAppArmorError(
|
||||||
f"Can't copy {profile_file}: {err}", _LOGGER.error
|
f"Can't copy {profile_file}: {err}", _LOGGER.error
|
||||||
) from err
|
) from err
|
||||||
|
@ -103,6 +106,8 @@ class AppArmorControl(CoreSysAttributes):
|
||||||
try:
|
try:
|
||||||
await self.sys_run_in_executor(profile_file.unlink)
|
await self.sys_run_in_executor(profile_file.unlink)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
|
if err.errno == errno.EBADMSG:
|
||||||
|
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||||
raise HostAppArmorError(
|
raise HostAppArmorError(
|
||||||
f"Can't remove profile: {err}", _LOGGER.error
|
f"Can't remove profile: {err}", _LOGGER.error
|
||||||
) from err
|
) from err
|
||||||
|
@ -117,6 +122,8 @@ class AppArmorControl(CoreSysAttributes):
|
||||||
try:
|
try:
|
||||||
await self.sys_run_in_executor(shutil.copy, profile_file, backup_file)
|
await self.sys_run_in_executor(shutil.copy, profile_file, backup_file)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
|
if err.errno == errno.EBADMSG:
|
||||||
|
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||||
raise HostAppArmorError(
|
raise HostAppArmorError(
|
||||||
f"Can't backup profile {profile_name}: {err}", _LOGGER.error
|
f"Can't backup profile {profile_name}: {err}", _LOGGER.error
|
||||||
) from err
|
) from err
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""OS support on supervisor."""
|
"""OS support on supervisor."""
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
|
import errno
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
@ -13,6 +14,7 @@ from ..dbus.rauc import RaucState
|
||||||
from ..exceptions import DBusError, HassOSJobError, HassOSUpdateError
|
from ..exceptions import DBusError, HassOSJobError, HassOSUpdateError
|
||||||
from ..jobs.const import JobCondition, JobExecutionLimit
|
from ..jobs.const import JobCondition, JobExecutionLimit
|
||||||
from ..jobs.decorator import Job
|
from ..jobs.decorator import Job
|
||||||
|
from ..resolution.const import UnhealthyReason
|
||||||
from .data_disk import DataDisk
|
from .data_disk import DataDisk
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
@ -120,6 +122,8 @@ class OSManager(CoreSysAttributes):
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
|
if err.errno == errno.EBADMSG:
|
||||||
|
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||||
raise HassOSUpdateError(
|
raise HassOSUpdateError(
|
||||||
f"Can't write OTA file: {err!s}", _LOGGER.error
|
f"Can't write OTA file: {err!s}", _LOGGER.error
|
||||||
) from err
|
) from err
|
||||||
|
|
|
@ -4,6 +4,7 @@ Code: https://github.com/home-assistant/plugin-audio
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
import errno
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
import shutil
|
import shutil
|
||||||
|
@ -25,6 +26,7 @@ from ..exceptions import (
|
||||||
)
|
)
|
||||||
from ..jobs.const import JobExecutionLimit
|
from ..jobs.const import JobExecutionLimit
|
||||||
from ..jobs.decorator import Job
|
from ..jobs.decorator import Job
|
||||||
|
from ..resolution.const import UnhealthyReason
|
||||||
from ..utils.json import write_json_file
|
from ..utils.json import write_json_file
|
||||||
from ..utils.sentry import capture_exception
|
from ..utils.sentry import capture_exception
|
||||||
from .base import PluginBase
|
from .base import PluginBase
|
||||||
|
@ -83,6 +85,9 @@ class PluginAudio(PluginBase):
|
||||||
PULSE_CLIENT_TMPL.read_text(encoding="utf-8")
|
PULSE_CLIENT_TMPL.read_text(encoding="utf-8")
|
||||||
)
|
)
|
||||||
except OSError as err:
|
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)
|
_LOGGER.error("Can't read pulse-client.tmpl: %s", err)
|
||||||
|
|
||||||
await super().load()
|
await super().load()
|
||||||
|
@ -93,6 +98,8 @@ class PluginAudio(PluginBase):
|
||||||
try:
|
try:
|
||||||
shutil.copy(ASOUND_TMPL, asound)
|
shutil.copy(ASOUND_TMPL, asound)
|
||||||
except OSError as err:
|
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)
|
_LOGGER.error("Can't create default asound: %s", err)
|
||||||
|
|
||||||
async def install(self) -> None:
|
async def install(self) -> None:
|
||||||
|
|
|
@ -4,6 +4,7 @@ Code: https://github.com/home-assistant/plugin-dns
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
import errno
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -29,7 +30,7 @@ from ..exceptions import (
|
||||||
)
|
)
|
||||||
from ..jobs.const import JobExecutionLimit
|
from ..jobs.const import JobExecutionLimit
|
||||||
from ..jobs.decorator import Job
|
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.json import write_json_file
|
||||||
from ..utils.sentry import capture_exception
|
from ..utils.sentry import capture_exception
|
||||||
from ..validate import dns_url
|
from ..validate import dns_url
|
||||||
|
@ -146,12 +147,16 @@ class PluginDns(PluginBase):
|
||||||
RESOLV_TMPL.read_text(encoding="utf-8")
|
RESOLV_TMPL.read_text(encoding="utf-8")
|
||||||
)
|
)
|
||||||
except OSError as err:
|
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)
|
_LOGGER.error("Can't read resolve.tmpl: %s", err)
|
||||||
try:
|
try:
|
||||||
self.hosts_template = jinja2.Template(
|
self.hosts_template = jinja2.Template(
|
||||||
HOSTS_TMPL.read_text(encoding="utf-8")
|
HOSTS_TMPL.read_text(encoding="utf-8")
|
||||||
)
|
)
|
||||||
except OSError as err:
|
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)
|
_LOGGER.error("Can't read hosts.tmpl: %s", err)
|
||||||
|
|
||||||
await self._init_hosts()
|
await self._init_hosts()
|
||||||
|
@ -364,6 +369,8 @@ class PluginDns(PluginBase):
|
||||||
self.hosts.write_text, data, encoding="utf-8"
|
self.hosts.write_text, data, encoding="utf-8"
|
||||||
)
|
)
|
||||||
except OSError as err:
|
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
|
raise CoreDNSError(f"Can't update hosts: {err}", _LOGGER.error) from err
|
||||||
|
|
||||||
async def add_host(
|
async def add_host(
|
||||||
|
@ -436,6 +443,12 @@ class PluginDns(PluginBase):
|
||||||
|
|
||||||
def _write_resolv(self, resolv_conf: Path) -> None:
|
def _write_resolv(self, resolv_conf: Path) -> None:
|
||||||
"""Update/Write resolv.conf file."""
|
"""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"]
|
nameservers = [str(self.sys_docker.network.dns), "127.0.0.11"]
|
||||||
|
|
||||||
# Read resolv config
|
# Read resolv config
|
||||||
|
@ -445,6 +458,8 @@ class PluginDns(PluginBase):
|
||||||
try:
|
try:
|
||||||
resolv_conf.write_text(data)
|
resolv_conf.write_text(data)
|
||||||
except OSError as err:
|
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)
|
_LOGGER.warning("Can't write/update %s: %s", resolv_conf, err)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -59,9 +59,10 @@ class UnhealthyReason(StrEnum):
|
||||||
"""Reasons for unsupported status."""
|
"""Reasons for unsupported status."""
|
||||||
|
|
||||||
DOCKER = "docker"
|
DOCKER = "docker"
|
||||||
|
OSERROR_BAD_MESSAGE = "oserror_bad_message"
|
||||||
|
PRIVILEGED = "privileged"
|
||||||
SUPERVISOR = "supervisor"
|
SUPERVISOR = "supervisor"
|
||||||
SETUP = "setup"
|
SETUP = "setup"
|
||||||
PRIVILEGED = "privileged"
|
|
||||||
UNTRUSTED = "untrusted"
|
UNTRUSTED = "untrusted"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""Evaluation class for Content Trust."""
|
"""Evaluation class for Content Trust."""
|
||||||
|
import errno
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
@ -6,7 +7,7 @@ from ...const import CoreState
|
||||||
from ...coresys import CoreSys
|
from ...coresys import CoreSys
|
||||||
from ...exceptions import CodeNotaryError, CodeNotaryUntrusted
|
from ...exceptions import CodeNotaryError, CodeNotaryUntrusted
|
||||||
from ...utils.codenotary import calc_checksum_path_sourcecode
|
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
|
from .base import EvaluateBase
|
||||||
|
|
||||||
_SUPERVISOR_SOURCE = Path("/usr/src/supervisor/supervisor")
|
_SUPERVISOR_SOURCE = Path("/usr/src/supervisor/supervisor")
|
||||||
|
@ -48,6 +49,9 @@ class EvaluateSourceMods(EvaluateBase):
|
||||||
calc_checksum_path_sourcecode, _SUPERVISOR_SOURCE
|
calc_checksum_path_sourcecode, _SUPERVISOR_SOURCE
|
||||||
)
|
)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
|
if err.errno == errno.EBADMSG:
|
||||||
|
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||||
|
|
||||||
self.sys_resolution.create_issue(
|
self.sys_resolution.create_issue(
|
||||||
IssueType.CORRUPT_FILESYSTEM, ContextType.SYSTEM
|
IssueType.CORRUPT_FILESYSTEM, ContextType.SYSTEM
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""Init file for Supervisor add-on data."""
|
"""Init file for Supervisor add-on data."""
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import errno
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@ -19,7 +20,7 @@ from ..const import (
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..exceptions import ConfigurationFileError
|
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.common import find_one_filetype, read_json_or_yaml_file
|
||||||
from ..utils.json import read_json_file
|
from ..utils.json import read_json_file
|
||||||
from .const import StoreType
|
from .const import StoreType
|
||||||
|
@ -157,7 +158,9 @@ class StoreData(CoreSysAttributes):
|
||||||
addon_list = await self.sys_run_in_executor(_get_addons_list)
|
addon_list = await self.sys_run_in_executor(_get_addons_list)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
suggestion = None
|
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]
|
suggestion = [SuggestionType.EXECUTE_RESET]
|
||||||
self.sys_resolution.create_issue(
|
self.sys_resolution.create_issue(
|
||||||
IssueType.CORRUPT_REPOSITORY,
|
IssueType.CORRUPT_REPOSITORY,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import errno
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -27,7 +28,7 @@ from .exceptions import (
|
||||||
)
|
)
|
||||||
from .jobs.const import JobCondition, JobExecutionLimit
|
from .jobs.const import JobCondition, JobExecutionLimit
|
||||||
from .jobs.decorator import Job
|
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.codenotary import calc_checksum
|
||||||
from .utils.sentry import capture_exception
|
from .utils.sentry import capture_exception
|
||||||
|
|
||||||
|
@ -155,6 +156,8 @@ class Supervisor(CoreSysAttributes):
|
||||||
try:
|
try:
|
||||||
profile_file.write_text(data, encoding="utf-8")
|
profile_file.write_text(data, encoding="utf-8")
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
|
if err.errno == errno.EBADMSG:
|
||||||
|
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||||
raise SupervisorAppArmorError(
|
raise SupervisorAppArmorError(
|
||||||
f"Can't write temporary profile: {err!s}", _LOGGER.error
|
f"Can't write temporary profile: {err!s}", _LOGGER.error
|
||||||
) from err
|
) from err
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import errno
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock, PropertyMock, patch
|
from unittest.mock import MagicMock, PropertyMock, patch
|
||||||
|
|
||||||
|
@ -696,3 +697,27 @@ async def test_local_example_ingress_port_set(
|
||||||
await install_addon_example.load()
|
await install_addon_example.load()
|
||||||
|
|
||||||
assert install_addon_example.ingress_port != 0
|
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."""
|
"""Test BackupManager class."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import errno
|
||||||
|
from pathlib import Path
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
from unittest.mock import ANY, AsyncMock, MagicMock, Mock, PropertyMock, patch
|
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) == {"hello": "world"}
|
||||||
assert read_json_file(test_db_wal) == {"hello": "world"}
|
assert read_json_file(test_db_wal) == {"hello": "world"}
|
||||||
assert not test_db_shm.exists()
|
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."""
|
"""Test hardware utils."""
|
||||||
# pylint: disable=protected-access
|
import errno
|
||||||
from pathlib import Path
|
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
|
from supervisor.hardware.data import Device
|
||||||
|
|
||||||
|
|
||||||
def test_have_audio(coresys):
|
def test_have_audio(coresys: CoreSys):
|
||||||
"""Test usb device filter."""
|
"""Test usb device filter."""
|
||||||
assert not coresys.hardware.helper.support_audio
|
assert not coresys.hardware.helper.support_audio
|
||||||
|
|
||||||
|
@ -26,7 +29,7 @@ def test_have_audio(coresys):
|
||||||
assert coresys.hardware.helper.support_audio
|
assert coresys.hardware.helper.support_audio
|
||||||
|
|
||||||
|
|
||||||
def test_have_usb(coresys):
|
def test_have_usb(coresys: CoreSys):
|
||||||
"""Test usb device filter."""
|
"""Test usb device filter."""
|
||||||
assert not coresys.hardware.helper.support_usb
|
assert not coresys.hardware.helper.support_usb
|
||||||
|
|
||||||
|
@ -46,7 +49,7 @@ def test_have_usb(coresys):
|
||||||
assert coresys.hardware.helper.support_usb
|
assert coresys.hardware.helper.support_usb
|
||||||
|
|
||||||
|
|
||||||
def test_have_gpio(coresys):
|
def test_have_gpio(coresys: CoreSys):
|
||||||
"""Test usb device filter."""
|
"""Test usb device filter."""
|
||||||
assert not coresys.hardware.helper.support_gpio
|
assert not coresys.hardware.helper.support_gpio
|
||||||
|
|
||||||
|
@ -66,7 +69,7 @@ def test_have_gpio(coresys):
|
||||||
assert coresys.hardware.helper.support_gpio
|
assert coresys.hardware.helper.support_gpio
|
||||||
|
|
||||||
|
|
||||||
def test_hide_virtual_device(coresys):
|
def test_hide_virtual_device(coresys: CoreSys):
|
||||||
"""Test hidding virtual devices."""
|
"""Test hidding virtual devices."""
|
||||||
udev_device = MagicMock()
|
udev_device = MagicMock()
|
||||||
|
|
||||||
|
@ -81,3 +84,15 @@ def test_hide_virtual_device(coresys):
|
||||||
|
|
||||||
udev_device.sys_path = "/sys/devices/virtual/vc/vcs1"
|
udev_device.sys_path = "/sys/devices/virtual/vc/vcs1"
|
||||||
assert coresys.hardware.helper.hide_virtual_device(udev_device)
|
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."""
|
"""Test Homeassistant module."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import errno
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from pytest import LogCaptureFixture
|
||||||
|
|
||||||
from supervisor.const import CoreState
|
from supervisor.const import CoreState
|
||||||
from supervisor.coresys import CoreSys
|
from supervisor.coresys import CoreSys
|
||||||
from supervisor.docker.interface import DockerInterface
|
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__(
|
assert [] == await coresys.homeassistant.get_users.__wrapped__(
|
||||||
coresys.homeassistant
|
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."""
|
"""Test audio plugin."""
|
||||||
|
import errno
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import AsyncMock, Mock, patch
|
from unittest.mock import AsyncMock, Mock, patch
|
||||||
|
|
||||||
|
@ -56,3 +57,26 @@ async def test_config_write(
|
||||||
"debug": True,
|
"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."""
|
"""Test DNS plugin."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import errno
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import AsyncMock, Mock, patch
|
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")
|
Suggestion(SuggestionType.EXECUTE_RESET, ContextType.PLUGIN, "dns")
|
||||||
]
|
]
|
||||||
rebuild.assert_called_once()
|
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."""
|
"""Test evaluation base."""
|
||||||
# pylint: disable=import-error,protected-access
|
# pylint: disable=import-error,protected-access
|
||||||
|
import errno
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from supervisor.const import CoreState
|
from supervisor.const import CoreState
|
||||||
|
@ -50,3 +51,20 @@ async def test_did_run(coresys: CoreSys):
|
||||||
await apparmor()
|
await apparmor()
|
||||||
evaluate.assert_not_called()
|
evaluate.assert_not_called()
|
||||||
evaluate.reset_mock()
|
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."""
|
"""Test evaluation base."""
|
||||||
# pylint: disable=import-error,protected-access
|
# pylint: disable=import-error,protected-access
|
||||||
|
import errno
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
@ -7,6 +8,8 @@ from unittest.mock import AsyncMock, patch
|
||||||
from supervisor.const import CoreState
|
from supervisor.const import CoreState
|
||||||
from supervisor.coresys import CoreSys
|
from supervisor.coresys import CoreSys
|
||||||
from supervisor.exceptions import CodeNotaryError, CodeNotaryUntrusted
|
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
|
from supervisor.resolution.evaluations.source_mods import EvaluateSourceMods
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,3 +59,30 @@ async def test_did_run(coresys: CoreSys):
|
||||||
await sourcemods()
|
await sourcemods()
|
||||||
evaluate.assert_not_called()
|
evaluate.assert_not_called()
|
||||||
evaluate.reset_mock()
|
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."""
|
"""Test that we are reading add-on files correctly."""
|
||||||
|
import errno
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from supervisor.coresys import CoreSys
|
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):
|
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 len(addon_list) == 1
|
||||||
assert str(addon_list[0]) == "addon/config.yml"
|
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."""
|
"""Testing handling with CoreState."""
|
||||||
# pylint: disable=W0212
|
# pylint: disable=W0212
|
||||||
import datetime
|
import datetime
|
||||||
|
import errno
|
||||||
from unittest.mock import AsyncMock, PropertyMock, patch
|
from unittest.mock import AsyncMock, PropertyMock, patch
|
||||||
|
|
||||||
|
from pytest import LogCaptureFixture
|
||||||
|
|
||||||
from supervisor.const import CoreState
|
from supervisor.const import CoreState
|
||||||
from supervisor.coresys import CoreSys
|
from supervisor.coresys import CoreSys
|
||||||
from supervisor.exceptions import WhoamiSSLError
|
from supervisor.exceptions import WhoamiSSLError
|
||||||
|
@ -14,7 +17,6 @@ from supervisor.utils.whoami import WhoamiData
|
||||||
|
|
||||||
def test_write_state(run_dir, coresys: CoreSys):
|
def test_write_state(run_dir, coresys: CoreSys):
|
||||||
"""Test write corestate to /run/supervisor."""
|
"""Test write corestate to /run/supervisor."""
|
||||||
|
|
||||||
coresys.core.state = CoreState.RUNNING
|
coresys.core.state = CoreState.RUNNING
|
||||||
|
|
||||||
assert run_dir.read_text() == 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_retrieve_whoami.assert_called_once()
|
||||||
mock_set_datetime.assert_called_once()
|
mock_set_datetime.assert_called_once()
|
||||||
mock_check_connectivity.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."""
|
"""Test supervisor object."""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import errno
|
||||||
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
|
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
|
||||||
|
|
||||||
from aiohttp import ClientTimeout
|
from aiohttp import ClientTimeout
|
||||||
|
@ -11,7 +12,11 @@ import pytest
|
||||||
from supervisor.const import UpdateChannel
|
from supervisor.const import UpdateChannel
|
||||||
from supervisor.coresys import CoreSys
|
from supervisor.coresys import CoreSys
|
||||||
from supervisor.docker.supervisor import DockerSupervisor
|
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.host.apparmor import AppArmorControl
|
||||||
from supervisor.resolution.const import ContextType, IssueType
|
from supervisor.resolution.const import ContextType, IssueType
|
||||||
from supervisor.resolution.data import Issue
|
from supervisor.resolution.data import Issue
|
||||||
|
@ -108,3 +113,22 @@ async def test_update_apparmor(
|
||||||
timeout=ClientTimeout(total=10),
|
timeout=ClientTimeout(total=10),
|
||||||
)
|
)
|
||||||
load_profile.assert_called_once()
|
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