Drop Docker config from Supervisor backup (#5605)

* Drop Docker config from Supervisor backup

The Docker config is part of the main backup metadata. Because we
consolidate encrypted and unencrypted backups today, this leads to
potential bugs when restoring a backup.

* Drop obsolete encrypt/decrypt functions

* Drop unused Backup Job stage
This commit is contained in:
Stefan Agner 2025-02-06 11:15:56 +01:00 committed by GitHub
parent 9a8e52d1fc
commit d254937590
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 2 additions and 96 deletions

View File

@ -1,7 +1,6 @@
"""Representation of a backup file.""" """Representation of a backup file."""
import asyncio import asyncio
from base64 import b64decode, b64encode
from collections import defaultdict from collections import defaultdict
from collections.abc import AsyncGenerator, Awaitable from collections.abc import AsyncGenerator, Awaitable
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
@ -20,7 +19,6 @@ from typing import Any, Self
from awesomeversion import AwesomeVersion, 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.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from securetar import SecureTarFile, atomic_contents_add, secure_path from securetar import SecureTarFile, atomic_contents_add, secure_path
import voluptuous as vol import voluptuous as vol
@ -38,16 +36,13 @@ from ..const import (
ATTR_FOLDERS, ATTR_FOLDERS,
ATTR_HOMEASSISTANT, ATTR_HOMEASSISTANT,
ATTR_NAME, ATTR_NAME,
ATTR_PASSWORD,
ATTR_PATH, ATTR_PATH,
ATTR_PROTECTED, ATTR_PROTECTED,
ATTR_REGISTRIES,
ATTR_REPOSITORIES, ATTR_REPOSITORIES,
ATTR_SIZE, ATTR_SIZE,
ATTR_SLUG, ATTR_SLUG,
ATTR_SUPERVISOR_VERSION, ATTR_SUPERVISOR_VERSION,
ATTR_TYPE, ATTR_TYPE,
ATTR_USERNAME,
ATTR_VERSION, ATTR_VERSION,
CRYPTO_AES128, CRYPTO_AES128,
) )
@ -371,28 +366,6 @@ class Backup(JobGroup):
backend=default_backend(), backend=default_backend(),
) )
def _encrypt_data(self, data: str) -> str:
"""Make data secure."""
if not self._key or data is None:
return data
encrypt = self._aes.encryptor()
padder = padding.PKCS7(128).padder()
data = padder.update(data.encode()) + padder.finalize()
return b64encode(encrypt.update(data)).decode()
def _decrypt_data(self, data: str) -> str:
"""Make data readable."""
if not self._key or data is None:
return data
decrypt = self._aes.decryptor()
padder = padding.PKCS7(128).unpadder()
data = padder.update(decrypt.update(b64decode(data))) + padder.finalize()
return data.decode()
async def validate_password(self, location: str | None) -> bool: async def validate_password(self, location: str | None) -> bool:
"""Validate backup password. """Validate backup password.
@ -899,32 +872,3 @@ class Backup(JobGroup):
return self.sys_store.update_repositories( return self.sys_store.update_repositories(
self.repositories, add_with_errors=True, replace=replace self.repositories, add_with_errors=True, replace=replace
) )
def store_dockerconfig(self):
"""Store the configuration for Docker."""
self.docker = {
ATTR_REGISTRIES: {
registry: {
ATTR_USERNAME: credentials[ATTR_USERNAME],
ATTR_PASSWORD: self._encrypt_data(credentials[ATTR_PASSWORD]),
}
for registry, credentials in self.sys_docker.config.registries.items()
}
}
def restore_dockerconfig(self, replace: bool = False):
"""Restore the configuration for Docker."""
if replace:
self.sys_docker.config.registries.clear()
if ATTR_REGISTRIES in self.docker:
self.sys_docker.config.registries.update(
{
registry: {
ATTR_USERNAME: credentials[ATTR_USERNAME],
ATTR_PASSWORD: self._decrypt_data(credentials[ATTR_PASSWORD]),
}
for registry, credentials in self.docker[ATTR_REGISTRIES].items()
}
)
self.sys_docker.config.save_data()

View File

@ -24,7 +24,6 @@ class BackupJobStage(StrEnum):
ADDON_REPOSITORIES = "addon_repositories" ADDON_REPOSITORIES = "addon_repositories"
ADDONS = "addons" ADDONS = "addons"
DOCKER_CONFIG = "docker_config"
FINISHING_FILE = "finishing_file" FINISHING_FILE = "finishing_file"
FOLDERS = "folders" FOLDERS = "folders"
HOME_ASSISTANT = "home_assistant" HOME_ASSISTANT = "home_assistant"
@ -39,7 +38,6 @@ class RestoreJobStage(StrEnum):
ADDONS = "addons" ADDONS = "addons"
AWAIT_ADDON_RESTARTS = "await_addon_restarts" AWAIT_ADDON_RESTARTS = "await_addon_restarts"
AWAIT_HOME_ASSISTANT_RESTART = "await_home_assistant_restart" AWAIT_HOME_ASSISTANT_RESTART = "await_home_assistant_restart"
DOCKER_CONFIG = "docker_config"
FOLDERS = "folders" FOLDERS = "folders"
HOME_ASSISTANT = "home_assistant" HOME_ASSISTANT = "home_assistant"
REMOVE_DELTA_ADDONS = "remove_delta_addons" REMOVE_DELTA_ADDONS = "remove_delta_addons"

