mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-13 04:06:33 +00:00
Move read_text
to executor (#5688)
* Move read_text to executor * Fix issues found by coderabbit * formated to formatted * switch to async_capture_exception * Find and replace got one too many * Update patch mock to async_capture_exception * Drop Sentry capture from format_message The error handling got introduced in #2052, however, #2100 essentially makes sure there will never be a byte object passed to this function. And even if, the Sentry aiohttp plug-in will properly catch such an exception. --------- Co-authored-by: Stefan Agner <stefan@agner.ch>
This commit is contained in:
parent
12c951f62d
commit
86133f8ecd
@ -54,7 +54,7 @@ if __name__ == "__main__":
|
||||
loop.set_debug(coresys.config.debug)
|
||||
loop.run_until_complete(coresys.core.connect())
|
||||
|
||||
bootstrap.supervisor_debugger(coresys)
|
||||
loop.run_until_complete(bootstrap.supervisor_debugger(coresys))
|
||||
|
||||
# Signal health startup for container
|
||||
run_os_startup_check_cleanup()
|
||||
|
@ -88,7 +88,7 @@ from ..store.addon import AddonStore
|
||||
from ..utils import check_port
|
||||
from ..utils.apparmor import adjust_profile
|
||||
from ..utils.json import read_json_file, write_json_file
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from .const import (
|
||||
WATCHDOG_MAX_ATTEMPTS,
|
||||
WATCHDOG_RETRY_SECONDS,
|
||||
@ -1530,7 +1530,7 @@ class Addon(AddonModel):
|
||||
except AddonsError as err:
|
||||
attempts = attempts + 1
|
||||
_LOGGER.error("Watchdog restart of addon %s failed!", self.name)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
else:
|
||||
break
|
||||
|
||||
|
@ -23,7 +23,7 @@ from ..exceptions import (
|
||||
from ..jobs.decorator import Job, JobCondition
|
||||
from ..resolution.const import ContextType, IssueType, SuggestionType
|
||||
from ..store.addon import AddonStore
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from .addon import Addon
|
||||
from .const import ADDON_UPDATE_CONDITIONS
|
||||
from .data import AddonsData
|
||||
@ -170,7 +170,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)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
@Job(
|
||||
name="addon_manager_install",
|
||||
@ -388,7 +388,7 @@ class AddonManager(CoreSysAttributes):
|
||||
reference=addon.slug,
|
||||
suggestions=[SuggestionType.EXECUTE_REPAIR],
|
||||
)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
else:
|
||||
add_host_coros.append(
|
||||
self.sys_plugins.dns.add_host(
|
||||
|
@ -210,18 +210,6 @@ class AddonModel(JobGroup, ABC):
|
||||
"""Return description of add-on."""
|
||||
return self.data[ATTR_DESCRIPTON]
|
||||
|
||||
@property
|
||||
def long_description(self) -> str | None:
|
||||
"""Return README.md as long_description."""
|
||||
readme = Path(self.path_location, "README.md")
|
||||
|
||||
# If readme not exists
|
||||
if not readme.exists():
|
||||
return None
|
||||
|
||||
# Return data
|
||||
return readme.read_text(encoding="utf-8")
|
||||
|
||||
@property
|
||||
def repository(self) -> str:
|
||||
"""Return repository of add-on."""
|
||||
@ -646,6 +634,21 @@ class AddonModel(JobGroup, ABC):
|
||||
"""Return breaking versions of addon."""
|
||||
return self.data[ATTR_BREAKING_VERSIONS]
|
||||
|
||||
async def long_description(self) -> str | None:
|
||||
"""Return README.md as long_description."""
|
||||
|
||||
def read_readme() -> str | None:
|
||||
readme = Path(self.path_location, "README.md")
|
||||
|
||||
# If readme not exists
|
||||
if not readme.exists():
|
||||
return None
|
||||
|
||||
# Return data
|
||||
return readme.read_text(encoding="utf-8")
|
||||
|
||||
return await self.sys_run_in_executor(read_readme)
|
||||
|
||||
def refresh_path_cache(self) -> Awaitable[None]:
|
||||
"""Refresh cache of existing paths."""
|
||||
|
||||
|
@ -10,7 +10,7 @@ from aiohttp import web
|
||||
from ..const import AddonState
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..exceptions import APIAddonNotInstalled, HostNotSupportedError
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from .addons import APIAddons
|
||||
from .audio import APIAudio
|
||||
from .auth import APIAuth
|
||||
@ -412,7 +412,7 @@ class RestAPI(CoreSysAttributes):
|
||||
if not isinstance(err, HostNotSupportedError):
|
||||
# No need to capture HostNotSupportedError to Sentry, the cause
|
||||
# is known and reported to the user using the resolution center.
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
kwargs.pop("follow", None) # Follow is not supported for Docker logs
|
||||
return await api_supervisor.logs(*args, **kwargs)
|
||||
|
||||
|
@ -212,7 +212,7 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_HOSTNAME: addon.hostname,
|
||||
ATTR_DNS: addon.dns,
|
||||
ATTR_DESCRIPTON: addon.description,
|
||||
ATTR_LONG_DESCRIPTION: addon.long_description,
|
||||
ATTR_LONG_DESCRIPTION: await addon.long_description(),
|
||||
ATTR_ADVANCED: addon.advanced,
|
||||
ATTR_STAGE: addon.stage,
|
||||
ATTR_REPOSITORY: addon.repository,
|
||||
|
@ -98,10 +98,10 @@ class APIHost(CoreSysAttributes):
|
||||
ATTR_VIRTUALIZATION: self.sys_host.info.virtualization,
|
||||
ATTR_CPE: self.sys_host.info.cpe,
|
||||
ATTR_DEPLOYMENT: self.sys_host.info.deployment,
|
||||
ATTR_DISK_FREE: self.sys_host.info.free_space,
|
||||
ATTR_DISK_TOTAL: self.sys_host.info.total_space,
|
||||
ATTR_DISK_USED: self.sys_host.info.used_space,
|
||||
ATTR_DISK_LIFE_TIME: self.sys_host.info.disk_life_time,
|
||||
ATTR_DISK_FREE: await self.sys_host.info.free_space(),
|
||||
ATTR_DISK_TOTAL: await self.sys_host.info.total_space(),
|
||||
ATTR_DISK_USED: await self.sys_host.info.used_space(),
|
||||
ATTR_DISK_LIFE_TIME: await self.sys_host.info.disk_life_time(),
|
||||
ATTR_FEATURES: self.sys_host.features,
|
||||
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
||||
ATTR_LLMNR_HOSTNAME: self.sys_host.info.llmnr_hostname,
|
||||
|
@ -69,12 +69,12 @@ SCHEMA_ADD_REPOSITORY = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
def _read_static_file(path: Path) -> Any:
|
||||
def _read_static_file(path: Path, binary: bool = False) -> Any:
|
||||
"""Read in a static file asset for API output.
|
||||
|
||||
Must be run in executor.
|
||||
"""
|
||||
with path.open("r") as asset:
|
||||
with path.open("rb" if binary else "r") as asset:
|
||||
return asset.read()
|
||||
|
||||
|
||||
@ -109,7 +109,7 @@ class APIStore(CoreSysAttributes):
|
||||
|
||||
return self.sys_store.get(repository_slug)
|
||||
|
||||
def _generate_addon_information(
|
||||
async def _generate_addon_information(
|
||||
self, addon: AddonStore, extended: bool = False
|
||||
) -> dict[str, Any]:
|
||||
"""Generate addon information."""
|
||||
@ -156,7 +156,7 @@ class APIStore(CoreSysAttributes):
|
||||
ATTR_HOST_NETWORK: addon.host_network,
|
||||
ATTR_HOST_PID: addon.host_pid,
|
||||
ATTR_INGRESS: addon.with_ingress,
|
||||
ATTR_LONG_DESCRIPTION: addon.long_description,
|
||||
ATTR_LONG_DESCRIPTION: await addon.long_description(),
|
||||
ATTR_RATING: rating_security(addon),
|
||||
ATTR_SIGNED: addon.signed,
|
||||
}
|
||||
@ -185,10 +185,12 @@ class APIStore(CoreSysAttributes):
|
||||
async def store_info(self, request: web.Request) -> dict[str, Any]:
|
||||
"""Return store information."""
|
||||
return {
|
||||
ATTR_ADDONS: [
|
||||
self._generate_addon_information(self.sys_addons.store[addon])
|
||||
for addon in self.sys_addons.store
|
||||
],
|
||||
ATTR_ADDONS: await asyncio.gather(
|
||||
*[
|
||||
self._generate_addon_information(self.sys_addons.store[addon])
|
||||
for addon in self.sys_addons.store
|
||||
]
|
||||
),
|
||||
ATTR_REPOSITORIES: [
|
||||
self._generate_repository_information(repository)
|
||||
for repository in self.sys_store.all
|
||||
@ -199,10 +201,12 @@ class APIStore(CoreSysAttributes):
|
||||
async def addons_list(self, request: web.Request) -> dict[str, Any]:
|
||||
"""Return all store add-ons."""
|
||||
return {
|
||||
ATTR_ADDONS: [
|
||||
self._generate_addon_information(self.sys_addons.store[addon])
|
||||
for addon in self.sys_addons.store
|
||||
]
|
||||
ATTR_ADDONS: await asyncio.gather(
|
||||
*[
|
||||
self._generate_addon_information(self.sys_addons.store[addon])
|
||||
for addon in self.sys_addons.store
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
@api_process
|
||||
@ -234,7 +238,7 @@ class APIStore(CoreSysAttributes):
|
||||
async def addons_addon_info_wrapped(self, request: web.Request) -> dict[str, Any]:
|
||||
"""Return add-on information directly (not api)."""
|
||||
addon: AddonStore = self._extract_addon(request)
|
||||
return self._generate_addon_information(addon, True)
|
||||
return await self._generate_addon_information(addon, True)
|
||||
|
||||
@api_process_raw(CONTENT_TYPE_PNG)
|
||||
async def addons_addon_icon(self, request: web.Request) -> bytes:
|
||||
@ -243,7 +247,7 @@ class APIStore(CoreSysAttributes):
|
||||
if not addon.with_icon:
|
||||
raise APIError(f"No icon found for add-on {addon.slug}!")
|
||||
|
||||
return await self.sys_run_in_executor(_read_static_file, addon.path_icon)
|
||||
return await self.sys_run_in_executor(_read_static_file, addon.path_icon, True)
|
||||
|
||||
@api_process_raw(CONTENT_TYPE_PNG)
|
||||
async def addons_addon_logo(self, request: web.Request) -> bytes:
|
||||
@ -252,7 +256,7 @@ class APIStore(CoreSysAttributes):
|
||||
if not addon.with_logo:
|
||||
raise APIError(f"No logo found for add-on {addon.slug}!")
|
||||
|
||||
return await self.sys_run_in_executor(_read_static_file, addon.path_logo)
|
||||
return await self.sys_run_in_executor(_read_static_file, addon.path_logo, True)
|
||||
|
||||
@api_process_raw(CONTENT_TYPE_TEXT)
|
||||
async def addons_addon_changelog(self, request: web.Request) -> str:
|
||||
|
@ -36,7 +36,7 @@ from ..resolution.const import UnhealthyReason
|
||||
from ..utils.common import FileConfiguration
|
||||
from ..utils.dt import utcnow
|
||||
from ..utils.sentinel import DEFAULT
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from .backup import Backup
|
||||
from .const import (
|
||||
DEFAULT_FREEZE_TIMEOUT,
|
||||
@ -525,7 +525,7 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||
return None
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Backup %s error", backup.slug)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
self.sys_jobs.current.capture_error(
|
||||
BackupError(f"Backup {backup.slug} error, see supervisor logs")
|
||||
)
|
||||
@ -718,7 +718,7 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||
raise
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Restore %s error", backup.slug)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
raise BackupError(
|
||||
f"Restore {backup.slug} error, see supervisor logs"
|
||||
) from err
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Bootstrap Supervisor."""
|
||||
|
||||
# ruff: noqa: T100
|
||||
from importlib import import_module
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
@ -306,12 +307,12 @@ def reg_signal(loop, coresys: CoreSys) -> None:
|
||||
_LOGGER.warning("Could not bind to SIGINT")
|
||||
|
||||
|
||||
def supervisor_debugger(coresys: CoreSys) -> None:
|
||||
async def supervisor_debugger(coresys: CoreSys) -> None:
|
||||
"""Start debugger if needed."""
|
||||
if not coresys.config.debug:
|
||||
return
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import debugpy
|
||||
|
||||
debugpy = await coresys.run_in_executor(import_module, "debugpy")
|
||||
|
||||
_LOGGER.info("Initializing Supervisor debugger")
|
||||
|
||||
|
@ -26,7 +26,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.sentry import async_capture_exception
|
||||
from .utils.whoami import WhoamiData, retrieve_whoami
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -172,7 +172,7 @@ class Core(CoreSysAttributes):
|
||||
"Fatal error happening on load Task %s: %s", setup_task, err
|
||||
)
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.SETUP
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
# Set OS Agent diagnostics if needed
|
||||
if (
|
||||
@ -189,7 +189,7 @@ class Core(CoreSysAttributes):
|
||||
self.sys_config.diagnostics,
|
||||
err,
|
||||
)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
# Evaluate the system
|
||||
await self.sys_resolution.evaluate.evaluate_system()
|
||||
@ -246,12 +246,12 @@ class Core(CoreSysAttributes):
|
||||
await self.sys_homeassistant.core.start()
|
||||
except HomeAssistantCrashError as err:
|
||||
_LOGGER.error("Can't start Home Assistant Core - rebuiling")
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
with suppress(HomeAssistantError):
|
||||
await self.sys_homeassistant.core.rebuild()
|
||||
except HomeAssistantError as err:
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
else:
|
||||
_LOGGER.info("Skipping start of Home Assistant")
|
||||
|
||||
|
@ -15,7 +15,7 @@ from ...exceptions import (
|
||||
HostNotSupportedError,
|
||||
NetworkInterfaceNotFound,
|
||||
)
|
||||
from ...utils.sentry import capture_exception
|
||||
from ...utils.sentry import async_capture_exception
|
||||
from ..const import (
|
||||
DBUS_ATTR_CONNECTION_ENABLED,
|
||||
DBUS_ATTR_DEVICES,
|
||||
@ -223,13 +223,13 @@ class NetworkManager(DBusInterfaceProxy):
|
||||
device,
|
||||
err,
|
||||
)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
return
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.exception(
|
||||
"Unkown error while processing %s: %s", device, err
|
||||
)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
continue
|
||||
|
||||
# Skeep interface
|
||||
|
@ -42,7 +42,7 @@ from ..hardware.data import Device
|
||||
from ..jobs.const import JobCondition, JobExecutionLimit
|
||||
from ..jobs.decorator import Job
|
||||
from ..resolution.const import CGROUP_V2_VERSION, ContextType, IssueType, SuggestionType
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from .const import (
|
||||
ENV_TIME,
|
||||
ENV_TOKEN,
|
||||
@ -606,7 +606,7 @@ class DockerAddon(DockerInterface):
|
||||
)
|
||||
except CoreDNSError as err:
|
||||
_LOGGER.warning("Can't update DNS for %s", self.name)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
# Hardware Access
|
||||
if self.addon.static_devices:
|
||||
@ -787,7 +787,7 @@ class DockerAddon(DockerInterface):
|
||||
await self.sys_plugins.dns.delete_host(self.addon.hostname)
|
||||
except CoreDNSError as err:
|
||||
_LOGGER.warning("Can't update DNS for %s", self.name)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
# Hardware
|
||||
if self._hw_listener:
|
||||
|
@ -42,7 +42,7 @@ from ..jobs.const import JOB_GROUP_DOCKER_INTERFACE, JobExecutionLimit
|
||||
from ..jobs.decorator import Job
|
||||
from ..jobs.job_group import JobGroup
|
||||
from ..resolution.const import ContextType, IssueType, SuggestionType
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from .const import ContainerState, RestartPolicy
|
||||
from .manager import CommandReturn
|
||||
from .monitor import DockerContainerStateEvent
|
||||
@ -278,7 +278,7 @@ class DockerInterface(JobGroup):
|
||||
f"Can't install {image}:{version!s}: {err}", _LOGGER.error
|
||||
) from err
|
||||
except (docker.errors.DockerException, requests.RequestException) as err:
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
raise DockerError(
|
||||
f"Unknown error with {image}:{version!s} -> {err!s}", _LOGGER.error
|
||||
) from err
|
||||
@ -394,7 +394,7 @@ class DockerInterface(JobGroup):
|
||||
)
|
||||
except DockerNotFound as err:
|
||||
# If image is missing, capture the exception as this shouldn't happen
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
raise
|
||||
|
||||
# Store metadata
|
||||
|
@ -49,17 +49,26 @@ class HwDisk(CoreSysAttributes):
|
||||
return False
|
||||
|
||||
def get_disk_total_space(self, path: str | Path) -> float:
|
||||
"""Return total space (GiB) on disk for path."""
|
||||
"""Return total space (GiB) on disk for path.
|
||||
|
||||
Must be run in executor.
|
||||
"""
|
||||
total, _, _ = shutil.disk_usage(path)
|
||||
return round(total / (1024.0**3), 1)
|
||||
|
||||
def get_disk_used_space(self, path: str | Path) -> float:
|
||||
"""Return used space (GiB) on disk for path."""
|
||||
"""Return used space (GiB) on disk for path.
|
||||
|
||||
Must be run in executor.
|
||||
"""
|
||||
_, used, _ = shutil.disk_usage(path)
|
||||
return round(used / (1024.0**3), 1)
|
||||
|
||||
def get_disk_free_space(self, path: str | Path) -> float:
|
||||
"""Return free space (GiB) on disk for path."""
|
||||
"""Return free space (GiB) on disk for path.
|
||||
|
||||
Must be run in executor.
|
||||
"""
|
||||
_, _, free = shutil.disk_usage(path)
|
||||
return round(free / (1024.0**3), 1)
|
||||
|
||||
@ -113,7 +122,10 @@ class HwDisk(CoreSysAttributes):
|
||||
return life_time_value * 10.0
|
||||
|
||||
def get_disk_life_time(self, path: str | Path) -> float:
|
||||
"""Return life time estimate of the underlying SSD drive."""
|
||||
"""Return life time estimate of the underlying SSD drive.
|
||||
|
||||
Must be run in executor.
|
||||
"""
|
||||
mount_source = self._get_mount_source(str(path))
|
||||
if mount_source == "overlay":
|
||||
return None
|
||||
|
@ -33,7 +33,7 @@ from ..jobs.decorator import Job, JobCondition
|
||||
from ..jobs.job_group import JobGroup
|
||||
from ..resolution.const import ContextType, IssueType
|
||||
from ..utils import convert_to_ascii
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from .const import (
|
||||
LANDINGPAGE,
|
||||
SAFE_MODE_FILENAME,
|
||||
@ -160,7 +160,7 @@ class HomeAssistantCore(JobGroup):
|
||||
except (DockerError, JobException):
|
||||
pass
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
_LOGGER.warning("Failed to install landingpage, retrying after 30sec")
|
||||
await asyncio.sleep(30)
|
||||
@ -192,7 +192,7 @@ class HomeAssistantCore(JobGroup):
|
||||
except (DockerError, JobException):
|
||||
pass
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
_LOGGER.warning("Error on Home Assistant installation. Retrying in 30sec")
|
||||
await asyncio.sleep(30)
|
||||
@ -557,7 +557,7 @@ class HomeAssistantCore(JobGroup):
|
||||
try:
|
||||
await self.start()
|
||||
except HomeAssistantError as err:
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
else:
|
||||
break
|
||||
|
||||
@ -569,7 +569,7 @@ class HomeAssistantCore(JobGroup):
|
||||
except HomeAssistantError as err:
|
||||
attempts = attempts + 1
|
||||
_LOGGER.error("Watchdog restart of Home Assistant failed!")
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
else:
|
||||
break
|
||||
|
||||
|
@ -102,39 +102,39 @@ class InfoCenter(CoreSysAttributes):
|
||||
"""Return the boot timestamp."""
|
||||
return self.sys_dbus.systemd.boot_timestamp
|
||||
|
||||
@property
|
||||
def total_space(self) -> float:
|
||||
"""Return total space (GiB) on disk for supervisor data directory."""
|
||||
return self.sys_hardware.disk.get_disk_total_space(
|
||||
self.coresys.config.path_supervisor
|
||||
)
|
||||
|
||||
@property
|
||||
def used_space(self) -> float:
|
||||
"""Return used space (GiB) on disk for supervisor data directory."""
|
||||
return self.sys_hardware.disk.get_disk_used_space(
|
||||
self.coresys.config.path_supervisor
|
||||
)
|
||||
|
||||
@property
|
||||
def free_space(self) -> float:
|
||||
"""Return available space (GiB) on disk for supervisor data directory."""
|
||||
return self.sys_hardware.disk.get_disk_free_space(
|
||||
self.coresys.config.path_supervisor
|
||||
)
|
||||
|
||||
@property
|
||||
def disk_life_time(self) -> float:
|
||||
"""Return the estimated life-time usage (in %) of the SSD storing the data directory."""
|
||||
return self.sys_hardware.disk.get_disk_life_time(
|
||||
self.coresys.config.path_supervisor
|
||||
)
|
||||
|
||||
@property
|
||||
def virtualization(self) -> str | None:
|
||||
"""Return virtualization hypervisor being used."""
|
||||
return self.sys_dbus.systemd.virtualization
|
||||
|
||||
async def total_space(self) -> float:
|
||||
"""Return total space (GiB) on disk for supervisor data directory."""
|
||||
return await self.sys_run_in_executor(
|
||||
self.sys_hardware.disk.get_disk_total_space,
|
||||
self.coresys.config.path_supervisor,
|
||||
)
|
||||
|
||||
async def used_space(self) -> float:
|
||||
"""Return used space (GiB) on disk for supervisor data directory."""
|
||||
return await self.sys_run_in_executor(
|
||||
self.sys_hardware.disk.get_disk_used_space,
|
||||
self.coresys.config.path_supervisor,
|
||||
)
|
||||
|
||||
async def free_space(self) -> float:
|
||||
"""Return available space (GiB) on disk for supervisor data directory."""
|
||||
return await self.sys_run_in_executor(
|
||||
self.sys_hardware.disk.get_disk_free_space,
|
||||
self.coresys.config.path_supervisor,
|
||||
)
|
||||
|
||||
async def disk_life_time(self) -> float:
|
||||
"""Return the estimated life-time usage (in %) of the SSD storing the data directory."""
|
||||
return await self.sys_run_in_executor(
|
||||
self.sys_hardware.disk.get_disk_life_time,
|
||||
self.coresys.config.path_supervisor,
|
||||
)
|
||||
|
||||
async def get_dmesg(self) -> bytes:
|
||||
"""Return host dmesg output."""
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
|
@ -20,7 +20,6 @@ from ..exceptions import HassioError, JobNotFound, JobStartException
|
||||
from ..homeassistant.const import WSEvent
|
||||
from ..utils.common import FileConfiguration
|
||||
from ..utils.dt import utcnow
|
||||
from ..utils.sentry import capture_exception
|
||||
from .const import ATTR_IGNORE_CONDITIONS, FILE_CONFIG_JOBS, JobCondition
|
||||
from .validate import SCHEMA_JOBS_CONFIG
|
||||
|
||||
@ -191,9 +190,10 @@ class JobManager(FileConfiguration, CoreSysAttributes):
|
||||
"""
|
||||
try:
|
||||
return self.get_job(_CURRENT_JOB.get())
|
||||
except (LookupError, JobNotFound) as err:
|
||||
capture_exception(err)
|
||||
raise RuntimeError("No job for the current asyncio task!") from None
|
||||
except (LookupError, JobNotFound):
|
||||
raise RuntimeError(
|
||||
"No job for the current asyncio task!", _LOGGER.critical
|
||||
) from None
|
||||
|
||||
@property
|
||||
def is_job(self) -> bool:
|
||||
|
@ -18,7 +18,7 @@ from ..exceptions import (
|
||||
)
|
||||
from ..host.const import HostFeature
|
||||
from ..resolution.const import MINIMUM_FREE_SPACE_THRESHOLD, ContextType, IssueType
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from . import SupervisorJob
|
||||
from .const import JobCondition, JobExecutionLimit
|
||||
from .job_group import JobGroup
|
||||
@ -313,7 +313,7 @@ class Job(CoreSysAttributes):
|
||||
except Exception as err:
|
||||
_LOGGER.exception("Unhandled exception: %s", err)
|
||||
job.capture_error()
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
raise JobException() from err
|
||||
finally:
|
||||
self._release_exception_limits()
|
||||
@ -373,13 +373,14 @@ class Job(CoreSysAttributes):
|
||||
|
||||
if (
|
||||
JobCondition.FREE_SPACE in used_conditions
|
||||
and coresys.sys_host.info.free_space < MINIMUM_FREE_SPACE_THRESHOLD
|
||||
and (free_space := await coresys.sys_host.info.free_space())
|
||||
< MINIMUM_FREE_SPACE_THRESHOLD
|
||||
):
|
||||
coresys.sys_resolution.create_issue(
|
||||
IssueType.FREE_SPACE, ContextType.SYSTEM
|
||||
)
|
||||
raise JobConditionException(
|
||||
f"'{method_name}' blocked from execution, not enough free space ({coresys.sys_host.info.free_space}GB) left on the device"
|
||||
f"'{method_name}' blocked from execution, not enough free space ({free_space}GB) left on the device"
|
||||
)
|
||||
|
||||
if JobCondition.INTERNET_SYSTEM in used_conditions:
|
||||
|
@ -80,7 +80,9 @@ def filter_data(coresys: CoreSys, event: dict, hint: dict) -> dict:
|
||||
"arch": coresys.arch.default,
|
||||
"board": coresys.os.board,
|
||||
"deployment": coresys.host.info.deployment,
|
||||
"disk_free_space": coresys.host.info.free_space,
|
||||
"disk_free_space": coresys.hardware.disk.get_disk_free_space(
|
||||
coresys.config.path_supervisor
|
||||
),
|
||||
"host": coresys.host.info.operating_system,
|
||||
"kernel": coresys.host.info.kernel,
|
||||
"machine": coresys.machine,
|
||||
|
@ -19,7 +19,7 @@ from ..homeassistant.const import LANDINGPAGE
|
||||
from ..jobs.decorator import Job, JobCondition, JobExecutionLimit
|
||||
from ..plugins.const import PLUGIN_UPDATE_CONDITIONS
|
||||
from ..utils.dt import utcnow
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
@ -224,7 +224,7 @@ class Tasks(CoreSysAttributes):
|
||||
await self.sys_homeassistant.core.restart()
|
||||
except HomeAssistantError as err:
|
||||
if reanimate_fails == 0 or safe_mode:
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
if safe_mode:
|
||||
_LOGGER.critical(
|
||||
@ -341,7 +341,7 @@ class Tasks(CoreSysAttributes):
|
||||
await (await addon.restart())
|
||||
except AddonsError as err:
|
||||
_LOGGER.error("%s watchdog reanimation failed with %s", addon.slug, err)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
finally:
|
||||
self._cache[addon.slug] = 0
|
||||
|
||||
|
@ -18,7 +18,7 @@ from ..jobs.const import JobCondition
|
||||
from ..jobs.decorator import Job
|
||||
from ..resolution.const import SuggestionType
|
||||
from ..utils.common import FileConfiguration
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from .const import (
|
||||
ATTR_DEFAULT_BACKUP_MOUNT,
|
||||
ATTR_MOUNTS,
|
||||
@ -177,7 +177,7 @@ class MountManager(FileConfiguration, CoreSysAttributes):
|
||||
if mounts[i].failed_issue in self.sys_resolution.issues:
|
||||
continue
|
||||
if not isinstance(errors[i], MountError):
|
||||
capture_exception(errors[i])
|
||||
await async_capture_exception(errors[i])
|
||||
|
||||
self.sys_resolution.add_issue(
|
||||
evolve(mounts[i].failed_issue),
|
||||
|
@ -40,7 +40,7 @@ from ..exceptions import (
|
||||
)
|
||||
from ..resolution.const import ContextType, IssueType
|
||||
from ..resolution.data import Issue
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from .const import (
|
||||
ATTR_PATH,
|
||||
ATTR_READ_ONLY,
|
||||
@ -208,7 +208,7 @@ class Mount(CoreSysAttributes, ABC):
|
||||
try:
|
||||
self._state = await self.unit.get_active_state()
|
||||
except DBusError as err:
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
raise MountError(
|
||||
f"Could not get active state of mount due to: {err!s}"
|
||||
) from err
|
||||
@ -221,7 +221,7 @@ class Mount(CoreSysAttributes, ABC):
|
||||
self._unit = None
|
||||
self._state = None
|
||||
except DBusError as err:
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
raise MountError(f"Could not get mount unit due to: {err!s}") from err
|
||||
return self.unit
|
||||
|
||||
|
@ -26,7 +26,7 @@ from ..jobs.const import JobCondition, JobExecutionLimit
|
||||
from ..jobs.decorator import Job
|
||||
from ..resolution.checks.disabled_data_disk import CheckDisabledDataDisk
|
||||
from ..resolution.checks.multiple_data_disks import CheckMultipleDataDisks
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from .const import (
|
||||
FILESYSTEM_LABEL_DATA_DISK,
|
||||
FILESYSTEM_LABEL_DISABLED_DATA_DISK,
|
||||
@ -337,7 +337,7 @@ class DataDisk(CoreSysAttributes):
|
||||
try:
|
||||
await block_device.format(FormatType.GPT)
|
||||
except DBusError as err:
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
raise HassOSDataDiskError(
|
||||
f"Could not format {new_disk.id}: {err!s}", _LOGGER.error
|
||||
) from err
|
||||
@ -354,7 +354,7 @@ class DataDisk(CoreSysAttributes):
|
||||
0, 0, LINUX_DATA_PARTITION_GUID, PARTITION_NAME_EXTERNAL_DATA_DISK
|
||||
)
|
||||
except DBusError as err:
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
raise HassOSDataDiskError(
|
||||
f"Could not create new data partition: {err!s}", _LOGGER.error
|
||||
) from err
|
||||
|
@ -24,7 +24,7 @@ from ..exceptions import (
|
||||
from ..jobs.const import JobCondition, JobExecutionLimit
|
||||
from ..jobs.decorator import Job
|
||||
from ..resolution.const import UnhealthyReason
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from .data_disk import DataDisk
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -385,7 +385,7 @@ class OSManager(CoreSysAttributes):
|
||||
RaucState.ACTIVE, self.get_slot_name(boot_name)
|
||||
)
|
||||
except DBusError as err:
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
raise HassOSSlotUpdateError(
|
||||
f"Can't mark {boot_name} as active!", _LOGGER.error
|
||||
) from err
|
||||
|
@ -27,7 +27,7 @@ from ..jobs.const import JobExecutionLimit
|
||||
from ..jobs.decorator import Job
|
||||
from ..resolution.const import UnhealthyReason
|
||||
from ..utils.json import write_json_file
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from .base import PluginBase
|
||||
from .const import (
|
||||
FILE_HASSIO_AUDIO,
|
||||
@ -163,7 +163,7 @@ class PluginAudio(PluginBase):
|
||||
await self.instance.install(self.version)
|
||||
except DockerError as err:
|
||||
_LOGGER.error("Repair of Audio failed")
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
def pulse_client(self, input_profile=None, output_profile=None) -> str:
|
||||
"""Generate an /etc/pulse/client.conf data."""
|
||||
|
@ -15,7 +15,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 ..utils.sentry import async_capture_exception
|
||||
from .const import WATCHDOG_MAX_ATTEMPTS, WATCHDOG_RETRY_SECONDS
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@ -129,7 +129,7 @@ class PluginBase(ABC, FileConfiguration, CoreSysAttributes):
|
||||
except PluginError as err:
|
||||
attempts = attempts + 1
|
||||
_LOGGER.error("Watchdog restart of %s plugin failed!", self.slug)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
else:
|
||||
break
|
||||
|
||||
|
@ -17,7 +17,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 ..utils.sentry import async_capture_exception
|
||||
from .base import PluginBase
|
||||
from .const import (
|
||||
FILE_HASSIO_CLI,
|
||||
@ -114,7 +114,7 @@ class PluginCli(PluginBase):
|
||||
await self.instance.install(self.version)
|
||||
except DockerError as err:
|
||||
_LOGGER.error("Repair of HA cli failed")
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
@Job(
|
||||
name="plugin_cli_restart_after_problem",
|
||||
|
@ -33,7 +33,7 @@ from ..jobs.const import JobExecutionLimit
|
||||
from ..jobs.decorator import Job
|
||||
from ..resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason
|
||||
from ..utils.json import write_json_file
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from ..validate import dns_url
|
||||
from .base import PluginBase
|
||||
from .const import (
|
||||
@ -410,7 +410,7 @@ class PluginDns(PluginBase):
|
||||
await self.instance.install(self.version)
|
||||
except DockerError as err:
|
||||
_LOGGER.error("Repair of CoreDNS failed")
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
def _write_resolv(self, resolv_conf: Path) -> None:
|
||||
"""Update/Write resolv.conf file."""
|
||||
|
@ -7,7 +7,7 @@ from typing import Self
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..exceptions import HassioError
|
||||
from ..resolution.const import ContextType, IssueType, SuggestionType
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from .audio import PluginAudio
|
||||
from .base import PluginBase
|
||||
from .cli import PluginCli
|
||||
@ -80,7 +80,7 @@ class PluginManager(CoreSysAttributes):
|
||||
reference=plugin.slug,
|
||||
suggestions=[SuggestionType.EXECUTE_REPAIR],
|
||||
)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
# Exit if supervisor out of date. Plugins can't update until then
|
||||
if self.sys_supervisor.need_update:
|
||||
@ -114,7 +114,7 @@ class PluginManager(CoreSysAttributes):
|
||||
)
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.warning("Can't update plugin %s: %s", plugin.slug, err)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
async def repair(self) -> None:
|
||||
"""Repair Supervisor plugins."""
|
||||
@ -132,4 +132,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)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
@ -19,7 +19,7 @@ from ..exceptions import (
|
||||
)
|
||||
from ..jobs.const import JobExecutionLimit
|
||||
from ..jobs.decorator import Job
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from .base import PluginBase
|
||||
from .const import (
|
||||
FILE_HASSIO_MULTICAST,
|
||||
@ -109,7 +109,7 @@ class PluginMulticast(PluginBase):
|
||||
await self.instance.install(self.version)
|
||||
except DockerError as err:
|
||||
_LOGGER.error("Repair of Multicast failed")
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
@Job(
|
||||
name="plugin_multicast_restart_after_problem",
|
||||
|
@ -22,7 +22,7 @@ from ..exceptions import (
|
||||
)
|
||||
from ..jobs.const import JobExecutionLimit
|
||||
from ..jobs.decorator import Job
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from .base import PluginBase
|
||||
from .const import (
|
||||
FILE_HASSIO_OBSERVER,
|
||||
@ -121,7 +121,7 @@ class PluginObserver(PluginBase):
|
||||
await self.instance.install(self.version)
|
||||
except DockerError as err:
|
||||
_LOGGER.error("Repair of HA observer failed")
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
@Job(
|
||||
name="plugin_observer_restart_after_problem",
|
||||
|
@ -7,7 +7,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 ..utils.sentry import async_capture_exception
|
||||
from .checks.base import CheckBase
|
||||
from .validate import get_valid_modules
|
||||
|
||||
@ -64,6 +64,6 @@ class ResolutionCheck(CoreSysAttributes):
|
||||
await check()
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.error("Error during processing %s: %s", check.issue, err)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
_LOGGER.info("System checks complete")
|
||||
|
@ -10,7 +10,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 ...utils.sentry import async_capture_exception
|
||||
from ..const import DNS_CHECK_HOST, ContextType, IssueType
|
||||
from .base import CheckBase
|
||||
|
||||
@ -42,7 +42,7 @@ class CheckDNSServer(CheckBase):
|
||||
ContextType.DNS_SERVER,
|
||||
reference=dns_servers[i],
|
||||
)
|
||||
capture_exception(results[i])
|
||||
await async_capture_exception(results[i])
|
||||
|
||||
@Job(name="check_dns_server_approve", conditions=[JobCondition.INTERNET_SYSTEM])
|
||||
async def approve_check(self, reference: str | None = None) -> bool:
|
||||
|
@ -10,7 +10,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 ...utils.sentry import async_capture_exception
|
||||
from ..const import DNS_CHECK_HOST, DNS_ERROR_NO_DATA, ContextType, IssueType
|
||||
from .base import CheckBase
|
||||
|
||||
@ -47,7 +47,7 @@ class CheckDNSServerIPv6(CheckBase):
|
||||
ContextType.DNS_SERVER,
|
||||
reference=dns_servers[i],
|
||||
)
|
||||
capture_exception(results[i])
|
||||
await async_capture_exception(results[i])
|
||||
|
||||
@Job(
|
||||
name="check_dns_server_ipv6_approve", conditions=[JobCondition.INTERNET_SYSTEM]
|
||||
|
@ -23,7 +23,7 @@ class CheckFreeSpace(CheckBase):
|
||||
|
||||
async def run_check(self) -> None:
|
||||
"""Run check if not affected by issue."""
|
||||
if self.sys_host.info.free_space > MINIMUM_FREE_SPACE_THRESHOLD:
|
||||
if await self.sys_host.info.free_space() > MINIMUM_FREE_SPACE_THRESHOLD:
|
||||
return
|
||||
|
||||
suggestions: list[SuggestionType] = []
|
||||
@ -45,7 +45,7 @@ class CheckFreeSpace(CheckBase):
|
||||
|
||||
async def approve_check(self, reference: str | None = None) -> bool:
|
||||
"""Approve check if it is affected by issue."""
|
||||
if self.sys_host.info.free_space > MINIMUM_FREE_SPACE_THRESHOLD:
|
||||
if await self.sys_host.info.free_space() > MINIMUM_FREE_SPACE_THRESHOLD:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
@ -5,7 +5,7 @@ import logging
|
||||
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..exceptions import ResolutionNotFound
|
||||
from ..utils.sentry import capture_exception
|
||||
from ..utils.sentry import async_capture_exception
|
||||
from .const import UnhealthyReason, UnsupportedReason
|
||||
from .evaluations.base import EvaluateBase
|
||||
from .validate import get_valid_modules
|
||||
@ -64,7 +64,7 @@ class ResolutionEvaluation(CoreSysAttributes):
|
||||
_LOGGER.warning(
|
||||
"Error during processing %s: %s", evaluation.reason, err
|
||||
)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
if any(reason in self.sys_resolution.unsupported for reason in UNHEALTHY):
|
||||
self.sys_resolution.unhealthy = UnhealthyReason.DOCKER
|
||||
|
@ -6,7 +6,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 ..utils.sentry import async_capture_exception
|
||||
from .data import Issue, Suggestion
|
||||
from .fixups.base import FixupBase
|
||||
from .validate import get_valid_modules
|
||||
@ -55,7 +55,7 @@ class ResolutionFixup(CoreSysAttributes):
|
||||
await fix()
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.warning("Error during processing %s: %s", fix.suggestion, err)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
|
||||
_LOGGER.info("System autofix complete")
|
||||
|
||||
|
@ -36,7 +36,7 @@ class ResolutionNotify(CoreSysAttributes):
|
||||
messages.append(
|
||||
{
|
||||
"title": "Available space is less than 1GB!",
|
||||
"message": f"Available space is {self.sys_host.info.free_space}GB, see https://www.home-assistant.io/more-info/free-space for more information.",
|
||||
"message": f"Available space is {await self.sys_host.info.free_space()}GB, see https://www.home-assistant.io/more-info/free-space for more information.",
|
||||
"notification_id": "supervisor_issue_free_space",
|
||||
}
|
||||
)
|
||||
|
@ -36,7 +36,7 @@ from .jobs.const import JobCondition, JobExecutionLimit
|
||||
from .jobs.decorator import Job
|
||||
from .resolution.const import ContextType, IssueType, UnhealthyReason
|
||||
from .utils.codenotary import calc_checksum
|
||||
from .utils.sentry import capture_exception
|
||||
from .utils.sentry import async_capture_exception
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
@ -219,7 +219,7 @@ class Supervisor(CoreSysAttributes):
|
||||
self.sys_resolution.create_issue(
|
||||
IssueType.UPDATE_FAILED, ContextType.SUPERVISOR
|
||||
)
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
raise SupervisorUpdateError(
|
||||
f"Update of Supervisor failed: {err!s}", _LOGGER.critical
|
||||
) from err
|
||||
|
@ -35,7 +35,7 @@ from ..exceptions import (
|
||||
DBusTimeoutError,
|
||||
HassioNotSupportedError,
|
||||
)
|
||||
from .sentry import capture_exception
|
||||
from .sentry import async_capture_exception
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
@ -124,7 +124,7 @@ class DBus:
|
||||
)
|
||||
raise DBus.from_dbus_error(err) from None
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
capture_exception(err)
|
||||
await async_capture_exception(err)
|
||||
raise DBusFatalError(str(err)) from err
|
||||
|
||||
def _add_interfaces(self):
|
||||
|
@ -3,8 +3,6 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from .sentry import capture_exception
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
RE_BIND_FAILED = re.compile(
|
||||
@ -13,13 +11,11 @@ RE_BIND_FAILED = re.compile(
|
||||
|
||||
|
||||
def format_message(message: str) -> str:
|
||||
"""Return a formated message if it's known."""
|
||||
try:
|
||||
match = RE_BIND_FAILED.match(message)
|
||||
if match:
|
||||
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)
|
||||
capture_exception(err)
|
||||
"""Return a formatted message if it's known."""
|
||||
match = RE_BIND_FAILED.match(message)
|
||||
if match:
|
||||
return (
|
||||
f"Port '{match.group(1)}' is already in use by something else on the host."
|
||||
)
|
||||
|
||||
return message
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Utilities for sentry."""
|
||||
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any
|
||||
@ -46,19 +47,43 @@ def init_sentry(coresys: CoreSys) -> None:
|
||||
|
||||
|
||||
def capture_event(event: dict[str, Any], only_once: str | None = None):
|
||||
"""Capture an event and send to sentry."""
|
||||
"""Capture an event and send to sentry.
|
||||
|
||||
Must be called in executor.
|
||||
"""
|
||||
if sentry_sdk.is_initialized():
|
||||
if only_once and only_once not in only_once_events:
|
||||
only_once_events.add(only_once)
|
||||
sentry_sdk.capture_event(event)
|
||||
|
||||
|
||||
async def async_capture_event(event: dict[str, Any], only_once: str | None = None):
|
||||
"""Capture an event and send to sentry.
|
||||
|
||||
Safe to call from event loop.
|
||||
"""
|
||||
await asyncio.get_running_loop().run_in_executor(
|
||||
None, capture_event, event, only_once
|
||||
)
|
||||
|
||||
|
||||
def capture_exception(err: Exception) -> None:
|
||||
"""Capture an exception and send to sentry."""
|
||||
"""Capture an exception and send to sentry.
|
||||
|
||||
Must be called in executor.
|
||||
"""
|
||||
if sentry_sdk.is_initialized():
|
||||
sentry_sdk.capture_exception(err)
|
||||
|
||||
|
||||
async def async_capture_exception(err: Exception) -> None:
|
||||
"""Capture an exception and send to sentry.
|
||||
|
||||
Safe to call in event loop.
|
||||
"""
|
||||
await asyncio.get_running_loop().run_in_executor(None, capture_exception, err)
|
||||
|
||||
|
||||
def close_sentry() -> None:
|
||||
"""Close the current sentry client.
|
||||
|
||||
|
@ -216,7 +216,7 @@ async def test_api_supervisor_fallback_log_capture(
|
||||
"No systemd-journal-gatewayd Unix socket available!"
|
||||
)
|
||||
|
||||
with patch("supervisor.api.capture_exception") as capture_exception:
|
||||
with patch("supervisor.api.async_capture_exception") as capture_exception:
|
||||
await api_client.get("/supervisor/logs")
|
||||
capture_exception.assert_not_called()
|
||||
|
||||
@ -224,7 +224,7 @@ async def test_api_supervisor_fallback_log_capture(
|
||||
|
||||
journald_logs.side_effect = HassioError("Something bad happened!")
|
||||
|
||||
with patch("supervisor.api.capture_exception") as capture_exception:
|
||||
with patch("supervisor.api.async_capture_exception") as capture_exception:
|
||||
await api_client.get("/supervisor/logs")
|
||||
capture_exception.assert_called_once()
|
||||
|
||||
|
@ -141,7 +141,7 @@ async def test_home_assistant_watchdog_rebuild_on_failure(coresys: CoreSys) -> N
|
||||
time=1,
|
||||
),
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
await asyncio.sleep(0.1)
|
||||
start.assert_called_once()
|
||||
rebuild.assert_called_once()
|
||||
|
||||
|
@ -5,10 +5,10 @@ from unittest.mock import patch
|
||||
from supervisor.host.info import InfoCenter
|
||||
|
||||
|
||||
def test_host_free_space(coresys):
|
||||
async def test_host_free_space(coresys):
|
||||
"""Test host free space."""
|
||||
info = InfoCenter(coresys)
|
||||
with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0**3))):
|
||||
free = info.free_space
|
||||
free = await info.free_space()
|
||||
|
||||
assert free == 2.0
|
||||
|
@ -19,9 +19,3 @@ def test_format_message_port_alternative():
|
||||
format_message(message)
|
||||
== "Port '80' is already in use by something else on the host."
|
||||
)
|
||||
|
||||
|
||||
def test_exeption():
|
||||
"""Tests the exception handling."""
|
||||
message = b"byte"
|
||||
assert format_message(message) == message
|
||||
|
Loading…
x
Reference in New Issue
Block a user