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 ..store.addon import AddonStore
from ..utils import check_exception_chain
from ..utils.sentry import capture_exception
from .addon import Addon
from .const import ADDON_UPDATE_CONDITIONS
from .data import AddonsData
@ -114,7 +115,7 @@ class AddonManager(CoreSysAttributes):
addon.boot = AddonBoot.MANUAL
addon.save_persist()
except Exception as err: # pylint: disable=broad-except
self.sys_capture_exception(err)
capture_exception(err)
else:
continue
@ -142,7 +143,7 @@ class AddonManager(CoreSysAttributes):
await addon.stop()
except Exception as err: # pylint: disable=broad-except
_LOGGER.warning("Can't stop Add-on %s: %s", addon.slug, err)
self.sys_capture_exception(err)
capture_exception(err)
@Job(
conditions=ADDON_UPDATE_CONDITIONS,
@ -421,7 +422,7 @@ class AddonManager(CoreSysAttributes):
reference=addon.slug,
suggestions=[SuggestionType.EXECUTE_REPAIR],
)
self.sys_capture_exception(err)
capture_exception(err)
else:
self.sys_plugins.dns.add_host(
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"
HEADER_TOKEN_OLD = "X-Hassio-Key"
HEADER_TOKEN = "X-Supervisor-Token"
ATTR_APPARMOR_VERSION = "apparmor_version"
ATTR_AGENT_VERSION = "agent_version"
ATTR_AVAILABLE_UPDATES = "available_updates"

View File

@ -22,9 +22,11 @@ from ..const import (
ATTR_PANELS,
ATTR_SESSION,
ATTR_TITLE,
HEADER_TOKEN,
HEADER_TOKEN_OLD,
)
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
_LOGGER: logging.Logger = logging.getLogger(__name__)

View File

@ -46,6 +46,7 @@ from ..const import (
from ..coresys import CoreSysAttributes
from ..exceptions import APIError
from ..store.validate import repositories
from ..utils.sentry import close_sentry, init_sentry
from ..utils.validate import validate_timezone
from ..validate import version_tag, wait_boot
from .const import CONTENT_TYPE_BINARY
@ -144,6 +145,11 @@ class APISupervisor(CoreSysAttributes):
self.sys_config.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:
self.sys_config.logging = body[ATTR_LOGGING]

View File

@ -10,6 +10,8 @@ import voluptuous as vol
from voluptuous.humanize import humanize_error
from ..const import (
HEADER_TOKEN,
HEADER_TOKEN_OLD,
JSON_DATA,
JSON_MESSAGE,
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.json import JSONEncoder
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:

View File

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

View File

@ -5,13 +5,6 @@ from pathlib import Path
import signal
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 .api import RestAPI
@ -28,7 +21,6 @@ from .const import (
ENV_SUPERVISOR_SHARE,
MACHINE_ID,
SOCKET_DOCKER,
SUPERVISOR_VERSION,
LogLevel,
UpdateChannel,
)
@ -42,7 +34,6 @@ from .homeassistant.module import HomeAssistant
from .host.manager import HostManager
from .ingress import Ingress
from .jobs import JobManager
from .misc.filter import filter_data
from .misc.scheduler import Scheduler
from .misc.tasks import Tasks
from .os.manager import OSManager
@ -54,6 +45,7 @@ from .store import StoreManager
from .store.validate import ensure_builtin_repositories
from .supervisor import Supervisor
from .updater import Updater
from .utils.sentry import init_sentry
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -90,7 +82,8 @@ async def initialize_coresys() -> CoreSys:
coresys.bus = Bus(coresys)
# diagnostics
setup_diagnostics(coresys)
if coresys.config.diagnostics:
init_sentry(coresys)
# bootstrap config
initialize_system(coresys)
@ -316,25 +309,3 @@ def supervisor_debugger(coresys: CoreSys) -> None:
if coresys.config.debug_block:
_LOGGER.info("Wait until debugger is attached")
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_OK = "ok"
HEADER_TOKEN_OLD = "X-Hassio-Key"
HEADER_TOKEN = "X-Supervisor-Token"
ENV_HOMEASSISTANT_REPOSITORY = "HOMEASSISTANT_REPOSITORY"
ENV_SUPERVISOR_DEV = "SUPERVISOR_DEV"
ENV_SUPERVISOR_MACHINE = "SUPERVISOR_MACHINE"

View File

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

View File

@ -10,7 +10,6 @@ from types import MappingProxyType
from typing import TYPE_CHECKING, Any, TypeVar
import aiohttp
import sentry_sdk
from .config import CoreConfig
from .const import ENV_SUPERVISOR_DEV, SERVER_SOFTWARE
@ -514,10 +513,6 @@ class CoreSys:
"""Create an async task."""
return self.loop.create_task(coroutine)
def capture_exception(self, err: Exception) -> None:
"""Capture a exception."""
sentry_sdk.capture_exception(err)
class CoreSysAttributes:
"""Inherit basic CoreSysAttributes."""
@ -692,7 +687,3 @@ class CoreSysAttributes:
def sys_create_task(self, coroutine: Coroutine) -> asyncio.Task:
"""Create an async task."""
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 dbus_fast.aio.message_bus import MessageBus
import sentry_sdk
from ...exceptions import (
DBusError,
@ -12,6 +11,7 @@ from ...exceptions import (
DBusInterfaceError,
HostNotSupportedError,
)
from ...utils.sentry import capture_exception
from ..const import (
DBUS_ATTR_CONNECTION_ENABLED,
DBUS_ATTR_DEVICES,
@ -194,7 +194,7 @@ class NetworkManager(DBusInterfaceProxy):
continue
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("Error while processing %s: %s", device, err)
sentry_sdk.capture_exception(err)
capture_exception(err)
continue
# Skeep interface

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@ from ..exceptions import (
from ..jobs.const import JobExecutionLimit
from ..jobs.decorator import Job
from ..utils.json import write_json_file
from ..utils.sentry import capture_exception
from .base import PluginBase
from .const import (
FILE_HASSIO_AUDIO,
@ -188,7 +189,7 @@ class PluginAudio(PluginBase):
await self.instance.install(self.version)
except DockerError as err:
_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:
"""Generate an /etc/pulse/client.conf data."""

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import logging
from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import HassioError
from ..resolution.const import ContextType, IssueType, SuggestionType
from ..utils.sentry import capture_exception
from .audio import PluginAudio
from .base import PluginBase
from .cli import PluginCli
@ -72,7 +73,7 @@ class PluginManager(CoreSysAttributes):
reference=plugin.slug,
suggestions=[SuggestionType.EXECUTE_REPAIR],
)
self.sys_capture_exception(err)
capture_exception(err)
# Check requirements
await self.sys_updater.reload()
@ -102,7 +103,7 @@ class PluginManager(CoreSysAttributes):
)
except Exception as err: # pylint: disable=broad-except
_LOGGER.warning("Can't update plugin %s: %s", plugin.slug, err)
self.sys_capture_exception(err)
capture_exception(err)
async def repair(self) -> None:
"""Repair Supervisor plugins."""
@ -118,4 +119,4 @@ class PluginManager(CoreSysAttributes):
await plugin.stop()
except Exception as err: # pylint: disable=broad-except
_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.decorator import Job
from ..utils.sentry import capture_exception
from .base import PluginBase
from .const import (
FILE_HASSIO_MULTICAST,
@ -142,7 +143,7 @@ class PluginMulticast(PluginBase):
await self.instance.install(self.version)
except DockerError as err:
_LOGGER.error("Repair of Multicast failed")
self.sys_capture_exception(err)
capture_exception(err)
@Job(
limit=JobExecutionLimit.THROTTLE_RATE_LIMIT,

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import logging
from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import ResolutionNotFound
from ..utils.sentry import capture_exception
from .const import UnhealthyReason, UnsupportedReason
from .evaluations.base import EvaluateBase
from .validate import get_valid_modules
@ -59,7 +60,7 @@ class ResolutionEvaluation(CoreSysAttributes):
_LOGGER.warning(
"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):
self.sys_resolution.unhealthy = UnhealthyReason.DOCKER

View File

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

View File

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

View File

@ -2,7 +2,7 @@
import logging
import re
import sentry_sdk
from .sentry import capture_exception
_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."
except TypeError as err:
_LOGGER.error("The type of message is not a string - %s", err)
sentry_sdk.capture_exception(err)
capture_exception(err)
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."""
from unittest.mock import PropertyMock, patch
from unittest.mock import Mock, PropertyMock, patch
from awesomeversion import AwesomeVersion
import pytest
from supervisor.addons.addon import Addon
from supervisor.arch import CpuArch
from supervisor.const import AddonBoot, AddonStartup, AddonState
from supervisor.coresys import CoreSys
from supervisor.docker.addon import DockerAddon
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.const import TEST_ADDON_SLUG
@ -65,3 +73,60 @@ async def test_image_added_removed_on_update(
await install_addon_ssh.update()
build.assert_called_once_with(AwesomeVersion("11.0.0"))
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)
dbus.clear()
response = await api_client.post("/supervisor/options", json={"diagnostics": True})
await asyncio.sleep(0)
with patch("supervisor.utils.sentry.sentry_sdk.init") as sentry_init:
response = await api_client.post(
"/supervisor/options", json={"diagnostics": True}
)
assert response.status == 200
sentry_init.assert_called_once()
assert response.status == 200
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"]

View File

@ -1,11 +1,13 @@
"""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.manager import BackupManager
from supervisor.const import FOLDER_HOMEASSISTANT, FOLDER_SHARE, CoreState
from supervisor.coresys import CoreSys
from supervisor.exceptions import AddonsError, DockerError
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")
):
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 pathlib import Path
import re
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch
from uuid import uuid4
from aiohttp import web
@ -302,7 +302,7 @@ async def coresys(
) -> CoreSys:
"""Create a CoreSys Mock."""
with patch("supervisor.bootstrap.initialize_system"), patch(
"supervisor.bootstrap.setup_diagnostics"
"supervisor.utils.sentry.sentry_sdk.init"
):
coresys_obj = await initialize_coresys()
@ -580,3 +580,12 @@ async def docker_logs(docker: DockerAPI) -> MagicMock:
os.environ = {"SUPERVISOR_NAME": "hassio_supervisor"}
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."""
import asyncio
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
import pytest
@ -114,7 +114,9 @@ async def test_removed_devices_disconnect(network_manager: NetworkManager):
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."""
caplog.clear()
@ -135,14 +137,12 @@ async def test_handling_bad_devices(
# Unparseable introspections shouldn't happen, this one is logged and captured
await network_manager.update()
with patch.object(
DBus, "_init_proxy", side_effect=(err := DBusParseError())
), patch("supervisor.dbus.network.sentry_sdk.capture_exception") as capture:
with patch.object(DBus, "_init_proxy", side_effect=(err := DBusParseError())):
await network_manager.update(
{"Devices": [device := "/org/freedesktop/NetworkManager/Devices/102"]}
)
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
caplog.set_level(logging.DEBUG, "supervisor.dbus.network")

View File

@ -1,14 +1,21 @@
"""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
from supervisor.addons import validate as vd
from supervisor.addons.addon import Addon
from supervisor.addons.model import Data
from supervisor.addons.options import AddonOptions
from supervisor.const import SYSTEMD_JOURNAL_PERSISTENT, SYSTEMD_JOURNAL_VOLATILE
from supervisor.coresys import CoreSys
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
@ -32,7 +39,7 @@ def fixture_addonsdata_user() -> dict[str, Data]:
yield mock
@pytest.fixture(name="os_environ", autouse=True)
@pytest.fixture(name="os_environ")
def fixture_os_environ():
"""Mock os.environ."""
with patch("supervisor.config.os.environ") as mock:
@ -52,7 +59,9 @@ def get_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."""
docker_addon = get_docker_addon(
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(
coresys: CoreSys, addonsdata_system: dict[str, Data]
coresys: CoreSys, addonsdata_system: dict[str, Data], os_environ
):
"""Validate defaults for mapped folders in addons."""
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
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."""
docker_addon = get_docker_addon(
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"
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."""
docker_addon = get_docker_addon(
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
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
):
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."""
from unittest.mock import PropertyMock, patch
from unittest.mock import MagicMock, Mock, PropertyMock, patch
import pytest
from supervisor.const import CpuArch
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):
@ -24,3 +34,99 @@ async def test_update_fails_if_out_of_date(coresys: CoreSys):
HomeAssistantJobError
):
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
import asyncio
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
import pytest
@ -512,3 +512,26 @@ async def test_unhealthy(coresys: CoreSys, caplog: pytest.LogCaptureFixture):
coresys.jobs.ignore_conditions = [JobCondition.HEALTHY]
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."""
import asyncio
from unittest.mock import PropertyMock, patch
from unittest.mock import Mock, PropertyMock, patch
from awesomeversion import AwesomeVersion
import pytest
from supervisor.const import BusEvent
from supervisor.const import BusEvent, CpuArch
from supervisor.coresys import CoreSys
from supervisor.docker.const import ContainerState
from supervisor.docker.interface import DockerInterface
from supervisor.docker.monitor import DockerContainerStateEvent
from supervisor.exceptions import (
AudioError,
AudioJobError,
CliError,
CliJobError,
CodeNotaryUntrusted,
CoreDNSError,
CoreDNSJobError,
DockerError,
@ -30,6 +32,7 @@ from supervisor.plugins.cli import PluginCli
from supervisor.plugins.dns import PluginDns
from supervisor.plugins.multicast import PluginMulticast
from supervisor.plugins.observer import PluginObserver
from supervisor.utils import check_exception_chain
@pytest.fixture(name="plugin")
@ -159,16 +162,16 @@ async def test_plugin_watchdog(coresys: CoreSys, plugin: PluginBase) -> None:
@pytest.mark.parametrize(
"plugin,error",
[
(PluginAudio, AudioError),
(PluginCli, CliError),
(PluginDns, CoreDNSError),
(PluginMulticast, MulticastError),
(PluginObserver, ObserverError),
(PluginAudio, AudioError()),
(PluginCli, CliError()),
(PluginDns, CoreDNSError()),
(PluginMulticast, MulticastError()),
(PluginObserver, ObserverError()),
],
indirect=["plugin"],
)
async def test_plugin_watchdog_rebuild_on_failure(
coresys: CoreSys, plugin: PluginBase, error: PluginError
coresys: CoreSys, capture_exception: Mock, plugin: PluginBase, error: PluginError
) -> None:
"""Test plugin watchdog rebuilds if start fails."""
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()
rebuild.assert_called_once()
capture_exception.assert_called_once_with(error)
@pytest.mark.parametrize(
"plugin",
@ -331,3 +336,25 @@ async def test_update_fails_if_out_of_date(
type(coresys.supervisor), "need_update", new=PropertyMock(return_value=True)
), pytest.raises(error):
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."""
from unittest.mock import AsyncMock, call, patch
from unittest.mock import AsyncMock, Mock, call, patch
from aiodns.error import DNSError
import pytest
@ -27,7 +27,7 @@ async def test_base(coresys: CoreSys):
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."""
dns_server = CheckDNSServer(coresys)
coresys.core.state = CoreState.RUNNING
@ -50,7 +50,7 @@ async def test_check(coresys: CoreSys, dns_query: AsyncMock):
coresys.plugins.dns.servers = []
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)
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].context is ContextType.DNS_SERVER
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):

View File

@ -1,5 +1,5 @@
"""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
import pytest
@ -27,7 +27,7 @@ async def test_base(coresys: CoreSys):
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."""
dns_server_ipv6 = CheckDNSServerIPv6(coresys)
coresys.core.state = CoreState.RUNNING
@ -56,7 +56,7 @@ async def test_check(coresys: CoreSys, dns_query: AsyncMock):
assert len(coresys.resolution.issues) == 0
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)
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].context is ContextType.DNS_SERVER
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):

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."""
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 awesomeversion import AwesomeVersion
import pytest
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
@ -63,3 +68,18 @@ async def test_connectivity_check_throttling(
await coresys.supervisor.check_connectivity()
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
)