1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2026-04-02 00:07:16 +01:00

Centralize OSError bad message handling in ResolutionManager (#6641)

Add check_oserror() method to ResolutionManager with an extensible
errno-to-unhealthy-reason mapping. Replace ~20 inline
`if err.errno == errno.EBADMSG` checks across 14 files with a single
call to self.sys_resolution.check_oserror(err). This makes it easy to
add handling for additional filesystem errors (e.g. EIO, ENOSPC) in
one place.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Agner
2026-03-25 16:47:36 +01:00
committed by GitHub
parent 2b2aca873b
commit c0cca1ff8b
14 changed files with 51 additions and 123 deletions

View File

@@ -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
)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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) | {

View File

@@ -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,

View File

@@ -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

View File

@@ -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