mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-09 18:26:30 +00:00
Network backups skip free space check (#4563)
This commit is contained in:
parent
e1232bc9e7
commit
dcf024387b
@ -272,7 +272,7 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||
|
||||
@Job(
|
||||
name="backup_manager_full_backup",
|
||||
conditions=[JobCondition.FREE_SPACE, JobCondition.RUNNING],
|
||||
conditions=[JobCondition.RUNNING],
|
||||
limit=JobExecutionLimit.GROUP_ONCE,
|
||||
on_condition=BackupJobError,
|
||||
)
|
||||
@ -284,6 +284,11 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||
location: Mount | type[DEFAULT] | None = DEFAULT,
|
||||
) -> Backup | None:
|
||||
"""Create a full backup."""
|
||||
if self._get_base_path(location) == self.sys_config.path_backup:
|
||||
await Job.check_conditions(
|
||||
self, {JobCondition.FREE_SPACE}, "BackupManager.do_backup_full"
|
||||
)
|
||||
|
||||
backup = self._create_backup(
|
||||
name, BackupType.FULL, password, compressed, location
|
||||
)
|
||||
@ -298,7 +303,7 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||
|
||||
@Job(
|
||||
name="backup_manager_partial_backup",
|
||||
conditions=[JobCondition.FREE_SPACE, JobCondition.RUNNING],
|
||||
conditions=[JobCondition.RUNNING],
|
||||
limit=JobExecutionLimit.GROUP_ONCE,
|
||||
on_condition=BackupJobError,
|
||||
)
|
||||
@ -313,6 +318,11 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||
location: Mount | type[DEFAULT] | None = DEFAULT,
|
||||
) -> Backup | None:
|
||||
"""Create a partial backup."""
|
||||
if self._get_base_path(location) == self.sys_config.path_backup:
|
||||
await Job.check_conditions(
|
||||
self, {JobCondition.FREE_SPACE}, "BackupManager.do_backup_partial"
|
||||
)
|
||||
|
||||
addons = addons or []
|
||||
folders = folders or []
|
||||
|
||||
|
@ -174,6 +174,14 @@ class Job(CoreSysAttributes):
|
||||
return obj
|
||||
return None
|
||||
|
||||
def _handle_job_condition_exception(self, err: JobConditionException) -> None:
|
||||
"""Handle a job condition failure."""
|
||||
error_msg = str(err)
|
||||
if self.on_condition is None:
|
||||
_LOGGER.info(error_msg)
|
||||
return
|
||||
raise self.on_condition(error_msg, _LOGGER.warning) from None
|
||||
|
||||
def __call__(self, method):
|
||||
"""Call the wrapper logic."""
|
||||
self._method = method
|
||||
@ -196,13 +204,11 @@ class Job(CoreSysAttributes):
|
||||
# Handle condition
|
||||
if self.conditions:
|
||||
try:
|
||||
await self._check_conditions()
|
||||
await Job.check_conditions(
|
||||
self, set(self.conditions), self._method.__qualname__
|
||||
)
|
||||
except JobConditionException as err:
|
||||
error_msg = str(err)
|
||||
if self.on_condition is None:
|
||||
_LOGGER.info(error_msg)
|
||||
return
|
||||
raise self.on_condition(error_msg, _LOGGER.warning) from None
|
||||
return self._handle_job_condition_exception(err)
|
||||
|
||||
# Handle exection limits
|
||||
if self.limit in (JobExecutionLimit.SINGLE_WAIT, JobExecutionLimit.ONCE):
|
||||
@ -266,6 +272,11 @@ class Job(CoreSysAttributes):
|
||||
)
|
||||
|
||||
return await self._method(obj, *args, **kwargs)
|
||||
|
||||
# If a method has a conditional JobCondition, they must check it in the method
|
||||
# These should be handled like normal JobConditions as much as possible
|
||||
except JobConditionException as err:
|
||||
return self._handle_job_condition_exception(err)
|
||||
except HassioError as err:
|
||||
raise err
|
||||
except Exception as err:
|
||||
@ -282,10 +293,13 @@ class Job(CoreSysAttributes):
|
||||
|
||||
return wrapper
|
||||
|
||||
async def _check_conditions(self):
|
||||
@staticmethod
|
||||
async def check_conditions(
|
||||
coresys: CoreSysAttributes, conditions: set[JobCondition], method_name: str
|
||||
):
|
||||
"""Check conditions."""
|
||||
used_conditions = set(self.conditions) - set(self.sys_jobs.ignore_conditions)
|
||||
ignored_conditions = set(self.conditions) & set(self.sys_jobs.ignore_conditions)
|
||||
used_conditions = set(conditions) - set(coresys.sys_jobs.ignore_conditions)
|
||||
ignored_conditions = set(conditions) & set(coresys.sys_jobs.ignore_conditions)
|
||||
|
||||
# Check if somethings is ignored
|
||||
if ignored_conditions:
|
||||
@ -294,93 +308,97 @@ class Job(CoreSysAttributes):
|
||||
ignored_conditions,
|
||||
)
|
||||
|
||||
if JobCondition.HEALTHY in used_conditions and not self.sys_core.healthy:
|
||||
if JobCondition.HEALTHY in used_conditions and not coresys.sys_core.healthy:
|
||||
raise JobConditionException(
|
||||
f"'{self._method.__qualname__}' blocked from execution, system is not healthy - {', '.join(self.sys_resolution.unhealthy)}"
|
||||
f"'{method_name}' blocked from execution, system is not healthy - {', '.join(coresys.sys_resolution.unhealthy)}"
|
||||
)
|
||||
|
||||
if (
|
||||
JobCondition.RUNNING in used_conditions
|
||||
and self.sys_core.state != CoreState.RUNNING
|
||||
and coresys.sys_core.state != CoreState.RUNNING
|
||||
):
|
||||
raise JobConditionException(
|
||||
f"'{self._method.__qualname__}' blocked from execution, system is not running - {self.sys_core.state!s}"
|
||||
f"'{method_name}' blocked from execution, system is not running - {coresys.sys_core.state!s}"
|
||||
)
|
||||
|
||||
if (
|
||||
JobCondition.FROZEN in used_conditions
|
||||
and self.sys_core.state != CoreState.FREEZE
|
||||
and coresys.sys_core.state != CoreState.FREEZE
|
||||
):
|
||||
raise JobConditionException(
|
||||
f"'{self._method.__qualname__}' blocked from execution, system is not frozen - {self.sys_core.state!s}"
|
||||
f"'{method_name}' blocked from execution, system is not frozen - {coresys.sys_core.state!s}"
|
||||
)
|
||||
|
||||
if (
|
||||
JobCondition.FREE_SPACE in used_conditions
|
||||
and self.sys_host.info.free_space < MINIMUM_FREE_SPACE_THRESHOLD
|
||||
and coresys.sys_host.info.free_space < MINIMUM_FREE_SPACE_THRESHOLD
|
||||
):
|
||||
self.sys_resolution.create_issue(IssueType.FREE_SPACE, ContextType.SYSTEM)
|
||||
coresys.sys_resolution.create_issue(
|
||||
IssueType.FREE_SPACE, ContextType.SYSTEM
|
||||
)
|
||||
raise JobConditionException(
|
||||
f"'{self._method.__qualname__}' blocked from execution, not enough free space ({self.sys_host.info.free_space}GB) left on the device"
|
||||
f"'{method_name}' blocked from execution, not enough free space ({coresys.sys_host.info.free_space}GB) left on the device"
|
||||
)
|
||||
|
||||
if JobCondition.INTERNET_SYSTEM in used_conditions:
|
||||
await self.sys_supervisor.check_connectivity()
|
||||
if not self.sys_supervisor.connectivity:
|
||||
await coresys.sys_supervisor.check_connectivity()
|
||||
if not coresys.sys_supervisor.connectivity:
|
||||
raise JobConditionException(
|
||||
f"'{self._method.__qualname__}' blocked from execution, no supervisor internet connection"
|
||||
f"'{method_name}' blocked from execution, no supervisor internet connection"
|
||||
)
|
||||
|
||||
if JobCondition.INTERNET_HOST in used_conditions:
|
||||
await self.sys_host.network.check_connectivity()
|
||||
await coresys.sys_host.network.check_connectivity()
|
||||
if (
|
||||
self.sys_host.network.connectivity is not None
|
||||
and not self.sys_host.network.connectivity
|
||||
coresys.sys_host.network.connectivity is not None
|
||||
and not coresys.sys_host.network.connectivity
|
||||
):
|
||||
raise JobConditionException(
|
||||
f"'{self._method.__qualname__}' blocked from execution, no host internet connection"
|
||||
f"'{method_name}' blocked from execution, no host internet connection"
|
||||
)
|
||||
|
||||
if JobCondition.HAOS in used_conditions and not self.sys_os.available:
|
||||
if JobCondition.HAOS in used_conditions and not coresys.sys_os.available:
|
||||
raise JobConditionException(
|
||||
f"'{self._method.__qualname__}' blocked from execution, no Home Assistant OS available"
|
||||
f"'{method_name}' blocked from execution, no Home Assistant OS available"
|
||||
)
|
||||
|
||||
if (
|
||||
JobCondition.OS_AGENT in used_conditions
|
||||
and HostFeature.OS_AGENT not in self.sys_host.features
|
||||
and HostFeature.OS_AGENT not in coresys.sys_host.features
|
||||
):
|
||||
raise JobConditionException(
|
||||
f"'{self._method.__qualname__}' blocked from execution, no Home Assistant OS-Agent available"
|
||||
f"'{method_name}' blocked from execution, no Home Assistant OS-Agent available"
|
||||
)
|
||||
|
||||
if (
|
||||
JobCondition.HOST_NETWORK in used_conditions
|
||||
and not self.sys_dbus.network.is_connected
|
||||
and not coresys.sys_dbus.network.is_connected
|
||||
):
|
||||
raise JobConditionException(
|
||||
f"'{self._method.__qualname__}' blocked from execution, host Network Manager not available"
|
||||
f"'{method_name}' blocked from execution, host Network Manager not available"
|
||||
)
|
||||
|
||||
if (
|
||||
JobCondition.AUTO_UPDATE in used_conditions
|
||||
and not self.sys_updater.auto_update
|
||||
and not coresys.sys_updater.auto_update
|
||||
):
|
||||
raise JobConditionException(
|
||||
f"'{self._method.__qualname__}' blocked from execution, supervisor auto updates disabled"
|
||||
f"'{method_name}' blocked from execution, supervisor auto updates disabled"
|
||||
)
|
||||
|
||||
if (
|
||||
JobCondition.SUPERVISOR_UPDATED in used_conditions
|
||||
and self.sys_supervisor.need_update
|
||||
and coresys.sys_supervisor.need_update
|
||||
):
|
||||
raise JobConditionException(
|
||||
f"'{self._method.__qualname__}' blocked from execution, supervisor needs to be updated first"
|
||||
f"'{method_name}' blocked from execution, supervisor needs to be updated first"
|
||||
)
|
||||
|
||||
if JobCondition.PLUGINS_UPDATED in used_conditions and (
|
||||
out_of_date := [
|
||||
plugin for plugin in self.sys_plugins.all_plugins if plugin.need_update
|
||||
plugin
|
||||
for plugin in coresys.sys_plugins.all_plugins
|
||||
if plugin.need_update
|
||||
]
|
||||
):
|
||||
errors = await asyncio.gather(
|
||||
@ -391,15 +409,15 @@ class Job(CoreSysAttributes):
|
||||
out_of_date[i].slug for i in range(len(errors)) if errors[i] is not None
|
||||
]:
|
||||
raise JobConditionException(
|
||||
f"'{self._method.__qualname__}' blocked from execution, was unable to update plugin(s) {', '.join(update_failures)} and all plugins must be up to date first"
|
||||
f"'{method_name}' blocked from execution, was unable to update plugin(s) {', '.join(update_failures)} and all plugins must be up to date first"
|
||||
)
|
||||
|
||||
if (
|
||||
JobCondition.MOUNT_AVAILABLE in used_conditions
|
||||
and HostFeature.MOUNT not in self.sys_host.features
|
||||
and HostFeature.MOUNT not in coresys.sys_host.features
|
||||
):
|
||||
raise JobConditionException(
|
||||
f"'{self._method.__qualname__}' blocked from execution, mounting not supported on system"
|
||||
f"'{method_name}' blocked from execution, mounting not supported on system"
|
||||
)
|
||||
|
||||
async def _acquire_exection_limit(self) -> None:
|
||||
|
@ -20,7 +20,7 @@ from supervisor.docker.addon import DockerAddon
|
||||
from supervisor.docker.const import ContainerState
|
||||
from supervisor.docker.homeassistant import DockerHomeAssistant
|
||||
from supervisor.docker.monitor import DockerContainerStateEvent
|
||||
from supervisor.exceptions import AddonsError, BackupError, DockerError
|
||||
from supervisor.exceptions import AddonsError, BackupError, BackupJobError, DockerError
|
||||
from supervisor.homeassistant.api import HomeAssistantAPI
|
||||
from supervisor.homeassistant.core import HomeAssistantCore
|
||||
from supervisor.homeassistant.module import HomeAssistant
|
||||
@ -1393,3 +1393,51 @@ async def test_restore_new_addon(
|
||||
assert await coresys.backups.do_restore_partial(backup, addons=["local_ssh"])
|
||||
|
||||
assert "local_ssh" in coresys.addons.local
|
||||
|
||||
|
||||
async def test_backup_to_mount_bypasses_free_space_condition(
|
||||
coresys: CoreSys,
|
||||
all_dbus_services: dict[str, DBusServiceMock],
|
||||
tmp_supervisor_data,
|
||||
path_extern,
|
||||
mount_propagation,
|
||||
):
|
||||
"""Test backing up to a mount bypasses the check on local free space."""
|
||||
coresys.core.state = CoreState.RUNNING
|
||||
coresys.hardware.disk.get_disk_free_space = lambda _: 0.1
|
||||
|
||||
# These fail due to lack of local free space
|
||||
with pytest.raises(BackupJobError):
|
||||
await coresys.backups.do_backup_full()
|
||||
with pytest.raises(BackupJobError):
|
||||
await coresys.backups.do_backup_partial(folders=["media"])
|
||||
|
||||
systemd_service: SystemdService = all_dbus_services["systemd"]
|
||||
systemd_service.response_get_unit = [
|
||||
DBusError("org.freedesktop.systemd1.NoSuchUnit", "error"),
|
||||
"/org/freedesktop/systemd1/unit/tmp_2dyellow_2emount",
|
||||
DBusError("org.freedesktop.systemd1.NoSuchUnit", "error"),
|
||||
"/org/freedesktop/systemd1/unit/tmp_2dyellow_2emount",
|
||||
"/org/freedesktop/systemd1/unit/tmp_2dyellow_2emount",
|
||||
"/org/freedesktop/systemd1/unit/tmp_2dyellow_2emount",
|
||||
]
|
||||
|
||||
# Add a backup mount
|
||||
await coresys.mounts.load()
|
||||
await coresys.mounts.create_mount(
|
||||
Mount.from_dict(
|
||||
coresys,
|
||||
{
|
||||
"name": "backup_test",
|
||||
"usage": "backup",
|
||||
"type": "cifs",
|
||||
"server": "test.local",
|
||||
"share": "test",
|
||||
},
|
||||
)
|
||||
)
|
||||
mount = coresys.mounts.get("backup_test")
|
||||
|
||||
# These succeed because local free space does not matter when using a mount
|
||||
await coresys.backups.do_backup_full(location=mount)
|
||||
await coresys.backups.do_backup_partial(folders=["media"], location=mount)
|
||||
|
Loading…
x
Reference in New Issue
Block a user