diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index 5b2557969..532caee6f 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -5,7 +5,6 @@ from collections.abc import Awaitable from contextlib import suppress from copy import deepcopy from datetime import datetime -import errno from functools import partial from ipaddress import IPv4Address import logging @@ -89,7 +88,7 @@ from ..hardware.data import Device from ..homeassistant.const import WSEvent from ..jobs.const import JobConcurrency, JobThrottle from ..jobs.decorator import Job -from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason +from ..resolution.const import ContextType, IssueType, SuggestionType from ..resolution.data import Issue from ..store.addon import AddonStore from ..utils import check_port @@ -1027,10 +1026,7 @@ class Addon(AddonModel): try: await self.sys_run_in_executor(write_pulse_config) except OSError as err: - if err.errno == errno.EBADMSG: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + self.sys_resolution.check_oserror(err) _LOGGER.error( "Add-on %s can't write pulse/client.config: %s", self.slug, err ) diff --git a/supervisor/api/backups.py b/supervisor/api/backups.py index 0548f9f45..26c678ca0 100644 --- a/supervisor/api/backups.py +++ b/supervisor/api/backups.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio -import errno from io import BufferedWriter import logging from pathlib import Path @@ -50,7 +49,6 @@ from ..const import ( from ..coresys import CoreSysAttributes from ..exceptions import APIError, APIForbidden, APINotFound from ..mounts.const import MountUsage -from ..resolution.const import UnhealthyReason from .const import ( ATTR_ADDITIONAL_LOCATIONS, ATTR_BACKGROUND, @@ -518,13 +516,8 @@ class APIBackups(CoreSysAttributes): ) ) except OSError as err: - if err.errno == errno.EBADMSG and location in { - LOCATION_CLOUD_BACKUP, - None, - }: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + if location in {LOCATION_CLOUD_BACKUP, None}: + self.sys_resolution.check_oserror(err) _LOGGER.error("Can't write new backup file: %s", err) return False diff --git a/supervisor/backups/manager.py b/supervisor/backups/manager.py index 5919ef8cd..caeac9ef3 100644 --- a/supervisor/backups/manager.py +++ b/supervisor/backups/manager.py @@ -210,13 +210,11 @@ class BackupManager(FileConfiguration, JobGroup): try: return await self.sys_run_in_executor(find_backups) except OSError as err: - if err.errno == errno.EBADMSG and path in { + if path in { self.sys_config.path_backup, self.sys_config.path_core_backup, }: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + self.sys_resolution.check_oserror(err) _LOGGER.error("Could not list backups from %s: %s", path.as_posix(), err) return [] @@ -365,13 +363,8 @@ class BackupManager(FileConfiguration, JobGroup): ) from err except OSError as err: msg = f"Could delete backup at {backup_tarfile.as_posix()}: {err!s}" - if err.errno == errno.EBADMSG and location in { - None, - LOCATION_CLOUD_BACKUP, - }: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + if location in {None, LOCATION_CLOUD_BACKUP}: + self.sys_resolution.check_oserror(err) raise BackupError(msg, _LOGGER.error) from err # If backup has been removed from all locations, remove it from cache @@ -403,12 +396,10 @@ class BackupManager(FileConfiguration, JobGroup): return (location_name, Path(path)) except OSError as err: msg = f"Could not copy backup to {location_name} due to: {err!s}" - - if err.errno == errno.EBADMSG and location in { - LOCATION_CLOUD_BACKUP, - None, - }: - raise BackupDataDiskBadMessageError(msg, _LOGGER.error) from err + if location in {LOCATION_CLOUD_BACKUP, None}: + self.sys_resolution.check_oserror(err) + if err.errno == errno.EBADMSG: + raise BackupDataDiskBadMessageError(msg, _LOGGER.error) from err raise BackupError(msg, _LOGGER.error) from err @Job(name="backup_copy_to_additional_locations", cleanup=False) @@ -468,10 +459,8 @@ class BackupManager(FileConfiguration, JobGroup): try: await self.sys_run_in_executor(backup.tarfile.rename, tar_file) except OSError as err: - if err.errno == errno.EBADMSG and location in {LOCATION_CLOUD_BACKUP, None}: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + if location in {LOCATION_CLOUD_BACKUP, None}: + self.sys_resolution.check_oserror(err) _LOGGER.error("Can't move backup file to storage: %s", err) return None diff --git a/supervisor/docker/manager.py b/supervisor/docker/manager.py index 0cd51f8e6..347914261 100644 --- a/supervisor/docker/manager.py +++ b/supervisor/docker/manager.py @@ -6,7 +6,6 @@ import asyncio from collections.abc import Mapping from contextlib import suppress from dataclasses import dataclass -import errno from http import HTTPStatus from io import BufferedReader, BufferedWriter from ipaddress import IPv4Address @@ -47,7 +46,6 @@ from ..exceptions import ( DockerNotFound, DockerRequestError, ) -from ..resolution.const import UnhealthyReason from ..utils.common import FileConfiguration from ..validate import SCHEMA_DOCKER_CONFIG from .const import ( @@ -1015,10 +1013,7 @@ class DockerAPI(CoreSysAttributes): f"Can't import image from tar: {err}", _LOGGER.error ) from err except OSError as err: - if err.errno == errno.EBADMSG: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + self.sys_resolution.check_oserror(err) raise DockerError( f"Can't read tar file {tar_file}: {err}", _LOGGER.error ) from err @@ -1071,10 +1066,7 @@ class DockerAPI(CoreSysAttributes): f"Can't fetch image {image}:{version}: {err}", _LOGGER.error ) from err except OSError as err: - if err.errno == errno.EBADMSG: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + self.sys_resolution.check_oserror(err) raise DockerError( f"Can't write tar file {tar_file}: {err}", _LOGGER.error ) from err diff --git a/supervisor/hardware/disk.py b/supervisor/hardware/disk.py index dd046471e..f26bbbff0 100644 --- a/supervisor/hardware/disk.py +++ b/supervisor/hardware/disk.py @@ -13,7 +13,6 @@ from ..exceptions import ( DBusObjectError, HardwareNotFound, ) -from ..resolution.const import UnhealthyReason from .const import UdevSubsystem from .data import Device @@ -114,10 +113,8 @@ class HwDisk(CoreSysAttributes): _LOGGER.warning("File not found: %s", child.as_posix()) continue except OSError as err: + self.sys_resolution.check_oserror(err) if err.errno == errno.EBADMSG: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) break continue diff --git a/supervisor/homeassistant/module.py b/supervisor/homeassistant/module.py index 0e9d77e0c..7ff52c406 100644 --- a/supervisor/homeassistant/module.py +++ b/supervisor/homeassistant/module.py @@ -1,7 +1,6 @@ """Home Assistant control object.""" import asyncio -import errno from ipaddress import IPv4Address import logging from pathlib import Path, PurePath @@ -47,7 +46,6 @@ from ..exceptions import ( from ..hardware.const import PolicyGroup from ..hardware.data import Device from ..jobs.decorator import Job -from ..resolution.const import UnhealthyReason from ..utils import remove_folder, remove_folder_with_excludes from ..utils.common import FileConfiguration from ..utils.json import read_json_file, write_json_file @@ -340,10 +338,7 @@ class HomeAssistant(FileConfiguration, CoreSysAttributes): try: await self.sys_run_in_executor(write_pulse_config) except OSError as err: - if err.errno == errno.EBADMSG: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + self.sys_resolution.check_oserror(err) _LOGGER.error("Home Assistant can't write pulse/client.config: %s", err) else: _LOGGER.info("Update pulse/client.config: %s", self.path_pulse) diff --git a/supervisor/host/apparmor.py b/supervisor/host/apparmor.py index f7bed7e5b..cc458b869 100644 --- a/supervisor/host/apparmor.py +++ b/supervisor/host/apparmor.py @@ -3,7 +3,6 @@ from __future__ import annotations from contextlib import suppress -import errno import logging from pathlib import Path import shutil @@ -12,7 +11,7 @@ from awesomeversion import AwesomeVersion from ..coresys import CoreSys, CoreSysAttributes from ..exceptions import DBusError, HostAppArmorError -from ..resolution.const import UnhealthyReason, UnsupportedReason +from ..resolution.const import UnsupportedReason from ..utils.apparmor import validate_profile from .const import HostFeature @@ -89,10 +88,7 @@ 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.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + self.sys_resolution.check_oserror(err) raise HostAppArmorError( f"Can't copy {profile_file}: {err}", _LOGGER.error ) from err @@ -116,10 +112,7 @@ 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.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + self.sys_resolution.check_oserror(err) raise HostAppArmorError( f"Can't remove profile: {err}", _LOGGER.error ) from err @@ -134,10 +127,7 @@ class AppArmorControl(CoreSysAttributes): try: shutil.copy(profile_file, backup_file) except OSError as err: - if err.errno == errno.EBADMSG: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + self.sys_resolution.check_oserror(err) raise HostAppArmorError( f"Can't backup profile {profile_name}: {err}", _LOGGER.error ) from err diff --git a/supervisor/os/manager.py b/supervisor/os/manager.py index 3746df11c..b6b6099ca 100644 --- a/supervisor/os/manager.py +++ b/supervisor/os/manager.py @@ -2,7 +2,6 @@ from dataclasses import dataclass from datetime import datetime -import errno import logging from pathlib import Path, PurePath from typing import cast @@ -23,7 +22,6 @@ from ..exceptions import ( ) from ..jobs.const import JobConcurrency, JobCondition from ..jobs.decorator import Job -from ..resolution.const import UnhealthyReason from ..utils.sentry import async_capture_exception from .data_disk import DataDisk @@ -214,10 +212,7 @@ class OSManager(CoreSysAttributes): ) from err except OSError as err: - if err.errno == errno.EBADMSG: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + self.sys_resolution.check_oserror(err) raise HassOSUpdateError( f"Can't write OTA file: {err!s}", _LOGGER.error ) from err diff --git a/supervisor/plugins/audio.py b/supervisor/plugins/audio.py index 32fda4157..3327af30d 100644 --- a/supervisor/plugins/audio.py +++ b/supervisor/plugins/audio.py @@ -3,7 +3,6 @@ Code: https://github.com/home-assistant/plugin-audio """ -import errno import logging from pathlib import Path, PurePath import shutil @@ -26,7 +25,6 @@ from ..exceptions import ( ) from ..jobs.const import JobThrottle from ..jobs.decorator import Job -from ..resolution.const import UnhealthyReason from ..utils.json import write_json_file from ..utils.sentry import async_capture_exception from .base import PluginBase @@ -94,11 +92,7 @@ class PluginAudio(PluginBase): ) ) except OSError as err: - if err.errno == errno.EBADMSG: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) - + self.sys_resolution.check_oserror(err) _LOGGER.error("Can't read pulse-client.tmpl: %s", err) await super().load() @@ -113,10 +107,7 @@ class PluginAudio(PluginBase): try: await self.sys_run_in_executor(setup_default_asound) except OSError as err: - if err.errno == errno.EBADMSG: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + self.sys_resolution.check_oserror(err) _LOGGER.error("Can't create default asound: %s", err) @Job( diff --git a/supervisor/plugins/dns.py b/supervisor/plugins/dns.py index 64d67b963..c4d72c8f8 100644 --- a/supervisor/plugins/dns.py +++ b/supervisor/plugins/dns.py @@ -5,7 +5,6 @@ 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 @@ -33,7 +32,7 @@ from ..exceptions import ( ) from ..jobs.const import JobThrottle from ..jobs.decorator import Job -from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason +from ..resolution.const import ContextType, IssueType, SuggestionType from ..utils.json import write_json_file from ..utils.sentry import async_capture_exception from ..validate import dns_url @@ -232,10 +231,7 @@ class PluginDns(PluginBase): await self.sys_run_in_executor(RESOLV_TMPL.read_text, encoding="utf-8") ) except OSError as err: - if err.errno == errno.EBADMSG: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + self.sys_resolution.check_oserror(err) _LOGGER.error("Can't read resolve.tmpl: %s", err) try: @@ -243,10 +239,7 @@ class PluginDns(PluginBase): await self.sys_run_in_executor(HOSTS_TMPL.read_text, encoding="utf-8") ) except OSError as err: - if err.errno == errno.EBADMSG: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + self.sys_resolution.check_oserror(err) _LOGGER.error("Can't read hosts.tmpl: %s", err) await self._init_hosts() @@ -448,10 +441,7 @@ class PluginDns(PluginBase): self.hosts.write_text, data, encoding="utf-8" ) except OSError as err: - if err.errno == errno.EBADMSG: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + self.sys_resolution.check_oserror(err) raise CoreDNSError(f"Can't update hosts: {err}", _LOGGER.error) from err async def add_host( @@ -533,10 +523,7 @@ class PluginDns(PluginBase): try: await self.sys_run_in_executor(resolv_conf.write_text, data) except OSError as err: - if err.errno == errno.EBADMSG: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + self.sys_resolution.check_oserror(err) _LOGGER.warning("Can't write/update %s: %s", resolv_conf, err) return diff --git a/supervisor/resolution/module.py b/supervisor/resolution/module.py index ff62ab3d0..1659713d4 100644 --- a/supervisor/resolution/module.py +++ b/supervisor/resolution/module.py @@ -1,5 +1,6 @@ """Supervisor resolution center.""" +import errno import logging from typing import Any @@ -153,6 +154,19 @@ class ResolutionManager(FileConfiguration, CoreSysAttributes): attr.asdict(HealthChanged(False, self.unhealthy)), ) + _OSERROR_UNHEALTHY_REASONS: dict[int, UnhealthyReason] = { + errno.EBADMSG: UnhealthyReason.OSERROR_BAD_MESSAGE, + } + + def check_oserror(self, err: OSError) -> None: + """Check OSError for known filesystem issues and mark system unhealthy. + + Must only be used on OSErrors that are caused by file operation on a + local path. + """ + if err.errno in self._OSERROR_UNHEALTHY_REASONS: + self.add_unhealthy_reason(self._OSERROR_UNHEALTHY_REASONS[err.errno]) + def _make_issue_message(self, issue: Issue) -> dict[str, Any]: """Make issue into message for core.""" return attr.asdict(issue) | { diff --git a/supervisor/store/data.py b/supervisor/store/data.py index 5450c2701..3572a0710 100644 --- a/supervisor/store/data.py +++ b/supervisor/store/data.py @@ -22,7 +22,7 @@ from ..const import ( ) from ..coresys import CoreSys, CoreSysAttributes from ..exceptions import ConfigurationFileError -from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason +from ..resolution.const import ContextType, IssueType, SuggestionType from ..utils.common import find_one_filetype, read_json_or_yaml_file from ..utils.json import read_json_file from .utils import extract_hash_from_path @@ -164,11 +164,8 @@ class StoreData(CoreSysAttributes): addon_list = await self.sys_run_in_executor(_get_addons_list) except OSError as err: suggestion = None - if err.errno == errno.EBADMSG: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) - elif repository != REPOSITORY_LOCAL: + self.sys_resolution.check_oserror(err) + if err.errno != errno.EBADMSG and repository != REPOSITORY_LOCAL: suggestion = [SuggestionType.EXECUTE_RESET] self.sys_resolution.create_issue( IssueType.CORRUPT_REPOSITORY, diff --git a/supervisor/store/git.py b/supervisor/store/git.py index 35b43cd1a..ad29fe9d5 100644 --- a/supervisor/store/git.py +++ b/supervisor/store/git.py @@ -1,7 +1,6 @@ """Init file for Supervisor add-on Git.""" import asyncio -import errno import functools as ft import logging from pathlib import Path @@ -13,7 +12,7 @@ from ..const import ATTR_BRANCH, ATTR_URL from ..coresys import CoreSys, CoreSysAttributes from ..exceptions import StoreGitCloneError, StoreGitError, StoreJobError from ..jobs.decorator import Job, JobCondition -from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason +from ..resolution.const import ContextType, IssueType, SuggestionType from ..utils import directory_missing_or_empty, remove_folder from .validate import RE_REPOSITORY @@ -112,10 +111,7 @@ class GitRepo(CoreSysAttributes): try: await self.sys_run_in_executor(move_clone) except OSError as err: - if err.errno == errno.EBADMSG: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + self.sys_resolution.check_oserror(err) raise StoreGitCloneError( f"Can't move clone due to: {err!s}", _LOGGER.error ) from err diff --git a/supervisor/supervisor.py b/supervisor/supervisor.py index b741884fe..5dda38cfc 100644 --- a/supervisor/supervisor.py +++ b/supervisor/supervisor.py @@ -3,7 +3,6 @@ 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 @@ -33,7 +32,7 @@ from .exceptions import ( from .jobs import ChildJobSyncFilter from .jobs.const import JobCondition, JobThrottle from .jobs.decorator import Job -from .resolution.const import ContextType, IssueType, UnhealthyReason +from .resolution.const import ContextType, IssueType from .utils.sentry import async_capture_exception _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -162,10 +161,7 @@ class Supervisor(CoreSysAttributes): await self.sys_host.apparmor.load_profile("hassio-supervisor", profile_file) except OSError as err: - if err.errno == errno.EBADMSG: - self.sys_resolution.add_unhealthy_reason( - UnhealthyReason.OSERROR_BAD_MESSAGE - ) + self.sys_resolution.check_oserror(err) raise SupervisorAppArmorError( f"Can't write temporary profile: {err!s}", _LOGGER.error ) from err