Remove customized unknown error types

This commit is contained in:
Mike Degatano
2025-11-18 22:02:56 +00:00
parent c399a3ef27
commit 772d074db9
8 changed files with 180 additions and 344 deletions

View File

@@ -66,30 +66,16 @@ from ..docker.const import ContainerState
from ..docker.monitor import DockerContainerStateEvent from ..docker.monitor import DockerContainerStateEvent
from ..docker.stats import DockerStats from ..docker.stats import DockerStats
from ..exceptions import ( from ..exceptions import (
AddonBackupAppArmorProfileUnknownError,
AddonBackupExportImageUnknownError,
AddonBackupMetadataInvalidError, AddonBackupMetadataInvalidError,
AddonBuildImageUnknownError,
AddonConfigurationFileUnknownError,
AddonConfigurationInvalidError, AddonConfigurationInvalidError,
AddonContainerRunCommandUnknownError,
AddonContainerStartUnknownError,
AddonContainerStatsUnknownError,
AddonContainerStopUnknownError,
AddonContainerWriteStdinUnknownError,
AddonCreateBackupFileUnknownError,
AddonCreateBackupMetadataFileUnknownError,
AddonExtractBackupFileUnknownError,
AddonInstallImageUnknownError,
AddonNotRunningError, AddonNotRunningError,
AddonNotSupportedError, AddonNotSupportedError,
AddonNotSupportedWriteStdinError, AddonNotSupportedWriteStdinError,
AddonPrePostBackupCommandReturnedError, AddonPrePostBackupCommandReturnedError,
AddonRemoveImageUnknownError,
AddonRestoreAppArmorProfileUnknownError,
AddonRestoreBackupDataUnknownError,
AddonsError, AddonsError,
AddonsJobError, AddonsJobError,
AddonUnknownError,
BackupRestoreUnknownError,
ConfigurationFileError, ConfigurationFileError,
DockerBuildError, DockerBuildError,
DockerError, DockerError,
@@ -747,7 +733,7 @@ class Addon(AddonModel):
) from None ) from None
except ConfigurationFileError as err: except ConfigurationFileError as err:
_LOGGER.error("Add-on %s can't write options", self.slug) _LOGGER.error("Add-on %s can't write options", self.slug)
raise AddonConfigurationFileUnknownError(addon=self.slug) from err raise AddonUnknownError(addon=self.slug) from err
_LOGGER.debug("Add-on %s write options: %s", self.slug, options) _LOGGER.debug("Add-on %s write options: %s", self.slug, options)
@@ -817,11 +803,13 @@ class Addon(AddonModel):
await self.sys_addons.data.uninstall(self) await self.sys_addons.data.uninstall(self)
raise raise
except DockerBuildError as err: except DockerBuildError as err:
_LOGGER.error("Could not build image for addon %s: %s", self.slug, err)
await self.sys_addons.data.uninstall(self) await self.sys_addons.data.uninstall(self)
raise AddonBuildImageUnknownError(addon=self.slug) from err raise AddonUnknownError(addon=self.slug) from err
except DockerError as err: except DockerError as err:
_LOGGER.error("Could not pull image to update addon %s: %s", self.slug, err)
await self.sys_addons.data.uninstall(self) await self.sys_addons.data.uninstall(self)
raise AddonInstallImageUnknownError(addon=self.slug) from err raise AddonUnknownError(addon=self.slug) from err
# Finish initialization and set up listeners # Finish initialization and set up listeners
await self.load() await self.load()
@@ -845,7 +833,8 @@ class Addon(AddonModel):
try: try:
await self.instance.remove(remove_image=remove_image) await self.instance.remove(remove_image=remove_image)
except DockerError as err: except DockerError as err:
raise AddonRemoveImageUnknownError(addon=self.slug) from err _LOGGER.error("Could not remove image for addon %s: %s", self.slug, err)
raise AddonUnknownError(addon=self.slug) from err
self.state = AddonState.UNKNOWN self.state = AddonState.UNKNOWN
@@ -919,9 +908,11 @@ class Addon(AddonModel):
try: try:
await self.instance.update(store.version, store.image, arch=self.arch) await self.instance.update(store.version, store.image, arch=self.arch)
except DockerBuildError as err: except DockerBuildError as err:
raise AddonBuildImageUnknownError(addon=self.slug) from err _LOGGER.error("Could not build image for addon %s: %s", self.slug, err)
raise AddonUnknownError(addon=self.slug) from err
except DockerError as err: except DockerError as err:
raise AddonInstallImageUnknownError(addon=self.slug) from err _LOGGER.error("Could not pull image to update addon %s: %s", self.slug, err)
raise AddonUnknownError(addon=self.slug) from err
# Stop the addon if running # Stop the addon if running
if (last_state := self.state) in {AddonState.STARTED, AddonState.STARTUP}: if (last_state := self.state) in {AddonState.STARTED, AddonState.STARTUP}:
@@ -967,14 +958,19 @@ class Addon(AddonModel):
try: try:
await self.instance.remove() await self.instance.remove()
except DockerError as err: except DockerError as err:
raise AddonRemoveImageUnknownError(addon=self.slug) from err _LOGGER.error("Could not remove image for addon %s: %s", self.slug, err)
raise AddonUnknownError(addon=self.slug) from err
try: try:
await self.instance.install(self.version) await self.instance.install(self.version)
except DockerBuildError as err: except DockerBuildError as err:
raise AddonBuildImageUnknownError(addon=self.slug) from err _LOGGER.error("Could not build image for addon %s: %s", self.slug, err)
raise AddonUnknownError(addon=self.slug) from err
except DockerError as err: except DockerError as err:
raise AddonInstallImageUnknownError(addon=self.slug) from err _LOGGER.error(
"Could not pull image to update addon %s: %s", self.slug, err
)
raise AddonUnknownError(addon=self.slug) from err
if self.addon_store: if self.addon_store:
await self.sys_addons.data.update(self.addon_store) await self.sys_addons.data.update(self.addon_store)
@@ -1145,8 +1141,9 @@ class Addon(AddonModel):
try: try:
await self.instance.run() await self.instance.run()
except DockerError as err: except DockerError as err:
_LOGGER.error("Could not start container for addon %s: %s", self.slug, err)
self.state = AddonState.ERROR self.state = AddonState.ERROR
raise AddonContainerStartUnknownError(addon=self.slug) from err raise AddonUnknownError(addon=self.slug) from err
return self.sys_create_task(self._wait_for_startup()) return self.sys_create_task(self._wait_for_startup())
@@ -1161,8 +1158,9 @@ class Addon(AddonModel):
try: try:
await self.instance.stop() await self.instance.stop()
except DockerError as err: except DockerError as err:
_LOGGER.error("Could not stop container for addon %s: %s", self.slug, err)
self.state = AddonState.ERROR self.state = AddonState.ERROR
raise AddonContainerStopUnknownError(addon=self.slug) from err raise AddonUnknownError(addon=self.slug) from err
@Job( @Job(
name="addon_restart", name="addon_restart",
@@ -1200,7 +1198,10 @@ class Addon(AddonModel):
return await self.instance.stats() return await self.instance.stats()
except DockerError as err: except DockerError as err:
raise AddonContainerStatsUnknownError(addon=self.slug) from err _LOGGER.error(
"Could not get stats of container for addon %s: %s", self.slug, err
)
raise AddonUnknownError(addon=self.slug) from err
@Job( @Job(
name="addon_write_stdin", name="addon_write_stdin",
@@ -1218,7 +1219,10 @@ class Addon(AddonModel):
await self.instance.write_stdin(data) await self.instance.write_stdin(data)
except DockerError as err: except DockerError as err:
raise AddonContainerWriteStdinUnknownError(addon=self.slug) from err _LOGGER.error(
"Could not write stdin to container for addon %s: %s", self.slug, err
)
raise AddonUnknownError(addon=self.slug) from err
async def _backup_command(self, command: str) -> None: async def _backup_command(self, command: str) -> None:
try: try:
@@ -1234,7 +1238,7 @@ class Addon(AddonModel):
_LOGGER.error( _LOGGER.error(
"Failed running pre-/post backup command %s: %s", command, err "Failed running pre-/post backup command %s: %s", command, err
) )
raise AddonContainerRunCommandUnknownError(addon=self.slug) from err raise AddonUnknownError(addon=self.slug) from err
@Job( @Job(
name="addon_begin_backup", name="addon_begin_backup",
@@ -1323,18 +1327,14 @@ class Addon(AddonModel):
try: try:
self.instance.export_image(temp_path.joinpath("image.tar")) self.instance.export_image(temp_path.joinpath("image.tar"))
except DockerError as err: except DockerError as err:
raise AddonBackupExportImageUnknownError( raise BackupRestoreUnknownError() from err
addon=self.slug
) from err
# Store local configs/state # Store local configs/state
try: try:
write_json_file(temp_path.joinpath("addon.json"), metadata) write_json_file(temp_path.joinpath("addon.json"), metadata)
except ConfigurationFileError as err: except ConfigurationFileError as err:
_LOGGER.error("Can't save meta for %s: %s", self.slug, err) _LOGGER.error("Can't save meta for %s: %s", self.slug, err)
raise AddonCreateBackupMetadataFileUnknownError( raise BackupRestoreUnknownError() from err
addon=self.slug
) from err
# Store AppArmor Profile # Store AppArmor Profile
if apparmor_profile: if apparmor_profile:
@@ -1344,9 +1344,7 @@ class Addon(AddonModel):
apparmor_profile, profile_backup_file apparmor_profile, profile_backup_file
) )
except HostAppArmorError as err: except HostAppArmorError as err:
raise AddonBackupAppArmorProfileUnknownError( raise BackupRestoreUnknownError() from err
addon=self.slug
) from err
# Write tarfile # Write tarfile
with tar_file as backup: with tar_file as backup:
@@ -1401,7 +1399,7 @@ class Addon(AddonModel):
_LOGGER.info("Finish backup for addon %s", self.slug) _LOGGER.info("Finish backup for addon %s", self.slug)
except (tarfile.TarError, OSError, AddFileError) as err: except (tarfile.TarError, OSError, AddFileError) as err:
_LOGGER.error("Can't write backup tarfile for addon %s: %s", self.slug, err) _LOGGER.error("Can't write backup tarfile for addon %s: %s", self.slug, err)
raise AddonCreateBackupFileUnknownError(addon=self.slug) from err raise BackupRestoreUnknownError() from err
finally: finally:
if was_running: if was_running:
wait_for_start = await self.end_backup() wait_for_start = await self.end_backup()
@@ -1444,9 +1442,9 @@ class Addon(AddonModel):
tmp, data = await self.sys_run_in_executor(_extract_tarfile) tmp, data = await self.sys_run_in_executor(_extract_tarfile)
except tarfile.TarError as err: except tarfile.TarError as err:
_LOGGER.error("Can't extract backup tarfile for %s: %s", self.slug, err) _LOGGER.error("Can't extract backup tarfile for %s: %s", self.slug, err)
raise AddonExtractBackupFileUnknownError(addon=self.slug) from err raise BackupRestoreUnknownError() from err
except ConfigurationFileError as err: except ConfigurationFileError as err:
raise AddonConfigurationFileUnknownError(addon=self.slug) from err raise AddonUnknownError(addon=self.slug) from err
try: try:
# Validate # Validate
@@ -1522,7 +1520,7 @@ class Addon(AddonModel):
_LOGGER.error( _LOGGER.error(
"Can't restore origin data for %s: %s", self.slug, err "Can't restore origin data for %s: %s", self.slug, err
) )
raise AddonRestoreBackupDataUnknownError(addon=self.slug) from err raise BackupRestoreUnknownError() from err
# Restore AppArmor # Restore AppArmor
profile_file = Path(tmp.name, "apparmor.txt") profile_file = Path(tmp.name, "apparmor.txt")
@@ -1537,9 +1535,7 @@ class Addon(AddonModel):
self.slug, self.slug,
err, err,
) )
raise AddonRestoreAppArmorProfileUnknownError( raise BackupRestoreUnknownError() from err
addon=self.slug
) from err
finally: finally:
# Is add-on loaded # Is add-on loaded

View File

@@ -628,9 +628,6 @@ class Backup(JobGroup):
if start_task := await self._addon_save(addon): if start_task := await self._addon_save(addon):
start_tasks.append(start_task) start_tasks.append(start_task)
except BackupError as err: except BackupError as err:
err = BackupError(
f"Can't backup add-on {addon.slug}: {str(err)}", _LOGGER.error
)
self.sys_jobs.current.capture_error(err) self.sys_jobs.current.capture_error(err)
return start_tasks return start_tasks

View File

@@ -358,26 +358,6 @@ class AddonConfigurationInvalidError(AddonConfigurationError, APIError):
super().__init__(None, logger) super().__init__(None, logger)
class AddonBackupMetadataInvalidError(AddonsError, APIError):
"""Raise if invalid metadata file provided for addon in backup."""
error_key = "addon_backup_metadata_invalid_error"
message_template = (
"Metadata file for add-on {addon} in backup is invalid: {validation_error}"
)
def __init__(
self,
logger: Callable[..., None] | None = None,
*,
addon: str,
validation_error: str,
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon, "validation_error": validation_error}
super().__init__(None, logger)
class AddonBootConfigCannotChangeError(AddonsError, APIError): class AddonBootConfigCannotChangeError(AddonsError, APIError):
"""Raise if user attempts to change addon boot config when it can't be changed.""" """Raise if user attempts to change addon boot config when it can't be changed."""
@@ -408,28 +388,6 @@ class AddonNotRunningError(AddonsError, APIError):
super().__init__(None, logger) super().__init__(None, logger)
class AddonPrePostBackupCommandReturnedError(AddonsError, APIError):
"""Raise when addon's pre/post backup command returns an error."""
error_key = "addon_pre_post_backup_command_returned_error"
message_template = (
"Pre-/Post backup command for add-on {addon} returned error code: "
"{exit_code}. Please report this to the addon developer. Enable debug "
"logging to capture complete command output using {debug_logging_command}"
)
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str, exit_code: int
) -> None:
"""Initialize exception."""
self.extra_fields = {
"addon": addon,
"exit_code": exit_code,
"debug_logging_command": "ha supervisor options --logging debug",
}
super().__init__(None, logger)
class AddonNotSupportedError(HassioNotSupportedError): class AddonNotSupportedError(HassioNotSupportedError):
"""Addon doesn't support a function.""" """Addon doesn't support a function."""
@@ -546,238 +504,11 @@ class AddonBuildArchitectureNotSupportedError(AddonNotSupportedError, APIError):
super().__init__(None, logger) super().__init__(None, logger)
# pylint: disable-next=too-many-ancestors class AddonUnknownError(AddonsError, APIUnknownSupervisorError):
class AddonConfigurationFileUnknownError( """Raise when unknown error occurs taking an action for an addon."""
AddonConfigurationError, APIUnknownSupervisorError
):
"""Raise when unknown error occurs trying to read/write addon configuration file."""
error_key = "addon_configuration_file_unknown_error" error_key = "addon_unknown_error"
message_template = ( message_template = "An unknown error occurred with addon {addon}"
"An unknown error occurred reading/writing configuration file for {addon}"
)
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(logger)
class AddonBuildImageUnknownError(AddonsError, APIUnknownSupervisorError):
"""Raise when an unknown error occurs during image build."""
error_key = "addon_build_image_unknown_error"
message_template = "An unknown error occurred during build of image for {addon}"
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(logger)
class AddonInstallImageUnknownError(AddonsError, APIUnknownSupervisorError):
"""Raise when an unknown error occurs during image install."""
error_key = "addon_install_image_unknown_error"
message_template = "An unknown error occurred during install of image for {addon}"
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(logger)
class AddonRemoveImageUnknownError(AddonsError, APIUnknownSupervisorError):
"""Raise when an unknown error occurs while removing an image."""
error_key = "addon_remove_image_unknown_error"
message_template = "An unknown error occurred while removing image for {addon}"
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(logger)
class AddonContainerStartUnknownError(AddonsError, APIUnknownSupervisorError):
"""Raise when an unknown error occurs while starting a container."""
error_key = "addon_container_start_unknown_error"
message_template = "An unknown error occurred while starting container for {addon}"
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(logger)
class AddonContainerStopUnknownError(AddonsError, APIUnknownSupervisorError):
"""Raise when an unknown error occurs while stopping a container."""
error_key = "addon_container_stop_unknown_error"
message_template = "An unknown error occurred while stopping container for {addon}"
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(logger)
class AddonContainerStatsUnknownError(AddonsError, APIUnknownSupervisorError):
"""Raise when an unknown error occurs while getting stats of a container."""
error_key = "addon_container_stats_unknown_error"
message_template = (
"An unknown error occurred while getting stats of container for {addon}"
)
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(logger)
class AddonContainerWriteStdinUnknownError(AddonsError, APIUnknownSupervisorError):
"""Raise when an unknown error occurs while writing to stdin of a container."""
error_key = "addon_container_write_stdin_unknown_error"
message_template = (
"An unknown error occurred while writing to stdin of container for {addon}"
)
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(logger)
class AddonContainerRunCommandUnknownError(AddonsError, APIUnknownSupervisorError):
"""Raise when an unknown error occurs while running command inside of a container."""
error_key = "addon_container_run_command_unknown_error"
message_template = "An unknown error occurred while running a command inside of container for {addon}"
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(logger)
class AddonCreateBackupFileUnknownError(AddonsError, APIUnknownSupervisorError):
"""Raise when an unknown error occurs while making the backup file for an addon."""
error_key = "addon_create_backup_file_unknown_error"
message_template = (
"An unknown error occurred while creating the backup file for {addon}"
)
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(logger)
class AddonCreateBackupMetadataFileUnknownError(AddonsError, APIUnknownSupervisorError):
"""Raise when an unknown error occurs while making the metadata file for an addon backup."""
error_key = "addon_create_backup_metadata_file_unknown_error"
message_template = "An unknown error occurred while creating the metadata file for backup of {addon}"
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(logger)
class AddonBackupAppArmorProfileUnknownError(AddonsError, APIUnknownSupervisorError):
"""Raise when an unknown error occurs while backing up the AppArmor profile of an addon."""
error_key = "addon_backup_app_armor_profile_unknown_error"
message_template = (
"An unknown error occurred while backing up the AppArmor profile of {addon}"
)
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(logger)
class AddonBackupExportImageUnknownError(AddonsError, APIUnknownSupervisorError):
"""Raise when an unknown error occurs while exporting image for an addon backup."""
error_key = "addon_backup_export_image_unknown_error"
message_template = (
"An unknown error occurred while exporting image to back up {addon}"
)
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(logger)
class AddonExtractBackupFileUnknownError(AddonsError, APIUnknownSupervisorError):
"""Raise when an unknown error occurs while extracting backup file for an addon."""
error_key = "addon_extract_backup_file_unknown_error"
message_template = (
"An unknown error occurred while extracting the backup file for {addon}"
)
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(logger)
class AddonRestoreBackupDataUnknownError(AddonsError, APIUnknownSupervisorError):
"""Raise when unknown error occurs while restoring data/config for addon from backup."""
error_key = "addon_restore_backup_data_unknown_error"
message_template = "An unknown error occurred while restoring data and config for {addon} from backup"
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon}
super().__init__(logger)
class AddonRestoreAppArmorProfileUnknownError(AddonsError, APIUnknownSupervisorError):
"""Raise when unknown error occurs while restoring AppArmor profile for addon from backup."""
error_key = "addon_restore_app_armor_profile_unknown_error"
message_template = "An unknown error occurred while restoring AppArmor profile for {addon} from backup"
def __init__( def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str self, logger: Callable[..., None] | None = None, *, addon: str
@@ -1262,6 +993,55 @@ class BackupFileExistError(BackupError):
"""Raise if the backup file already exists.""" """Raise if the backup file already exists."""
class AddonBackupMetadataInvalidError(BackupError, APIError):
"""Raise if invalid metadata file provided for addon in backup."""
error_key = "addon_backup_metadata_invalid_error"
message_template = (
"Metadata file for add-on {addon} in backup is invalid: {validation_error}"
)
def __init__(
self,
logger: Callable[..., None] | None = None,
*,
addon: str,
validation_error: str,
) -> None:
"""Initialize exception."""
self.extra_fields = {"addon": addon, "validation_error": validation_error}
super().__init__(None, logger)
class AddonPrePostBackupCommandReturnedError(BackupError, APIError):
"""Raise when addon's pre/post backup command returns an error."""
error_key = "addon_pre_post_backup_command_returned_error"
message_template = (
"Pre-/Post backup command for add-on {addon} returned error code: "
"{exit_code}. Please report this to the addon developer. Enable debug "
"logging to capture complete command output using {debug_logging_command}"
)
def __init__(
self, logger: Callable[..., None] | None = None, *, addon: str, exit_code: int
) -> None:
"""Initialize exception."""
self.extra_fields = {
"addon": addon,
"exit_code": exit_code,
"debug_logging_command": "ha supervisor options --logging debug",
}
super().__init__(None, logger)
class BackupRestoreUnknownError(BackupError, APIUnknownSupervisorError):
"""Raise when an unknown error occurs during backup or restore."""
error_key = "backup_restore_unknown_error"
message_template = "An unknown error occurred during backup/restore"
# Security # Security

View File

@@ -102,13 +102,17 @@ class SupervisorJobError:
"Unknown error, see Supervisor logs (check with 'ha supervisor logs')" "Unknown error, see Supervisor logs (check with 'ha supervisor logs')"
) )
stage: str | None = None stage: str | None = None
error_key: str | None = None
extra_fields: dict[str, Any] | None = None
def as_dict(self) -> dict[str, str | None]: def as_dict(self) -> dict[str, Any]:
"""Return dictionary representation.""" """Return dictionary representation."""
return { return {
"type": self.type_.__name__, "type": self.type_.__name__,
"message": self.message, "message": self.message,
"stage": self.stage, "stage": self.stage,
"error_key": self.error_key,
"extra_fields": self.extra_fields,
} }
@@ -158,7 +162,9 @@ class SupervisorJob:
def capture_error(self, err: HassioError | None = None) -> None: def capture_error(self, err: HassioError | None = None) -> None:
"""Capture an error or record that an unknown error has occurred.""" """Capture an error or record that an unknown error has occurred."""
if err: if err:
new_error = SupervisorJobError(type(err), str(err), self.stage) new_error = SupervisorJobError(
type(err), str(err), self.stage, err.error_key, err.extra_fields
)
else: else:
new_error = SupervisorJobError(stage=self.stage) new_error = SupervisorJobError(stage=self.stage)
self.errors += [new_error] self.errors += [new_error]

