From 772d074db9da330ab908cf7a59b15f81a319357c Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Tue, 18 Nov 2025 22:02:56 +0000 Subject: [PATCH] Remove customized unknown error types --- supervisor/addons/addon.py | 88 +++++----- supervisor/backups/backup.py | 3 - supervisor/exceptions.py | 326 ++++++----------------------------- supervisor/jobs/__init__.py | 10 +- tests/addons/test_addon.py | 38 ++-- tests/api/test_addons.py | 4 +- tests/api/test_backups.py | 51 ++++++ tests/api/test_jobs.py | 4 + 8 files changed, 180 insertions(+), 344 deletions(-) diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index 313cbd803..16d553fa4 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -66,30 +66,16 @@ from ..docker.const import ContainerState from ..docker.monitor import DockerContainerStateEvent from ..docker.stats import DockerStats from ..exceptions import ( - AddonBackupAppArmorProfileUnknownError, - AddonBackupExportImageUnknownError, AddonBackupMetadataInvalidError, - AddonBuildImageUnknownError, - AddonConfigurationFileUnknownError, AddonConfigurationInvalidError, - AddonContainerRunCommandUnknownError, - AddonContainerStartUnknownError, - AddonContainerStatsUnknownError, - AddonContainerStopUnknownError, - AddonContainerWriteStdinUnknownError, - AddonCreateBackupFileUnknownError, - AddonCreateBackupMetadataFileUnknownError, - AddonExtractBackupFileUnknownError, - AddonInstallImageUnknownError, AddonNotRunningError, AddonNotSupportedError, AddonNotSupportedWriteStdinError, AddonPrePostBackupCommandReturnedError, - AddonRemoveImageUnknownError, - AddonRestoreAppArmorProfileUnknownError, - AddonRestoreBackupDataUnknownError, AddonsError, AddonsJobError, + AddonUnknownError, + BackupRestoreUnknownError, ConfigurationFileError, DockerBuildError, DockerError, @@ -747,7 +733,7 @@ class Addon(AddonModel): ) from None except ConfigurationFileError as err: _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) @@ -817,11 +803,13 @@ class Addon(AddonModel): await self.sys_addons.data.uninstall(self) raise except DockerBuildError as err: + _LOGGER.error("Could not build image for addon %s: %s", self.slug, err) 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: + _LOGGER.error("Could not pull image to update addon %s: %s", self.slug, err) 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 await self.load() @@ -845,7 +833,8 @@ class Addon(AddonModel): try: await self.instance.remove(remove_image=remove_image) 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 @@ -919,9 +908,11 @@ class Addon(AddonModel): try: await self.instance.update(store.version, store.image, arch=self.arch) 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: - 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 if (last_state := self.state) in {AddonState.STARTED, AddonState.STARTUP}: @@ -967,14 +958,19 @@ class Addon(AddonModel): try: await self.instance.remove() 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: await self.instance.install(self.version) 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: - 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: await self.sys_addons.data.update(self.addon_store) @@ -1145,8 +1141,9 @@ class Addon(AddonModel): try: await self.instance.run() except DockerError as err: + _LOGGER.error("Could not start container for addon %s: %s", self.slug, err) 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()) @@ -1161,8 +1158,9 @@ class Addon(AddonModel): try: await self.instance.stop() except DockerError as err: + _LOGGER.error("Could not stop container for addon %s: %s", self.slug, err) self.state = AddonState.ERROR - raise AddonContainerStopUnknownError(addon=self.slug) from err + raise AddonUnknownError(addon=self.slug) from err @Job( name="addon_restart", @@ -1200,7 +1198,10 @@ class Addon(AddonModel): return await self.instance.stats() 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( name="addon_write_stdin", @@ -1218,7 +1219,10 @@ class Addon(AddonModel): await self.instance.write_stdin(data) 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: try: @@ -1234,7 +1238,7 @@ class Addon(AddonModel): _LOGGER.error( "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( name="addon_begin_backup", @@ -1323,18 +1327,14 @@ class Addon(AddonModel): try: self.instance.export_image(temp_path.joinpath("image.tar")) except DockerError as err: - raise AddonBackupExportImageUnknownError( - addon=self.slug - ) from err + raise BackupRestoreUnknownError() from err # Store local configs/state try: write_json_file(temp_path.joinpath("addon.json"), metadata) except ConfigurationFileError as err: _LOGGER.error("Can't save meta for %s: %s", self.slug, err) - raise AddonCreateBackupMetadataFileUnknownError( - addon=self.slug - ) from err + raise BackupRestoreUnknownError() from err # Store AppArmor Profile if apparmor_profile: @@ -1344,9 +1344,7 @@ class Addon(AddonModel): apparmor_profile, profile_backup_file ) except HostAppArmorError as err: - raise AddonBackupAppArmorProfileUnknownError( - addon=self.slug - ) from err + raise BackupRestoreUnknownError() from err # Write tarfile with tar_file as backup: @@ -1401,7 +1399,7 @@ class Addon(AddonModel): _LOGGER.info("Finish backup for addon %s", self.slug) except (tarfile.TarError, OSError, AddFileError) as 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: if was_running: wait_for_start = await self.end_backup() @@ -1444,9 +1442,9 @@ class Addon(AddonModel): tmp, data = await self.sys_run_in_executor(_extract_tarfile) except tarfile.TarError as 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: - raise AddonConfigurationFileUnknownError(addon=self.slug) from err + raise AddonUnknownError(addon=self.slug) from err try: # Validate @@ -1522,7 +1520,7 @@ class Addon(AddonModel): _LOGGER.error( "Can't restore origin data for %s: %s", self.slug, err ) - raise AddonRestoreBackupDataUnknownError(addon=self.slug) from err + raise BackupRestoreUnknownError() from err # Restore AppArmor profile_file = Path(tmp.name, "apparmor.txt") @@ -1537,9 +1535,7 @@ class Addon(AddonModel): self.slug, err, ) - raise AddonRestoreAppArmorProfileUnknownError( - addon=self.slug - ) from err + raise BackupRestoreUnknownError() from err finally: # Is add-on loaded diff --git a/supervisor/backups/backup.py b/supervisor/backups/backup.py index ee132cc3e..9f09c609b 100644 --- a/supervisor/backups/backup.py +++ b/supervisor/backups/backup.py @@ -628,9 +628,6 @@ class Backup(JobGroup): if start_task := await self._addon_save(addon): start_tasks.append(start_task) 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) return start_tasks diff --git a/supervisor/exceptions.py b/supervisor/exceptions.py index 3f09f409b..66a6542a6 100644 --- a/supervisor/exceptions.py +++ b/supervisor/exceptions.py @@ -358,26 +358,6 @@ class AddonConfigurationInvalidError(AddonConfigurationError, APIError): 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): """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) -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): """Addon doesn't support a function.""" @@ -546,238 +504,11 @@ class AddonBuildArchitectureNotSupportedError(AddonNotSupportedError, APIError): super().__init__(None, logger) -# pylint: disable-next=too-many-ancestors -class AddonConfigurationFileUnknownError( - AddonConfigurationError, APIUnknownSupervisorError -): - """Raise when unknown error occurs trying to read/write addon configuration file.""" +class AddonUnknownError(AddonsError, APIUnknownSupervisorError): + """Raise when unknown error occurs taking an action for an addon.""" - error_key = "addon_configuration_file_unknown_error" - message_template = ( - "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" + error_key = "addon_unknown_error" + message_template = "An unknown error occurred with addon {addon}" def __init__( self, logger: Callable[..., None] | None = None, *, addon: str @@ -1262,6 +993,55 @@ class BackupFileExistError(BackupError): """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 diff --git a/supervisor/jobs/__init__.py b/supervisor/jobs/__init__.py index a33ae5250..16d749030 100644 --- a/supervisor/jobs/__init__.py +++ b/supervisor/jobs/__init__.py @@ -102,13 +102,17 @@ class SupervisorJobError: "Unknown error, see Supervisor logs (check with 'ha supervisor logs')" ) 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 { "type": self.type_.__name__, "message": self.message, "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: """Capture an error or record that an unknown error has occurred.""" 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: new_error = SupervisorJobError(stage=self.stage) self.errors += [new_error] diff --git a/tests/addons/test_addon.py b/tests/addons/test_addon.py index 3e68c57e6..0fb4f5cf5 100644 --- a/tests/addons/test_addon.py +++ b/tests/addons/test_addon.py @@ -5,6 +5,7 @@ from datetime import timedelta import errno from http import HTTPStatus from pathlib import Path +from typing import Any from unittest.mock import MagicMock, PropertyMock, call, patch import aiodocker @@ -23,7 +24,13 @@ from supervisor.docker.addon import DockerAddon from supervisor.docker.const import ContainerState from supervisor.docker.manager import CommandReturn, DockerAPI 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.ingress import Ingress from supervisor.store.repository import Repository @@ -502,31 +509,26 @@ async def test_backup_with_pre_post_command( @pytest.mark.parametrize( - "get_error,exception_on_exec", + ("container_get_side_effect", "exec_run_side_effect", "exc_type_raised"), [ - (NotFound("missing"), False), - (DockerException(), False), - (None, True), - (None, False), + (NotFound("missing"), [(1, None)], AddonUnknownError), + (DockerException(), [(1, None)], AddonUnknownError), + (None, DockerException(), AddonUnknownError), + (None, [(1, None)], AddonPrePostBackupCommandReturnedError), ], ) +@pytest.mark.usefixtures("tmp_supervisor_data", "path_extern") async def test_backup_with_pre_command_error( coresys: CoreSys, install_addon_ssh: Addon, container: MagicMock, - get_error: DockerException | None, - exception_on_exec: bool, - tmp_supervisor_data, - path_extern, + container_get_side_effect: DockerException | None, + exec_run_side_effect: DockerException | list[tuple[int, Any]], + exc_type_raised: type[HassioError], ) -> None: """Test backing up an addon with error running pre command.""" - if get_error: - coresys.docker.containers.get.side_effect = get_error - - if exception_on_exec: - container.exec_run.side_effect = DockerException() - else: - container.exec_run.return_value = (1, None) + coresys.docker.containers.get.side_effect = container_get_side_effect + container.exec_run.side_effect = exec_run_side_effect install_addon_ssh.path_data.mkdir() await install_addon_ssh.load() @@ -535,7 +537,7 @@ async def test_backup_with_pre_command_error( with ( patch.object(DockerAddon, "is_running", return_value=True), 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 diff --git a/tests/api/test_addons.py b/tests/api/test_addons.py index 14bffd8d6..28b9d13c3 100644 --- a/tests/api/test_addons.py +++ b/tests/api/test_addons.py @@ -590,9 +590,9 @@ async def test_addon_start_options_error( body = await resp.json() assert ( 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"] == { "addon": "local_example", "logs_command": "ha supervisor logs", diff --git a/tests/api/test_backups.py b/tests/api/test_backups.py index ed5c364ed..38c4995d5 100644 --- a/tests/api/test_backups.py +++ b/tests/api/test_backups.py @@ -17,6 +17,7 @@ from supervisor.const import CoreState from supervisor.coresys import CoreSys from supervisor.docker.manager import DockerAPI from supervisor.exceptions import ( + AddonPrePostBackupCommandReturnedError, AddonsError, BackupInvalidError, HomeAssistantBackupError, @@ -24,6 +25,7 @@ from supervisor.exceptions import ( from supervisor.homeassistant.core import HomeAssistantCore from supervisor.homeassistant.module import HomeAssistant from supervisor.homeassistant.websocket import HomeAssistantWebSocket +from supervisor.jobs import SupervisorJob from supervisor.mounts.mount import Mount from supervisor.supervisor import Supervisor @@ -401,6 +403,8 @@ async def test_api_backup_errors( "type": "BackupError", "message": str(err), "stage": None, + "error_key": None, + "extra_fields": None, } ] assert job["child_jobs"][2]["name"] == "backup_store_folders" @@ -437,6 +441,8 @@ async def test_api_backup_errors( "type": "HomeAssistantBackupError", "message": "Backup error", "stage": "home_assistant", + "error_key": None, + "extra_fields": None, } ] assert job["child_jobs"][0]["name"] == "backup_store_homeassistant" @@ -445,6 +451,8 @@ async def test_api_backup_errors( "type": "HomeAssistantBackupError", "message": "Backup error", "stage": None, + "error_key": None, + "extra_fields": None, } ] assert len(job["child_jobs"]) == 1 @@ -749,6 +757,8 @@ async def test_backup_to_multiple_locations_error_on_copy( "type": "BackupError", "message": "Could not copy backup to .cloud_backup due to: ", "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() assert len(result["data"]["backups"]) == 1 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", + } diff --git a/tests/api/test_jobs.py b/tests/api/test_jobs.py index 5048507be..0e592354f 100644 --- a/tests/api/test_jobs.py +++ b/tests/api/test_jobs.py @@ -374,6 +374,8 @@ async def test_job_with_error( "type": "SupervisorError", "message": "bad", "stage": "test", + "error_key": None, + "extra_fields": None, } ], "child_jobs": [ @@ -391,6 +393,8 @@ async def test_job_with_error( "type": "SupervisorError", "message": "bad", "stage": None, + "error_key": None, + "extra_fields": None, } ], "child_jobs": [],