View File

@ -215,8 +215,6 @@ class BackupManager(FileConfiguration, JobGroup):
self._change_stage(BackupJobStage.ADDON_REPOSITORIES, backup) self._change_stage(BackupJobStage.ADDON_REPOSITORIES, backup)
backup.store_repositories() backup.store_repositories()
self._change_stage(BackupJobStage.DOCKER_CONFIG, backup)
backup.store_dockerconfig()
return backup return backup
@ -655,10 +653,6 @@ class BackupManager(FileConfiguration, JobGroup):
try: try:
task_hass: asyncio.Task | None = None task_hass: asyncio.Task | None = None
async with backup.open(location): async with backup.open(location):
# Restore docker config
self._change_stage(RestoreJobStage.DOCKER_CONFIG, backup)
backup.restore_dockerconfig(replace)
# Process folders # Process folders
if folder_list: if folder_list:
self._change_stage(RestoreJobStage.FOLDERS, backup) self._change_stage(RestoreJobStage.FOLDERS, backup)

View File

@ -63,7 +63,6 @@ async def test_do_backup_full(coresys: CoreSys, backup_mock, install_addon_ssh):
backup_instance.store_homeassistant.assert_called_once() backup_instance.store_homeassistant.assert_called_once()
backup_instance.store_repositories.assert_called_once() backup_instance.store_repositories.assert_called_once()
backup_instance.store_dockerconfig.assert_called_once()
backup_instance.store_addons.assert_called_once() backup_instance.store_addons.assert_called_once()
assert install_addon_ssh in backup_instance.store_addons.call_args[0][0] assert install_addon_ssh in backup_instance.store_addons.call_args[0][0]
@ -115,7 +114,6 @@ async def test_do_backup_full_uncompressed(
backup_instance.store_homeassistant.assert_called_once() backup_instance.store_homeassistant.assert_called_once()
backup_instance.store_repositories.assert_called_once() backup_instance.store_repositories.assert_called_once()
backup_instance.store_dockerconfig.assert_called_once()
backup_instance.store_addons.assert_called_once() backup_instance.store_addons.assert_called_once()
assert install_addon_ssh in backup_instance.store_addons.call_args[0][0] assert install_addon_ssh in backup_instance.store_addons.call_args[0][0]
@ -146,7 +144,6 @@ async def test_do_backup_partial_minimal(
backup_instance.store_homeassistant.assert_not_called() backup_instance.store_homeassistant.assert_not_called()
backup_instance.store_repositories.assert_called_once() backup_instance.store_repositories.assert_called_once()
backup_instance.store_dockerconfig.assert_called_once()
backup_instance.store_addons.assert_not_called() backup_instance.store_addons.assert_not_called()
@ -176,7 +173,6 @@ async def test_do_backup_partial_minimal_uncompressed(
backup_instance.store_homeassistant.assert_not_called() backup_instance.store_homeassistant.assert_not_called()
backup_instance.store_repositories.assert_called_once() backup_instance.store_repositories.assert_called_once()
backup_instance.store_dockerconfig.assert_called_once()
backup_instance.store_addons.assert_not_called() backup_instance.store_addons.assert_not_called()
@ -208,7 +204,6 @@ async def test_do_backup_partial_maximal(
backup_instance.store_homeassistant.assert_called_once() backup_instance.store_homeassistant.assert_called_once()
backup_instance.store_repositories.assert_called_once() backup_instance.store_repositories.assert_called_once()
backup_instance.store_dockerconfig.assert_called_once()
backup_instance.store_addons.assert_called_once() backup_instance.store_addons.assert_called_once()
assert install_addon_ssh in backup_instance.store_addons.call_args[0][0] assert install_addon_ssh in backup_instance.store_addons.call_args[0][0]
@ -240,7 +235,6 @@ async def test_do_restore_full(coresys: CoreSys, full_backup_mock, install_addon
backup_instance.restore_homeassistant.assert_called_once() backup_instance.restore_homeassistant.assert_called_once()
backup_instance.restore_repositories.assert_called_once() backup_instance.restore_repositories.assert_called_once()
backup_instance.restore_dockerconfig.assert_called_once()
backup_instance.restore_addons.assert_called_once() backup_instance.restore_addons.assert_called_once()
install_addon_ssh.uninstall.assert_not_called() install_addon_ssh.uninstall.assert_not_called()
@ -273,7 +267,6 @@ async def test_do_restore_full_different_addon(
backup_instance.restore_homeassistant.assert_called_once() backup_instance.restore_homeassistant.assert_called_once()
backup_instance.restore_repositories.assert_called_once() backup_instance.restore_repositories.assert_called_once()
backup_instance.restore_dockerconfig.assert_called_once()
backup_instance.restore_addons.assert_called_once() backup_instance.restore_addons.assert_called_once()
install_addon_ssh.uninstall.assert_called_once() install_addon_ssh.uninstall.assert_called_once()
@ -300,7 +293,6 @@ async def test_do_restore_partial_minimal(
backup_instance.restore_homeassistant.assert_not_called() backup_instance.restore_homeassistant.assert_not_called()
backup_instance.restore_repositories.assert_not_called() backup_instance.restore_repositories.assert_not_called()
backup_instance.restore_dockerconfig.assert_called_once()
backup_instance.restore_addons.assert_not_called() backup_instance.restore_addons.assert_not_called()
@ -329,7 +321,6 @@ 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()
backup_instance.restore_repositories.assert_called_once() backup_instance.restore_repositories.assert_called_once()
backup_instance.restore_dockerconfig.assert_called_once()
backup_instance.restore_addons.assert_called_once() backup_instance.restore_addons.assert_called_once()
@ -431,12 +422,12 @@ async def test_restore_error(
backup_instance = full_backup_mock.return_value backup_instance = full_backup_mock.return_value
backup_instance.protected = False backup_instance.protected = False
backup_instance.restore_dockerconfig.side_effect = BackupError() backup_instance.restore_homeassistant.side_effect = BackupError()
with pytest.raises(BackupError): with pytest.raises(BackupError):
await coresys.backups.do_restore_full(backup_instance) await coresys.backups.do_restore_full(backup_instance)
capture_exception.assert_not_called() capture_exception.assert_not_called()
backup_instance.restore_dockerconfig.side_effect = (err := DockerError()) backup_instance.restore_homeassistant.side_effect = (err := DockerError())
with pytest.raises(BackupError): with pytest.raises(BackupError):
await coresys.backups.do_restore_full(backup_instance) await coresys.backups.do_restore_full(backup_instance)
capture_exception.assert_called_once_with(err) capture_exception.assert_called_once_with(err)
@ -1129,9 +1120,6 @@ async def test_backup_progress(
_make_backup_message_for_assert( _make_backup_message_for_assert(
reference=full_backup.slug, stage="addon_repositories" reference=full_backup.slug, stage="addon_repositories"
), ),
_make_backup_message_for_assert(
reference=full_backup.slug, stage="docker_config"
),
_make_backup_message_for_assert( _make_backup_message_for_assert(
reference=full_backup.slug, stage="home_assistant" reference=full_backup.slug, stage="home_assistant"
), ),
@ -1173,11 +1161,6 @@ async def test_backup_progress(
reference=partial_backup.slug, reference=partial_backup.slug,
stage="addon_repositories", stage="addon_repositories",
), ),
_make_backup_message_for_assert(
action="partial_backup",
reference=partial_backup.slug,
stage="docker_config",
),
_make_backup_message_for_assert( _make_backup_message_for_assert(
action="partial_backup", reference=partial_backup.slug, stage="addons" action="partial_backup", reference=partial_backup.slug, stage="addons"
), ),
@ -1244,9 +1227,6 @@ async def test_restore_progress(
_make_backup_message_for_assert( _make_backup_message_for_assert(
action="full_restore", reference=full_backup.slug, stage=None action="full_restore", reference=full_backup.slug, stage=None
), ),
_make_backup_message_for_assert(
action="full_restore", reference=full_backup.slug, stage="docker_config"
),
_make_backup_message_for_assert( _make_backup_message_for_assert(
action="full_restore", reference=full_backup.slug, stage="folders" action="full_restore", reference=full_backup.slug, stage="folders"
), ),
@ -1311,11 +1291,6 @@ async def test_restore_progress(
reference=folders_backup.slug, reference=folders_backup.slug,
stage=None, stage=None,
), ),
_make_backup_message_for_assert(
action="partial_restore",
reference=folders_backup.slug,
stage="docker_config",
),
_make_backup_message_for_assert( _make_backup_message_for_assert(
action="partial_restore", action="partial_restore",
reference=folders_backup.slug, reference=folders_backup.slug,
@ -1357,11 +1332,6 @@ async def test_restore_progress(
reference=addon_backup.slug, reference=addon_backup.slug,
stage=None, stage=None,
), ),
_make_backup_message_for_assert(
action="partial_restore",
reference=addon_backup.slug,
stage="docker_config",
),
_make_backup_message_for_assert( _make_backup_message_for_assert(
action="partial_restore", action="partial_restore",
reference=addon_backup.slug, reference=addon_backup.slug,