View File

@@ -5,6 +5,7 @@ from datetime import timedelta
import errno import errno
from http import HTTPStatus from http import HTTPStatus
from pathlib import Path from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, PropertyMock, call, patch from unittest.mock import MagicMock, PropertyMock, call, patch
import aiodocker import aiodocker
@@ -23,7 +24,13 @@ from supervisor.docker.addon import DockerAddon
from supervisor.docker.const import ContainerState from supervisor.docker.const import ContainerState
from supervisor.docker.manager import CommandReturn, DockerAPI from supervisor.docker.manager import CommandReturn, DockerAPI
from supervisor.docker.monitor import DockerContainerStateEvent from supervisor.docker.monitor import DockerContainerStateEvent
from supervisor.exceptions import AddonsError, AddonsJobError, AudioUpdateError from supervisor.exceptions import (
AddonPrePostBackupCommandReturnedError,
AddonsJobError,
AddonUnknownError,
AudioUpdateError,
HassioError,
)
from supervisor.hardware.helper import HwHelper from supervisor.hardware.helper import HwHelper
from supervisor.ingress import Ingress from supervisor.ingress import Ingress
from supervisor.store.repository import Repository from supervisor.store.repository import Repository
@@ -502,31 +509,26 @@ async def test_backup_with_pre_post_command(
@pytest.mark.parametrize( @pytest.mark.parametrize(
"get_error,exception_on_exec", ("container_get_side_effect", "exec_run_side_effect", "exc_type_raised"),
[ [
(NotFound("missing"), False), (NotFound("missing"), [(1, None)], AddonUnknownError),
(DockerException(), False), (DockerException(), [(1, None)], AddonUnknownError),
(None, True), (None, DockerException(), AddonUnknownError),
(None, False), (None, [(1, None)], AddonPrePostBackupCommandReturnedError),
], ],
) )
@pytest.mark.usefixtures("tmp_supervisor_data", "path_extern")
async def test_backup_with_pre_command_error( async def test_backup_with_pre_command_error(
coresys: CoreSys, coresys: CoreSys,
install_addon_ssh: Addon, install_addon_ssh: Addon,
container: MagicMock, container: MagicMock,
get_error: DockerException | None, container_get_side_effect: DockerException | None,
exception_on_exec: bool, exec_run_side_effect: DockerException | list[tuple[int, Any]],
tmp_supervisor_data, exc_type_raised: type[HassioError],
path_extern,
) -> None: ) -> None:
"""Test backing up an addon with error running pre command.""" """Test backing up an addon with error running pre command."""
if get_error: coresys.docker.containers.get.side_effect = container_get_side_effect
coresys.docker.containers.get.side_effect = get_error container.exec_run.side_effect = exec_run_side_effect
if exception_on_exec:
container.exec_run.side_effect = DockerException()
else:
container.exec_run.return_value = (1, None)
install_addon_ssh.path_data.mkdir() install_addon_ssh.path_data.mkdir()
await install_addon_ssh.load() await install_addon_ssh.load()
@@ -535,7 +537,7 @@ async def test_backup_with_pre_command_error(
with ( with (
patch.object(DockerAddon, "is_running", return_value=True), patch.object(DockerAddon, "is_running", return_value=True),
patch.object(Addon, "backup_pre", new=PropertyMock(return_value="backup_pre")), patch.object(Addon, "backup_pre", new=PropertyMock(return_value="backup_pre")),
pytest.raises(AddonsError), pytest.raises(exc_type_raised),
): ):
assert await install_addon_ssh.backup(tarfile) is None assert await install_addon_ssh.backup(tarfile) is None

View File

@@ -590,9 +590,9 @@ async def test_addon_start_options_error(
body = await resp.json() body = await resp.json()
assert ( assert (
body["message"] body["message"]
== "An unknown error occurred reading/writing configuration file for local_example. Check supervisor logs for details (check with 'ha supervisor logs')" == "An unknown error occurred with addon local_example. Check supervisor logs for details (check with 'ha supervisor logs')"
) )
assert body["error_key"] == "addon_configuration_file_unknown_error" assert body["error_key"] == "addon_unknown_error"
assert body["extra_fields"] == { assert body["extra_fields"] == {
"addon": "local_example", "addon": "local_example",
"logs_command": "ha supervisor logs", "logs_command": "ha supervisor logs",

View File

@@ -17,6 +17,7 @@ from supervisor.const import CoreState
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.docker.manager import DockerAPI from supervisor.docker.manager import DockerAPI
from supervisor.exceptions import ( from supervisor.exceptions import (
AddonPrePostBackupCommandReturnedError,
AddonsError, AddonsError,
BackupInvalidError, BackupInvalidError,
HomeAssistantBackupError, HomeAssistantBackupError,
@@ -24,6 +25,7 @@ from supervisor.exceptions import (
from supervisor.homeassistant.core import HomeAssistantCore from supervisor.homeassistant.core import HomeAssistantCore
from supervisor.homeassistant.module import HomeAssistant from supervisor.homeassistant.module import HomeAssistant
from supervisor.homeassistant.websocket import HomeAssistantWebSocket from supervisor.homeassistant.websocket import HomeAssistantWebSocket
from supervisor.jobs import SupervisorJob
from supervisor.mounts.mount import Mount from supervisor.mounts.mount import Mount
from supervisor.supervisor import Supervisor from supervisor.supervisor import Supervisor
@@ -401,6 +403,8 @@ async def test_api_backup_errors(
"type": "BackupError", "type": "BackupError",
"message": str(err), "message": str(err),
"stage": None, "stage": None,
"error_key": None,
"extra_fields": None,
} }
] ]
assert job["child_jobs"][2]["name"] == "backup_store_folders" assert job["child_jobs"][2]["name"] == "backup_store_folders"
@@ -437,6 +441,8 @@ async def test_api_backup_errors(
"type": "HomeAssistantBackupError", "type": "HomeAssistantBackupError",
"message": "Backup error", "message": "Backup error",
"stage": "home_assistant", "stage": "home_assistant",
"error_key": None,
"extra_fields": None,
} }
] ]
assert job["child_jobs"][0]["name"] == "backup_store_homeassistant" assert job["child_jobs"][0]["name"] == "backup_store_homeassistant"
@@ -445,6 +451,8 @@ async def test_api_backup_errors(
"type": "HomeAssistantBackupError", "type": "HomeAssistantBackupError",
"message": "Backup error", "message": "Backup error",
"stage": None, "stage": None,
"error_key": None,
"extra_fields": None,
} }
] ]
assert len(job["child_jobs"]) == 1 assert len(job["child_jobs"]) == 1
@@ -749,6 +757,8 @@ async def test_backup_to_multiple_locations_error_on_copy(
"type": "BackupError", "type": "BackupError",
"message": "Could not copy backup to .cloud_backup due to: ", "message": "Could not copy backup to .cloud_backup due to: ",
"stage": None, "stage": None,
"error_key": None,
"extra_fields": None,
} }
] ]
@@ -1483,3 +1493,44 @@ async def test_immediate_list_after_missing_file_restore(
result = await resp.json() result = await resp.json()
assert len(result["data"]["backups"]) == 1 assert len(result["data"]["backups"]) == 1
assert result["data"]["backups"][0]["slug"] == "93b462f8" assert result["data"]["backups"][0]["slug"] == "93b462f8"
@pytest.mark.parametrize("command", ["backup_pre", "backup_post"])
@pytest.mark.usefixtures("install_addon_example", "tmp_supervisor_data")
async def test_pre_post_backup_command_error(
api_client: TestClient, coresys: CoreSys, container: MagicMock, command: str
):
"""Test pre/post backup command error."""
await coresys.core.set_state(CoreState.RUNNING)
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
container.status = "running"
container.exec_run.return_value = (1, b"")
with patch.object(Addon, command, return_value=PropertyMock(return_value="test")):
resp = await api_client.post(
"/backups/new/partial", json={"addons": ["local_example"]}
)
assert resp.status == 200
body = await resp.json()
job_id = body["data"]["job_id"]
job: SupervisorJob | None = None
for j in coresys.jobs.jobs:
if j.name == "backup_store_addons" and j.parent_id == job_id:
job = j
break
assert job
assert job.done is True
assert job.errors[0].type_ == AddonPrePostBackupCommandReturnedError
assert job.errors[0].message == (
"Pre-/Post backup command for add-on local_example returned error code: "
"1. Please report this to the addon developer. Enable debug "
"logging to capture complete command output using ha supervisor options --logging debug"
)
assert job.errors[0].error_key == "addon_pre_post_backup_command_returned_error"
assert job.errors[0].extra_fields == {
"addon": "local_example",
"exit_code": 1,
"debug_logging_command": "ha supervisor options --logging debug",
}

View File

@@ -374,6 +374,8 @@ async def test_job_with_error(
"type": "SupervisorError", "type": "SupervisorError",
"message": "bad", "message": "bad",
"stage": "test", "stage": "test",
"error_key": None,
"extra_fields": None,
} }
], ],
"child_jobs": [ "child_jobs": [
@@ -391,6 +393,8 @@ async def test_job_with_error(
"type": "SupervisorError", "type": "SupervisorError",
"message": "bad", "message": "bad",
"stage": None, "stage": None,
"error_key": None,
"extra_fields": None,
} }
], ],
"child_jobs": [], "child_jobs": [],