mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-08 17:56:33 +00:00
Sentry only loaded when diagnostics on (#3993)
* Sentry only loaded when diagnostics on * Logging when sentry is closed
This commit is contained in:
parent
14cd261b76
commit
14fcda5d78
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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__)
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
|
||||||
)
|
|
||||||
|
@ -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"
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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."""
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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")
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
60
supervisor/utils/sentry.py
Normal file
60
supervisor/utils/sentry.py
Normal 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()
|
@ -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)
|
||||||
|
)
|
||||||
|
@ -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"]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
21
tests/resolution/test_check.py
Normal file
21
tests/resolution/test_check.py
Normal 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)
|
21
tests/resolution/test_evaluation.py
Normal file
21
tests/resolution/test_evaluation.py
Normal 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)
|
@ -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
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user