mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-27 11:06:32 +00:00
Improve error handling with job condition (#2322)
* Improve error handling with job condition * fix * first patch * last patch * Address comments * Revert strange replace
This commit is contained in:
parent
6462eea2ef
commit
f8fd7b5933
@ -10,6 +10,7 @@ from ..coresys import CoreSys, CoreSysAttributes
|
|||||||
from ..exceptions import (
|
from ..exceptions import (
|
||||||
AddonConfigurationError,
|
AddonConfigurationError,
|
||||||
AddonsError,
|
AddonsError,
|
||||||
|
AddonsJobError,
|
||||||
AddonsNotSupportedError,
|
AddonsNotSupportedError,
|
||||||
CoreDNSError,
|
CoreDNSError,
|
||||||
DockerAPIError,
|
DockerAPIError,
|
||||||
@ -147,7 +148,8 @@ class AddonManager(CoreSysAttributes):
|
|||||||
JobCondition.FREE_SPACE,
|
JobCondition.FREE_SPACE,
|
||||||
JobCondition.INTERNET_HOST,
|
JobCondition.INTERNET_HOST,
|
||||||
JobCondition.HEALTHY,
|
JobCondition.HEALTHY,
|
||||||
]
|
],
|
||||||
|
on_condition=AddonsJobError,
|
||||||
)
|
)
|
||||||
async def install(self, slug: str) -> None:
|
async def install(self, slug: str) -> None:
|
||||||
"""Install an add-on."""
|
"""Install an add-on."""
|
||||||
@ -248,7 +250,8 @@ class AddonManager(CoreSysAttributes):
|
|||||||
JobCondition.FREE_SPACE,
|
JobCondition.FREE_SPACE,
|
||||||
JobCondition.INTERNET_HOST,
|
JobCondition.INTERNET_HOST,
|
||||||
JobCondition.HEALTHY,
|
JobCondition.HEALTHY,
|
||||||
]
|
],
|
||||||
|
on_condition=AddonsJobError,
|
||||||
)
|
)
|
||||||
async def update(self, slug: str) -> None:
|
async def update(self, slug: str) -> None:
|
||||||
"""Update add-on."""
|
"""Update add-on."""
|
||||||
@ -297,7 +300,8 @@ class AddonManager(CoreSysAttributes):
|
|||||||
JobCondition.FREE_SPACE,
|
JobCondition.FREE_SPACE,
|
||||||
JobCondition.INTERNET_HOST,
|
JobCondition.INTERNET_HOST,
|
||||||
JobCondition.HEALTHY,
|
JobCondition.HEALTHY,
|
||||||
]
|
],
|
||||||
|
on_condition=AddonsJobError,
|
||||||
)
|
)
|
||||||
async def rebuild(self, slug: str) -> None:
|
async def rebuild(self, slug: str) -> None:
|
||||||
"""Perform a rebuild of local build add-on."""
|
"""Perform a rebuild of local build add-on."""
|
||||||
@ -339,7 +343,8 @@ class AddonManager(CoreSysAttributes):
|
|||||||
JobCondition.FREE_SPACE,
|
JobCondition.FREE_SPACE,
|
||||||
JobCondition.INTERNET_HOST,
|
JobCondition.INTERNET_HOST,
|
||||||
JobCondition.HEALTHY,
|
JobCondition.HEALTHY,
|
||||||
]
|
],
|
||||||
|
on_condition=AddonsJobError,
|
||||||
)
|
)
|
||||||
async def restore(self, slug: str, tar_file: tarfile.TarFile) -> None:
|
async def restore(self, slug: str, tar_file: tarfile.TarFile) -> None:
|
||||||
"""Restore state of an add-on."""
|
"""Restore state of an add-on."""
|
||||||
|
@ -9,6 +9,13 @@ class HassioNotSupportedError(HassioError):
|
|||||||
"""Function is not supported."""
|
"""Function is not supported."""
|
||||||
|
|
||||||
|
|
||||||
|
# JobManager
|
||||||
|
|
||||||
|
|
||||||
|
class JobException(HassioError):
|
||||||
|
"""Base job exception."""
|
||||||
|
|
||||||
|
|
||||||
# HomeAssistant
|
# HomeAssistant
|
||||||
|
|
||||||
|
|
||||||
@ -32,6 +39,10 @@ class HomeAssistantAuthError(HomeAssistantAPIError):
|
|||||||
"""Home Assistant Auth API exception."""
|
"""Home Assistant Auth API exception."""
|
||||||
|
|
||||||
|
|
||||||
|
class HomeAssistantJobError(HomeAssistantError, JobException):
|
||||||
|
"""Raise on Home Assistant job error."""
|
||||||
|
|
||||||
|
|
||||||
# Supervisor
|
# Supervisor
|
||||||
|
|
||||||
|
|
||||||
@ -43,6 +54,10 @@ class SupervisorUpdateError(SupervisorError):
|
|||||||
"""Supervisor update error."""
|
"""Supervisor update error."""
|
||||||
|
|
||||||
|
|
||||||
|
class SupervisorJobError(SupervisorError, JobException):
|
||||||
|
"""Raise on job errors."""
|
||||||
|
|
||||||
|
|
||||||
# HassOS
|
# HassOS
|
||||||
|
|
||||||
|
|
||||||
@ -128,6 +143,10 @@ class AddonsNotSupportedError(HassioNotSupportedError):
|
|||||||
"""Addons don't support a function."""
|
"""Addons don't support a function."""
|
||||||
|
|
||||||
|
|
||||||
|
class AddonsJobError(AddonsError, JobException):
|
||||||
|
"""Raise on job errors."""
|
||||||
|
|
||||||
|
|
||||||
# Arch
|
# Arch
|
||||||
|
|
||||||
|
|
||||||
@ -138,10 +157,14 @@ class HassioArchNotFound(HassioNotSupportedError):
|
|||||||
# Updater
|
# Updater
|
||||||
|
|
||||||
|
|
||||||
class HassioUpdaterError(HassioError):
|
class UpdaterError(HassioError):
|
||||||
"""Error on Updater."""
|
"""Error on Updater."""
|
||||||
|
|
||||||
|
|
||||||
|
class UpdaterJobError(UpdaterError, JobException):
|
||||||
|
"""Raise on job error."""
|
||||||
|
|
||||||
|
|
||||||
# Auth
|
# Auth
|
||||||
|
|
||||||
|
|
||||||
@ -312,10 +335,3 @@ class StoreGitError(StoreError):
|
|||||||
|
|
||||||
class StoreNotFound(StoreError):
|
class StoreNotFound(StoreError):
|
||||||
"""Raise if slug is not known."""
|
"""Raise if slug is not known."""
|
||||||
|
|
||||||
|
|
||||||
# JobManager
|
|
||||||
|
|
||||||
|
|
||||||
class JobException(HassioError):
|
|
||||||
"""Base job exception."""
|
|
||||||
|
@ -19,6 +19,7 @@ from ..exceptions import (
|
|||||||
DockerError,
|
DockerError,
|
||||||
HomeAssistantCrashError,
|
HomeAssistantCrashError,
|
||||||
HomeAssistantError,
|
HomeAssistantError,
|
||||||
|
HomeAssistantJobError,
|
||||||
HomeAssistantUpdateError,
|
HomeAssistantUpdateError,
|
||||||
)
|
)
|
||||||
from ..jobs.decorator import Job, JobCondition
|
from ..jobs.decorator import Job, JobCondition
|
||||||
@ -158,7 +159,8 @@ class HomeAssistantCore(CoreSysAttributes):
|
|||||||
JobCondition.FREE_SPACE,
|
JobCondition.FREE_SPACE,
|
||||||
JobCondition.HEALTHY,
|
JobCondition.HEALTHY,
|
||||||
JobCondition.INTERNET_HOST,
|
JobCondition.INTERNET_HOST,
|
||||||
]
|
],
|
||||||
|
on_condition=HomeAssistantJobError,
|
||||||
)
|
)
|
||||||
async def update(self, version: Optional[str] = None) -> None:
|
async def update(self, version: Optional[str] = None) -> None:
|
||||||
"""Update HomeAssistant version."""
|
"""Update HomeAssistant version."""
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Job decorator."""
|
"""Job decorator."""
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Optional
|
from typing import Any, List, Optional
|
||||||
|
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
|
|
||||||
@ -21,11 +21,13 @@ class Job:
|
|||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
conditions: Optional[List[JobCondition]] = None,
|
conditions: Optional[List[JobCondition]] = None,
|
||||||
cleanup: bool = True,
|
cleanup: bool = True,
|
||||||
|
on_condition: Optional[JobException] = None,
|
||||||
):
|
):
|
||||||
"""Initialize the Job class."""
|
"""Initialize the Job class."""
|
||||||
self.name = name
|
self.name = name
|
||||||
self.conditions = conditions
|
self.conditions = conditions
|
||||||
self.cleanup = cleanup
|
self.cleanup = cleanup
|
||||||
|
self.on_condition = on_condition
|
||||||
self._coresys: Optional[CoreSys] = None
|
self._coresys: Optional[CoreSys] = None
|
||||||
self._method = None
|
self._method = None
|
||||||
|
|
||||||
@ -33,23 +35,28 @@ class Job:
|
|||||||
"""Call the wrapper logic."""
|
"""Call the wrapper logic."""
|
||||||
self._method = method
|
self._method = method
|
||||||
|
|
||||||
async def wrapper(*args, **kwargs):
|
async def wrapper(*args, **kwargs) -> Any:
|
||||||
"""Wrap the method."""
|
"""Wrap the method."""
|
||||||
if self.name is None:
|
if self.name is None:
|
||||||
self.name = str(self._method.__qualname__).lower().replace(".", "_")
|
self.name = str(self._method.__qualname__).lower().replace(".", "_")
|
||||||
|
|
||||||
|
# Evaluate coresys
|
||||||
try:
|
try:
|
||||||
self._coresys = args[0].coresys
|
self._coresys = args[0].coresys
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return False
|
pass
|
||||||
|
|
||||||
if not self._coresys:
|
if not self._coresys:
|
||||||
raise JobException(f"coresys is missing on {self.name}")
|
raise JobException(f"coresys is missing on {self.name}")
|
||||||
|
|
||||||
job = self._coresys.jobs.get_job(self.name)
|
job = self._coresys.jobs.get_job(self.name)
|
||||||
|
|
||||||
|
# Handle condition
|
||||||
if self.conditions and not self._check_conditions():
|
if self.conditions and not self._check_conditions():
|
||||||
return False
|
if self.on_condition is None:
|
||||||
|
return
|
||||||
|
raise self.on_condition()
|
||||||
|
|
||||||
|
# Execute Job
|
||||||
try:
|
try:
|
||||||
return await self._method(*args, **kwargs)
|
return await self._method(*args, **kwargs)
|
||||||
except HassioError as err:
|
except HassioError as err:
|
||||||
|
@ -122,7 +122,7 @@ class SnapshotManager(CoreSysAttributes):
|
|||||||
self.snapshots_obj[snapshot.slug] = snapshot
|
self.snapshots_obj[snapshot.slug] = snapshot
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
@Job(conditions=[JobCondition.FREE_SPACE])
|
@Job(conditions=[JobCondition.FREE_SPACE, JobCondition.RUNNING])
|
||||||
async def do_snapshot_full(self, name="", password=None):
|
async def do_snapshot_full(self, name="", password=None):
|
||||||
"""Create a full snapshot."""
|
"""Create a full snapshot."""
|
||||||
if self.lock.locked():
|
if self.lock.locked():
|
||||||
@ -144,9 +144,9 @@ class SnapshotManager(CoreSysAttributes):
|
|||||||
_LOGGER.info("Snapshotting %s store folders", snapshot.slug)
|
_LOGGER.info("Snapshotting %s store folders", snapshot.slug)
|
||||||
await snapshot.store_folders()
|
await snapshot.store_folders()
|
||||||
|
|
||||||
except Exception as excep: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
_LOGGER.exception("Snapshot %s error", snapshot.slug)
|
_LOGGER.exception("Snapshot %s error", snapshot.slug)
|
||||||
print(excep)
|
self.sys_capture_exception(err)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -158,7 +158,7 @@ class SnapshotManager(CoreSysAttributes):
|
|||||||
self.sys_core.state = CoreState.RUNNING
|
self.sys_core.state = CoreState.RUNNING
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
|
|
||||||
@Job(conditions=[JobCondition.FREE_SPACE])
|
@Job(conditions=[JobCondition.FREE_SPACE, JobCondition.RUNNING])
|
||||||
async def do_snapshot_partial(
|
async def do_snapshot_partial(
|
||||||
self, name="", addons=None, folders=None, password=None
|
self, name="", addons=None, folders=None, password=None
|
||||||
):
|
):
|
||||||
@ -195,8 +195,9 @@ class SnapshotManager(CoreSysAttributes):
|
|||||||
_LOGGER.info("Snapshotting %s store folders", snapshot.slug)
|
_LOGGER.info("Snapshotting %s store folders", snapshot.slug)
|
||||||
await snapshot.store_folders(folders)
|
await snapshot.store_folders(folders)
|
||||||
|
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
_LOGGER.exception("Snapshot %s error", snapshot.slug)
|
_LOGGER.exception("Snapshot %s error", snapshot.slug)
|
||||||
|
self.sys_capture_exception(err)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -216,6 +217,7 @@ class SnapshotManager(CoreSysAttributes):
|
|||||||
JobCondition.HEALTHY,
|
JobCondition.HEALTHY,
|
||||||
JobCondition.INTERNET_HOST,
|
JobCondition.INTERNET_HOST,
|
||||||
JobCondition.INTERNET_SYSTEM,
|
JobCondition.INTERNET_SYSTEM,
|
||||||
|
JobCondition.RUNNING,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
async def do_restore_full(self, snapshot, password=None):
|
async def do_restore_full(self, snapshot, password=None):
|
||||||
@ -282,8 +284,9 @@ class SnapshotManager(CoreSysAttributes):
|
|||||||
await task_hass
|
await task_hass
|
||||||
await self.sys_homeassistant.core.start()
|
await self.sys_homeassistant.core.start()
|
||||||
|
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
_LOGGER.exception("Restore %s error", snapshot.slug)
|
_LOGGER.exception("Restore %s error", snapshot.slug)
|
||||||
|
self.sys_capture_exception(err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -300,6 +303,7 @@ class SnapshotManager(CoreSysAttributes):
|
|||||||
JobCondition.HEALTHY,
|
JobCondition.HEALTHY,
|
||||||
JobCondition.INTERNET_HOST,
|
JobCondition.INTERNET_HOST,
|
||||||
JobCondition.INTERNET_SYSTEM,
|
JobCondition.INTERNET_SYSTEM,
|
||||||
|
JobCondition.RUNNING,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
async def do_restore_partial(
|
async def do_restore_partial(
|
||||||
@ -368,8 +372,9 @@ class SnapshotManager(CoreSysAttributes):
|
|||||||
_LOGGER.warning("Need restart HomeAssistant for API")
|
_LOGGER.warning("Need restart HomeAssistant for API")
|
||||||
await self.sys_homeassistant.core.restart()
|
await self.sys_homeassistant.core.restart()
|
||||||
|
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
_LOGGER.exception("Restore %s error", snapshot.slug)
|
_LOGGER.exception("Restore %s error", snapshot.slug)
|
||||||
|
self.sys_capture_exception(err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -21,6 +21,7 @@ from .exceptions import (
|
|||||||
DockerError,
|
DockerError,
|
||||||
HostAppArmorError,
|
HostAppArmorError,
|
||||||
SupervisorError,
|
SupervisorError,
|
||||||
|
SupervisorJobError,
|
||||||
SupervisorUpdateError,
|
SupervisorUpdateError,
|
||||||
)
|
)
|
||||||
from .resolution.const import ContextType, IssueType
|
from .resolution.const import ContextType, IssueType
|
||||||
@ -147,7 +148,7 @@ class Supervisor(CoreSysAttributes):
|
|||||||
await self.update_apparmor()
|
await self.update_apparmor()
|
||||||
self.sys_create_task(self.sys_core.stop())
|
self.sys_create_task(self.sys_core.stop())
|
||||||
|
|
||||||
@Job(conditions=[JobCondition.RUNNING])
|
@Job(conditions=[JobCondition.RUNNING], on_condition=SupervisorJobError)
|
||||||
async def restart(self) -> None:
|
async def restart(self) -> None:
|
||||||
"""Restart Supervisor soft."""
|
"""Restart Supervisor soft."""
|
||||||
self.sys_core.exit_code = 100
|
self.sys_core.exit_code = 100
|
||||||
|
@ -25,7 +25,7 @@ from .const import (
|
|||||||
UpdateChannel,
|
UpdateChannel,
|
||||||
)
|
)
|
||||||
from .coresys import CoreSysAttributes
|
from .coresys import CoreSysAttributes
|
||||||
from .exceptions import HassioUpdaterError
|
from .exceptions import UpdaterError, UpdaterJobError
|
||||||
from .jobs.decorator import Job, JobCondition
|
from .jobs.decorator import Job, JobCondition
|
||||||
from .utils import AsyncThrottle
|
from .utils import AsyncThrottle
|
||||||
from .utils.json import JsonConfig
|
from .utils.json import JsonConfig
|
||||||
@ -44,12 +44,12 @@ class Updater(JsonConfig, CoreSysAttributes):
|
|||||||
|
|
||||||
async def load(self) -> None:
|
async def load(self) -> None:
|
||||||
"""Update internal data."""
|
"""Update internal data."""
|
||||||
with suppress(HassioUpdaterError):
|
with suppress(UpdaterError):
|
||||||
await self.fetch_data()
|
await self.fetch_data()
|
||||||
|
|
||||||
async def reload(self) -> None:
|
async def reload(self) -> None:
|
||||||
"""Update internal data."""
|
"""Update internal data."""
|
||||||
with suppress(HassioUpdaterError):
|
with suppress(UpdaterJobError):
|
||||||
await self.fetch_data()
|
await self.fetch_data()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -165,7 +165,10 @@ class Updater(JsonConfig, CoreSysAttributes):
|
|||||||
self._data[ATTR_CHANNEL] = value
|
self._data[ATTR_CHANNEL] = value
|
||||||
|
|
||||||
@AsyncThrottle(timedelta(seconds=30))
|
@AsyncThrottle(timedelta(seconds=30))
|
||||||
@Job(conditions=[JobCondition.INTERNET_SYSTEM])
|
@Job(
|
||||||
|
conditions=[JobCondition.INTERNET_SYSTEM],
|
||||||
|
on_condition=UpdaterJobError,
|
||||||
|
)
|
||||||
async def fetch_data(self):
|
async def fetch_data(self):
|
||||||
"""Fetch current versions from Github.
|
"""Fetch current versions from Github.
|
||||||
|
|
||||||
@ -181,16 +184,16 @@ class Updater(JsonConfig, CoreSysAttributes):
|
|||||||
|
|
||||||
except (aiohttp.ClientError, asyncio.TimeoutError) as err:
|
except (aiohttp.ClientError, asyncio.TimeoutError) as err:
|
||||||
_LOGGER.warning("Can't fetch versions from %s: %s", url, err)
|
_LOGGER.warning("Can't fetch versions from %s: %s", url, err)
|
||||||
raise HassioUpdaterError() from err
|
raise UpdaterError() from err
|
||||||
|
|
||||||
except json.JSONDecodeError as err:
|
except json.JSONDecodeError as err:
|
||||||
_LOGGER.warning("Can't parse versions from %s: %s", url, err)
|
_LOGGER.warning("Can't parse versions from %s: %s", url, err)
|
||||||
raise HassioUpdaterError() from err
|
raise UpdaterError() from err
|
||||||
|
|
||||||
# data valid?
|
# data valid?
|
||||||
if not data or data.get(ATTR_CHANNEL) != self.channel:
|
if not data or data.get(ATTR_CHANNEL) != self.channel:
|
||||||
_LOGGER.warning("Invalid data from %s", url)
|
_LOGGER.warning("Invalid data from %s", url)
|
||||||
raise HassioUpdaterError()
|
raise UpdaterError()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Update supervisor version
|
# Update supervisor version
|
||||||
@ -222,7 +225,7 @@ class Updater(JsonConfig, CoreSysAttributes):
|
|||||||
|
|
||||||
except KeyError as err:
|
except KeyError as err:
|
||||||
_LOGGER.warning("Can't process version data: %s", err)
|
_LOGGER.warning("Can't process version data: %s", err)
|
||||||
raise HassioUpdaterError() from err
|
raise UpdaterError() from err
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.save_data()
|
self.save_data()
|
||||||
|
@ -232,3 +232,28 @@ async def test_ignore_conditions(coresys: CoreSys):
|
|||||||
|
|
||||||
coresys.jobs.ignore_conditions = [JobCondition.RUNNING]
|
coresys.jobs.ignore_conditions = [JobCondition.RUNNING]
|
||||||
assert await test.execute()
|
assert await test.execute()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_exception_conditions(coresys: CoreSys):
|
||||||
|
"""Test the ignore conditions decorator."""
|
||||||
|
|
||||||
|
class TestClass:
|
||||||
|
"""Test class."""
|
||||||
|
|
||||||
|
def __init__(self, coresys: CoreSys):
|
||||||
|
"""Initialize the test class."""
|
||||||
|
self.coresys = coresys
|
||||||
|
|
||||||
|
@Job(conditions=[JobCondition.RUNNING], on_condition=HassioError)
|
||||||
|
async def execute(self):
|
||||||
|
"""Execute the class method."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
test = TestClass(coresys)
|
||||||
|
|
||||||
|
coresys.core.state = CoreState.RUNNING
|
||||||
|
assert await test.execute()
|
||||||
|
|
||||||
|
coresys.core.state = CoreState.FREEZE
|
||||||
|
with pytest.raises(HassioError):
|
||||||
|
await test.execute()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user