mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-19 15:16:33 +00:00
Add auto update option (#3769)
* Add update freeze option * Freeze to auto update and plugin condition * Add tests * Add supervisor_version evaluation * OS updates require supervisor up to date * Run version check during startup
This commit is contained in:
parent
e82cb5da45
commit
c8f184f24c
@ -24,6 +24,7 @@ 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 .addon import Addon
|
from .addon import Addon
|
||||||
|
from .const import ADDON_UPDATE_CONDITIONS
|
||||||
from .data import AddonsData
|
from .data import AddonsData
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@ -144,11 +145,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
self.sys_capture_exception(err)
|
self.sys_capture_exception(err)
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
conditions=[
|
conditions=ADDON_UPDATE_CONDITIONS,
|
||||||
JobCondition.FREE_SPACE,
|
|
||||||
JobCondition.INTERNET_HOST,
|
|
||||||
JobCondition.HEALTHY,
|
|
||||||
],
|
|
||||||
on_condition=AddonsJobError,
|
on_condition=AddonsJobError,
|
||||||
)
|
)
|
||||||
async def install(self, slug: str) -> None:
|
async def install(self, slug: str) -> None:
|
||||||
@ -246,11 +243,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
_LOGGER.info("Add-on '%s' successfully removed", slug)
|
_LOGGER.info("Add-on '%s' successfully removed", slug)
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
conditions=[
|
conditions=ADDON_UPDATE_CONDITIONS,
|
||||||
JobCondition.FREE_SPACE,
|
|
||||||
JobCondition.INTERNET_HOST,
|
|
||||||
JobCondition.HEALTHY,
|
|
||||||
],
|
|
||||||
on_condition=AddonsJobError,
|
on_condition=AddonsJobError,
|
||||||
)
|
)
|
||||||
async def update(self, slug: str, backup: Optional[bool] = False) -> None:
|
async def update(self, slug: str, backup: Optional[bool] = False) -> None:
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
from ..jobs.const import JobCondition
|
||||||
|
|
||||||
|
|
||||||
class AddonBackupMode(str, Enum):
|
class AddonBackupMode(str, Enum):
|
||||||
"""Backup mode of an Add-on."""
|
"""Backup mode of an Add-on."""
|
||||||
@ -16,3 +18,11 @@ WATCHDOG_RETRY_SECONDS = 10
|
|||||||
WATCHDOG_MAX_ATTEMPTS = 5
|
WATCHDOG_MAX_ATTEMPTS = 5
|
||||||
WATCHDOG_THROTTLE_PERIOD = timedelta(minutes=30)
|
WATCHDOG_THROTTLE_PERIOD = timedelta(minutes=30)
|
||||||
WATCHDOG_THROTTLE_MAX_CALLS = 10
|
WATCHDOG_THROTTLE_MAX_CALLS = 10
|
||||||
|
|
||||||
|
ADDON_UPDATE_CONDITIONS = [
|
||||||
|
JobCondition.FREE_SPACE,
|
||||||
|
JobCondition.HEALTHY,
|
||||||
|
JobCondition.INTERNET_HOST,
|
||||||
|
JobCondition.PLUGINS_UPDATED,
|
||||||
|
JobCondition.SUPERVISOR_UPDATED,
|
||||||
|
]
|
||||||
|
@ -10,6 +10,7 @@ from ..const import (
|
|||||||
ATTR_ADDONS,
|
ATTR_ADDONS,
|
||||||
ATTR_ADDONS_REPOSITORIES,
|
ATTR_ADDONS_REPOSITORIES,
|
||||||
ATTR_ARCH,
|
ATTR_ARCH,
|
||||||
|
ATTR_AUTO_UPDATE,
|
||||||
ATTR_BLK_READ,
|
ATTR_BLK_READ,
|
||||||
ATTR_BLK_WRITE,
|
ATTR_BLK_WRITE,
|
||||||
ATTR_CHANNEL,
|
ATTR_CHANNEL,
|
||||||
@ -64,6 +65,7 @@ SCHEMA_OPTIONS = vol.Schema(
|
|||||||
vol.Optional(ATTR_DIAGNOSTICS): vol.Boolean(),
|
vol.Optional(ATTR_DIAGNOSTICS): vol.Boolean(),
|
||||||
vol.Optional(ATTR_CONTENT_TRUST): vol.Boolean(),
|
vol.Optional(ATTR_CONTENT_TRUST): vol.Boolean(),
|
||||||
vol.Optional(ATTR_FORCE_SECURITY): vol.Boolean(),
|
vol.Optional(ATTR_FORCE_SECURITY): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -96,6 +98,7 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
ATTR_DEBUG: self.sys_config.debug,
|
ATTR_DEBUG: self.sys_config.debug,
|
||||||
ATTR_DEBUG_BLOCK: self.sys_config.debug_block,
|
ATTR_DEBUG_BLOCK: self.sys_config.debug_block,
|
||||||
ATTR_DIAGNOSTICS: self.sys_config.diagnostics,
|
ATTR_DIAGNOSTICS: self.sys_config.diagnostics,
|
||||||
|
ATTR_AUTO_UPDATE: self.sys_updater.auto_update,
|
||||||
# Depricated
|
# Depricated
|
||||||
ATTR_ADDONS: [
|
ATTR_ADDONS: [
|
||||||
{
|
{
|
||||||
@ -143,6 +146,9 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
if ATTR_LOGGING in body:
|
if ATTR_LOGGING in body:
|
||||||
self.sys_config.logging = body[ATTR_LOGGING]
|
self.sys_config.logging = body[ATTR_LOGGING]
|
||||||
|
|
||||||
|
if ATTR_AUTO_UPDATE in body:
|
||||||
|
self.sys_updater.auto_update = body[ATTR_AUTO_UPDATE]
|
||||||
|
|
||||||
# Save changes before processing addons in case of errors
|
# Save changes before processing addons in case of errors
|
||||||
self.sys_updater.save_data()
|
self.sys_updater.save_data()
|
||||||
self.sys_config.save_data()
|
self.sys_config.save_data()
|
||||||
|
@ -7,7 +7,7 @@ import tarfile
|
|||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from typing import Any, Awaitable, Optional
|
from typing import Any, Awaitable, Optional
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersionCompareException
|
from awesomeversion import AwesomeVersion, AwesomeVersionCompareException
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives import padding
|
from cryptography.hazmat.primitives import padding
|
||||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
@ -31,6 +31,7 @@ from ..const import (
|
|||||||
ATTR_REPOSITORIES,
|
ATTR_REPOSITORIES,
|
||||||
ATTR_SIZE,
|
ATTR_SIZE,
|
||||||
ATTR_SLUG,
|
ATTR_SLUG,
|
||||||
|
ATTR_SUPERVISOR_VERSION,
|
||||||
ATTR_TYPE,
|
ATTR_TYPE,
|
||||||
ATTR_USERNAME,
|
ATTR_USERNAME,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
@ -121,7 +122,7 @@ class Backup(CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def homeassistant_version(self):
|
def homeassistant_version(self):
|
||||||
"""Return backupbackup Home Assistant version."""
|
"""Return backup Home Assistant version."""
|
||||||
if self.homeassistant is None:
|
if self.homeassistant is None:
|
||||||
return None
|
return None
|
||||||
return self._data[ATTR_HOMEASSISTANT][ATTR_VERSION]
|
return self._data[ATTR_HOMEASSISTANT][ATTR_VERSION]
|
||||||
@ -131,6 +132,11 @@ class Backup(CoreSysAttributes):
|
|||||||
"""Return backup Home Assistant data."""
|
"""Return backup Home Assistant data."""
|
||||||
return self._data[ATTR_HOMEASSISTANT]
|
return self._data[ATTR_HOMEASSISTANT]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supervisor_version(self) -> AwesomeVersion:
|
||||||
|
"""Return backup Supervisor version."""
|
||||||
|
return self._data[ATTR_SUPERVISOR_VERSION]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def docker(self):
|
def docker(self):
|
||||||
"""Return backup Docker config data."""
|
"""Return backup Docker config data."""
|
||||||
@ -166,6 +172,7 @@ class Backup(CoreSysAttributes):
|
|||||||
self._data[ATTR_NAME] = name
|
self._data[ATTR_NAME] = name
|
||||||
self._data[ATTR_DATE] = date
|
self._data[ATTR_DATE] = date
|
||||||
self._data[ATTR_TYPE] = sys_type
|
self._data[ATTR_TYPE] = sys_type
|
||||||
|
self._data[ATTR_SUPERVISOR_VERSION] = self.sys_supervisor.version
|
||||||
|
|
||||||
# Add defaults
|
# Add defaults
|
||||||
self._data = SCHEMA_BACKUP(self._data)
|
self._data = SCHEMA_BACKUP(self._data)
|
||||||
|
@ -316,6 +316,14 @@ class BackupManager(CoreSysAttributes):
|
|||||||
_LOGGER.error("Invalid password for backup %s", backup.slug)
|
_LOGGER.error("Invalid password for backup %s", backup.slug)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if backup.supervisor_version > self.sys_supervisor.version:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Backup was made on supervisor version %s, can't restore on %s. Must update supervisor first.",
|
||||||
|
backup.supervisor_version,
|
||||||
|
self.sys_supervisor.version,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
_LOGGER.info("Full-Restore %s start", backup.slug)
|
_LOGGER.info("Full-Restore %s start", backup.slug)
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
self.sys_core.state = CoreState.FREEZE
|
self.sys_core.state = CoreState.FREEZE
|
||||||
@ -370,6 +378,14 @@ class BackupManager(CoreSysAttributes):
|
|||||||
_LOGGER.error("No Home Assistant Core data inside the backup")
|
_LOGGER.error("No Home Assistant Core data inside the backup")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if backup.supervisor_version > self.sys_supervisor.version:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Backup was made on supervisor version %s, can't restore on %s. Must update supervisor first.",
|
||||||
|
backup.supervisor_version,
|
||||||
|
self.sys_supervisor.version,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
_LOGGER.info("Partial-Restore %s start", backup.slug)
|
_LOGGER.info("Partial-Restore %s start", backup.slug)
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
self.sys_core.state = CoreState.FREEZE
|
self.sys_core.state = CoreState.FREEZE
|
||||||
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from awesomeversion import AwesomeVersion
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from ..backups.const import BackupType
|
from ..backups.const import BackupType
|
||||||
@ -19,6 +20,7 @@ from ..const import (
|
|||||||
ATTR_REPOSITORIES,
|
ATTR_REPOSITORIES,
|
||||||
ATTR_SIZE,
|
ATTR_SIZE,
|
||||||
ATTR_SLUG,
|
ATTR_SLUG,
|
||||||
|
ATTR_SUPERVISOR_VERSION,
|
||||||
ATTR_TYPE,
|
ATTR_TYPE,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
CRYPTO_AES128,
|
CRYPTO_AES128,
|
||||||
@ -79,6 +81,9 @@ def v1_protected(protected: bool | str) -> bool:
|
|||||||
SCHEMA_BACKUP = vol.Schema(
|
SCHEMA_BACKUP = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(ATTR_VERSION, default=1): vol.All(vol.Coerce(int), vol.In((1, 2))),
|
vol.Optional(ATTR_VERSION, default=1): vol.All(vol.Coerce(int), vol.In((1, 2))),
|
||||||
|
vol.Optional(
|
||||||
|
ATTR_SUPERVISOR_VERSION, default=AwesomeVersion("2022.08.3")
|
||||||
|
): version_tag,
|
||||||
vol.Required(ATTR_SLUG): str,
|
vol.Required(ATTR_SLUG): str,
|
||||||
vol.Required(ATTR_TYPE): vol.Coerce(BackupType),
|
vol.Required(ATTR_TYPE): vol.Coerce(BackupType),
|
||||||
vol.Required(ATTR_NAME): str,
|
vol.Required(ATTR_NAME): str,
|
||||||
|
@ -237,6 +237,7 @@ ATTR_PANEL_TITLE = "panel_title"
|
|||||||
ATTR_PANELS = "panels"
|
ATTR_PANELS = "panels"
|
||||||
ATTR_PARENT = "parent"
|
ATTR_PARENT = "parent"
|
||||||
ATTR_PASSWORD = "password"
|
ATTR_PASSWORD = "password"
|
||||||
|
ATTR_PLUGINS = "plugins"
|
||||||
ATTR_PORT = "port"
|
ATTR_PORT = "port"
|
||||||
ATTR_PORTS = "ports"
|
ATTR_PORTS = "ports"
|
||||||
ATTR_PORTS_DESCRIPTION = "ports_description"
|
ATTR_PORTS_DESCRIPTION = "ports_description"
|
||||||
@ -278,6 +279,7 @@ ATTR_STORAGE = "storage"
|
|||||||
ATTR_SUGGESTIONS = "suggestions"
|
ATTR_SUGGESTIONS = "suggestions"
|
||||||
ATTR_SUPERVISOR = "supervisor"
|
ATTR_SUPERVISOR = "supervisor"
|
||||||
ATTR_SUPERVISOR_INTERNET = "supervisor_internet"
|
ATTR_SUPERVISOR_INTERNET = "supervisor_internet"
|
||||||
|
ATTR_SUPERVISOR_VERSION = "supervisor_version"
|
||||||
ATTR_SUPPORTED = "supported"
|
ATTR_SUPPORTED = "supported"
|
||||||
ATTR_SUPPORTED_ARCH = "supported_arch"
|
ATTR_SUPPORTED_ARCH = "supported_arch"
|
||||||
ATTR_SYSTEM = "system"
|
ATTR_SYSTEM = "system"
|
||||||
|
@ -182,8 +182,8 @@ class Core(CoreSysAttributes):
|
|||||||
# Mark booted partition as healthy
|
# Mark booted partition as healthy
|
||||||
await self.sys_os.mark_healthy()
|
await self.sys_os.mark_healthy()
|
||||||
|
|
||||||
# On release channel, try update itself
|
# On release channel, try update itself if auto update enabled
|
||||||
if self.sys_supervisor.need_update:
|
if self.sys_supervisor.need_update and self.sys_updater.auto_update:
|
||||||
try:
|
try:
|
||||||
if not self.healthy:
|
if not self.healthy:
|
||||||
_LOGGER.warning("Ignoring Supervisor updates!")
|
_LOGGER.warning("Ignoring Supervisor updates!")
|
||||||
|
@ -182,6 +182,8 @@ class HomeAssistantCore(CoreSysAttributes):
|
|||||||
JobCondition.FREE_SPACE,
|
JobCondition.FREE_SPACE,
|
||||||
JobCondition.HEALTHY,
|
JobCondition.HEALTHY,
|
||||||
JobCondition.INTERNET_HOST,
|
JobCondition.INTERNET_HOST,
|
||||||
|
JobCondition.PLUGINS_UPDATED,
|
||||||
|
JobCondition.SUPERVISOR_UPDATED,
|
||||||
],
|
],
|
||||||
on_condition=HomeAssistantJobError,
|
on_condition=HomeAssistantJobError,
|
||||||
)
|
)
|
||||||
|
@ -12,22 +12,24 @@ ATTR_IGNORE_CONDITIONS = "ignore_conditions"
|
|||||||
class JobCondition(str, Enum):
|
class JobCondition(str, Enum):
|
||||||
"""Job condition enum."""
|
"""Job condition enum."""
|
||||||
|
|
||||||
|
AUTO_UPDATE = "auto_update"
|
||||||
FREE_SPACE = "free_space"
|
FREE_SPACE = "free_space"
|
||||||
HEALTHY = "healthy"
|
|
||||||
INTERNET_SYSTEM = "internet_system"
|
|
||||||
INTERNET_HOST = "internet_host"
|
|
||||||
RUNNING = "running"
|
|
||||||
HAOS = "haos"
|
HAOS = "haos"
|
||||||
OS_AGENT = "os_agent"
|
HEALTHY = "healthy"
|
||||||
HOST_NETWORK = "host_network"
|
HOST_NETWORK = "host_network"
|
||||||
|
INTERNET_HOST = "internet_host"
|
||||||
|
INTERNET_SYSTEM = "internet_system"
|
||||||
|
OS_AGENT = "os_agent"
|
||||||
|
PLUGINS_UPDATED = "plugins_updated"
|
||||||
|
RUNNING = "running"
|
||||||
SUPERVISOR_UPDATED = "supervisor_updated"
|
SUPERVISOR_UPDATED = "supervisor_updated"
|
||||||
|
|
||||||
|
|
||||||
class JobExecutionLimit(str, Enum):
|
class JobExecutionLimit(str, Enum):
|
||||||
"""Job Execution limits."""
|
"""Job Execution limits."""
|
||||||
|
|
||||||
SINGLE_WAIT = "single_wait"
|
|
||||||
ONCE = "once"
|
ONCE = "once"
|
||||||
|
SINGLE_WAIT = "single_wait"
|
||||||
THROTTLE = "throttle"
|
THROTTLE = "throttle"
|
||||||
THROTTLE_WAIT = "throttle_wait"
|
THROTTLE_WAIT = "throttle_wait"
|
||||||
THROTTLE_RATE_LIMIT = "throttle_rate_limit"
|
THROTTLE_RATE_LIMIT = "throttle_rate_limit"
|
||||||
|
@ -222,6 +222,14 @@ class Job(CoreSysAttributes):
|
|||||||
f"'{self._method.__qualname__}' blocked from execution, host Network Manager not available"
|
f"'{self._method.__qualname__}' blocked from execution, host Network Manager not available"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
JobCondition.AUTO_UPDATE in self.conditions
|
||||||
|
and not self.sys_updater.auto_update
|
||||||
|
):
|
||||||
|
raise JobConditionException(
|
||||||
|
f"'{self._method.__qualname__}' blocked from execution, supervisor auto updates disabled"
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
JobCondition.SUPERVISOR_UPDATED in self.conditions
|
JobCondition.SUPERVISOR_UPDATED in self.conditions
|
||||||
and self.sys_supervisor.need_update
|
and self.sys_supervisor.need_update
|
||||||
@ -230,6 +238,13 @@ class Job(CoreSysAttributes):
|
|||||||
f"'{self._method.__qualname__}' blocked from execution, supervisor needs to be updated first"
|
f"'{self._method.__qualname__}' blocked from execution, supervisor needs to be updated first"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if JobCondition.PLUGINS_UPDATED in self.conditions and 0 < len(
|
||||||
|
[plugin for plugin in self.sys_plugins.all_plugins if plugin.need_update]
|
||||||
|
):
|
||||||
|
raise JobConditionException(
|
||||||
|
f"'{self._method.__qualname__}' blocked from execution, plugin(s) {', '.join([plugin.slug for plugin in self.sys_plugins.all_plugins if plugin.need_update])} need to be updated first"
|
||||||
|
)
|
||||||
|
|
||||||
async def _acquire_exection_limit(self) -> None:
|
async def _acquire_exection_limit(self) -> None:
|
||||||
"""Process exection limits."""
|
"""Process exection limits."""
|
||||||
if self.limit not in (
|
if self.limit not in (
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
"""A collection of tasks."""
|
"""A collection of tasks."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from ..addons.const import ADDON_UPDATE_CONDITIONS
|
||||||
from ..const import AddonState
|
from ..const import AddonState
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import AddonsError, HomeAssistantError, ObserverError
|
from ..exceptions import AddonsError, HomeAssistantError, ObserverError
|
||||||
from ..host.const import HostFeature
|
from ..host.const import HostFeature
|
||||||
from ..jobs.decorator import Job, JobCondition
|
from ..jobs.decorator import Job, JobCondition
|
||||||
|
from ..plugins.const import PLUGIN_UPDATE_CONDITIONS
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -34,6 +36,8 @@ RUN_REFRESH_ADDON = 15
|
|||||||
|
|
||||||
RUN_CHECK_CONNECTIVITY = 30
|
RUN_CHECK_CONNECTIVITY = 30
|
||||||
|
|
||||||
|
PLUGIN_AUTO_UPDATE_CONDITIONS = PLUGIN_UPDATE_CONDITIONS + [JobCondition.RUNNING]
|
||||||
|
|
||||||
|
|
||||||
class Tasks(CoreSysAttributes):
|
class Tasks(CoreSysAttributes):
|
||||||
"""Handle Tasks inside Supervisor."""
|
"""Handle Tasks inside Supervisor."""
|
||||||
@ -82,15 +86,7 @@ class Tasks(CoreSysAttributes):
|
|||||||
|
|
||||||
_LOGGER.info("All core tasks are scheduled")
|
_LOGGER.info("All core tasks are scheduled")
|
||||||
|
|
||||||
@Job(
|
@Job(conditions=ADDON_UPDATE_CONDITIONS + [JobCondition.RUNNING])
|
||||||
conditions=[
|
|
||||||
JobCondition.HEALTHY,
|
|
||||||
JobCondition.FREE_SPACE,
|
|
||||||
JobCondition.INTERNET_HOST,
|
|
||||||
JobCondition.RUNNING,
|
|
||||||
JobCondition.SUPERVISOR_UPDATED,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
async def _update_addons(self):
|
async def _update_addons(self):
|
||||||
"""Check if an update is available for an Add-on and update it."""
|
"""Check if an update is available for an Add-on and update it."""
|
||||||
for addon in self.sys_addons.all:
|
for addon in self.sys_addons.all:
|
||||||
@ -116,7 +112,9 @@ class Tasks(CoreSysAttributes):
|
|||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
conditions=[
|
conditions=[
|
||||||
|
JobCondition.AUTO_UPDATE,
|
||||||
JobCondition.FREE_SPACE,
|
JobCondition.FREE_SPACE,
|
||||||
|
JobCondition.HEALTHY,
|
||||||
JobCondition.INTERNET_HOST,
|
JobCondition.INTERNET_HOST,
|
||||||
JobCondition.RUNNING,
|
JobCondition.RUNNING,
|
||||||
]
|
]
|
||||||
@ -173,7 +171,7 @@ class Tasks(CoreSysAttributes):
|
|||||||
finally:
|
finally:
|
||||||
self._cache[HASS_WATCHDOG_API] = 0
|
self._cache[HASS_WATCHDOG_API] = 0
|
||||||
|
|
||||||
@Job(conditions=[JobCondition.RUNNING, JobCondition.SUPERVISOR_UPDATED])
|
@Job(conditions=PLUGIN_AUTO_UPDATE_CONDITIONS)
|
||||||
async def _update_cli(self):
|
async def _update_cli(self):
|
||||||
"""Check and run update of cli."""
|
"""Check and run update of cli."""
|
||||||
if not self.sys_plugins.cli.need_update:
|
if not self.sys_plugins.cli.need_update:
|
||||||
@ -184,7 +182,7 @@ class Tasks(CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
await self.sys_plugins.cli.update()
|
await self.sys_plugins.cli.update()
|
||||||
|
|
||||||
@Job(conditions=[JobCondition.RUNNING, JobCondition.SUPERVISOR_UPDATED])
|
@Job(conditions=PLUGIN_AUTO_UPDATE_CONDITIONS)
|
||||||
async def _update_dns(self):
|
async def _update_dns(self):
|
||||||
"""Check and run update of CoreDNS plugin."""
|
"""Check and run update of CoreDNS plugin."""
|
||||||
if not self.sys_plugins.dns.need_update:
|
if not self.sys_plugins.dns.need_update:
|
||||||
@ -196,7 +194,7 @@ class Tasks(CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
await self.sys_plugins.dns.update()
|
await self.sys_plugins.dns.update()
|
||||||
|
|
||||||
@Job(conditions=[JobCondition.RUNNING, JobCondition.SUPERVISOR_UPDATED])
|
@Job(conditions=PLUGIN_AUTO_UPDATE_CONDITIONS)
|
||||||
async def _update_audio(self):
|
async def _update_audio(self):
|
||||||
"""Check and run update of PulseAudio plugin."""
|
"""Check and run update of PulseAudio plugin."""
|
||||||
if not self.sys_plugins.audio.need_update:
|
if not self.sys_plugins.audio.need_update:
|
||||||
@ -208,7 +206,7 @@ class Tasks(CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
await self.sys_plugins.audio.update()
|
await self.sys_plugins.audio.update()
|
||||||
|
|
||||||
@Job(conditions=[JobCondition.RUNNING, JobCondition.SUPERVISOR_UPDATED])
|
@Job(conditions=PLUGIN_AUTO_UPDATE_CONDITIONS)
|
||||||
async def _update_observer(self):
|
async def _update_observer(self):
|
||||||
"""Check and run update of Observer plugin."""
|
"""Check and run update of Observer plugin."""
|
||||||
if not self.sys_plugins.observer.need_update:
|
if not self.sys_plugins.observer.need_update:
|
||||||
@ -220,7 +218,7 @@ class Tasks(CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
await self.sys_plugins.observer.update()
|
await self.sys_plugins.observer.update()
|
||||||
|
|
||||||
@Job(conditions=[JobCondition.RUNNING, JobCondition.SUPERVISOR_UPDATED])
|
@Job(conditions=PLUGIN_AUTO_UPDATE_CONDITIONS)
|
||||||
async def _update_multicast(self):
|
async def _update_multicast(self):
|
||||||
"""Check and run update of multicast."""
|
"""Check and run update of multicast."""
|
||||||
if not self.sys_plugins.multicast.need_update:
|
if not self.sys_plugins.multicast.need_update:
|
||||||
|
@ -173,6 +173,7 @@ class OSManager(CoreSysAttributes):
|
|||||||
JobCondition.HAOS,
|
JobCondition.HAOS,
|
||||||
JobCondition.INTERNET_SYSTEM,
|
JobCondition.INTERNET_SYSTEM,
|
||||||
JobCondition.RUNNING,
|
JobCondition.RUNNING,
|
||||||
|
JobCondition.SUPERVISOR_UPDATED,
|
||||||
],
|
],
|
||||||
limit=JobExecutionLimit.ONCE,
|
limit=JobExecutionLimit.ONCE,
|
||||||
on_condition=HassOSJobError,
|
on_condition=HassOSJobError,
|
||||||
|
@ -30,6 +30,7 @@ from ..utils.json import write_json_file
|
|||||||
from .base import PluginBase
|
from .base import PluginBase
|
||||||
from .const import (
|
from .const import (
|
||||||
FILE_HASSIO_AUDIO,
|
FILE_HASSIO_AUDIO,
|
||||||
|
PLUGIN_UPDATE_CONDITIONS,
|
||||||
WATCHDOG_THROTTLE_MAX_CALLS,
|
WATCHDOG_THROTTLE_MAX_CALLS,
|
||||||
WATCHDOG_THROTTLE_PERIOD,
|
WATCHDOG_THROTTLE_PERIOD,
|
||||||
)
|
)
|
||||||
@ -112,6 +113,10 @@ class PluginAudio(PluginBase):
|
|||||||
self.image = self.sys_updater.image_audio
|
self.image = self.sys_updater.image_audio
|
||||||
self.save_data()
|
self.save_data()
|
||||||
|
|
||||||
|
@Job(
|
||||||
|
conditions=PLUGIN_UPDATE_CONDITIONS,
|
||||||
|
on_condition=AudioJobError,
|
||||||
|
)
|
||||||
async def update(self, version: Optional[str] = None) -> None:
|
async def update(self, version: Optional[str] = None) -> None:
|
||||||
"""Update Audio plugin."""
|
"""Update Audio plugin."""
|
||||||
version = version or self.latest_version
|
version = version or self.latest_version
|
||||||
|
@ -21,6 +21,7 @@ from ..jobs.decorator import Job
|
|||||||
from .base import PluginBase
|
from .base import PluginBase
|
||||||
from .const import (
|
from .const import (
|
||||||
FILE_HASSIO_CLI,
|
FILE_HASSIO_CLI,
|
||||||
|
PLUGIN_UPDATE_CONDITIONS,
|
||||||
WATCHDOG_THROTTLE_MAX_CALLS,
|
WATCHDOG_THROTTLE_MAX_CALLS,
|
||||||
WATCHDOG_THROTTLE_PERIOD,
|
WATCHDOG_THROTTLE_PERIOD,
|
||||||
)
|
)
|
||||||
@ -72,6 +73,10 @@ class PluginCli(PluginBase):
|
|||||||
self.image = self.sys_updater.image_cli
|
self.image = self.sys_updater.image_cli
|
||||||
self.save_data()
|
self.save_data()
|
||||||
|
|
||||||
|
@Job(
|
||||||
|
conditions=PLUGIN_UPDATE_CONDITIONS,
|
||||||
|
on_condition=CliJobError,
|
||||||
|
)
|
||||||
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
|
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
|
||||||
"""Update local HA cli."""
|
"""Update local HA cli."""
|
||||||
version = version or self.latest_version
|
version = version or self.latest_version
|
||||||
|
@ -3,6 +3,7 @@ from datetime import timedelta
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ..const import SUPERVISOR_DATA
|
from ..const import SUPERVISOR_DATA
|
||||||
|
from ..jobs.const import JobCondition
|
||||||
|
|
||||||
FILE_HASSIO_AUDIO = Path(SUPERVISOR_DATA, "audio.json")
|
FILE_HASSIO_AUDIO = Path(SUPERVISOR_DATA, "audio.json")
|
||||||
FILE_HASSIO_CLI = Path(SUPERVISOR_DATA, "cli.json")
|
FILE_HASSIO_CLI = Path(SUPERVISOR_DATA, "cli.json")
|
||||||
@ -15,3 +16,10 @@ WATCHDOG_RETRY_SECONDS = 10
|
|||||||
WATCHDOG_MAX_ATTEMPTS = 5
|
WATCHDOG_MAX_ATTEMPTS = 5
|
||||||
WATCHDOG_THROTTLE_PERIOD = timedelta(minutes=30)
|
WATCHDOG_THROTTLE_PERIOD = timedelta(minutes=30)
|
||||||
WATCHDOG_THROTTLE_MAX_CALLS = 10
|
WATCHDOG_THROTTLE_MAX_CALLS = 10
|
||||||
|
|
||||||
|
PLUGIN_UPDATE_CONDITIONS = [
|
||||||
|
JobCondition.FREE_SPACE,
|
||||||
|
JobCondition.HEALTHY,
|
||||||
|
JobCondition.INTERNET_HOST,
|
||||||
|
JobCondition.SUPERVISOR_UPDATED,
|
||||||
|
]
|
||||||
|
@ -37,6 +37,7 @@ from .base import PluginBase
|
|||||||
from .const import (
|
from .const import (
|
||||||
ATTR_FALLBACK,
|
ATTR_FALLBACK,
|
||||||
FILE_HASSIO_DNS,
|
FILE_HASSIO_DNS,
|
||||||
|
PLUGIN_UPDATE_CONDITIONS,
|
||||||
WATCHDOG_THROTTLE_MAX_CALLS,
|
WATCHDOG_THROTTLE_MAX_CALLS,
|
||||||
WATCHDOG_THROTTLE_PERIOD,
|
WATCHDOG_THROTTLE_PERIOD,
|
||||||
)
|
)
|
||||||
@ -179,6 +180,10 @@ class PluginDns(PluginBase):
|
|||||||
# Init Hosts
|
# Init Hosts
|
||||||
self.write_hosts()
|
self.write_hosts()
|
||||||
|
|
||||||
|
@Job(
|
||||||
|
conditions=PLUGIN_UPDATE_CONDITIONS,
|
||||||
|
on_condition=CoreDNSJobError,
|
||||||
|
)
|
||||||
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
|
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
|
||||||
"""Update CoreDNS plugin."""
|
"""Update CoreDNS plugin."""
|
||||||
version = version or self.latest_version
|
version = version or self.latest_version
|
||||||
|
@ -24,6 +24,7 @@ from ..jobs.decorator import Job
|
|||||||
from .base import PluginBase
|
from .base import PluginBase
|
||||||
from .const import (
|
from .const import (
|
||||||
FILE_HASSIO_MULTICAST,
|
FILE_HASSIO_MULTICAST,
|
||||||
|
PLUGIN_UPDATE_CONDITIONS,
|
||||||
WATCHDOG_THROTTLE_MAX_CALLS,
|
WATCHDOG_THROTTLE_MAX_CALLS,
|
||||||
WATCHDOG_THROTTLE_PERIOD,
|
WATCHDOG_THROTTLE_PERIOD,
|
||||||
)
|
)
|
||||||
@ -69,6 +70,10 @@ class PluginMulticast(PluginBase):
|
|||||||
self.image = self.sys_updater.image_multicast
|
self.image = self.sys_updater.image_multicast
|
||||||
self.save_data()
|
self.save_data()
|
||||||
|
|
||||||
|
@Job(
|
||||||
|
conditions=PLUGIN_UPDATE_CONDITIONS,
|
||||||
|
on_condition=MulticastJobError,
|
||||||
|
)
|
||||||
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
|
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
|
||||||
"""Update Multicast plugin."""
|
"""Update Multicast plugin."""
|
||||||
version = version or self.latest_version
|
version = version or self.latest_version
|
||||||
|
@ -27,6 +27,7 @@ from ..jobs.decorator import Job
|
|||||||
from .base import PluginBase
|
from .base import PluginBase
|
||||||
from .const import (
|
from .const import (
|
||||||
FILE_HASSIO_OBSERVER,
|
FILE_HASSIO_OBSERVER,
|
||||||
|
PLUGIN_UPDATE_CONDITIONS,
|
||||||
WATCHDOG_THROTTLE_MAX_CALLS,
|
WATCHDOG_THROTTLE_MAX_CALLS,
|
||||||
WATCHDOG_THROTTLE_PERIOD,
|
WATCHDOG_THROTTLE_PERIOD,
|
||||||
)
|
)
|
||||||
@ -77,6 +78,10 @@ class PluginObserver(PluginBase):
|
|||||||
self.image = self.sys_updater.image_observer
|
self.image = self.sys_updater.image_observer
|
||||||
self.save_data()
|
self.save_data()
|
||||||
|
|
||||||
|
@Job(
|
||||||
|
conditions=PLUGIN_UPDATE_CONDITIONS,
|
||||||
|
on_condition=ObserverJobError,
|
||||||
|
)
|
||||||
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
|
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
|
||||||
"""Update local HA observer."""
|
"""Update local HA observer."""
|
||||||
version = version or self.latest_version
|
version = version or self.latest_version
|
||||||
|
@ -46,6 +46,7 @@ class UnsupportedReason(str, Enum):
|
|||||||
PRIVILEGED = "privileged"
|
PRIVILEGED = "privileged"
|
||||||
SOFTWARE = "software"
|
SOFTWARE = "software"
|
||||||
SOURCE_MODS = "source_mods"
|
SOURCE_MODS = "source_mods"
|
||||||
|
SUPERVISOR_VERSION = "supervisor_version"
|
||||||
SYSTEMD = "systemd"
|
SYSTEMD = "systemd"
|
||||||
SYSTEMD_RESOLVED = "systemd_resolved"
|
SYSTEMD_RESOLVED = "systemd_resolved"
|
||||||
|
|
||||||
|
34
supervisor/resolution/evaluations/supervisor_version.py
Normal file
34
supervisor/resolution/evaluations/supervisor_version.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
"""Evaluation class for supervisor version."""
|
||||||
|
|
||||||
|
from ...const import CoreState
|
||||||
|
from ...coresys import CoreSys
|
||||||
|
from ..const import UnsupportedReason
|
||||||
|
from .base import EvaluateBase
|
||||||
|
|
||||||
|
|
||||||
|
def setup(coresys: CoreSys) -> EvaluateBase:
|
||||||
|
"""Initialize evaluation-setup function."""
|
||||||
|
return EvaluateSupervisorVersion(coresys)
|
||||||
|
|
||||||
|
|
||||||
|
class EvaluateSupervisorVersion(EvaluateBase):
|
||||||
|
"""Evaluate supervisor version."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reason(self) -> UnsupportedReason:
|
||||||
|
"""Return a UnsupportedReason enum."""
|
||||||
|
return UnsupportedReason.SUPERVISOR_VERSION
|
||||||
|
|
||||||
|
@property
|
||||||
|
def on_failure(self) -> str:
|
||||||
|
"""Return a string that is printed when self.evaluate is False."""
|
||||||
|
return "Not using latest version of Supervisor and auto update is disabled."
|
||||||
|
|
||||||
|
@property
|
||||||
|
def states(self) -> list[CoreState]:
|
||||||
|
"""Return a list of valid states when this evaluation can run."""
|
||||||
|
return [CoreState.RUNNING, CoreState.STARTUP]
|
||||||
|
|
||||||
|
async def evaluate(self) -> None:
|
||||||
|
"""Run evaluation."""
|
||||||
|
return not self.sys_updater.auto_update and self.sys_supervisor.need_update
|
@ -159,7 +159,7 @@ class Supervisor(CoreSysAttributes):
|
|||||||
) from err
|
) from err
|
||||||
|
|
||||||
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
|
async def update(self, version: Optional[AwesomeVersion] = None) -> None:
|
||||||
"""Update Home Assistant version."""
|
"""Update Supervisor version."""
|
||||||
version = version or self.latest_version
|
version = version or self.latest_version
|
||||||
|
|
||||||
if version == self.sys_supervisor.version:
|
if version == self.sys_supervisor.version:
|
||||||
|
@ -11,6 +11,7 @@ from awesomeversion import AwesomeVersion
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_AUDIO,
|
ATTR_AUDIO,
|
||||||
|
ATTR_AUTO_UPDATE,
|
||||||
ATTR_CHANNEL,
|
ATTR_CHANNEL,
|
||||||
ATTR_CLI,
|
ATTR_CLI,
|
||||||
ATTR_DNS,
|
ATTR_DNS,
|
||||||
@ -170,6 +171,16 @@ class Updater(FileConfiguration, CoreSysAttributes):
|
|||||||
"""Set upstream mode."""
|
"""Set upstream mode."""
|
||||||
self._data[ATTR_CHANNEL] = value
|
self._data[ATTR_CHANNEL] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auto_update(self) -> bool:
|
||||||
|
"""Return if Supervisor auto updates enabled."""
|
||||||
|
return self._data[ATTR_AUTO_UPDATE]
|
||||||
|
|
||||||
|
@auto_update.setter
|
||||||
|
def auto_update(self, value: bool) -> None:
|
||||||
|
"""Set Supervisor auto updates enabled."""
|
||||||
|
self._data[ATTR_AUTO_UPDATE] = value
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
conditions=[JobCondition.INTERNET_SYSTEM],
|
conditions=[JobCondition.INTERNET_SYSTEM],
|
||||||
on_condition=UpdaterJobError,
|
on_condition=UpdaterJobError,
|
||||||
|
@ -9,6 +9,7 @@ import voluptuous as vol
|
|||||||
from .const import (
|
from .const import (
|
||||||
ATTR_ADDONS_CUSTOM_LIST,
|
ATTR_ADDONS_CUSTOM_LIST,
|
||||||
ATTR_AUDIO,
|
ATTR_AUDIO,
|
||||||
|
ATTR_AUTO_UPDATE,
|
||||||
ATTR_CHANNEL,
|
ATTR_CHANNEL,
|
||||||
ATTR_CLI,
|
ATTR_CLI,
|
||||||
ATTR_CONTENT_TRUST,
|
ATTR_CONTENT_TRUST,
|
||||||
@ -134,6 +135,7 @@ SCHEMA_UPDATER_CONFIG = vol.Schema(
|
|||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
),
|
),
|
||||||
vol.Optional(ATTR_OTA): vol.Url(),
|
vol.Optional(ATTR_OTA): vol.Url(),
|
||||||
|
vol.Optional(ATTR_AUTO_UPDATE, default=True): bool,
|
||||||
},
|
},
|
||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
)
|
)
|
||||||
|
@ -4,12 +4,14 @@ import asyncio
|
|||||||
from unittest.mock import MagicMock, PropertyMock, patch
|
from unittest.mock import MagicMock, PropertyMock, patch
|
||||||
|
|
||||||
from docker.errors import DockerException
|
from docker.errors import DockerException
|
||||||
|
import pytest
|
||||||
|
|
||||||
from supervisor.addons.addon import Addon
|
from supervisor.addons.addon import Addon
|
||||||
from supervisor.const import AddonState, BusEvent
|
from supervisor.const import AddonState, BusEvent
|
||||||
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.monitor import DockerContainerStateEvent
|
from supervisor.docker.monitor import DockerContainerStateEvent
|
||||||
|
from supervisor.exceptions import AddonsJobError
|
||||||
|
|
||||||
from ..const import TEST_ADDON_SLUG
|
from ..const import TEST_ADDON_SLUG
|
||||||
|
|
||||||
@ -267,3 +269,26 @@ async def test_listener_attached_on_install(coresys: CoreSys, repository):
|
|||||||
)
|
)
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
assert coresys.addons.get(TEST_ADDON_SLUG).state == AddonState.STARTED
|
assert coresys.addons.get(TEST_ADDON_SLUG).state == AddonState.STARTED
|
||||||
|
|
||||||
|
|
||||||
|
async def test_install_update_fails_if_out_of_date(
|
||||||
|
coresys: CoreSys, install_addon_ssh: Addon
|
||||||
|
):
|
||||||
|
"""Test install or update of addon fails when supervisor or plugin is out of date."""
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
type(coresys.supervisor), "need_update", new=PropertyMock(return_value=True)
|
||||||
|
):
|
||||||
|
with pytest.raises(AddonsJobError):
|
||||||
|
await coresys.addons.install(TEST_ADDON_SLUG)
|
||||||
|
with pytest.raises(AddonsJobError):
|
||||||
|
await install_addon_ssh.update()
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
type(coresys.plugins.audio), "need_update", new=PropertyMock(return_value=True)
|
||||||
|
):
|
||||||
|
with pytest.raises(AddonsJobError):
|
||||||
|
await coresys.addons.install(TEST_ADDON_SLUG)
|
||||||
|
with pytest.raises(AddonsJobError):
|
||||||
|
await install_addon_ssh.update()
|
||||||
|
@ -101,3 +101,16 @@ async def test_api_supervisor_options_repo_error_with_config_change(
|
|||||||
assert coresys.config.debug
|
assert coresys.config.debug
|
||||||
coresys.updater.save_data.assert_called_once()
|
coresys.updater.save_data.assert_called_once()
|
||||||
coresys.config.save_data.assert_called_once()
|
coresys.config.save_data.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_api_supervisor_options_auto_update(
|
||||||
|
api_client: TestClient, coresys: CoreSys
|
||||||
|
):
|
||||||
|
"""Test disabling auto update via api."""
|
||||||
|
assert coresys.updater.auto_update is True
|
||||||
|
|
||||||
|
response = await api_client.post("/supervisor/options", json={"auto_update": False})
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
assert coresys.updater.auto_update is False
|
||||||
|
@ -36,6 +36,7 @@ def partial_backup_mock(backup_mock):
|
|||||||
backup_instance.sys_type = BackupType.PARTIAL
|
backup_instance.sys_type = BackupType.PARTIAL
|
||||||
backup_instance.folders = []
|
backup_instance.folders = []
|
||||||
backup_instance.addon_list = [TEST_ADDON_SLUG]
|
backup_instance.addon_list = [TEST_ADDON_SLUG]
|
||||||
|
backup_instance.supervisor_version = "DEV"
|
||||||
yield backup_mock
|
yield backup_mock
|
||||||
|
|
||||||
|
|
||||||
@ -46,4 +47,5 @@ def full_backup_mock(backup_mock):
|
|||||||
backup_instance.sys_type = BackupType.FULL
|
backup_instance.sys_type = BackupType.FULL
|
||||||
backup_instance.folders = ALL_FOLDERS
|
backup_instance.folders = ALL_FOLDERS
|
||||||
backup_instance.addon_list = [TEST_ADDON_SLUG]
|
backup_instance.addon_list = [TEST_ADDON_SLUG]
|
||||||
|
backup_instance.supervisor_version = "DEV"
|
||||||
yield backup_mock
|
yield backup_mock
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Test BackupManager class."""
|
"""Test BackupManager class."""
|
||||||
|
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
|
||||||
|
|
||||||
from supervisor.backups.const import BackupType
|
from supervisor.backups.const import BackupType
|
||||||
from supervisor.backups.manager import BackupManager
|
from supervisor.backups.manager import BackupManager
|
||||||
@ -271,3 +271,53 @@ async def test_do_restore_partial_maximal(coresys: CoreSys, partial_backup_mock)
|
|||||||
backup_instance.restore_homeassistant.assert_called_once()
|
backup_instance.restore_homeassistant.assert_called_once()
|
||||||
|
|
||||||
assert coresys.core.state == CoreState.RUNNING
|
assert coresys.core.state == CoreState.RUNNING
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fail_invalid_full_backup(coresys: CoreSys, full_backup_mock: MagicMock):
|
||||||
|
"""Test restore fails with invalid backup."""
|
||||||
|
coresys.core.state = CoreState.RUNNING
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
|
||||||
|
manager = BackupManager(coresys)
|
||||||
|
|
||||||
|
backup_instance = full_backup_mock.return_value
|
||||||
|
backup_instance.protected = True
|
||||||
|
backup_instance.set_password.return_value = False
|
||||||
|
|
||||||
|
assert await manager.do_restore_full(backup_instance) is False
|
||||||
|
|
||||||
|
backup_instance.protected = False
|
||||||
|
backup_instance.supervisor_version = "2022.08.4"
|
||||||
|
with patch.object(
|
||||||
|
type(coresys.supervisor), "version", new=PropertyMock(return_value="2022.08.3")
|
||||||
|
):
|
||||||
|
assert await manager.do_restore_full(backup_instance) is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fail_invalid_partial_backup(
|
||||||
|
coresys: CoreSys, partial_backup_mock: MagicMock
|
||||||
|
):
|
||||||
|
"""Test restore fails with invalid backup."""
|
||||||
|
coresys.core.state = CoreState.RUNNING
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
|
||||||
|
manager = BackupManager(coresys)
|
||||||
|
|
||||||
|
backup_instance = partial_backup_mock.return_value
|
||||||
|
backup_instance.protected = True
|
||||||
|
backup_instance.set_password.return_value = False
|
||||||
|
|
||||||
|
assert await manager.do_restore_partial(backup_instance) is False
|
||||||
|
|
||||||
|
backup_instance.protected = False
|
||||||
|
backup_instance.homeassistant = None
|
||||||
|
|
||||||
|
assert (
|
||||||
|
await manager.do_restore_partial(backup_instance, homeassistant=True) is False
|
||||||
|
)
|
||||||
|
|
||||||
|
backup_instance.supervisor_version = "2022.08.4"
|
||||||
|
with patch.object(
|
||||||
|
type(coresys.supervisor), "version", new=PropertyMock(return_value="2022.08.3")
|
||||||
|
):
|
||||||
|
assert await manager.do_restore_partial(backup_instance) is False
|
||||||
|
22
tests/homeassistant/test_core.py
Normal file
22
tests/homeassistant/test_core.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"""Test Home Assistant core."""
|
||||||
|
from unittest.mock import PropertyMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from supervisor.coresys import CoreSys
|
||||||
|
from supervisor.exceptions import HomeAssistantJobError
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_fails_if_out_of_date(coresys: CoreSys):
|
||||||
|
"""Test update of Home Assistant fails when supervisor or plugin is out of date."""
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
type(coresys.supervisor), "need_update", new=PropertyMock(return_value=True)
|
||||||
|
), pytest.raises(HomeAssistantJobError):
|
||||||
|
await coresys.homeassistant.core.update()
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
type(coresys.plugins.audio), "need_update", new=PropertyMock(return_value=True)
|
||||||
|
), pytest.raises(HomeAssistantJobError):
|
||||||
|
await coresys.homeassistant.core.update()
|
@ -424,3 +424,53 @@ async def test_supervisor_updated(coresys: CoreSys):
|
|||||||
type(coresys.supervisor), "need_update", new=PropertyMock(return_value=True)
|
type(coresys.supervisor), "need_update", new=PropertyMock(return_value=True)
|
||||||
):
|
):
|
||||||
assert not await test.execute()
|
assert not await test.execute()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_plugins_updated(coresys: CoreSys):
|
||||||
|
"""Test the plugins updated decorator."""
|
||||||
|
|
||||||
|
class TestClass:
|
||||||
|
"""Test class."""
|
||||||
|
|
||||||
|
def __init__(self, coresys: CoreSys):
|
||||||
|
"""Initialize the test class."""
|
||||||
|
self.coresys = coresys
|
||||||
|
|
||||||
|
@Job(conditions=JobCondition.PLUGINS_UPDATED)
|
||||||
|
async def execute(self) -> bool:
|
||||||
|
"""Execute the class method."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
test = TestClass(coresys)
|
||||||
|
assert 0 == len(
|
||||||
|
[plugin.slug for plugin in coresys.plugins.all_plugins if plugin.need_update]
|
||||||
|
)
|
||||||
|
assert await test.execute()
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
type(coresys.plugins.audio), "need_update", new=PropertyMock(return_value=True)
|
||||||
|
):
|
||||||
|
assert not await test.execute()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_auto_update(coresys: CoreSys):
|
||||||
|
"""Test the auto update decorator."""
|
||||||
|
|
||||||
|
class TestClass:
|
||||||
|
"""Test class."""
|
||||||
|
|
||||||
|
def __init__(self, coresys: CoreSys):
|
||||||
|
"""Initialize the test class."""
|
||||||
|
self.coresys = coresys
|
||||||
|
|
||||||
|
@Job(conditions=JobCondition.AUTO_UPDATE)
|
||||||
|
async def execute(self) -> bool:
|
||||||
|
"""Execute the class method."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
test = TestClass(coresys)
|
||||||
|
assert coresys.updater.auto_update is True
|
||||||
|
assert await test.execute()
|
||||||
|
|
||||||
|
coresys.updater.auto_update = False
|
||||||
|
assert not await test.execute()
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
"""Test Home Assistant OS functionality."""
|
"""Test Home Assistant OS functionality."""
|
||||||
|
|
||||||
|
from unittest.mock import PropertyMock, patch
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from supervisor.const import CoreState
|
||||||
from supervisor.coresys import CoreSys
|
from supervisor.coresys import CoreSys
|
||||||
|
from supervisor.exceptions import HassOSJobError
|
||||||
|
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
|
|
||||||
@ -57,3 +61,16 @@ def test_ota_url_os_name_rel_5_downgrade(coresys: CoreSys) -> None:
|
|||||||
|
|
||||||
url = coresys.os._get_download_url(AwesomeVersion(versionstr))
|
url = coresys.os._get_download_url(AwesomeVersion(versionstr))
|
||||||
assert url == url_formatted
|
assert url == url_formatted
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_fails_if_out_of_date(coresys: CoreSys) -> None:
|
||||||
|
"""Test update of OS fails if Supervisor is out of date."""
|
||||||
|
coresys.core.state = CoreState.RUNNING
|
||||||
|
with patch.object(
|
||||||
|
type(coresys.supervisor), "need_update", new=PropertyMock(return_value=True)
|
||||||
|
), patch.object(
|
||||||
|
type(coresys.os), "available", new=PropertyMock(return_value=True)
|
||||||
|
), pytest.raises(
|
||||||
|
HassOSJobError
|
||||||
|
):
|
||||||
|
await coresys.os.update()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Test base plugin functionality."""
|
"""Test base plugin functionality."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from unittest.mock import patch
|
from unittest.mock import PropertyMock, patch
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
import pytest
|
import pytest
|
||||||
@ -11,12 +11,18 @@ from supervisor.docker.const import ContainerState
|
|||||||
from supervisor.docker.monitor import DockerContainerStateEvent
|
from supervisor.docker.monitor import DockerContainerStateEvent
|
||||||
from supervisor.exceptions import (
|
from supervisor.exceptions import (
|
||||||
AudioError,
|
AudioError,
|
||||||
|
AudioJobError,
|
||||||
CliError,
|
CliError,
|
||||||
|
CliJobError,
|
||||||
CoreDNSError,
|
CoreDNSError,
|
||||||
|
CoreDNSJobError,
|
||||||
DockerError,
|
DockerError,
|
||||||
MulticastError,
|
MulticastError,
|
||||||
|
MulticastJobError,
|
||||||
ObserverError,
|
ObserverError,
|
||||||
|
ObserverJobError,
|
||||||
PluginError,
|
PluginError,
|
||||||
|
PluginJobError,
|
||||||
)
|
)
|
||||||
from supervisor.plugins.audio import PluginAudio
|
from supervisor.plugins.audio import PluginAudio
|
||||||
from supervisor.plugins.base import PluginBase
|
from supervisor.plugins.base import PluginBase
|
||||||
@ -302,3 +308,26 @@ async def test_plugin_load_missing_container(
|
|||||||
)
|
)
|
||||||
install.assert_called_once()
|
install.assert_called_once()
|
||||||
start.assert_called_once()
|
start.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"plugin,error",
|
||||||
|
[
|
||||||
|
(PluginAudio, AudioJobError),
|
||||||
|
(PluginCli, CliJobError),
|
||||||
|
(PluginDns, CoreDNSJobError),
|
||||||
|
(PluginMulticast, MulticastJobError),
|
||||||
|
(PluginObserver, ObserverJobError),
|
||||||
|
],
|
||||||
|
indirect=["plugin"],
|
||||||
|
)
|
||||||
|
async def test_update_fails_if_out_of_date(
|
||||||
|
coresys: CoreSys, plugin: PluginBase, error: PluginJobError
|
||||||
|
):
|
||||||
|
"""Test update of plugins fail when supervisor is out of date."""
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
type(coresys.supervisor), "need_update", new=PropertyMock(return_value=True)
|
||||||
|
), pytest.raises(error):
|
||||||
|
await plugin.update()
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
"""Test evaluate supervisor version."""
|
||||||
|
|
||||||
|
from unittest.mock import PropertyMock, patch
|
||||||
|
|
||||||
|
from supervisor.const import CoreState
|
||||||
|
from supervisor.coresys import CoreSys
|
||||||
|
from supervisor.resolution.evaluations.supervisor_version import (
|
||||||
|
EvaluateSupervisorVersion,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_evaluation(coresys: CoreSys):
|
||||||
|
"""Test evaluation."""
|
||||||
|
need_update_mock = PropertyMock()
|
||||||
|
with patch.object(type(coresys.supervisor), "need_update", new=need_update_mock):
|
||||||
|
supervisor_version = EvaluateSupervisorVersion(coresys)
|
||||||
|
coresys.core.state = CoreState.RUNNING
|
||||||
|
need_update_mock.return_value = False
|
||||||
|
|
||||||
|
# Only unsupported if out of date and auto update is off
|
||||||
|
assert supervisor_version.reason not in coresys.resolution.unsupported
|
||||||
|
need_update_mock.return_value = True
|
||||||
|
await supervisor_version()
|
||||||
|
assert supervisor_version.reason not in coresys.resolution.unsupported
|
||||||
|
|
||||||
|
coresys.updater.auto_update = False
|
||||||
|
await supervisor_version()
|
||||||
|
assert supervisor_version.reason in coresys.resolution.unsupported
|
||||||
|
|
||||||
|
|
||||||
|
async def test_did_run(coresys: CoreSys):
|
||||||
|
"""Test that the evaluation ran as expected."""
|
||||||
|
supervisor_version = EvaluateSupervisorVersion(coresys)
|
||||||
|
should_run = supervisor_version.states
|
||||||
|
should_not_run = [state for state in CoreState if state not in should_run]
|
||||||
|
assert len(should_run) != 0
|
||||||
|
assert len(should_not_run) != 0
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"supervisor.resolution.evaluations.supervisor_version.EvaluateSupervisorVersion.evaluate",
|
||||||
|
return_value=None,
|
||||||
|
) as evaluate:
|
||||||
|
for state in should_run:
|
||||||
|
coresys.core.state = state
|
||||||
|
await supervisor_version()
|
||||||
|
evaluate.assert_called_once()
|
||||||
|
evaluate.reset_mock()
|
||||||
|
|
||||||
|
for state in should_not_run:
|
||||||
|
coresys.core.state = state
|
||||||
|
await supervisor_version()
|
||||||
|
evaluate.assert_not_called()
|
||||||
|
evaluate.reset_mock()
|
Loading…
x
Reference in New Issue
Block a user