Sentry only loaded when diagnostics on (#3993)

* Sentry only loaded when diagnostics on

* Logging when sentry is closed
This commit is contained in:
Mike Degatano 2022-11-13 15:23:52 -05:00 committed by GitHub
parent 14cd261b76
commit 14fcda5d78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 619 additions and 133 deletions

View File

@ -23,6 +23,7 @@ from ..jobs.decorator import Job, JobCondition
from ..resolution.const import ContextType, IssueType, SuggestionType from ..resolution.const import ContextType, IssueType, SuggestionType
from ..store.addon import AddonStore from ..store.addon import AddonStore
from ..utils import check_exception_chain from ..utils import check_exception_chain
from ..utils.sentry import capture_exception
from .addon import Addon from .addon import Addon
from .const import ADDON_UPDATE_CONDITIONS from .const import ADDON_UPDATE_CONDITIONS
from .data import AddonsData from .data import AddonsData
@ -114,7 +115,7 @@ class AddonManager(CoreSysAttributes):
addon.boot = AddonBoot.MANUAL addon.boot = AddonBoot.MANUAL
addon.save_persist() addon.save_persist()
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
self.sys_capture_exception(err) capture_exception(err)
else: else:
continue continue
@ -142,7 +143,7 @@ class AddonManager(CoreSysAttributes):
await addon.stop() await addon.stop()
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.warning("Can't stop Add-on %s: %s", addon.slug, err) _LOGGER.warning("Can't stop Add-on %s: %s", addon.slug, err)
self.sys_capture_exception(err) capture_exception(err)
@Job( @Job(
conditions=ADDON_UPDATE_CONDITIONS, conditions=ADDON_UPDATE_CONDITIONS,
@ -421,7 +422,7 @@ class AddonManager(CoreSysAttributes):
reference=addon.slug, reference=addon.slug,
suggestions=[SuggestionType.EXECUTE_REPAIR], suggestions=[SuggestionType.EXECUTE_REPAIR],
) )
self.sys_capture_exception(err) capture_exception(err)
else: else:
self.sys_plugins.dns.add_host( self.sys_plugins.dns.add_host(
ipv4=addon.ip_address, names=[addon.hostname], write=False ipv4=addon.ip_address, names=[addon.hostname], write=False

View File

@ -9,9 +9,6 @@ CONTENT_TYPE_URL = "application/x-www-form-urlencoded"
COOKIE_INGRESS = "ingress_session" COOKIE_INGRESS = "ingress_session"
HEADER_TOKEN_OLD = "X-Hassio-Key"
HEADER_TOKEN = "X-Supervisor-Token"
ATTR_APPARMOR_VERSION = "apparmor_version" ATTR_APPARMOR_VERSION = "apparmor_version"
ATTR_AGENT_VERSION = "agent_version" ATTR_AGENT_VERSION = "agent_version"
ATTR_AVAILABLE_UPDATES = "available_updates" ATTR_AVAILABLE_UPDATES = "available_updates"

View File

@ -22,9 +22,11 @@ from ..const import (
ATTR_PANELS, ATTR_PANELS,
ATTR_SESSION, ATTR_SESSION,
ATTR_TITLE, ATTR_TITLE,
HEADER_TOKEN,
HEADER_TOKEN_OLD,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from .const import COOKIE_INGRESS, HEADER_TOKEN, HEADER_TOKEN_OLD from .const import COOKIE_INGRESS
from .utils import api_process, api_validate, require_home_assistant from .utils import api_process, api_validate, require_home_assistant
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)

View File

@ -46,6 +46,7 @@ from ..const import (
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIError from ..exceptions import APIError
from ..store.validate import repositories from ..store.validate import repositories
from ..utils.sentry import close_sentry, init_sentry
from ..utils.validate import validate_timezone from ..utils.validate import validate_timezone
from ..validate import version_tag, wait_boot from ..validate import version_tag, wait_boot
from .const import CONTENT_TYPE_BINARY from .const import CONTENT_TYPE_BINARY
@ -144,6 +145,11 @@ class APISupervisor(CoreSysAttributes):
self.sys_config.diagnostics = body[ATTR_DIAGNOSTICS] self.sys_config.diagnostics = body[ATTR_DIAGNOSTICS]
self.sys_dbus.agent.diagnostics = body[ATTR_DIAGNOSTICS] self.sys_dbus.agent.diagnostics = body[ATTR_DIAGNOSTICS]
if body[ATTR_DIAGNOSTICS]:
init_sentry(self.coresys)
else:
close_sentry()
if ATTR_LOGGING in body: if ATTR_LOGGING in body:
self.sys_config.logging = body[ATTR_LOGGING] self.sys_config.logging = body[ATTR_LOGGING]

View File

@ -10,6 +10,8 @@ import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
from ..const import ( from ..const import (
HEADER_TOKEN,
HEADER_TOKEN_OLD,
JSON_DATA, JSON_DATA,
JSON_MESSAGE, JSON_MESSAGE,
JSON_RESULT, JSON_RESULT,
@ -22,7 +24,7 @@ from ..exceptions import APIError, APIForbidden, DockerAPIError, HassioError
from ..utils import check_exception_chain, get_message_from_exception_chain from ..utils import check_exception_chain, get_message_from_exception_chain
from ..utils.json import JSONEncoder from ..utils.json import JSONEncoder
from ..utils.log_format import format_message from ..utils.log_format import format_message
from .const import CONTENT_TYPE_BINARY, HEADER_TOKEN, HEADER_TOKEN_OLD from .const import CONTENT_TYPE_BINARY
def excract_supervisor_token(request: web.Request) -> str | None: def excract_supervisor_token(request: web.Request) -> str | None:

View File

@ -17,6 +17,7 @@ from ..exceptions import AddonsError
from ..jobs.decorator import Job, JobCondition from ..jobs.decorator import Job, JobCondition
from ..utils.common import FileConfiguration from ..utils.common import FileConfiguration
from ..utils.dt import utcnow from ..utils.dt import utcnow
from ..utils.sentry import capture_exception
from .backup import Backup from .backup import Backup
from .const import BackupType from .const import BackupType
from .utils import create_slug from .utils import create_slug
@ -172,7 +173,7 @@ class BackupManager(FileConfiguration, CoreSysAttributes):
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Backup %s error", backup.slug) _LOGGER.exception("Backup %s error", backup.slug)
self.sys_capture_exception(err) capture_exception(err)
return None return None
else: else:
self._backups[backup.slug] = backup self._backups[backup.slug] = backup
@ -296,7 +297,7 @@ class BackupManager(FileConfiguration, CoreSysAttributes):
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Restore %s error", backup.slug) _LOGGER.exception("Restore %s error", backup.slug)
self.sys_capture_exception(err) capture_exception(err)
return False return False
else: else:
return True return True

View File

@ -5,13 +5,6 @@ from pathlib import Path
import signal import signal
from colorlog import ColoredFormatter from colorlog import ColoredFormatter
import sentry_sdk
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
from sentry_sdk.integrations.atexit import AtexitIntegration
from sentry_sdk.integrations.dedupe import DedupeIntegration
from sentry_sdk.integrations.excepthook import ExcepthookIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.integrations.threading import ThreadingIntegration
from .addons import AddonManager from .addons import AddonManager
from .api import RestAPI from .api import RestAPI
@ -28,7 +21,6 @@ from .const import (
ENV_SUPERVISOR_SHARE, ENV_SUPERVISOR_SHARE,
MACHINE_ID, MACHINE_ID,
SOCKET_DOCKER, SOCKET_DOCKER,
SUPERVISOR_VERSION,
LogLevel, LogLevel,
UpdateChannel, UpdateChannel,
) )
@ -42,7 +34,6 @@ from .homeassistant.module import HomeAssistant
from .host.manager import HostManager from .host.manager import HostManager
from .ingress import Ingress from .ingress import Ingress
from .jobs import JobManager from .jobs import JobManager
from .misc.filter import filter_data
from .misc.scheduler import Scheduler from .misc.scheduler import Scheduler
from .misc.tasks import Tasks from .misc.tasks import Tasks
from .os.manager import OSManager from .os.manager import OSManager
@ -54,6 +45,7 @@ from .store import StoreManager
from .store.validate import ensure_builtin_repositories from .store.validate import ensure_builtin_repositories
from .supervisor import Supervisor from .supervisor import Supervisor
from .updater import Updater from .updater import Updater
from .utils.sentry import init_sentry
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -90,7 +82,8 @@ async def initialize_coresys() -> CoreSys:
coresys.bus = Bus(coresys) coresys.bus = Bus(coresys)
# diagnostics # diagnostics
setup_diagnostics(coresys) if coresys.config.diagnostics:
init_sentry(coresys)
# bootstrap config # bootstrap config
initialize_system(coresys) initialize_system(coresys)
@ -316,25 +309,3 @@ def supervisor_debugger(coresys: CoreSys) -> None:
if coresys.config.debug_block: if coresys.config.debug_block:
_LOGGER.info("Wait until debugger is attached") _LOGGER.info("Wait until debugger is attached")
debugpy.wait_for_client() debugpy.wait_for_client()
def setup_diagnostics(coresys: CoreSys) -> None:
"""Sentry diagnostic backend."""
_LOGGER.info("Initializing Supervisor Sentry")
# pylint: disable=abstract-class-instantiated
sentry_sdk.init(
dsn="https://9c6ea70f49234442b4746e447b24747e@o427061.ingest.sentry.io/5370612",
before_send=lambda event, hint: filter_data(coresys, event, hint),
auto_enabling_integrations=False,
default_integrations=False,
integrations=[
AioHttpIntegration(),
ExcepthookIntegration(),
DedupeIntegration(),
AtexitIntegration(),
ThreadingIntegration(),
LoggingIntegration(level=logging.WARNING, event_level=logging.CRITICAL),
],
release=SUPERVISOR_VERSION,
max_breadcrumbs=30,
)

View File

@ -69,6 +69,9 @@ JSON_RESULT = "result"
RESULT_ERROR = "error" RESULT_ERROR = "error"
RESULT_OK = "ok" RESULT_OK = "ok"
HEADER_TOKEN_OLD = "X-Hassio-Key"
HEADER_TOKEN = "X-Supervisor-Token"
ENV_HOMEASSISTANT_REPOSITORY = "HOMEASSISTANT_REPOSITORY" ENV_HOMEASSISTANT_REPOSITORY = "HOMEASSISTANT_REPOSITORY"
ENV_SUPERVISOR_DEV = "SUPERVISOR_DEV" ENV_SUPERVISOR_DEV = "SUPERVISOR_DEV"
ENV_SUPERVISOR_MACHINE = "SUPERVISOR_MACHINE" ENV_SUPERVISOR_MACHINE = "SUPERVISOR_MACHINE"

View File

@ -20,6 +20,7 @@ from .exceptions import (
from .homeassistant.core import LANDINGPAGE from .homeassistant.core import LANDINGPAGE
from .resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason from .resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason
from .utils.dt import utcnow from .utils.dt import utcnow
from .utils.sentry import capture_exception
from .utils.whoami import retrieve_whoami from .utils.whoami import retrieve_whoami
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -153,7 +154,7 @@ class Core(CoreSysAttributes):
"Fatal error happening on load Task %s: %s", setup_task, err "Fatal error happening on load Task %s: %s", setup_task, err
) )
self.sys_resolution.unhealthy = UnhealthyReason.SETUP self.sys_resolution.unhealthy = UnhealthyReason.SETUP
self.sys_capture_exception(err) capture_exception(err)
# Set OS Agent diagnostics if needed # Set OS Agent diagnostics if needed
if ( if (
@ -196,7 +197,7 @@ class Core(CoreSysAttributes):
"future versions of Home Assistant!" "future versions of Home Assistant!"
) )
self.sys_resolution.unhealthy = UnhealthyReason.SUPERVISOR self.sys_resolution.unhealthy = UnhealthyReason.SUPERVISOR
self.sys_capture_exception(err) capture_exception(err)
# Start addon mark as initialize # Start addon mark as initialize
await self.sys_addons.boot(AddonStartup.INITIALIZE) await self.sys_addons.boot(AddonStartup.INITIALIZE)
@ -226,12 +227,12 @@ class Core(CoreSysAttributes):
await self.sys_homeassistant.core.start() await self.sys_homeassistant.core.start()
except HomeAssistantCrashError as err: except HomeAssistantCrashError as err:
_LOGGER.error("Can't start Home Assistant Core - rebuiling") _LOGGER.error("Can't start Home Assistant Core - rebuiling")
self.sys_capture_exception(err) capture_exception(err)
with suppress(HomeAssistantError): with suppress(HomeAssistantError):
await self.sys_homeassistant.core.rebuild() await self.sys_homeassistant.core.rebuild()
except HomeAssistantError as err: except HomeAssistantError as err:
self.sys_capture_exception(err) capture_exception(err)
else: else:
_LOGGER.info("Skiping start of Home Assistant") _LOGGER.info("Skiping start of Home Assistant")

View File

@ -10,7 +10,6 @@ from types import MappingProxyType
from typing import TYPE_CHECKING, Any, TypeVar from typing import TYPE_CHECKING, Any, TypeVar
import aiohttp import aiohttp
import sentry_sdk
from .config import CoreConfig from .config import CoreConfig
from .const import ENV_SUPERVISOR_DEV, SERVER_SOFTWARE from .const import ENV_SUPERVISOR_DEV, SERVER_SOFTWARE
@ -514,10 +513,6 @@ class CoreSys:
"""Create an async task.""" """Create an async task."""
return self.loop.create_task(coroutine) return self.loop.create_task(coroutine)
def capture_exception(self, err: Exception) -> None:
"""Capture a exception."""
sentry_sdk.capture_exception(err)
class CoreSysAttributes: class CoreSysAttributes:
"""Inherit basic CoreSysAttributes.""" """Inherit basic CoreSysAttributes."""
@ -692,7 +687,3 @@ class CoreSysAttributes:
def sys_create_task(self, coroutine: Coroutine) -> asyncio.Task: def sys_create_task(self, coroutine: Coroutine) -> asyncio.Task:
"""Create an async task.""" """Create an async task."""
return self.coresys.create_task(coroutine) return self.coresys.create_task(coroutine)
def sys_capture_exception(self, err: Exception) -> None:
"""Capture a exception."""
self.coresys.capture_exception(err)

View File

@ -4,7 +4,6 @@ from typing import Any
from awesomeversion import AwesomeVersion, AwesomeVersionException from awesomeversion import AwesomeVersion, AwesomeVersionException
from dbus_fast.aio.message_bus import MessageBus from dbus_fast.aio.message_bus import MessageBus
import sentry_sdk
from ...exceptions import ( from ...exceptions import (
DBusError, DBusError,
@ -12,6 +11,7 @@ from ...exceptions import (
DBusInterfaceError, DBusInterfaceError,
HostNotSupportedError, HostNotSupportedError,
) )
from ...utils.sentry import capture_exception
from ..const import ( from ..const import (
DBUS_ATTR_CONNECTION_ENABLED, DBUS_ATTR_CONNECTION_ENABLED,
DBUS_ATTR_DEVICES, DBUS_ATTR_DEVICES,
@ -194,7 +194,7 @@ class NetworkManager(DBusInterfaceProxy):
continue continue
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Error while processing %s: %s", device, err) _LOGGER.exception("Error while processing %s: %s", device, err)
sentry_sdk.capture_exception(err) capture_exception(err)
continue continue
# Skeep interface # Skeep interface

View File

@ -44,6 +44,7 @@ from ..hardware.data import Device
from ..jobs.decorator import Job, JobCondition, JobExecutionLimit from ..jobs.decorator import Job, JobCondition, JobExecutionLimit
from ..resolution.const import ContextType, IssueType, SuggestionType from ..resolution.const import ContextType, IssueType, SuggestionType
from ..utils import process_lock from ..utils import process_lock
from ..utils.sentry import capture_exception
from .const import ( from .const import (
DBUS_PATH, DBUS_PATH,
DBUS_VOLUME, DBUS_VOLUME,
@ -516,7 +517,7 @@ class DockerAddon(DockerInterface):
) )
except CoreDNSError as err: except CoreDNSError as err:
_LOGGER.warning("Can't update DNS for %s", self.name) _LOGGER.warning("Can't update DNS for %s", self.name)
self.sys_capture_exception(err) capture_exception(err)
# Hardware Access # Hardware Access
if self.addon.static_devices: if self.addon.static_devices:
@ -699,7 +700,7 @@ class DockerAddon(DockerInterface):
self.sys_plugins.dns.delete_host(self.addon.hostname) self.sys_plugins.dns.delete_host(self.addon.hostname)
except CoreDNSError as err: except CoreDNSError as err:
_LOGGER.warning("Can't update DNS for %s", self.name) _LOGGER.warning("Can't update DNS for %s", self.name)
self.sys_capture_exception(err) capture_exception(err)
# Hardware # Hardware
if self._hw_listener: if self._hw_listener:

View File

@ -36,6 +36,7 @@ from ..exceptions import (
) )
from ..resolution.const import ContextType, IssueType, SuggestionType from ..resolution.const import ContextType, IssueType, SuggestionType
from ..utils import process_lock from ..utils import process_lock
from ..utils.sentry import capture_exception
from .const import ContainerState, RestartPolicy from .const import ContainerState, RestartPolicy
from .manager import CommandReturn from .manager import CommandReturn
from .monitor import DockerContainerStateEvent from .monitor import DockerContainerStateEvent
@ -259,7 +260,7 @@ class DockerInterface(CoreSysAttributes):
f"Can't install {image}:{version!s}: {err}", _LOGGER.error f"Can't install {image}:{version!s}: {err}", _LOGGER.error
) from err ) from err
except (docker.errors.DockerException, requests.RequestException) as err: except (docker.errors.DockerException, requests.RequestException) as err:
self.sys_capture_exception(err) capture_exception(err)
raise DockerError( raise DockerError(
f"Unknown error with {image}:{version!s} -> {err!s}", _LOGGER.error f"Unknown error with {image}:{version!s} -> {err!s}", _LOGGER.error
) from err ) from err

View File

@ -27,6 +27,7 @@ from ..jobs.const import JobExecutionLimit
from ..jobs.decorator import Job, JobCondition from ..jobs.decorator import Job, JobCondition
from ..resolution.const import ContextType, IssueType from ..resolution.const import ContextType, IssueType
from ..utils import convert_to_ascii, process_lock from ..utils import convert_to_ascii, process_lock
from ..utils.sentry import capture_exception
from .const import ( from .const import (
LANDINGPAGE, LANDINGPAGE,
WATCHDOG_MAX_ATTEMPTS, WATCHDOG_MAX_ATTEMPTS,
@ -125,16 +126,18 @@ class HomeAssistantCore(CoreSysAttributes):
await self.instance.install( await self.instance.install(
LANDINGPAGE, image=self.sys_updater.image_homeassistant LANDINGPAGE, image=self.sys_updater.image_homeassistant
) )
break
except DockerError: except DockerError:
pass
except Exception as err: # pylint: disable=broad-except
capture_exception(err)
_LOGGER.warning("Fails install landingpage, retry after 30sec") _LOGGER.warning("Fails install landingpage, retry after 30sec")
await asyncio.sleep(30) await asyncio.sleep(30)
except Exception as err: # pylint: disable=broad-except
self.sys_capture_exception(err)
else:
self.sys_homeassistant.version = LANDINGPAGE self.sys_homeassistant.version = LANDINGPAGE
self.sys_homeassistant.image = self.sys_updater.image_homeassistant self.sys_homeassistant.image = self.sys_updater.image_homeassistant
self.sys_homeassistant.save_data() self.sys_homeassistant.save_data()
break
@process_lock @process_lock
async def install(self) -> None: async def install(self) -> None:
@ -155,7 +158,7 @@ class HomeAssistantCore(CoreSysAttributes):
except DockerError: except DockerError:
pass pass
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
self.sys_capture_exception(err) capture_exception(err)
_LOGGER.warning("Error on Home Assistant installation. Retry in 30sec") _LOGGER.warning("Error on Home Assistant installation. Retry in 30sec")
await asyncio.sleep(30) await asyncio.sleep(30)
@ -473,7 +476,7 @@ class HomeAssistantCore(CoreSysAttributes):
try: try:
await self.start() await self.start()
except HomeAssistantError as err: except HomeAssistantError as err:
self.sys_capture_exception(err) capture_exception(err)
else: else:
break break
@ -485,7 +488,7 @@ class HomeAssistantCore(CoreSysAttributes):
except HomeAssistantError as err: except HomeAssistantError as err:
attempts = attempts + 1 attempts = attempts + 1
_LOGGER.error("Watchdog restart of Home Assistant failed!") _LOGGER.error("Watchdog restart of Home Assistant failed!")
self.sys_capture_exception(err) capture_exception(err)
else: else:
break break

View File

@ -6,13 +6,12 @@ from functools import wraps
import logging import logging
from typing import Any from typing import Any
import sentry_sdk
from ..const import CoreState from ..const import CoreState
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import HassioError, JobConditionException, JobException from ..exceptions import HassioError, JobConditionException, JobException
from ..host.const import HostFeature from ..host.const import HostFeature
from ..resolution.const import MINIMUM_FREE_SPACE_THRESHOLD, ContextType, IssueType from ..resolution.const import MINIMUM_FREE_SPACE_THRESHOLD, ContextType, IssueType
from ..utils.sentry import capture_exception
from .const import JobCondition, JobExecutionLimit from .const import JobCondition, JobExecutionLimit
_LOGGER: logging.Logger = logging.getLogger(__package__) _LOGGER: logging.Logger = logging.getLogger(__package__)
@ -157,7 +156,7 @@ class Job(CoreSysAttributes):
raise err raise err
except Exception as err: except Exception as err:
_LOGGER.exception("Unhandled exception: %s", err) _LOGGER.exception("Unhandled exception: %s", err)
sentry_sdk.capture_exception(err) capture_exception(err)
raise JobException() from err raise JobException() from err
finally: finally:
if self.cleanup: if self.cleanup:

View File

@ -5,8 +5,7 @@ import re
from aiohttp import hdrs from aiohttp import hdrs
import attr import attr
from ..api.const import HEADER_TOKEN, HEADER_TOKEN_OLD from ..const import HEADER_TOKEN, HEADER_TOKEN_OLD, CoreState
from ..const import CoreState
from ..coresys import CoreSys from ..coresys import CoreSys
from ..exceptions import AddonConfigurationError from ..exceptions import AddonConfigurationError

View File

@ -7,6 +7,7 @@ from ..coresys import CoreSysAttributes
from ..exceptions import AddonsError, HomeAssistantError, ObserverError from ..exceptions import AddonsError, HomeAssistantError, ObserverError
from ..jobs.decorator import Job, JobCondition from ..jobs.decorator import Job, JobCondition
from ..plugins.const import PLUGIN_UPDATE_CONDITIONS from ..plugins.const import PLUGIN_UPDATE_CONDITIONS
from ..utils.sentry import capture_exception
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -159,7 +160,7 @@ class Tasks(CoreSysAttributes):
await self.sys_homeassistant.core.restart() await self.sys_homeassistant.core.restart()
except HomeAssistantError as err: except HomeAssistantError as err:
_LOGGER.error("Home Assistant watchdog reanimation failed!") _LOGGER.error("Home Assistant watchdog reanimation failed!")
self.sys_capture_exception(err) capture_exception(err)
finally: finally:
self._cache[HASS_WATCHDOG_API] = 0 self._cache[HASS_WATCHDOG_API] = 0
@ -265,7 +266,7 @@ class Tasks(CoreSysAttributes):
await addon.restart() await addon.restart()
except AddonsError as err: except AddonsError as err:
_LOGGER.error("%s watchdog reanimation failed with %s", addon.slug, err) _LOGGER.error("%s watchdog reanimation failed with %s", addon.slug, err)
self.sys_capture_exception(err) capture_exception(err)
finally: finally:
self._cache[addon.slug] = 0 self._cache[addon.slug] = 0

View File

@ -26,6 +26,7 @@ from ..exceptions import (
from ..jobs.const import JobExecutionLimit from ..jobs.const import JobExecutionLimit
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..utils.json import write_json_file from ..utils.json import write_json_file
from ..utils.sentry import capture_exception
from .base import PluginBase from .base import PluginBase
from .const import ( from .const import (
FILE_HASSIO_AUDIO, FILE_HASSIO_AUDIO,
@ -188,7 +189,7 @@ class PluginAudio(PluginBase):
await self.instance.install(self.version) await self.instance.install(self.version)
except DockerError as err: except DockerError as err:
_LOGGER.error("Repair of Audio failed") _LOGGER.error("Repair of Audio failed")
self.sys_capture_exception(err) capture_exception(err)
def pulse_client(self, input_profile=None, output_profile=None) -> str: def pulse_client(self, input_profile=None, output_profile=None) -> str:
"""Generate an /etc/pulse/client.conf data.""" """Generate an /etc/pulse/client.conf data."""

View File

@ -14,6 +14,7 @@ from ..docker.interface import DockerInterface
from ..docker.monitor import DockerContainerStateEvent from ..docker.monitor import DockerContainerStateEvent
from ..exceptions import DockerError, PluginError from ..exceptions import DockerError, PluginError
from ..utils.common import FileConfiguration from ..utils.common import FileConfiguration
from ..utils.sentry import capture_exception
from .const import WATCHDOG_MAX_ATTEMPTS, WATCHDOG_RETRY_SECONDS from .const import WATCHDOG_MAX_ATTEMPTS, WATCHDOG_RETRY_SECONDS
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -129,7 +130,7 @@ class PluginBase(ABC, FileConfiguration, CoreSysAttributes):
except PluginError as err: except PluginError as err:
attempts = attempts + 1 attempts = attempts + 1
_LOGGER.error("Watchdog restart of %s plugin failed!", self.slug) _LOGGER.error("Watchdog restart of %s plugin failed!", self.slug)
self.sys_capture_exception(err) capture_exception(err)
else: else:
break break

View File

@ -18,6 +18,7 @@ from ..docker.stats import DockerStats
from ..exceptions import CliError, CliJobError, CliUpdateError, DockerError from ..exceptions import CliError, CliJobError, CliUpdateError, DockerError
from ..jobs.const import JobExecutionLimit from ..jobs.const import JobExecutionLimit
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..utils.sentry import capture_exception
from .base import PluginBase from .base import PluginBase
from .const import ( from .const import (
FILE_HASSIO_CLI, FILE_HASSIO_CLI,
@ -147,7 +148,7 @@ class PluginCli(PluginBase):
await self.instance.install(self.version) await self.instance.install(self.version)
except DockerError as err: except DockerError as err:
_LOGGER.error("Repair of HA cli failed") _LOGGER.error("Repair of HA cli failed")
self.sys_capture_exception(err) capture_exception(err)
@Job( @Job(
limit=JobExecutionLimit.THROTTLE_RATE_LIMIT, limit=JobExecutionLimit.THROTTLE_RATE_LIMIT,

View File

@ -31,6 +31,7 @@ from ..jobs.const import JobExecutionLimit
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..resolution.const import ContextType, IssueType, SuggestionType from ..resolution.const import ContextType, IssueType, SuggestionType
from ..utils.json import write_json_file from ..utils.json import write_json_file
from ..utils.sentry import capture_exception
from ..validate import dns_url from ..validate import dns_url
from .base import PluginBase from .base import PluginBase
from .const import ( from .const import (
@ -421,7 +422,7 @@ class PluginDns(PluginBase):
await self.instance.install(self.version) await self.instance.install(self.version)
except DockerError as err: except DockerError as err:
_LOGGER.error("Repair of CoreDNS failed") _LOGGER.error("Repair of CoreDNS failed")
self.sys_capture_exception(err) capture_exception(err)
def _write_resolv(self, resolv_conf: Path) -> None: def _write_resolv(self, resolv_conf: Path) -> None:
"""Update/Write resolv.conf file.""" """Update/Write resolv.conf file."""

View File

@ -5,6 +5,7 @@ import logging
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import HassioError from ..exceptions import HassioError
from ..resolution.const import ContextType, IssueType, SuggestionType from ..resolution.const import ContextType, IssueType, SuggestionType
from ..utils.sentry import capture_exception
from .audio import PluginAudio from .audio import PluginAudio
from .base import PluginBase from .base import PluginBase
from .cli import PluginCli from .cli import PluginCli
@ -72,7 +73,7 @@ class PluginManager(CoreSysAttributes):
reference=plugin.slug, reference=plugin.slug,
suggestions=[SuggestionType.EXECUTE_REPAIR], suggestions=[SuggestionType.EXECUTE_REPAIR],
) )
self.sys_capture_exception(err) capture_exception(err)
# Check requirements # Check requirements
await self.sys_updater.reload() await self.sys_updater.reload()
@ -102,7 +103,7 @@ class PluginManager(CoreSysAttributes):
) )
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.warning("Can't update plugin %s: %s", plugin.slug, err) _LOGGER.warning("Can't update plugin %s: %s", plugin.slug, err)
self.sys_capture_exception(err) capture_exception(err)
async def repair(self) -> None: async def repair(self) -> None:
"""Repair Supervisor plugins.""" """Repair Supervisor plugins."""
@ -118,4 +119,4 @@ class PluginManager(CoreSysAttributes):
await plugin.stop() await plugin.stop()
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.warning("Can't stop plugin %s: %s", plugin.slug, err) _LOGGER.warning("Can't stop plugin %s: %s", plugin.slug, err)
self.sys_capture_exception(err) capture_exception(err)

View File

@ -20,6 +20,7 @@ from ..exceptions import (
) )
from ..jobs.const import JobExecutionLimit from ..jobs.const import JobExecutionLimit
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..utils.sentry import capture_exception
from .base import PluginBase from .base import PluginBase
from .const import ( from .const import (
FILE_HASSIO_MULTICAST, FILE_HASSIO_MULTICAST,
@ -142,7 +143,7 @@ class PluginMulticast(PluginBase):
await self.instance.install(self.version) await self.instance.install(self.version)
except DockerError as err: except DockerError as err:
_LOGGER.error("Repair of Multicast failed") _LOGGER.error("Repair of Multicast failed")
self.sys_capture_exception(err) capture_exception(err)
@Job( @Job(
limit=JobExecutionLimit.THROTTLE_RATE_LIMIT, limit=JobExecutionLimit.THROTTLE_RATE_LIMIT,

View File

@ -23,6 +23,7 @@ from ..exceptions import (
) )
from ..jobs.const import JobExecutionLimit from ..jobs.const import JobExecutionLimit
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..utils.sentry import capture_exception
from .base import PluginBase from .base import PluginBase
from .const import ( from .const import (
FILE_HASSIO_OBSERVER, FILE_HASSIO_OBSERVER,
@ -152,7 +153,7 @@ class PluginObserver(PluginBase):
await self.instance.install(self.version) await self.instance.install(self.version)
except DockerError as err: except DockerError as err:
_LOGGER.error("Repair of HA observer failed") _LOGGER.error("Repair of HA observer failed")
self.sys_capture_exception(err) capture_exception(err)
@Job( @Job(
limit=JobExecutionLimit.THROTTLE_RATE_LIMIT, limit=JobExecutionLimit.THROTTLE_RATE_LIMIT,

View File

@ -6,6 +6,7 @@ from typing import Any
from ..const import ATTR_CHECKS from ..const import ATTR_CHECKS
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import ResolutionNotFound from ..exceptions import ResolutionNotFound
from ..utils.sentry import capture_exception
from .checks.base import CheckBase from .checks.base import CheckBase
from .validate import get_valid_modules from .validate import get_valid_modules
@ -59,6 +60,6 @@ class ResolutionCheck(CoreSysAttributes):
await check() await check()
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.error("Error during processing %s: %s", check.issue, err) _LOGGER.error("Error during processing %s: %s", check.issue, err)
self.sys_capture_exception(err) capture_exception(err)
_LOGGER.info("System checks complete") _LOGGER.info("System checks complete")

View File

@ -9,6 +9,7 @@ from ...const import CoreState
from ...coresys import CoreSys from ...coresys import CoreSys
from ...jobs.const import JobCondition, JobExecutionLimit from ...jobs.const import JobCondition, JobExecutionLimit
from ...jobs.decorator import Job from ...jobs.decorator import Job
from ...utils.sentry import capture_exception
from ..const import DNS_CHECK_HOST, ContextType, IssueType from ..const import DNS_CHECK_HOST, ContextType, IssueType
from .base import CheckBase from .base import CheckBase
@ -39,7 +40,7 @@ class CheckDNSServer(CheckBase):
ContextType.DNS_SERVER, ContextType.DNS_SERVER,
reference=dns_servers[i], reference=dns_servers[i],
) )
self.sys_capture_exception(results[i]) capture_exception(results[i])
@Job(conditions=[JobCondition.INTERNET_SYSTEM]) @Job(conditions=[JobCondition.INTERNET_SYSTEM])
async def approve_check(self, reference: str | None = None) -> bool: async def approve_check(self, reference: str | None = None) -> bool:

View File

@ -9,6 +9,7 @@ from ...const import CoreState
from ...coresys import CoreSys from ...coresys import CoreSys
from ...jobs.const import JobCondition, JobExecutionLimit from ...jobs.const import JobCondition, JobExecutionLimit
from ...jobs.decorator import Job from ...jobs.decorator import Job
from ...utils.sentry import capture_exception
from ..const import DNS_CHECK_HOST, DNS_ERROR_NO_DATA, ContextType, IssueType from ..const import DNS_CHECK_HOST, DNS_ERROR_NO_DATA, ContextType, IssueType
from .base import CheckBase from .base import CheckBase
@ -44,7 +45,7 @@ class CheckDNSServerIPv6(CheckBase):
ContextType.DNS_SERVER, ContextType.DNS_SERVER,
reference=dns_servers[i], reference=dns_servers[i],
) )
self.sys_capture_exception(results[i]) capture_exception(results[i])
@Job(conditions=[JobCondition.INTERNET_SYSTEM]) @Job(conditions=[JobCondition.INTERNET_SYSTEM])
async def approve_check(self, reference: str | None = None) -> bool: async def approve_check(self, reference: str | None = None) -> bool:

View File

@ -4,6 +4,7 @@ import logging
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import ResolutionNotFound from ..exceptions import ResolutionNotFound
from ..utils.sentry import capture_exception
from .const import UnhealthyReason, UnsupportedReason from .const import UnhealthyReason, UnsupportedReason
from .evaluations.base import EvaluateBase from .evaluations.base import EvaluateBase
from .validate import get_valid_modules from .validate import get_valid_modules
@ -59,7 +60,7 @@ class ResolutionEvaluation(CoreSysAttributes):
_LOGGER.warning( _LOGGER.warning(
"Error during processing %s: %s", evaluation.reason, err "Error during processing %s: %s", evaluation.reason, err
) )
self.sys_capture_exception(err) capture_exception(err)
if any(reason in self.sys_resolution.unsupported for reason in UNHEALTHY): if any(reason in self.sys_resolution.unsupported for reason in UNHEALTHY):
self.sys_resolution.unhealthy = UnhealthyReason.DOCKER self.sys_resolution.unhealthy = UnhealthyReason.DOCKER

View File

@ -5,6 +5,7 @@ import logging
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..jobs.const import JobCondition from ..jobs.const import JobCondition
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..utils.sentry import capture_exception
from .data import Issue, Suggestion from .data import Issue, Suggestion
from .fixups.base import FixupBase from .fixups.base import FixupBase
from .validate import get_valid_modules from .validate import get_valid_modules
@ -47,7 +48,7 @@ class ResolutionFixup(CoreSysAttributes):
await fix() await fix()
except Exception as err: # pylint: disable=broad-except except Exception as err: # pylint: disable=broad-except
_LOGGER.warning("Error during processing %s: %s", fix.suggestion, err) _LOGGER.warning("Error during processing %s: %s", fix.suggestion, err)
self.sys_capture_exception(err) capture_exception(err)
_LOGGER.info("System autofix complete") _LOGGER.info("System autofix complete")

View File

@ -30,6 +30,7 @@ from .jobs.const import JobCondition, JobExecutionLimit
from .jobs.decorator import Job from .jobs.decorator import Job
from .resolution.const import ContextType, IssueType from .resolution.const import ContextType, IssueType
from .utils.codenotary import calc_checksum from .utils.codenotary import calc_checksum
from .utils.sentry import capture_exception
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -199,7 +200,7 @@ class Supervisor(CoreSysAttributes):
self.sys_resolution.create_issue( self.sys_resolution.create_issue(
IssueType.UPDATE_FAILED, ContextType.SUPERVISOR IssueType.UPDATE_FAILED, ContextType.SUPERVISOR
) )
self.sys_capture_exception(err) capture_exception(err)
raise SupervisorUpdateError( raise SupervisorUpdateError(
f"Update of Supervisor failed: {err!s}", _LOGGER.error f"Update of Supervisor failed: {err!s}", _LOGGER.error
) from err ) from err

View File

@ -2,7 +2,7 @@
import logging import logging
import re import re
import sentry_sdk from .sentry import capture_exception
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -19,6 +19,6 @@ def format_message(message: str) -> str:
return f"Port '{match.group(1)}' is already in use by something else on the host." return f"Port '{match.group(1)}' is already in use by something else on the host."
except TypeError as err: except TypeError as err:
_LOGGER.error("The type of message is not a string - %s", err) _LOGGER.error("The type of message is not a string - %s", err)
sentry_sdk.capture_exception(err) capture_exception(err)
return message return message

View File

@ -0,0 +1,60 @@
"""Utilities for sentry."""
import logging
import sentry_sdk
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
from sentry_sdk.integrations.atexit import AtexitIntegration
from sentry_sdk.integrations.dedupe import DedupeIntegration
from sentry_sdk.integrations.excepthook import ExcepthookIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.integrations.threading import ThreadingIntegration
from ..const import SUPERVISOR_VERSION
from ..coresys import CoreSys
from ..misc.filter import filter_data
_LOGGER: logging.Logger = logging.getLogger(__name__)
def sentry_connected() -> bool:
"""Is sentry connected."""
return sentry_sdk.Hub.current.client and sentry_sdk.Hub.current.client.transport
def init_sentry(coresys: CoreSys) -> None:
"""Initialize sentry client."""
if not sentry_connected():
_LOGGER.info("Initializing Supervisor Sentry")
sentry_sdk.init(
dsn="https://9c6ea70f49234442b4746e447b24747e@o427061.ingest.sentry.io/5370612",
before_send=lambda event, hint: filter_data(coresys, event, hint),
auto_enabling_integrations=False,
default_integrations=False,
integrations=[
AioHttpIntegration(),
ExcepthookIntegration(),
DedupeIntegration(),
AtexitIntegration(),
ThreadingIntegration(),
LoggingIntegration(level=logging.WARNING, event_level=logging.CRITICAL),
],
release=SUPERVISOR_VERSION,
max_breadcrumbs=30,
)
def capture_exception(err: Exception) -> None:
"""Capture an exception and send to sentry."""
if sentry_connected():
sentry_sdk.capture_exception(err)
def close_sentry() -> None:
"""Close the current sentry client.
This method is irreversible. A new client will have to be initialized to re-open connetion.
"""
if sentry_connected():
_LOGGER.info("Closing connection to Supervisor Sentry")
sentry_sdk.Hub.current.client.close()

View File

@ -1,15 +1,23 @@
"""Test addon manager.""" """Test addon manager."""
from unittest.mock import PropertyMock, patch from unittest.mock import Mock, PropertyMock, patch
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
import pytest import pytest
from supervisor.addons.addon import Addon from supervisor.addons.addon import Addon
from supervisor.arch import CpuArch from supervisor.arch import CpuArch
from supervisor.const import AddonBoot, AddonStartup, AddonState
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.docker.addon import DockerAddon from supervisor.docker.addon import DockerAddon
from supervisor.docker.interface import DockerInterface from supervisor.docker.interface import DockerInterface
from supervisor.exceptions import (
AddonConfigurationError,
AddonsError,
DockerAPIError,
DockerNotFound,
)
from supervisor.utils import check_exception_chain
from tests.common import load_json_fixture from tests.common import load_json_fixture
from tests.const import TEST_ADDON_SLUG from tests.const import TEST_ADDON_SLUG
@ -65,3 +73,60 @@ async def test_image_added_removed_on_update(
await install_addon_ssh.update() await install_addon_ssh.update()
build.assert_called_once_with(AwesomeVersion("11.0.0")) build.assert_called_once_with(AwesomeVersion("11.0.0"))
install.assert_not_called() install.assert_not_called()
@pytest.mark.parametrize("err", [DockerAPIError, DockerNotFound])
async def test_addon_boot_system_error(
coresys: CoreSys, install_addon_ssh: Addon, capture_exception: Mock, err
):
"""Test system errors during addon boot."""
install_addon_ssh.boot = AddonBoot.AUTO
with patch.object(Addon, "write_options"), patch.object(
DockerAddon, "run", side_effect=err
):
await coresys.addons.boot(AddonStartup.APPLICATION)
assert install_addon_ssh.boot == AddonBoot.MANUAL
capture_exception.assert_not_called()
async def test_addon_boot_user_error(
coresys: CoreSys, install_addon_ssh: Addon, capture_exception: Mock
):
"""Test user error during addon boot."""
install_addon_ssh.boot = AddonBoot.AUTO
with patch.object(Addon, "write_options", side_effect=AddonConfigurationError):
await coresys.addons.boot(AddonStartup.APPLICATION)
assert install_addon_ssh.boot == AddonBoot.MANUAL
capture_exception.assert_not_called()
async def test_addon_boot_other_error(
coresys: CoreSys, install_addon_ssh: Addon, capture_exception: Mock
):
"""Test other errors captured during addon boot."""
install_addon_ssh.boot = AddonBoot.AUTO
err = OSError()
with patch.object(Addon, "write_options"), patch.object(
DockerAddon, "run", side_effect=err
):
await coresys.addons.boot(AddonStartup.APPLICATION)
assert install_addon_ssh.boot == AddonBoot.AUTO
capture_exception.assert_called_once_with(err)
async def test_addon_shutdown_error(
coresys: CoreSys, install_addon_ssh: Addon, capture_exception: Mock
):
"""Test errors captured during addon shutdown."""
install_addon_ssh.state = AddonState.STARTED
with patch.object(DockerAddon, "stop", side_effect=DockerNotFound()):
await coresys.addons.shutdown(AddonStartup.APPLICATION)
assert install_addon_ssh.state == AddonState.ERROR
capture_exception.assert_called_once()
assert check_exception_chain(
capture_exception.call_args[0][0], (AddonsError, DockerNotFound)
)

View File

@ -124,10 +124,25 @@ async def test_api_supervisor_options_diagnostics(
await coresys.dbus.agent.connect(coresys.dbus.bus) await coresys.dbus.agent.connect(coresys.dbus.bus)
dbus.clear() dbus.clear()
response = await api_client.post("/supervisor/options", json={"diagnostics": True}) with patch("supervisor.utils.sentry.sentry_sdk.init") as sentry_init:
await asyncio.sleep(0) response = await api_client.post(
"/supervisor/options", json={"diagnostics": True}
)
assert response.status == 200 assert response.status == 200
sentry_init.assert_called_once()
await asyncio.sleep(0)
assert dbus == ["/io/hass/os-io.hass.os.Diagnostics"]
dbus.clear()
with patch("supervisor.api.supervisor.close_sentry") as close_sentry:
response = await api_client.post(
"/supervisor/options", json={"diagnostics": False}
)
assert response.status == 200
close_sentry.assert_called_once()
await asyncio.sleep(0)
assert dbus == ["/io/hass/os-io.hass.os.Diagnostics"] assert dbus == ["/io/hass/os-io.hass.os.Diagnostics"]

View File

@ -1,11 +1,13 @@
"""Test BackupManager class.""" """Test BackupManager class."""
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch
from supervisor.addons.addon import Addon
from supervisor.backups.const import BackupType from supervisor.backups.const import BackupType
from supervisor.backups.manager import BackupManager from supervisor.backups.manager import BackupManager
from supervisor.const import FOLDER_HOMEASSISTANT, FOLDER_SHARE, CoreState from supervisor.const import FOLDER_HOMEASSISTANT, FOLDER_SHARE, CoreState
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.exceptions import AddonsError, DockerError
from tests.const import TEST_ADDON_SLUG from tests.const import TEST_ADDON_SLUG
@ -321,3 +323,34 @@ async def test_fail_invalid_partial_backup(
type(coresys.supervisor), "version", new=PropertyMock(return_value="2022.08.3") type(coresys.supervisor), "version", new=PropertyMock(return_value="2022.08.3")
): ):
assert await manager.do_restore_partial(backup_instance) is False assert await manager.do_restore_partial(backup_instance) is False
async def test_backup_error(
coresys: CoreSys,
backup_mock: MagicMock,
install_addon_ssh: Addon,
capture_exception: Mock,
):
"""Test error captured when backup fails."""
coresys.core.state = CoreState.RUNNING
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
backup_mock.return_value.store_addons.side_effect = (err := AddonsError())
await coresys.backups.do_backup_full()
capture_exception.assert_called_once_with(err)
async def test_restore_error(
coresys: CoreSys, full_backup_mock: MagicMock, capture_exception: Mock
):
"""Test restoring full Backup."""
coresys.core.state = CoreState.RUNNING
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
coresys.homeassistant.core.start = AsyncMock(return_value=None)
backup_instance = full_backup_mock.return_value
backup_instance.restore_dockerconfig.side_effect = (err := DockerError())
await coresys.backups.do_restore_full(backup_instance)
capture_exception.assert_called_once_with(err)

View File

@ -3,7 +3,7 @@ from functools import partial
from inspect import unwrap from inspect import unwrap
from pathlib import Path from pathlib import Path
import re import re
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch
from uuid import uuid4 from uuid import uuid4
from aiohttp import web from aiohttp import web
@ -302,7 +302,7 @@ async def coresys(
) -> CoreSys: ) -> CoreSys:
"""Create a CoreSys Mock.""" """Create a CoreSys Mock."""
with patch("supervisor.bootstrap.initialize_system"), patch( with patch("supervisor.bootstrap.initialize_system"), patch(
"supervisor.bootstrap.setup_diagnostics" "supervisor.utils.sentry.sentry_sdk.init"
): ):
coresys_obj = await initialize_coresys() coresys_obj = await initialize_coresys()
@ -580,3 +580,12 @@ async def docker_logs(docker: DockerAPI) -> MagicMock:
os.environ = {"SUPERVISOR_NAME": "hassio_supervisor"} os.environ = {"SUPERVISOR_NAME": "hassio_supervisor"}
yield container_mock.logs yield container_mock.logs
@pytest.fixture
async def capture_exception() -> Mock:
"""Mock capture exception method for testing."""
with patch("supervisor.utils.sentry.sentry_connected", return_value=True), patch(
"supervisor.utils.sentry.sentry_sdk.capture_exception"
) as capture_exception:
yield capture_exception

View File

@ -1,7 +1,7 @@
"""Test NetworkInterface.""" """Test NetworkInterface."""
import asyncio import asyncio
import logging import logging
from unittest.mock import AsyncMock, PropertyMock, patch from unittest.mock import AsyncMock, Mock, PropertyMock, patch
from dbus_fast.aio.message_bus import MessageBus from dbus_fast.aio.message_bus import MessageBus
import pytest import pytest
@ -114,7 +114,9 @@ async def test_removed_devices_disconnect(network_manager: NetworkManager):
async def test_handling_bad_devices( async def test_handling_bad_devices(
network_manager: NetworkManager, caplog: pytest.LogCaptureFixture network_manager: NetworkManager,
caplog: pytest.LogCaptureFixture,
capture_exception: Mock,
): ):
"""Test handling of bad and disappearing devices.""" """Test handling of bad and disappearing devices."""
caplog.clear() caplog.clear()
@ -135,14 +137,12 @@ async def test_handling_bad_devices(
# Unparseable introspections shouldn't happen, this one is logged and captured # Unparseable introspections shouldn't happen, this one is logged and captured
await network_manager.update() await network_manager.update()
with patch.object( with patch.object(DBus, "_init_proxy", side_effect=(err := DBusParseError())):
DBus, "_init_proxy", side_effect=(err := DBusParseError())
), patch("supervisor.dbus.network.sentry_sdk.capture_exception") as capture:
await network_manager.update( await network_manager.update(
{"Devices": [device := "/org/freedesktop/NetworkManager/Devices/102"]} {"Devices": [device := "/org/freedesktop/NetworkManager/Devices/102"]}
) )
assert f"Error while processing {device}" in caplog.text assert f"Error while processing {device}" in caplog.text
capture.assert_called_once_with(err) capture_exception.assert_called_once_with(err)
# We should be able to debug these situations if necessary # We should be able to debug these situations if necessary
caplog.set_level(logging.DEBUG, "supervisor.dbus.network") caplog.set_level(logging.DEBUG, "supervisor.dbus.network")

View File

@ -1,14 +1,21 @@
"""Test docker addon setup.""" """Test docker addon setup."""
from unittest.mock import MagicMock, PropertyMock, patch from ipaddress import IPv4Address
from unittest.mock import MagicMock, Mock, PropertyMock, patch
from docker.errors import NotFound
import pytest import pytest
from supervisor.addons import validate as vd from supervisor.addons import validate as vd
from supervisor.addons.addon import Addon from supervisor.addons.addon import Addon
from supervisor.addons.model import Data from supervisor.addons.model import Data
from supervisor.addons.options import AddonOptions
from supervisor.const import SYSTEMD_JOURNAL_PERSISTENT, SYSTEMD_JOURNAL_VOLATILE from supervisor.const import SYSTEMD_JOURNAL_PERSISTENT, SYSTEMD_JOURNAL_VOLATILE
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.docker.addon import DockerAddon from supervisor.docker.addon import DockerAddon
from supervisor.exceptions import CoreDNSError, DockerNotFound
from supervisor.plugins.dns import PluginDns
from supervisor.resolution.const import ContextType, IssueType
from supervisor.resolution.data import Issue
from ..common import load_json_fixture from ..common import load_json_fixture
@ -32,7 +39,7 @@ def fixture_addonsdata_user() -> dict[str, Data]:
yield mock yield mock
@pytest.fixture(name="os_environ", autouse=True) @pytest.fixture(name="os_environ")
def fixture_os_environ(): def fixture_os_environ():
"""Mock os.environ.""" """Mock os.environ."""
with patch("supervisor.config.os.environ") as mock: with patch("supervisor.config.os.environ") as mock:
@ -52,7 +59,9 @@ def get_docker_addon(
return docker_addon return docker_addon
def test_base_volumes_included(coresys: CoreSys, addonsdata_system: dict[str, Data]): def test_base_volumes_included(
coresys: CoreSys, addonsdata_system: dict[str, Data], os_environ
):
"""Dev and data volumes always included.""" """Dev and data volumes always included."""
docker_addon = get_docker_addon( docker_addon = get_docker_addon(
coresys, addonsdata_system, "basic-addon-config.json" coresys, addonsdata_system, "basic-addon-config.json"
@ -72,7 +81,7 @@ def test_base_volumes_included(coresys: CoreSys, addonsdata_system: dict[str, Da
def test_addon_map_folder_defaults( def test_addon_map_folder_defaults(
coresys: CoreSys, addonsdata_system: dict[str, Data] coresys: CoreSys, addonsdata_system: dict[str, Data], os_environ
): ):
"""Validate defaults for mapped folders in addons.""" """Validate defaults for mapped folders in addons."""
docker_addon = get_docker_addon( docker_addon = get_docker_addon(
@ -96,7 +105,9 @@ def test_addon_map_folder_defaults(
assert str(docker_addon.sys_config.path_extern_share) not in volumes assert str(docker_addon.sys_config.path_extern_share) not in volumes
def test_journald_addon(coresys: CoreSys, addonsdata_system: dict[str, Data]): def test_journald_addon(
coresys: CoreSys, addonsdata_system: dict[str, Data], os_environ
):
"""Validate volume for journald option.""" """Validate volume for journald option."""
docker_addon = get_docker_addon( docker_addon = get_docker_addon(
coresys, addonsdata_system, "journald-addon-config.json" coresys, addonsdata_system, "journald-addon-config.json"
@ -115,7 +126,9 @@ def test_journald_addon(coresys: CoreSys, addonsdata_system: dict[str, Data]):
assert volumes.get(str(SYSTEMD_JOURNAL_VOLATILE)).get("mode") == "ro" assert volumes.get(str(SYSTEMD_JOURNAL_VOLATILE)).get("mode") == "ro"
def test_not_journald_addon(coresys: CoreSys, addonsdata_system: dict[str, Data]): def test_not_journald_addon(
coresys: CoreSys, addonsdata_system: dict[str, Data], os_environ
):
"""Validate journald option defaults off.""" """Validate journald option defaults off."""
docker_addon = get_docker_addon( docker_addon = get_docker_addon(
coresys, addonsdata_system, "basic-addon-config.json" coresys, addonsdata_system, "basic-addon-config.json"
@ -123,3 +136,68 @@ def test_not_journald_addon(coresys: CoreSys, addonsdata_system: dict[str, Data]
volumes = docker_addon.volumes volumes = docker_addon.volumes
assert str(SYSTEMD_JOURNAL_PERSISTENT) not in volumes assert str(SYSTEMD_JOURNAL_PERSISTENT) not in volumes
async def test_addon_run_docker_error(
coresys: CoreSys,
addonsdata_system: dict[str, Data],
capture_exception: Mock,
os_environ,
):
"""Test docker error when addon is run."""
await coresys.dbus.timedate.connect(coresys.dbus.bus)
coresys.docker.docker.containers.create.side_effect = NotFound("Missing")
docker_addon = get_docker_addon(
coresys, addonsdata_system, "basic-addon-config.json"
)
with patch.object(DockerAddon, "_stop"), patch.object(
AddonOptions, "validate", new=PropertyMock(return_value=lambda _: None)
), pytest.raises(DockerNotFound):
await docker_addon.run()
assert (
Issue(IssueType.MISSING_IMAGE, ContextType.ADDON, reference="test_addon")
in coresys.resolution.issues
)
capture_exception.assert_not_called()
async def test_addon_run_add_host_error(
coresys: CoreSys,
addonsdata_system: dict[str, Data],
capture_exception: Mock,
os_environ,
):
"""Test error adding host when addon is run."""
await coresys.dbus.timedate.connect(coresys.dbus.bus)
docker_addon = get_docker_addon(
coresys, addonsdata_system, "basic-addon-config.json"
)
with patch.object(DockerAddon, "_stop"), patch.object(
AddonOptions, "validate", new=PropertyMock(return_value=lambda _: None)
), patch.object(PluginDns, "add_host", side_effect=(err := CoreDNSError())):
await docker_addon.run()
capture_exception.assert_called_once_with(err)
async def test_addon_stop_delete_host_error(
coresys: CoreSys,
addonsdata_system: dict[str, Data],
capture_exception: Mock,
):
"""Test error deleting host when addon is stopped."""
docker_addon = get_docker_addon(
coresys, addonsdata_system, "basic-addon-config.json"
)
with patch.object(
DockerAddon,
"ip_address",
new=PropertyMock(return_value=IPv4Address("172.30.33.1")),
), patch.object(PluginDns, "delete_host", side_effect=(err := CoreDNSError())):
await docker_addon.stop()
capture_exception.assert_called_once_with(err)

View File

@ -206,3 +206,17 @@ async def test_attach_total_failure(coresys: CoreSys):
DockerError DockerError
): ):
await coresys.homeassistant.core.instance.attach(AwesomeVersion("2022.7.3")) await coresys.homeassistant.core.instance.attach(AwesomeVersion("2022.7.3"))
@pytest.mark.parametrize("err", [DockerException(), RequestException()])
async def test_image_pull_fail(
coresys: CoreSys, capture_exception: Mock, err: Exception
):
"""Test failure to pull image."""
coresys.docker.images.pull.side_effect = err
with pytest.raises(DockerError):
await coresys.homeassistant.core.instance.install(
AwesomeVersion("2022.7.3"), arch=CpuArch.AMD64
)
capture_exception.assert_called_once_with(err)

View File

@ -1,10 +1,20 @@
"""Test Home Assistant core.""" """Test Home Assistant core."""
from unittest.mock import PropertyMock, patch from unittest.mock import MagicMock, Mock, PropertyMock, patch
import pytest import pytest
from supervisor.const import CpuArch
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.exceptions import AudioUpdateError, HomeAssistantJobError from supervisor.docker.homeassistant import DockerHomeAssistant
from supervisor.docker.interface import DockerInterface
from supervisor.exceptions import (
AudioUpdateError,
CodeNotaryError,
DockerError,
HomeAssistantJobError,
)
from supervisor.homeassistant.core import HomeAssistantCore
from supervisor.updater import Updater
async def test_update_fails_if_out_of_date(coresys: CoreSys): async def test_update_fails_if_out_of_date(coresys: CoreSys):
@ -24,3 +34,99 @@ async def test_update_fails_if_out_of_date(coresys: CoreSys):
HomeAssistantJobError HomeAssistantJobError
): ):
await coresys.homeassistant.core.update() await coresys.homeassistant.core.update()
async def test_install_landingpage_docker_error(
coresys: CoreSys, capture_exception: Mock, caplog: pytest.LogCaptureFixture
):
"""Test install landing page fails due to docker error."""
coresys.security.force = True
with patch.object(
DockerHomeAssistant, "attach", side_effect=DockerError
), patch.object(
Updater, "image_homeassistant", new=PropertyMock(return_value="homeassistant")
), patch.object(
DockerInterface, "arch", new=PropertyMock(return_value=CpuArch.AMD64)
), patch(
"supervisor.homeassistant.core.asyncio.sleep"
) as sleep, patch(
"supervisor.security.module.cas_validate",
side_effect=[CodeNotaryError, None],
):
await coresys.homeassistant.core.install_landingpage()
sleep.assert_awaited_once_with(30)
assert "Fails install landingpage, retry after 30sec" in caplog.text
capture_exception.assert_not_called()
async def test_install_landingpage_other_error(
coresys: CoreSys, capture_exception: Mock, caplog: pytest.LogCaptureFixture
):
"""Test install landing page fails due to other error."""
coresys.docker.images.pull.side_effect = [(err := OSError()), MagicMock()]
with patch.object(
DockerHomeAssistant, "attach", side_effect=DockerError
), patch.object(
Updater, "image_homeassistant", new=PropertyMock(return_value="homeassistant")
), patch.object(
DockerInterface, "arch", new=PropertyMock(return_value=CpuArch.AMD64)
), patch(
"supervisor.homeassistant.core.asyncio.sleep"
) as sleep:
await coresys.homeassistant.core.install_landingpage()
sleep.assert_awaited_once_with(30)
assert "Fails install landingpage, retry after 30sec" in caplog.text
capture_exception.assert_called_once_with(err)
async def test_install_docker_error(
coresys: CoreSys, capture_exception: Mock, caplog: pytest.LogCaptureFixture
):
"""Test install fails due to docker error."""
coresys.security.force = True
with patch.object(HomeAssistantCore, "_start"), patch.object(
DockerHomeAssistant, "cleanup"
), patch.object(
Updater, "image_homeassistant", new=PropertyMock(return_value="homeassistant")
), patch.object(
Updater, "version_homeassistant", new=PropertyMock(return_value="2022.7.3")
), patch.object(
DockerInterface, "arch", new=PropertyMock(return_value=CpuArch.AMD64)
), patch(
"supervisor.homeassistant.core.asyncio.sleep"
) as sleep, patch(
"supervisor.security.module.cas_validate",
side_effect=[CodeNotaryError, None],
):
await coresys.homeassistant.core.install()
sleep.assert_awaited_once_with(30)
assert "Error on Home Assistant installation. Retry in 30sec" in caplog.text
capture_exception.assert_not_called()
async def test_install_other_error(
coresys: CoreSys, capture_exception: Mock, caplog: pytest.LogCaptureFixture
):
"""Test install fails due to other error."""
coresys.docker.images.pull.side_effect = [(err := OSError()), MagicMock()]
with patch.object(HomeAssistantCore, "_start"), patch.object(
DockerHomeAssistant, "cleanup"
), patch.object(
Updater, "image_homeassistant", new=PropertyMock(return_value="homeassistant")
), patch.object(
Updater, "version_homeassistant", new=PropertyMock(return_value="2022.7.3")
), patch.object(
DockerInterface, "arch", new=PropertyMock(return_value=CpuArch.AMD64)
), patch(
"supervisor.homeassistant.core.asyncio.sleep"
) as sleep:
await coresys.homeassistant.core.install()
sleep.assert_awaited_once_with(30)
assert "Error on Home Assistant installation. Retry in 30sec" in caplog.text
capture_exception.assert_called_once_with(err)

View File

@ -2,7 +2,7 @@
# pylint: disable=protected-access,import-error # pylint: disable=protected-access,import-error
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
from unittest.mock import AsyncMock, PropertyMock, patch from unittest.mock import AsyncMock, Mock, PropertyMock, patch
from aiohttp.client_exceptions import ClientError from aiohttp.client_exceptions import ClientError
import pytest import pytest
@ -512,3 +512,26 @@ async def test_unhealthy(coresys: CoreSys, caplog: pytest.LogCaptureFixture):
coresys.jobs.ignore_conditions = [JobCondition.HEALTHY] coresys.jobs.ignore_conditions = [JobCondition.HEALTHY]
assert await test.execute() assert await test.execute()
async def test_unhandled_exception(coresys: CoreSys, capture_exception: Mock):
"""Test an unhandled exception from job."""
err = OSError()
class TestClass:
"""Test class."""
def __init__(self, coresys: CoreSys):
"""Initialize the test class."""
self.coresys = coresys
@Job(conditions=[JobCondition.HEALTHY])
async def execute(self) -> None:
"""Execute the class method."""
raise err
test = TestClass(coresys)
with pytest.raises(JobException):
await test.execute()
capture_exception.assert_called_once_with(err)

View File

@ -1,19 +1,21 @@
"""Test base plugin functionality.""" """Test base plugin functionality."""
import asyncio import asyncio
from unittest.mock import PropertyMock, patch from unittest.mock import Mock, PropertyMock, patch
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
import pytest import pytest
from supervisor.const import BusEvent from supervisor.const import BusEvent, CpuArch
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.docker.const import ContainerState from supervisor.docker.const import ContainerState
from supervisor.docker.interface import DockerInterface
from supervisor.docker.monitor import DockerContainerStateEvent from supervisor.docker.monitor import DockerContainerStateEvent
from supervisor.exceptions import ( from supervisor.exceptions import (
AudioError, AudioError,
AudioJobError, AudioJobError,
CliError, CliError,
CliJobError, CliJobError,
CodeNotaryUntrusted,
CoreDNSError, CoreDNSError,
CoreDNSJobError, CoreDNSJobError,
DockerError, DockerError,
@ -30,6 +32,7 @@ from supervisor.plugins.cli import PluginCli
from supervisor.plugins.dns import PluginDns from supervisor.plugins.dns import PluginDns
from supervisor.plugins.multicast import PluginMulticast from supervisor.plugins.multicast import PluginMulticast
from supervisor.plugins.observer import PluginObserver from supervisor.plugins.observer import PluginObserver
from supervisor.utils import check_exception_chain
@pytest.fixture(name="plugin") @pytest.fixture(name="plugin")
@ -159,16 +162,16 @@ async def test_plugin_watchdog(coresys: CoreSys, plugin: PluginBase) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"plugin,error", "plugin,error",
[ [
(PluginAudio, AudioError), (PluginAudio, AudioError()),
(PluginCli, CliError), (PluginCli, CliError()),
(PluginDns, CoreDNSError), (PluginDns, CoreDNSError()),
(PluginMulticast, MulticastError), (PluginMulticast, MulticastError()),
(PluginObserver, ObserverError), (PluginObserver, ObserverError()),
], ],
indirect=["plugin"], indirect=["plugin"],
) )
async def test_plugin_watchdog_rebuild_on_failure( async def test_plugin_watchdog_rebuild_on_failure(
coresys: CoreSys, plugin: PluginBase, error: PluginError coresys: CoreSys, capture_exception: Mock, plugin: PluginBase, error: PluginError
) -> None: ) -> None:
"""Test plugin watchdog rebuilds if start fails.""" """Test plugin watchdog rebuilds if start fails."""
with patch.object(type(plugin.instance), "attach"), patch.object( with patch.object(type(plugin.instance), "attach"), patch.object(
@ -201,6 +204,8 @@ async def test_plugin_watchdog_rebuild_on_failure(
start.assert_called_once() start.assert_called_once()
rebuild.assert_called_once() rebuild.assert_called_once()
capture_exception.assert_called_once_with(error)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"plugin", "plugin",
@ -331,3 +336,25 @@ async def test_update_fails_if_out_of_date(
type(coresys.supervisor), "need_update", new=PropertyMock(return_value=True) type(coresys.supervisor), "need_update", new=PropertyMock(return_value=True)
), pytest.raises(error): ), pytest.raises(error):
await plugin.update() await plugin.update()
@pytest.mark.parametrize(
"plugin",
[PluginAudio, PluginCli, PluginDns, PluginMulticast, PluginObserver],
indirect=True,
)
async def test_repair_failed(
coresys: CoreSys, capture_exception: Mock, plugin: PluginBase
):
"""Test repair failed."""
with patch.object(
DockerInterface, "exists", return_value=mock_is_running(False)
), patch.object(
DockerInterface, "arch", new=PropertyMock(return_value=CpuArch.AMD64)
), patch(
"supervisor.security.module.cas_validate", side_effect=CodeNotaryUntrusted
):
await plugin.repair()
capture_exception.assert_called_once()
assert check_exception_chain(capture_exception.call_args[0][0], CodeNotaryUntrusted)

View File

@ -1,5 +1,5 @@
"""Test check DNS Servers for failures.""" """Test check DNS Servers for failures."""
from unittest.mock import AsyncMock, call, patch from unittest.mock import AsyncMock, Mock, call, patch
from aiodns.error import DNSError from aiodns.error import DNSError
import pytest import pytest
@ -27,7 +27,7 @@ async def test_base(coresys: CoreSys):
assert dns_server.enabled assert dns_server.enabled
async def test_check(coresys: CoreSys, dns_query: AsyncMock): async def test_check(coresys: CoreSys, dns_query: AsyncMock, capture_exception: Mock):
"""Test check for DNS server failures.""" """Test check for DNS server failures."""
dns_server = CheckDNSServer(coresys) dns_server = CheckDNSServer(coresys)
coresys.core.state = CoreState.RUNNING coresys.core.state = CoreState.RUNNING
@ -50,7 +50,7 @@ async def test_check(coresys: CoreSys, dns_query: AsyncMock):
coresys.plugins.dns.servers = [] coresys.plugins.dns.servers = []
assert dns_server.dns_servers == ["dns://192.168.30.1"] assert dns_server.dns_servers == ["dns://192.168.30.1"]
dns_query.side_effect = DNSError() dns_query.side_effect = (err := DNSError())
await dns_server.run_check.__wrapped__(dns_server) await dns_server.run_check.__wrapped__(dns_server)
dns_query.assert_called_once_with("_checkdns.home-assistant.io", "A") dns_query.assert_called_once_with("_checkdns.home-assistant.io", "A")
@ -58,6 +58,7 @@ async def test_check(coresys: CoreSys, dns_query: AsyncMock):
assert coresys.resolution.issues[0].type is IssueType.DNS_SERVER_FAILED assert coresys.resolution.issues[0].type is IssueType.DNS_SERVER_FAILED
assert coresys.resolution.issues[0].context is ContextType.DNS_SERVER assert coresys.resolution.issues[0].context is ContextType.DNS_SERVER
assert coresys.resolution.issues[0].reference == "dns://192.168.30.1" assert coresys.resolution.issues[0].reference == "dns://192.168.30.1"
capture_exception.assert_called_once_with(err)
async def test_approve(coresys: CoreSys, dns_query: AsyncMock): async def test_approve(coresys: CoreSys, dns_query: AsyncMock):

View File

@ -1,5 +1,5 @@
"""Test check DNS Servers for IPv6 errors.""" """Test check DNS Servers for IPv6 errors."""
from unittest.mock import AsyncMock, call, patch from unittest.mock import AsyncMock, Mock, call, patch
from aiodns.error import DNSError from aiodns.error import DNSError
import pytest import pytest
@ -27,7 +27,7 @@ async def test_base(coresys: CoreSys):
assert dns_server_ipv6.enabled assert dns_server_ipv6.enabled
async def test_check(coresys: CoreSys, dns_query: AsyncMock): async def test_check(coresys: CoreSys, dns_query: AsyncMock, capture_exception: Mock):
"""Test check for DNS server IPv6 errors.""" """Test check for DNS server IPv6 errors."""
dns_server_ipv6 = CheckDNSServerIPv6(coresys) dns_server_ipv6 = CheckDNSServerIPv6(coresys)
coresys.core.state = CoreState.RUNNING coresys.core.state = CoreState.RUNNING
@ -56,7 +56,7 @@ async def test_check(coresys: CoreSys, dns_query: AsyncMock):
assert len(coresys.resolution.issues) == 0 assert len(coresys.resolution.issues) == 0
dns_query.reset_mock() dns_query.reset_mock()
dns_query.side_effect = DNSError(4, "Domain name not found") dns_query.side_effect = (err := DNSError(4, "Domain name not found"))
await dns_server_ipv6.run_check.__wrapped__(dns_server_ipv6) await dns_server_ipv6.run_check.__wrapped__(dns_server_ipv6)
dns_query.assert_called_once_with("_checkdns.home-assistant.io", "AAAA") dns_query.assert_called_once_with("_checkdns.home-assistant.io", "AAAA")
@ -64,6 +64,7 @@ async def test_check(coresys: CoreSys, dns_query: AsyncMock):
assert coresys.resolution.issues[0].type is IssueType.DNS_SERVER_IPV6_ERROR assert coresys.resolution.issues[0].type is IssueType.DNS_SERVER_IPV6_ERROR
assert coresys.resolution.issues[0].context is ContextType.DNS_SERVER assert coresys.resolution.issues[0].context is ContextType.DNS_SERVER
assert coresys.resolution.issues[0].reference == "dns://192.168.30.1" assert coresys.resolution.issues[0].reference == "dns://192.168.30.1"
capture_exception.assert_called_once_with(err)
async def test_approve(coresys: CoreSys, dns_query: AsyncMock): async def test_approve(coresys: CoreSys, dns_query: AsyncMock):

View File

@ -0,0 +1,21 @@
"""Test checks."""
from unittest.mock import Mock, patch
from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.resolution.checks.core_security import CheckCoreSecurity
from supervisor.utils import check_exception_chain
async def test_check_system_error(coresys: CoreSys, capture_exception: Mock):
"""Test error while checking system."""
coresys.core.state = CoreState.STARTUP
with patch.object(CheckCoreSecurity, "run_check", side_effect=ValueError), patch(
"shutil.disk_usage", return_value=(42, 42, 2 * (1024.0**3))
):
await coresys.resolution.check.check_system()
capture_exception.assert_called_once()
assert check_exception_chain(capture_exception.call_args[0][0], ValueError)

View File

@ -0,0 +1,21 @@
"""Test evaluations."""
from unittest.mock import Mock, patch
from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.utils import check_exception_chain
async def test_evaluate_system_error(coresys: CoreSys, capture_exception: Mock):
"""Test error while evaluating system."""
coresys.core.state = CoreState.RUNNING
with patch(
"supervisor.resolution.evaluations.source_mods.calc_checksum_path_sourcecode",
side_effect=OSError,
):
await coresys.resolution.evaluate.evaluate_system()
capture_exception.assert_called_once()
assert check_exception_chain(capture_exception.call_args[0][0], OSError)

View File

@ -1,12 +1,17 @@
"""Test supervisor object.""" """Test supervisor object."""
from datetime import timedelta from datetime import timedelta
from unittest.mock import AsyncMock, PropertyMock, patch from unittest.mock import AsyncMock, Mock, PropertyMock, patch
from aiohttp.client_exceptions import ClientError from aiohttp.client_exceptions import ClientError
from awesomeversion import AwesomeVersion
import pytest import pytest
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.docker.supervisor import DockerSupervisor
from supervisor.exceptions import DockerError, SupervisorUpdateError
from supervisor.resolution.const import ContextType, IssueType
from supervisor.resolution.data import Issue
from supervisor.supervisor import Supervisor from supervisor.supervisor import Supervisor
@ -63,3 +68,18 @@ async def test_connectivity_check_throttling(
await coresys.supervisor.check_connectivity() await coresys.supervisor.check_connectivity()
assert websession.head.call_count == call_count assert websession.head.call_count == call_count
async def test_update_failed(coresys: CoreSys, capture_exception: Mock):
"""Test update failure."""
err = DockerError()
with patch.object(DockerSupervisor, "install", side_effect=err), patch.object(
type(coresys.supervisor), "update_apparmor"
), pytest.raises(SupervisorUpdateError):
await coresys.supervisor.update(AwesomeVersion("1.0"))
capture_exception.assert_called_once_with(err)
assert (
Issue(IssueType.UPDATE_FAILED, ContextType.SUPERVISOR)
in coresys.resolution.issues
)