mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-09 02:06:30 +00:00
Allow adoption of existing data disk (#4991)
* Allow adoption of existing data disk * Fix existing tests * Add test cases and fix image issues * Fix addon build test * Run checks during setup not startup * Addon load mimics plugin and HA load for docker part * Default image accessible in except
This commit is contained in:
parent
55ed63cc79
commit
50a2e8fde3
@ -195,9 +195,20 @@ class Addon(AddonModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
await self._check_ingress_port()
|
await self._check_ingress_port()
|
||||||
with suppress(DockerError):
|
default_image = self._image(self.data)
|
||||||
|
try:
|
||||||
await self.instance.attach(version=self.version)
|
await self.instance.attach(version=self.version)
|
||||||
|
|
||||||
|
# Ensure we are using correct image for this system
|
||||||
|
await self.instance.check_image(self.version, default_image, self.arch)
|
||||||
|
except DockerError:
|
||||||
|
_LOGGER.info("No %s addon Docker image %s found", self.slug, self.image)
|
||||||
|
with suppress(AddonsError):
|
||||||
|
await self.instance.install(self.version, default_image, arch=self.arch)
|
||||||
|
|
||||||
|
self.persist[ATTR_IMAGE] = default_image
|
||||||
|
self.save_persist()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ip_address(self) -> IPv4Address:
|
def ip_address(self) -> IPv4Address:
|
||||||
"""Return IP of add-on instance."""
|
"""Return IP of add-on instance."""
|
||||||
|
@ -102,11 +102,11 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
|
|||||||
except HassioArchNotFound:
|
except HassioArchNotFound:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_docker_args(self, version: AwesomeVersion):
|
def get_docker_args(self, version: AwesomeVersion, image: str | None = None):
|
||||||
"""Create a dict with Docker build arguments."""
|
"""Create a dict with Docker build arguments."""
|
||||||
args = {
|
args = {
|
||||||
"path": str(self.addon.path_location),
|
"path": str(self.addon.path_location),
|
||||||
"tag": f"{self.addon.image}:{version!s}",
|
"tag": f"{image or self.addon.image}:{version!s}",
|
||||||
"dockerfile": str(self.dockerfile),
|
"dockerfile": str(self.dockerfile),
|
||||||
"pull": True,
|
"pull": True,
|
||||||
"forcerm": not self.sys_dev,
|
"forcerm": not self.sys_dev,
|
||||||
|
@ -93,6 +93,9 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
|
|
||||||
if ATTR_IMAGE in body:
|
if ATTR_IMAGE in body:
|
||||||
self.sys_homeassistant.image = body[ATTR_IMAGE]
|
self.sys_homeassistant.image = body[ATTR_IMAGE]
|
||||||
|
self.sys_homeassistant.override_image = (
|
||||||
|
self.sys_homeassistant.image != self.sys_homeassistant.default_image
|
||||||
|
)
|
||||||
|
|
||||||
if ATTR_BOOT in body:
|
if ATTR_BOOT in body:
|
||||||
self.sys_homeassistant.boot = body[ATTR_BOOT]
|
self.sys_homeassistant.boot = body[ATTR_BOOT]
|
||||||
|
@ -641,11 +641,11 @@ class DockerAddon(DockerInterface):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Pull Docker image or build it."""
|
"""Pull Docker image or build it."""
|
||||||
if need_build is None and self.addon.need_build or need_build:
|
if need_build is None and self.addon.need_build or need_build:
|
||||||
await self._build(version)
|
await self._build(version, image)
|
||||||
else:
|
else:
|
||||||
await super().install(version, image, latest, arch)
|
await super().install(version, image, latest, arch)
|
||||||
|
|
||||||
async def _build(self, version: AwesomeVersion) -> None:
|
async def _build(self, version: AwesomeVersion, image: str | None = None) -> None:
|
||||||
"""Build a Docker container."""
|
"""Build a Docker container."""
|
||||||
build_env = AddonBuild(self.coresys, self.addon)
|
build_env = AddonBuild(self.coresys, self.addon)
|
||||||
if not build_env.is_valid:
|
if not build_env.is_valid:
|
||||||
@ -657,7 +657,7 @@ class DockerAddon(DockerInterface):
|
|||||||
image, log = await self.sys_run_in_executor(
|
image, log = await self.sys_run_in_executor(
|
||||||
self.sys_docker.images.build,
|
self.sys_docker.images.build,
|
||||||
use_config_proxy=False,
|
use_config_proxy=False,
|
||||||
**build_env.get_docker_args(version),
|
**build_env.get_docker_args(version, image),
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER.debug("Build %s:%s done: %s", self.image, version, log)
|
_LOGGER.debug("Build %s:%s done: %s", self.image, version, log)
|
||||||
|
@ -14,6 +14,7 @@ from awesomeversion import AwesomeVersion
|
|||||||
from awesomeversion.strategy import AwesomeVersionStrategy
|
from awesomeversion.strategy import AwesomeVersionStrategy
|
||||||
import docker
|
import docker
|
||||||
from docker.models.containers import Container
|
from docker.models.containers import Container
|
||||||
|
from docker.models.images import Image
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
@ -438,6 +439,44 @@ class DockerInterface(JobGroup):
|
|||||||
)
|
)
|
||||||
self._meta = None
|
self._meta = None
|
||||||
|
|
||||||
|
@Job(
|
||||||
|
name="docker_interface_check_image",
|
||||||
|
limit=JobExecutionLimit.GROUP_ONCE,
|
||||||
|
on_condition=DockerJobError,
|
||||||
|
)
|
||||||
|
async def check_image(
|
||||||
|
self,
|
||||||
|
version: AwesomeVersion,
|
||||||
|
expected_image: str,
|
||||||
|
expected_arch: CpuArch | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Check we have expected image with correct arch."""
|
||||||
|
expected_arch = expected_arch or self.sys_arch.supervisor
|
||||||
|
image_name = f"{expected_image}:{version!s}"
|
||||||
|
if self.image == expected_image:
|
||||||
|
try:
|
||||||
|
image: Image = await self.sys_run_in_executor(
|
||||||
|
self.sys_docker.images.get, image_name
|
||||||
|
)
|
||||||
|
except (docker.errors.DockerException, requests.RequestException) as err:
|
||||||
|
raise DockerError(
|
||||||
|
f"Could not get {image_name} for check due to: {err!s}",
|
||||||
|
_LOGGER.error,
|
||||||
|
) from err
|
||||||
|
|
||||||
|
image_arch = f"{image.attrs['Os']}/{image.attrs['Architecture']}"
|
||||||
|
if "Variant" in image.attrs:
|
||||||
|
image_arch = f"{image_arch}/{image.attrs['Variant']}"
|
||||||
|
|
||||||
|
# If we have an image and its the right arch, all set
|
||||||
|
if MAP_ARCH[expected_arch] == image_arch:
|
||||||
|
return
|
||||||
|
|
||||||
|
# We're missing the image we need. Stop and clean up what we have then pull the right one
|
||||||
|
with suppress(DockerError):
|
||||||
|
await self.remove()
|
||||||
|
await self.install(version, expected_image, arch=expected_arch)
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
name="docker_interface_update",
|
name="docker_interface_update",
|
||||||
limit=JobExecutionLimit.GROUP_ONCE,
|
limit=JobExecutionLimit.GROUP_ONCE,
|
||||||
|
@ -6,6 +6,7 @@ from awesomeversion import AwesomeVersion
|
|||||||
|
|
||||||
from ..const import CoreState
|
from ..const import CoreState
|
||||||
|
|
||||||
|
ATTR_OVERRIDE_IMAGE = "override_image"
|
||||||
LANDINGPAGE: AwesomeVersion = AwesomeVersion("landingpage")
|
LANDINGPAGE: AwesomeVersion = AwesomeVersion("landingpage")
|
||||||
WATCHDOG_RETRY_SECONDS = 10
|
WATCHDOG_RETRY_SECONDS = 10
|
||||||
WATCHDOG_MAX_ATTEMPTS = 5
|
WATCHDOG_MAX_ATTEMPTS = 5
|
||||||
|
@ -89,6 +89,13 @@ class HomeAssistantCore(JobGroup):
|
|||||||
await self.instance.attach(
|
await self.instance.attach(
|
||||||
version=self.sys_homeassistant.version, skip_state_event_if_down=True
|
version=self.sys_homeassistant.version, skip_state_event_if_down=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Ensure we are using correct image for this system (unless user has overridden it)
|
||||||
|
if not self.sys_homeassistant.override_image:
|
||||||
|
await self.instance.check_image(
|
||||||
|
self.sys_homeassistant.version, self.sys_homeassistant.default_image
|
||||||
|
)
|
||||||
|
self.sys_homeassistant.image = self.sys_homeassistant.default_image
|
||||||
except DockerError:
|
except DockerError:
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"No Home Assistant Docker image %s found.", self.sys_homeassistant.image
|
"No Home Assistant Docker image %s found.", self.sys_homeassistant.image
|
||||||
|
@ -48,7 +48,7 @@ from ..utils import remove_folder
|
|||||||
from ..utils.common import FileConfiguration
|
from ..utils.common import FileConfiguration
|
||||||
from ..utils.json import read_json_file, write_json_file
|
from ..utils.json import read_json_file, write_json_file
|
||||||
from .api import HomeAssistantAPI
|
from .api import HomeAssistantAPI
|
||||||
from .const import WSType
|
from .const import ATTR_OVERRIDE_IMAGE, WSType
|
||||||
from .core import HomeAssistantCore
|
from .core import HomeAssistantCore
|
||||||
from .secrets import HomeAssistantSecrets
|
from .secrets import HomeAssistantSecrets
|
||||||
from .validate import SCHEMA_HASS_CONFIG
|
from .validate import SCHEMA_HASS_CONFIG
|
||||||
@ -170,18 +170,33 @@ class HomeAssistant(FileConfiguration, CoreSysAttributes):
|
|||||||
"""Return last available version of Home Assistant."""
|
"""Return last available version of Home Assistant."""
|
||||||
return self.sys_updater.version_homeassistant
|
return self.sys_updater.version_homeassistant
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_image(self) -> str:
|
||||||
|
"""Return the default image for this system."""
|
||||||
|
return f"ghcr.io/home-assistant/{self.sys_machine}-homeassistant"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self) -> str:
|
def image(self) -> str:
|
||||||
"""Return image name of the Home Assistant container."""
|
"""Return image name of the Home Assistant container."""
|
||||||
if self._data.get(ATTR_IMAGE):
|
if self._data.get(ATTR_IMAGE):
|
||||||
return self._data[ATTR_IMAGE]
|
return self._data[ATTR_IMAGE]
|
||||||
return f"ghcr.io/home-assistant/{self.sys_machine}-homeassistant"
|
return self.default_image
|
||||||
|
|
||||||
@image.setter
|
@image.setter
|
||||||
def image(self, value: str | None) -> None:
|
def image(self, value: str | None) -> None:
|
||||||
"""Set image name of Home Assistant container."""
|
"""Set image name of Home Assistant container."""
|
||||||
self._data[ATTR_IMAGE] = value
|
self._data[ATTR_IMAGE] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def override_image(self) -> bool:
|
||||||
|
"""Return if user has overridden the image to use for Home Assistant."""
|
||||||
|
return self._data[ATTR_OVERRIDE_IMAGE]
|
||||||
|
|
||||||
|
@override_image.setter
|
||||||
|
def override_image(self, value: bool) -> None:
|
||||||
|
"""Enable/disable image override."""
|
||||||
|
self._data[ATTR_OVERRIDE_IMAGE] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self) -> AwesomeVersion | None:
|
def version(self) -> AwesomeVersion | None:
|
||||||
"""Return version of local version."""
|
"""Return version of local version."""
|
||||||
|
@ -18,6 +18,7 @@ from ..const import (
|
|||||||
ATTR_WATCHDOG,
|
ATTR_WATCHDOG,
|
||||||
)
|
)
|
||||||
from ..validate import docker_image, network_port, token, uuid_match, version_tag
|
from ..validate import docker_image, network_port, token, uuid_match, version_tag
|
||||||
|
from .const import ATTR_OVERRIDE_IMAGE
|
||||||
|
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
SCHEMA_HASS_CONFIG = vol.Schema(
|
SCHEMA_HASS_CONFIG = vol.Schema(
|
||||||
@ -34,6 +35,7 @@ SCHEMA_HASS_CONFIG = vol.Schema(
|
|||||||
vol.Optional(ATTR_AUDIO_OUTPUT, default=None): vol.Maybe(str),
|
vol.Optional(ATTR_AUDIO_OUTPUT, default=None): vol.Maybe(str),
|
||||||
vol.Optional(ATTR_AUDIO_INPUT, default=None): vol.Maybe(str),
|
vol.Optional(ATTR_AUDIO_INPUT, default=None): vol.Maybe(str),
|
||||||
vol.Optional(ATTR_BACKUPS_EXCLUDE_DATABASE, default=False): vol.Boolean(),
|
vol.Optional(ATTR_BACKUPS_EXCLUDE_DATABASE, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_OVERRIDE_IMAGE, default=False): vol.Boolean(),
|
||||||
},
|
},
|
||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
)
|
)
|
||||||
|
@ -123,9 +123,9 @@ class DataDisk(CoreSysAttributes):
|
|||||||
vendor="",
|
vendor="",
|
||||||
model="",
|
model="",
|
||||||
serial="",
|
serial="",
|
||||||
id=self.sys_dbus.agent.datadisk.current_device,
|
id=self.sys_dbus.agent.datadisk.current_device.as_posix(),
|
||||||
size=0,
|
size=0,
|
||||||
device_path=self.sys_dbus.agent.datadisk.current_device,
|
device_path=self.sys_dbus.agent.datadisk.current_device.as_posix(),
|
||||||
object_path="",
|
object_path="",
|
||||||
device_object_path="",
|
device_object_path="",
|
||||||
)
|
)
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
Code: https://github.com/home-assistant/plugin-audio
|
Code: https://github.com/home-assistant/plugin-audio
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
from contextlib import suppress
|
|
||||||
import errno
|
import errno
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
@ -72,6 +70,11 @@ class PluginAudio(PluginBase):
|
|||||||
"""Return Path to pulse audio config file."""
|
"""Return Path to pulse audio config file."""
|
||||||
return Path(self.sys_config.path_audio, "pulse_audio.json")
|
return Path(self.sys_config.path_audio, "pulse_audio.json")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_image(self) -> str:
|
||||||
|
"""Return default image for audio plugin."""
|
||||||
|
return self.sys_updater.image_audio
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latest_version(self) -> AwesomeVersion | None:
|
def latest_version(self) -> AwesomeVersion | None:
|
||||||
"""Return latest version of Audio."""
|
"""Return latest version of Audio."""
|
||||||
@ -102,28 +105,6 @@ class PluginAudio(PluginBase):
|
|||||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||||
_LOGGER.error("Can't create default asound: %s", err)
|
_LOGGER.error("Can't create default asound: %s", err)
|
||||||
|
|
||||||
async def install(self) -> None:
|
|
||||||
"""Install Audio."""
|
|
||||||
_LOGGER.info("Setup Audio plugin")
|
|
||||||
while True:
|
|
||||||
# read audio tag and install it
|
|
||||||
if not self.latest_version:
|
|
||||||
await self.sys_updater.reload()
|
|
||||||
|
|
||||||
if self.latest_version:
|
|
||||||
with suppress(DockerError):
|
|
||||||
await self.instance.install(
|
|
||||||
self.latest_version, image=self.sys_updater.image_audio
|
|
||||||
)
|
|
||||||
break
|
|
||||||
_LOGGER.warning("Error on installing Audio plugin, retrying in 30sec")
|
|
||||||
await asyncio.sleep(30)
|
|
||||||
|
|
||||||
_LOGGER.info("Audio plugin now installed")
|
|
||||||
self.version = self.instance.version
|
|
||||||
self.image = self.sys_updater.image_audio
|
|
||||||
self.save_data()
|
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
name="plugin_audio_update",
|
name="plugin_audio_update",
|
||||||
conditions=PLUGIN_UPDATE_CONDITIONS,
|
conditions=PLUGIN_UPDATE_CONDITIONS,
|
||||||
@ -131,29 +112,11 @@ class PluginAudio(PluginBase):
|
|||||||
)
|
)
|
||||||
async def update(self, version: str | None = None) -> None:
|
async def update(self, version: str | None = None) -> None:
|
||||||
"""Update Audio plugin."""
|
"""Update Audio plugin."""
|
||||||
version = version or self.latest_version
|
|
||||||
old_image = self.image
|
|
||||||
|
|
||||||
if version == self.version:
|
|
||||||
_LOGGER.warning("Version %s is already installed for Audio", version)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.instance.update(version, image=self.sys_updater.image_audio)
|
await super().update(version)
|
||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
raise AudioUpdateError("Audio update failed", _LOGGER.error) from err
|
raise AudioUpdateError("Audio update failed", _LOGGER.error) from err
|
||||||
|
|
||||||
self.version = version
|
|
||||||
self.image = self.sys_updater.image_audio
|
|
||||||
self.save_data()
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
with suppress(DockerError):
|
|
||||||
await self.instance.cleanup(old_image=old_image)
|
|
||||||
|
|
||||||
# Start Audio
|
|
||||||
await self.start()
|
|
||||||
|
|
||||||
async def restart(self) -> None:
|
async def restart(self) -> None:
|
||||||
"""Restart Audio plugin."""
|
"""Restart Audio plugin."""
|
||||||
_LOGGER.info("Restarting Audio plugin")
|
_LOGGER.info("Restarting Audio plugin")
|
||||||
|
@ -36,12 +36,17 @@ class PluginBase(ABC, FileConfiguration, CoreSysAttributes):
|
|||||||
"""Set current version of the plugin."""
|
"""Set current version of the plugin."""
|
||||||
self._data[ATTR_VERSION] = value
|
self._data[ATTR_VERSION] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_image(self) -> str:
|
||||||
|
"""Return default image for plugin."""
|
||||||
|
return f"ghcr.io/home-assistant/{self.sys_arch.supervisor}-hassio-{self.slug}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self) -> str:
|
def image(self) -> str:
|
||||||
"""Return current image of plugin."""
|
"""Return current image of plugin."""
|
||||||
if self._data.get(ATTR_IMAGE):
|
if self._data.get(ATTR_IMAGE):
|
||||||
return self._data[ATTR_IMAGE]
|
return self._data[ATTR_IMAGE]
|
||||||
return f"ghcr.io/home-assistant/{self.sys_arch.supervisor}-hassio-{self.slug}"
|
return self.default_image
|
||||||
|
|
||||||
@image.setter
|
@image.setter
|
||||||
def image(self, value: str) -> None:
|
def image(self, value: str) -> None:
|
||||||
@ -160,6 +165,8 @@ class PluginBase(ABC, FileConfiguration, CoreSysAttributes):
|
|||||||
await self.instance.attach(
|
await self.instance.attach(
|
||||||
version=self.version, skip_state_event_if_down=True
|
version=self.version, skip_state_event_if_down=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await self.instance.check_image(self.version, self.default_image)
|
||||||
except DockerError:
|
except DockerError:
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"No %s plugin Docker image %s found.", self.slug, self.instance.image
|
"No %s plugin Docker image %s found.", self.slug, self.instance.image
|
||||||
@ -170,7 +177,7 @@ class PluginBase(ABC, FileConfiguration, CoreSysAttributes):
|
|||||||
await self.install()
|
await self.install()
|
||||||
else:
|
else:
|
||||||
self.version = self.instance.version
|
self.version = self.instance.version
|
||||||
self.image = self.instance.image
|
self.image = self.default_image
|
||||||
self.save_data()
|
self.save_data()
|
||||||
|
|
||||||
# Run plugin
|
# Run plugin
|
||||||
@ -178,13 +185,52 @@ class PluginBase(ABC, FileConfiguration, CoreSysAttributes):
|
|||||||
if not await self.instance.is_running():
|
if not await self.instance.is_running():
|
||||||
await self.start()
|
await self.start()
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def install(self) -> None:
|
async def install(self) -> None:
|
||||||
"""Install system plugin."""
|
"""Install system plugin."""
|
||||||
|
_LOGGER.info("Setup %s plugin", self.slug)
|
||||||
|
while True:
|
||||||
|
# read plugin tag and install it
|
||||||
|
if not self.latest_version:
|
||||||
|
await self.sys_updater.reload()
|
||||||
|
|
||||||
|
if self.latest_version:
|
||||||
|
with suppress(DockerError):
|
||||||
|
await self.instance.install(
|
||||||
|
self.latest_version, image=self.default_image
|
||||||
|
)
|
||||||
|
break
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Error on installing %s plugin, retrying in 30sec", self.slug
|
||||||
|
)
|
||||||
|
await asyncio.sleep(30)
|
||||||
|
|
||||||
|
_LOGGER.info("%s plugin now installed", self.slug)
|
||||||
|
self.version = self.instance.version
|
||||||
|
self.image = self.default_image
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def update(self, version: str | None = None) -> None:
|
async def update(self, version: str | None = None) -> None:
|
||||||
"""Update system plugin."""
|
"""Update system plugin."""
|
||||||
|
version = version or self.latest_version
|
||||||
|
old_image = self.image
|
||||||
|
|
||||||
|
if version == self.version:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Version %s is already installed for %s", version, self.slug
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.instance.update(version, image=self.default_image)
|
||||||
|
self.version = self.instance.version
|
||||||
|
self.image = self.default_image
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
with suppress(DockerError):
|
||||||
|
await self.instance.cleanup(old_image=old_image)
|
||||||
|
|
||||||
|
# Start plugin
|
||||||
|
await self.start()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def repair(self) -> None:
|
async def repair(self) -> None:
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
Code: https://github.com/home-assistant/plugin-cli
|
Code: https://github.com/home-assistant/plugin-cli
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
from contextlib import suppress
|
|
||||||
import logging
|
import logging
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
@ -41,6 +39,11 @@ class PluginCli(PluginBase):
|
|||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self.instance: DockerCli = DockerCli(coresys)
|
self.instance: DockerCli = DockerCli(coresys)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_image(self) -> str:
|
||||||
|
"""Return default image for cli plugin."""
|
||||||
|
return self.sys_updater.image_cli
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latest_version(self) -> AwesomeVersion | None:
|
def latest_version(self) -> AwesomeVersion | None:
|
||||||
"""Return version of latest cli."""
|
"""Return version of latest cli."""
|
||||||
@ -51,29 +54,6 @@ class PluginCli(PluginBase):
|
|||||||
"""Return an access token for the Supervisor API."""
|
"""Return an access token for the Supervisor API."""
|
||||||
return self._data.get(ATTR_ACCESS_TOKEN)
|
return self._data.get(ATTR_ACCESS_TOKEN)
|
||||||
|
|
||||||
async def install(self) -> None:
|
|
||||||
"""Install cli."""
|
|
||||||
_LOGGER.info("Running setup for CLI plugin")
|
|
||||||
while True:
|
|
||||||
# read cli tag and install it
|
|
||||||
if not self.latest_version:
|
|
||||||
await self.sys_updater.reload()
|
|
||||||
|
|
||||||
if self.latest_version:
|
|
||||||
with suppress(DockerError):
|
|
||||||
await self.instance.install(
|
|
||||||
self.latest_version,
|
|
||||||
image=self.sys_updater.image_cli,
|
|
||||||
)
|
|
||||||
break
|
|
||||||
_LOGGER.warning("Error on install cli plugin. Retrying in 30sec")
|
|
||||||
await asyncio.sleep(30)
|
|
||||||
|
|
||||||
_LOGGER.info("CLI plugin is now installed")
|
|
||||||
self.version = self.instance.version
|
|
||||||
self.image = self.sys_updater.image_cli
|
|
||||||
self.save_data()
|
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
name="plugin_cli_update",
|
name="plugin_cli_update",
|
||||||
conditions=PLUGIN_UPDATE_CONDITIONS,
|
conditions=PLUGIN_UPDATE_CONDITIONS,
|
||||||
@ -81,29 +61,11 @@ class PluginCli(PluginBase):
|
|||||||
)
|
)
|
||||||
async def update(self, version: AwesomeVersion | None = None) -> None:
|
async def update(self, version: AwesomeVersion | None = None) -> None:
|
||||||
"""Update local HA cli."""
|
"""Update local HA cli."""
|
||||||
version = version or self.latest_version
|
|
||||||
old_image = self.image
|
|
||||||
|
|
||||||
if version == self.version:
|
|
||||||
_LOGGER.warning("Version %s is already installed for CLI", version)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.instance.update(version, image=self.sys_updater.image_cli)
|
await super().update(version)
|
||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
raise CliUpdateError("CLI update failed", _LOGGER.error) from err
|
raise CliUpdateError("CLI update failed", _LOGGER.error) from err
|
||||||
|
|
||||||
self.version = version
|
|
||||||
self.image = self.sys_updater.image_cli
|
|
||||||
self.save_data()
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
with suppress(DockerError):
|
|
||||||
await self.instance.cleanup(old_image=old_image)
|
|
||||||
|
|
||||||
# Start cli
|
|
||||||
await self.start()
|
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
"""Run cli."""
|
"""Run cli."""
|
||||||
# Create new API token
|
# Create new API token
|
||||||
|
@ -108,6 +108,11 @@ class PluginDns(PluginBase):
|
|||||||
"""Return list of DNS servers."""
|
"""Return list of DNS servers."""
|
||||||
self._data[ATTR_SERVERS] = value
|
self._data[ATTR_SERVERS] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_image(self) -> str:
|
||||||
|
"""Return default image for dns plugin."""
|
||||||
|
return self.sys_updater.image_dns
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latest_version(self) -> AwesomeVersion | None:
|
def latest_version(self) -> AwesomeVersion | None:
|
||||||
"""Return latest version of CoreDNS."""
|
"""Return latest version of CoreDNS."""
|
||||||
@ -168,25 +173,7 @@ class PluginDns(PluginBase):
|
|||||||
|
|
||||||
async def install(self) -> None:
|
async def install(self) -> None:
|
||||||
"""Install CoreDNS."""
|
"""Install CoreDNS."""
|
||||||
_LOGGER.info("Running setup for CoreDNS plugin")
|
await super().install()
|
||||||
while True:
|
|
||||||
# read homeassistant tag and install it
|
|
||||||
if not self.latest_version:
|
|
||||||
await self.sys_updater.reload()
|
|
||||||
|
|
||||||
if self.latest_version:
|
|
||||||
with suppress(DockerError):
|
|
||||||
await self.instance.install(
|
|
||||||
self.latest_version, image=self.sys_updater.image_dns
|
|
||||||
)
|
|
||||||
break
|
|
||||||
_LOGGER.warning("Error on install CoreDNS plugin. Retrying in 30sec")
|
|
||||||
await asyncio.sleep(30)
|
|
||||||
|
|
||||||
_LOGGER.info("CoreDNS plugin now installed")
|
|
||||||
self.version = self.instance.version
|
|
||||||
self.image = self.sys_updater.image_dns
|
|
||||||
self.save_data()
|
|
||||||
|
|
||||||
# Init Hosts
|
# Init Hosts
|
||||||
await self.write_hosts()
|
await self.write_hosts()
|
||||||
@ -198,30 +185,11 @@ class PluginDns(PluginBase):
|
|||||||
)
|
)
|
||||||
async def update(self, version: AwesomeVersion | None = None) -> None:
|
async def update(self, version: AwesomeVersion | None = None) -> None:
|
||||||
"""Update CoreDNS plugin."""
|
"""Update CoreDNS plugin."""
|
||||||
version = version or self.latest_version
|
|
||||||
old_image = self.image
|
|
||||||
|
|
||||||
if version == self.version:
|
|
||||||
_LOGGER.warning("Version %s is already installed for CoreDNS", version)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Update
|
|
||||||
try:
|
try:
|
||||||
await self.instance.update(version, image=self.sys_updater.image_dns)
|
await super().update(version)
|
||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
raise CoreDNSUpdateError("CoreDNS update failed", _LOGGER.error) from err
|
raise CoreDNSUpdateError("CoreDNS update failed", _LOGGER.error) from err
|
||||||
|
|
||||||
self.version = version
|
|
||||||
self.image = self.sys_updater.image_dns
|
|
||||||
self.save_data()
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
with suppress(DockerError):
|
|
||||||
await self.instance.cleanup(old_image=old_image)
|
|
||||||
|
|
||||||
# Start CoreDNS
|
|
||||||
await self.start()
|
|
||||||
|
|
||||||
async def restart(self) -> None:
|
async def restart(self) -> None:
|
||||||
"""Restart CoreDNS plugin."""
|
"""Restart CoreDNS plugin."""
|
||||||
self._write_config()
|
self._write_config()
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
Code: https://github.com/home-assistant/plugin-multicast
|
Code: https://github.com/home-assistant/plugin-multicast
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
from contextlib import suppress
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
@ -43,33 +41,16 @@ class PluginMulticast(PluginBase):
|
|||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self.instance: DockerMulticast = DockerMulticast(coresys)
|
self.instance: DockerMulticast = DockerMulticast(coresys)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_image(self) -> str:
|
||||||
|
"""Return default image for multicast plugin."""
|
||||||
|
return self.sys_updater.image_multicast
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latest_version(self) -> AwesomeVersion | None:
|
def latest_version(self) -> AwesomeVersion | None:
|
||||||
"""Return latest version of Multicast."""
|
"""Return latest version of Multicast."""
|
||||||
return self.sys_updater.version_multicast
|
return self.sys_updater.version_multicast
|
||||||
|
|
||||||
async def install(self) -> None:
|
|
||||||
"""Install Multicast."""
|
|
||||||
_LOGGER.info("Running setup for Multicast plugin")
|
|
||||||
while True:
|
|
||||||
# read multicast tag and install it
|
|
||||||
if not self.latest_version:
|
|
||||||
await self.sys_updater.reload()
|
|
||||||
|
|
||||||
if self.latest_version:
|
|
||||||
with suppress(DockerError):
|
|
||||||
await self.instance.install(
|
|
||||||
self.latest_version, image=self.sys_updater.image_multicast
|
|
||||||
)
|
|
||||||
break
|
|
||||||
_LOGGER.warning("Error on install Multicast plugin. Retrying in 30sec")
|
|
||||||
await asyncio.sleep(30)
|
|
||||||
|
|
||||||
_LOGGER.info("Multicast plugin is now installed")
|
|
||||||
self.version = self.instance.version
|
|
||||||
self.image = self.sys_updater.image_multicast
|
|
||||||
self.save_data()
|
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
name="plugin_multicast_update",
|
name="plugin_multicast_update",
|
||||||
conditions=PLUGIN_UPDATE_CONDITIONS,
|
conditions=PLUGIN_UPDATE_CONDITIONS,
|
||||||
@ -77,32 +58,13 @@ class PluginMulticast(PluginBase):
|
|||||||
)
|
)
|
||||||
async def update(self, version: AwesomeVersion | None = None) -> None:
|
async def update(self, version: AwesomeVersion | None = None) -> None:
|
||||||
"""Update Multicast plugin."""
|
"""Update Multicast plugin."""
|
||||||
version = version or self.latest_version
|
|
||||||
old_image = self.image
|
|
||||||
|
|
||||||
if version == self.version:
|
|
||||||
_LOGGER.warning("Version %s is already installed for Multicast", version)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Update
|
|
||||||
try:
|
try:
|
||||||
await self.instance.update(version, image=self.sys_updater.image_multicast)
|
await super().update(version)
|
||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
raise MulticastUpdateError(
|
raise MulticastUpdateError(
|
||||||
"Multicast update failed", _LOGGER.error
|
"Multicast update failed", _LOGGER.error
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
self.version = version
|
|
||||||
self.image = self.sys_updater.image_multicast
|
|
||||||
self.save_data()
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
with suppress(DockerError):
|
|
||||||
await self.instance.cleanup(old_image=old_image)
|
|
||||||
|
|
||||||
# Start Multicast plugin
|
|
||||||
await self.start()
|
|
||||||
|
|
||||||
async def restart(self) -> None:
|
async def restart(self) -> None:
|
||||||
"""Restart Multicast plugin."""
|
"""Restart Multicast plugin."""
|
||||||
_LOGGER.info("Restarting Multicast plugin")
|
_LOGGER.info("Restarting Multicast plugin")
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
Code: https://github.com/home-assistant/plugin-observer
|
Code: https://github.com/home-assistant/plugin-observer
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
from contextlib import suppress
|
|
||||||
import logging
|
import logging
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
@ -46,6 +44,11 @@ class PluginObserver(PluginBase):
|
|||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self.instance: DockerObserver = DockerObserver(coresys)
|
self.instance: DockerObserver = DockerObserver(coresys)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_image(self) -> str:
|
||||||
|
"""Return default image for observer plugin."""
|
||||||
|
return self.sys_updater.image_observer
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latest_version(self) -> AwesomeVersion | None:
|
def latest_version(self) -> AwesomeVersion | None:
|
||||||
"""Return version of latest observer."""
|
"""Return version of latest observer."""
|
||||||
@ -56,28 +59,6 @@ class PluginObserver(PluginBase):
|
|||||||
"""Return an access token for the Observer API."""
|
"""Return an access token for the Observer API."""
|
||||||
return self._data.get(ATTR_ACCESS_TOKEN)
|
return self._data.get(ATTR_ACCESS_TOKEN)
|
||||||
|
|
||||||
async def install(self) -> None:
|
|
||||||
"""Install observer."""
|
|
||||||
_LOGGER.info("Running setup for observer plugin")
|
|
||||||
while True:
|
|
||||||
# read observer tag and install it
|
|
||||||
if not self.latest_version:
|
|
||||||
await self.sys_updater.reload()
|
|
||||||
|
|
||||||
if self.latest_version:
|
|
||||||
with suppress(DockerError):
|
|
||||||
await self.instance.install(
|
|
||||||
self.latest_version, image=self.sys_updater.image_observer
|
|
||||||
)
|
|
||||||
break
|
|
||||||
_LOGGER.warning("Error on install observer plugin. Retrying in 30sec")
|
|
||||||
await asyncio.sleep(30)
|
|
||||||
|
|
||||||
_LOGGER.info("observer plugin now installed")
|
|
||||||
self.version = self.instance.version
|
|
||||||
self.image = self.sys_updater.image_observer
|
|
||||||
self.save_data()
|
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
name="plugin_observer_update",
|
name="plugin_observer_update",
|
||||||
conditions=PLUGIN_UPDATE_CONDITIONS,
|
conditions=PLUGIN_UPDATE_CONDITIONS,
|
||||||
@ -85,29 +66,12 @@ class PluginObserver(PluginBase):
|
|||||||
)
|
)
|
||||||
async def update(self, version: AwesomeVersion | None = None) -> None:
|
async def update(self, version: AwesomeVersion | None = None) -> None:
|
||||||
"""Update local HA observer."""
|
"""Update local HA observer."""
|
||||||
version = version or self.latest_version
|
|
||||||
old_image = self.image
|
|
||||||
|
|
||||||
if version == self.version:
|
|
||||||
_LOGGER.warning("Version %s is already installed for observer", version)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.instance.update(version, image=self.sys_updater.image_observer)
|
await super().update(version)
|
||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
_LOGGER.error("HA observer update failed")
|
raise ObserverUpdateError(
|
||||||
raise ObserverUpdateError() from err
|
"HA observer update failed", _LOGGER.error
|
||||||
|
) from err
|
||||||
self.version = version
|
|
||||||
self.image = self.sys_updater.image_observer
|
|
||||||
self.save_data()
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
with suppress(DockerError):
|
|
||||||
await self.instance.cleanup(old_image=old_image)
|
|
||||||
|
|
||||||
# Start observer
|
|
||||||
await self.start()
|
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
"""Run observer."""
|
"""Run observer."""
|
||||||
|
@ -27,7 +27,10 @@ class CheckMultipleDataDisks(CheckBase):
|
|||||||
IssueType.MULTIPLE_DATA_DISKS,
|
IssueType.MULTIPLE_DATA_DISKS,
|
||||||
ContextType.SYSTEM,
|
ContextType.SYSTEM,
|
||||||
reference=block_device.device.as_posix(),
|
reference=block_device.device.as_posix(),
|
||||||
suggestions=[SuggestionType.RENAME_DATA_DISK],
|
suggestions=[
|
||||||
|
SuggestionType.RENAME_DATA_DISK,
|
||||||
|
SuggestionType.ADOPT_DATA_DISK,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def approve_check(self, reference: str | None = None) -> bool:
|
async def approve_check(self, reference: str | None = None) -> bool:
|
||||||
@ -58,4 +61,4 @@ class CheckMultipleDataDisks(CheckBase):
|
|||||||
@property
|
@property
|
||||||
def states(self) -> list[CoreState]:
|
def states(self) -> list[CoreState]:
|
||||||
"""Return a list of valid states when this check can run."""
|
"""Return a list of valid states when this check can run."""
|
||||||
return [CoreState.RUNNING, CoreState.STARTUP]
|
return [CoreState.RUNNING, CoreState.SETUP]
|
||||||
|
@ -96,6 +96,7 @@ class IssueType(StrEnum):
|
|||||||
class SuggestionType(StrEnum):
|
class SuggestionType(StrEnum):
|
||||||
"""Sugestion type."""
|
"""Sugestion type."""
|
||||||
|
|
||||||
|
ADOPT_DATA_DISK = "adopt_data_disk"
|
||||||
CLEAR_FULL_BACKUP = "clear_full_backup"
|
CLEAR_FULL_BACKUP = "clear_full_backup"
|
||||||
CREATE_FULL_BACKUP = "create_full_backup"
|
CREATE_FULL_BACKUP = "create_full_backup"
|
||||||
EXECUTE_INTEGRITY = "execute_integrity"
|
EXECUTE_INTEGRITY = "execute_integrity"
|
||||||
|
90
supervisor/resolution/fixups/system_adopt_data_disk.py
Normal file
90
supervisor/resolution/fixups/system_adopt_data_disk.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
"""Adopt data disk fixup."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ...coresys import CoreSys
|
||||||
|
from ...dbus.udisks2.data import DeviceSpecification
|
||||||
|
from ...exceptions import DBusError, HostError, ResolutionFixupError
|
||||||
|
from ...os.const import FILESYSTEM_LABEL_OLD_DATA_DISK
|
||||||
|
from ..const import ContextType, IssueType, SuggestionType
|
||||||
|
from .base import FixupBase
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(coresys: CoreSys) -> FixupBase:
|
||||||
|
"""Check setup function."""
|
||||||
|
return FixupSystemAdoptDataDisk(coresys)
|
||||||
|
|
||||||
|
|
||||||
|
class FixupSystemAdoptDataDisk(FixupBase):
|
||||||
|
"""Storage class for fixup."""
|
||||||
|
|
||||||
|
async def process_fixup(self, reference: str | None = None) -> None:
|
||||||
|
"""Initialize the fixup class."""
|
||||||
|
if not await self.sys_dbus.udisks2.resolve_device(
|
||||||
|
DeviceSpecification(path=Path(reference))
|
||||||
|
):
|
||||||
|
_LOGGER.info(
|
||||||
|
"Data disk at %s with name conflict was removed, skipping adopt",
|
||||||
|
reference,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
current = self.sys_dbus.agent.datadisk.current_device
|
||||||
|
if (
|
||||||
|
not current
|
||||||
|
or not (
|
||||||
|
resolved := await self.sys_dbus.udisks2.resolve_device(
|
||||||
|
DeviceSpecification(path=current)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or not resolved[0].filesystem
|
||||||
|
):
|
||||||
|
raise ResolutionFixupError(
|
||||||
|
"Cannot resolve current data disk for rename", _LOGGER.error
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.info(
|
||||||
|
"Renaming current data disk at %s to %s so new data disk at %s becomes primary ",
|
||||||
|
self.sys_dbus.agent.datadisk.current_device,
|
||||||
|
FILESYSTEM_LABEL_OLD_DATA_DISK,
|
||||||
|
reference,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await resolved[0].filesystem.set_label(FILESYSTEM_LABEL_OLD_DATA_DISK)
|
||||||
|
except DBusError as err:
|
||||||
|
raise ResolutionFixupError(
|
||||||
|
f"Could not rename filesystem at {current.as_posix()}: {err!s}",
|
||||||
|
_LOGGER.error,
|
||||||
|
) from err
|
||||||
|
|
||||||
|
_LOGGER.info("Rebooting the host to finish adoption")
|
||||||
|
try:
|
||||||
|
await self.sys_host.control.reboot()
|
||||||
|
except (HostError, DBusError) as err:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Could not reboot host to finish data disk adoption, manual reboot required to finish process: %s",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
self.sys_resolution.create_issue(
|
||||||
|
IssueType.REBOOT_REQUIRED,
|
||||||
|
ContextType.SYSTEM,
|
||||||
|
suggestions=[SuggestionType.EXECUTE_REBOOT],
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def suggestion(self) -> SuggestionType:
|
||||||
|
"""Return a SuggestionType enum."""
|
||||||
|
return SuggestionType.ADOPT_DATA_DISK
|
||||||
|
|
||||||
|
@property
|
||||||
|
def context(self) -> ContextType:
|
||||||
|
"""Return a ContextType enum."""
|
||||||
|
return ContextType.SYSTEM
|
||||||
|
|
||||||
|
@property
|
||||||
|
def issues(self) -> list[IssueType]:
|
||||||
|
"""Return a IssueType enum list."""
|
||||||
|
return [IssueType.MULTIPLE_DATA_DISKS]
|
@ -7,7 +7,7 @@ from pathlib import Path
|
|||||||
from unittest.mock import MagicMock, PropertyMock, patch
|
from unittest.mock import MagicMock, PropertyMock, patch
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
from docker.errors import DockerException, NotFound
|
from docker.errors import DockerException, ImageNotFound, NotFound
|
||||||
import pytest
|
import pytest
|
||||||
from securetar import SecureTarFile
|
from securetar import SecureTarFile
|
||||||
|
|
||||||
@ -763,3 +763,57 @@ async def test_paths_cache(coresys: CoreSys, install_addon_ssh: Addon):
|
|||||||
assert install_addon_ssh.with_icon
|
assert install_addon_ssh.with_icon
|
||||||
assert install_addon_ssh.with_changelog
|
assert install_addon_ssh.with_changelog
|
||||||
assert install_addon_ssh.with_documentation
|
assert install_addon_ssh.with_documentation
|
||||||
|
|
||||||
|
|
||||||
|
async def test_addon_loads_wrong_image(
|
||||||
|
coresys: CoreSys,
|
||||||
|
install_addon_ssh: Addon,
|
||||||
|
container: MagicMock,
|
||||||
|
mock_amd64_arch_supported,
|
||||||
|
):
|
||||||
|
"""Test addon is loaded with incorrect image for architecture."""
|
||||||
|
coresys.addons.data.save_data.reset_mock()
|
||||||
|
install_addon_ssh.persist["image"] = "local/aarch64-addon-ssh"
|
||||||
|
assert install_addon_ssh.image == "local/aarch64-addon-ssh"
|
||||||
|
|
||||||
|
with patch("pathlib.Path.is_file", return_value=True):
|
||||||
|
await install_addon_ssh.load()
|
||||||
|
|
||||||
|
container.remove.assert_called_once_with(force=True)
|
||||||
|
assert coresys.docker.images.remove.call_args_list[0].kwargs == {
|
||||||
|
"image": "local/aarch64-addon-ssh:latest",
|
||||||
|
"force": True,
|
||||||
|
}
|
||||||
|
assert coresys.docker.images.remove.call_args_list[1].kwargs == {
|
||||||
|
"image": "local/aarch64-addon-ssh:9.2.1",
|
||||||
|
"force": True,
|
||||||
|
}
|
||||||
|
coresys.docker.images.build.assert_called_once()
|
||||||
|
assert (
|
||||||
|
coresys.docker.images.build.call_args.kwargs["tag"]
|
||||||
|
== "local/amd64-addon-ssh:9.2.1"
|
||||||
|
)
|
||||||
|
assert coresys.docker.images.build.call_args.kwargs["platform"] == "linux/amd64"
|
||||||
|
assert install_addon_ssh.image == "local/amd64-addon-ssh"
|
||||||
|
coresys.addons.data.save_data.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_addon_loads_missing_image(
|
||||||
|
coresys: CoreSys,
|
||||||
|
install_addon_ssh: Addon,
|
||||||
|
container: MagicMock,
|
||||||
|
mock_amd64_arch_supported,
|
||||||
|
):
|
||||||
|
"""Test addon corrects a missing image on load."""
|
||||||
|
coresys.docker.images.get.side_effect = ImageNotFound("missing")
|
||||||
|
|
||||||
|
with patch("pathlib.Path.is_file", return_value=True):
|
||||||
|
await install_addon_ssh.load()
|
||||||
|
|
||||||
|
coresys.docker.images.build.assert_called_once()
|
||||||
|
assert (
|
||||||
|
coresys.docker.images.build.call_args.kwargs["tag"]
|
||||||
|
== "local/amd64-addon-ssh:9.2.1"
|
||||||
|
)
|
||||||
|
assert coresys.docker.images.build.call_args.kwargs["platform"] == "linux/amd64"
|
||||||
|
assert install_addon_ssh.image == "local/amd64-addon-ssh"
|
||||||
|
@ -86,7 +86,7 @@ async def test_image_added_removed_on_update(
|
|||||||
DockerAddon, "_build"
|
DockerAddon, "_build"
|
||||||
) as build:
|
) as build:
|
||||||
await coresys.addons.update(TEST_ADDON_SLUG)
|
await coresys.addons.update(TEST_ADDON_SLUG)
|
||||||
build.assert_called_once_with(AwesomeVersion("11.0.0"))
|
build.assert_called_once_with(AwesomeVersion("11.0.0"), "local/amd64-addon-ssh")
|
||||||
install.assert_not_called()
|
install.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,3 +62,33 @@ async def test_api_set_options(api_client: TestClient, coresys: CoreSys):
|
|||||||
result = await resp.json()
|
result = await resp.json()
|
||||||
assert result["data"]["watchdog"] is False
|
assert result["data"]["watchdog"] is False
|
||||||
assert result["data"]["backups_exclude_database"] is True
|
assert result["data"]["backups_exclude_database"] is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_api_set_image(api_client: TestClient, coresys: CoreSys):
|
||||||
|
"""Test changing the image for homeassistant."""
|
||||||
|
assert (
|
||||||
|
coresys.homeassistant.image == "ghcr.io/home-assistant/qemux86-64-homeassistant"
|
||||||
|
)
|
||||||
|
assert coresys.homeassistant.override_image is False
|
||||||
|
|
||||||
|
with patch.object(HomeAssistant, "save_data"):
|
||||||
|
resp = await api_client.post(
|
||||||
|
"/homeassistant/options",
|
||||||
|
json={"image": "test_image"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status == 200
|
||||||
|
assert coresys.homeassistant.image == "test_image"
|
||||||
|
assert coresys.homeassistant.override_image is True
|
||||||
|
|
||||||
|
with patch.object(HomeAssistant, "save_data"):
|
||||||
|
resp = await api_client.post(
|
||||||
|
"/homeassistant/options",
|
||||||
|
json={"image": "ghcr.io/home-assistant/qemux86-64-homeassistant"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status == 200
|
||||||
|
assert (
|
||||||
|
coresys.homeassistant.image == "ghcr.io/home-assistant/qemux86-64-homeassistant"
|
||||||
|
)
|
||||||
|
assert coresys.homeassistant.override_image is False
|
||||||
|
@ -77,6 +77,8 @@ async def supervisor_name() -> None:
|
|||||||
async def docker() -> DockerAPI:
|
async def docker() -> DockerAPI:
|
||||||
"""Mock DockerAPI."""
|
"""Mock DockerAPI."""
|
||||||
images = [MagicMock(tags=["ghcr.io/home-assistant/amd64-hassio-supervisor:latest"])]
|
images = [MagicMock(tags=["ghcr.io/home-assistant/amd64-hassio-supervisor:latest"])]
|
||||||
|
image = MagicMock()
|
||||||
|
image.attrs = {"Os": "linux", "Architecture": "amd64"}
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"supervisor.docker.manager.DockerClient", return_value=MagicMock()
|
"supervisor.docker.manager.DockerClient", return_value=MagicMock()
|
||||||
@ -86,6 +88,8 @@ async def docker() -> DockerAPI:
|
|||||||
"supervisor.docker.manager.DockerAPI.containers", return_value=MagicMock()
|
"supervisor.docker.manager.DockerAPI.containers", return_value=MagicMock()
|
||||||
), patch(
|
), patch(
|
||||||
"supervisor.docker.manager.DockerAPI.api", return_value=MagicMock()
|
"supervisor.docker.manager.DockerAPI.api", return_value=MagicMock()
|
||||||
|
), patch(
|
||||||
|
"supervisor.docker.manager.DockerAPI.images.get", return_value=image
|
||||||
), patch(
|
), patch(
|
||||||
"supervisor.docker.manager.DockerAPI.images.list", return_value=images
|
"supervisor.docker.manager.DockerAPI.images.list", return_value=images
|
||||||
), patch(
|
), patch(
|
||||||
@ -317,6 +321,9 @@ async def coresys(
|
|||||||
coresys_obj._mounts.save_data = MagicMock()
|
coresys_obj._mounts.save_data = MagicMock()
|
||||||
|
|
||||||
# Mock test client
|
# Mock test client
|
||||||
|
coresys_obj._supervisor.instance._meta = {
|
||||||
|
"Config": {"Labels": {"io.hass.arch": "amd64"}}
|
||||||
|
}
|
||||||
coresys_obj.arch._default_arch = "amd64"
|
coresys_obj.arch._default_arch = "amd64"
|
||||||
coresys_obj.arch._supported_set = {"amd64"}
|
coresys_obj.arch._supported_set = {"amd64"}
|
||||||
coresys_obj._machine = "qemux86-64"
|
coresys_obj._machine = "qemux86-64"
|
||||||
@ -716,15 +723,15 @@ async def container(docker: DockerAPI) -> MagicMock:
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_amd64_arch_supported(coresys: CoreSys) -> None:
|
def mock_amd64_arch_supported(coresys: CoreSys) -> None:
|
||||||
"""Mock amd64 arch as supported."""
|
"""Mock amd64 arch as supported."""
|
||||||
with patch.object(coresys.arch, "_supported_set", {"amd64"}):
|
coresys.arch._supported_arch = ["amd64"]
|
||||||
yield
|
coresys.arch._supported_set = {"amd64"}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_aarch64_arch_supported(coresys: CoreSys) -> None:
|
def mock_aarch64_arch_supported(coresys: CoreSys) -> None:
|
||||||
"""Mock aarch64 arch as supported."""
|
"""Mock aarch64 arch as supported."""
|
||||||
with patch.object(coresys.arch, "_supported_set", {"aarch64"}):
|
coresys.arch._supported_arch = ["amd64"]
|
||||||
yield
|
coresys.arch._supported_set = {"amd64"}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -314,3 +314,78 @@ async def test_api_check_success(
|
|||||||
|
|
||||||
assert coresys.homeassistant.api.get_api_state.call_count == 1
|
assert coresys.homeassistant.api.get_api_state.call_count == 1
|
||||||
assert "Detect a running Home Assistant instance" in caplog.text
|
assert "Detect a running Home Assistant instance" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_core_loads_wrong_image_for_machine(
|
||||||
|
coresys: CoreSys, container: MagicMock
|
||||||
|
):
|
||||||
|
"""Test core is loaded with wrong image for machine."""
|
||||||
|
coresys.homeassistant.image = "ghcr.io/home-assistant/odroid-n2-homeassistant"
|
||||||
|
coresys.homeassistant.version = AwesomeVersion("2024.4.0")
|
||||||
|
container.attrs["Config"] = {"Labels": {"io.hass.version": "2024.4.0"}}
|
||||||
|
|
||||||
|
await coresys.homeassistant.core.load()
|
||||||
|
|
||||||
|
container.remove.assert_called_once_with(force=True)
|
||||||
|
assert coresys.docker.images.remove.call_args_list[0].kwargs == {
|
||||||
|
"image": "ghcr.io/home-assistant/odroid-n2-homeassistant:latest",
|
||||||
|
"force": True,
|
||||||
|
}
|
||||||
|
assert coresys.docker.images.remove.call_args_list[1].kwargs == {
|
||||||
|
"image": "ghcr.io/home-assistant/odroid-n2-homeassistant:2024.4.0",
|
||||||
|
"force": True,
|
||||||
|
}
|
||||||
|
coresys.docker.images.pull.assert_called_once_with(
|
||||||
|
"ghcr.io/home-assistant/qemux86-64-homeassistant:2024.4.0",
|
||||||
|
platform="linux/amd64",
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
coresys.homeassistant.image == "ghcr.io/home-assistant/qemux86-64-homeassistant"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_core_load_allows_image_override(coresys: CoreSys, container: MagicMock):
|
||||||
|
"""Test core does not change image if user overrode it."""
|
||||||
|
coresys.homeassistant.image = "ghcr.io/home-assistant/odroid-n2-homeassistant"
|
||||||
|
coresys.homeassistant.version = AwesomeVersion("2024.4.0")
|
||||||
|
container.attrs["Config"] = {"Labels": {"io.hass.version": "2024.4.0"}}
|
||||||
|
|
||||||
|
coresys.homeassistant.override_image = True
|
||||||
|
await coresys.homeassistant.core.load()
|
||||||
|
|
||||||
|
container.remove.assert_not_called()
|
||||||
|
coresys.docker.images.remove.assert_not_called()
|
||||||
|
coresys.docker.images.pull.assert_not_called()
|
||||||
|
assert (
|
||||||
|
coresys.homeassistant.image == "ghcr.io/home-assistant/odroid-n2-homeassistant"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_core_loads_wrong_image_for_architecture(
|
||||||
|
coresys: CoreSys, container: MagicMock
|
||||||
|
):
|
||||||
|
"""Test core is loaded with wrong image for architecture."""
|
||||||
|
coresys.homeassistant.version = AwesomeVersion("2024.4.0")
|
||||||
|
container.attrs["Config"] = {"Labels": {"io.hass.version": "2024.4.0"}}
|
||||||
|
coresys.docker.images.get("ghcr.io/home-assistant/qemux86-64-homeassistant").attrs[
|
||||||
|
"Architecture"
|
||||||
|
] = "arm64"
|
||||||
|
|
||||||
|
await coresys.homeassistant.core.load()
|
||||||
|
|
||||||
|
container.remove.assert_called_once_with(force=True)
|
||||||
|
assert coresys.docker.images.remove.call_args_list[0].kwargs == {
|
||||||
|
"image": "ghcr.io/home-assistant/qemux86-64-homeassistant:latest",
|
||||||
|
"force": True,
|
||||||
|
}
|
||||||
|
assert coresys.docker.images.remove.call_args_list[1].kwargs == {
|
||||||
|
"image": "ghcr.io/home-assistant/qemux86-64-homeassistant:2024.4.0",
|
||||||
|
"force": True,
|
||||||
|
}
|
||||||
|
coresys.docker.images.pull.assert_called_once_with(
|
||||||
|
"ghcr.io/home-assistant/qemux86-64-homeassistant:2024.4.0",
|
||||||
|
platform="linux/amd64",
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
coresys.homeassistant.image == "ghcr.io/home-assistant/qemux86-64-homeassistant"
|
||||||
|
)
|
||||||
|
@ -22,6 +22,8 @@ async def test_load(
|
|||||||
|
|
||||||
# Unwrap read_secrets to prevent throttling between tests
|
# Unwrap read_secrets to prevent throttling between tests
|
||||||
with patch.object(DockerInterface, "attach") as attach, patch.object(
|
with patch.object(DockerInterface, "attach") as attach, patch.object(
|
||||||
|
DockerInterface, "check_image"
|
||||||
|
) as check_image, patch.object(
|
||||||
HomeAssistantSecrets,
|
HomeAssistantSecrets,
|
||||||
"_read_secrets",
|
"_read_secrets",
|
||||||
new=HomeAssistantSecrets._read_secrets.__wrapped__,
|
new=HomeAssistantSecrets._read_secrets.__wrapped__,
|
||||||
@ -29,6 +31,7 @@ async def test_load(
|
|||||||
await coresys.homeassistant.load()
|
await coresys.homeassistant.load()
|
||||||
|
|
||||||
attach.assert_called_once()
|
attach.assert_called_once()
|
||||||
|
check_image.assert_called_once()
|
||||||
|
|
||||||
assert coresys.homeassistant.secrets.secrets == {"hello": "world"}
|
assert coresys.homeassistant.secrets.secrets == {"hello": "world"}
|
||||||
|
|
||||||
|
@ -325,3 +325,38 @@ async def test_repair_failed(
|
|||||||
|
|
||||||
capture_exception.assert_called_once()
|
capture_exception.assert_called_once()
|
||||||
assert check_exception_chain(capture_exception.call_args[0][0], CodeNotaryUntrusted)
|
assert check_exception_chain(capture_exception.call_args[0][0], CodeNotaryUntrusted)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"plugin",
|
||||||
|
[PluginAudio, PluginCli, PluginDns, PluginMulticast, PluginObserver],
|
||||||
|
indirect=True,
|
||||||
|
)
|
||||||
|
async def test_load_with_incorrect_image(
|
||||||
|
coresys: CoreSys, container: MagicMock, plugin: PluginBase
|
||||||
|
):
|
||||||
|
"""Test plugin loads with the incorrect image."""
|
||||||
|
plugin.image = old_image = f"ghcr.io/home-assistant/aarch64-hassio-{plugin.slug}"
|
||||||
|
correct_image = f"ghcr.io/home-assistant/amd64-hassio-{plugin.slug}"
|
||||||
|
coresys.updater._data["image"][plugin.slug] = correct_image # pylint: disable=protected-access
|
||||||
|
plugin.version = AwesomeVersion("2024.4.0")
|
||||||
|
|
||||||
|
container.status = "running"
|
||||||
|
container.attrs["Config"] = {"Labels": {"io.hass.version": "2024.4.0"}}
|
||||||
|
|
||||||
|
await plugin.load()
|
||||||
|
|
||||||
|
container.remove.assert_called_once_with(force=True)
|
||||||
|
assert coresys.docker.images.remove.call_args_list[0].kwargs == {
|
||||||
|
"image": f"{old_image}:latest",
|
||||||
|
"force": True,
|
||||||
|
}
|
||||||
|
assert coresys.docker.images.remove.call_args_list[1].kwargs == {
|
||||||
|
"image": f"{old_image}:2024.4.0",
|
||||||
|
"force": True,
|
||||||
|
}
|
||||||
|
coresys.docker.images.pull.assert_called_once_with(
|
||||||
|
f"{correct_image}:2024.4.0",
|
||||||
|
platform="linux/amd64",
|
||||||
|
)
|
||||||
|
assert plugin.image == correct_image
|
||||||
|
@ -53,7 +53,10 @@ async def test_check(coresys: CoreSys, sda1_block_service: BlockService):
|
|||||||
assert coresys.resolution.suggestions == [
|
assert coresys.resolution.suggestions == [
|
||||||
Suggestion(
|
Suggestion(
|
||||||
SuggestionType.RENAME_DATA_DISK, ContextType.SYSTEM, reference="/dev/sda1"
|
SuggestionType.RENAME_DATA_DISK, ContextType.SYSTEM, reference="/dev/sda1"
|
||||||
)
|
),
|
||||||
|
Suggestion(
|
||||||
|
SuggestionType.ADOPT_DATA_DISK, ContextType.SYSTEM, reference="/dev/sda1"
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
158
tests/resolution/fixup/test_system_adopt_data_disk.py
Normal file
158
tests/resolution/fixup/test_system_adopt_data_disk.py
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
"""Test system fixup adopt data disk."""
|
||||||
|
|
||||||
|
from dbus_fast import DBusError, ErrorType, Variant
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from supervisor.coresys import CoreSys
|
||||||
|
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
||||||
|
from supervisor.resolution.data import Issue, Suggestion
|
||||||
|
from supervisor.resolution.fixups.system_adopt_data_disk import FixupSystemAdoptDataDisk
|
||||||
|
|
||||||
|
from tests.dbus_service_mocks.base import DBusServiceMock
|
||||||
|
from tests.dbus_service_mocks.logind import Logind as LogindService
|
||||||
|
from tests.dbus_service_mocks.udisks2_filesystem import Filesystem as FilesystemService
|
||||||
|
from tests.dbus_service_mocks.udisks2_manager import (
|
||||||
|
UDisks2Manager as UDisks2ManagerService,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="sda1_filesystem_service")
|
||||||
|
async def fixture_sda1_filesystem_service(
|
||||||
|
udisks2_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
||||||
|
) -> FilesystemService:
|
||||||
|
"""Return sda1 filesystem service."""
|
||||||
|
return udisks2_services["udisks2_filesystem"][
|
||||||
|
"/org/freedesktop/UDisks2/block_devices/sda1"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="mmcblk1p3_filesystem_service")
|
||||||
|
async def fixture_mmcblk1p3_filesystem_service(
|
||||||
|
udisks2_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
||||||
|
) -> FilesystemService:
|
||||||
|
"""Return mmcblk1p3 filesystem service."""
|
||||||
|
return udisks2_services["udisks2_filesystem"][
|
||||||
|
"/org/freedesktop/UDisks2/block_devices/mmcblk1p3"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="udisks2_service")
|
||||||
|
async def fixture_udisks2_service(
|
||||||
|
udisks2_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
||||||
|
) -> UDisks2ManagerService:
|
||||||
|
"""Return udisks2 manager service."""
|
||||||
|
return udisks2_services["udisks2_manager"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="logind_service")
|
||||||
|
async def fixture_logind_service(
|
||||||
|
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
||||||
|
) -> LogindService:
|
||||||
|
"""Return logind service."""
|
||||||
|
return all_dbus_services["logind"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fixup(
|
||||||
|
coresys: CoreSys,
|
||||||
|
mmcblk1p3_filesystem_service: FilesystemService,
|
||||||
|
udisks2_service: UDisks2ManagerService,
|
||||||
|
logind_service: LogindService,
|
||||||
|
):
|
||||||
|
"""Test fixup."""
|
||||||
|
mmcblk1p3_filesystem_service.SetLabel.calls.clear()
|
||||||
|
logind_service.Reboot.calls.clear()
|
||||||
|
system_adopt_data_disk = FixupSystemAdoptDataDisk(coresys)
|
||||||
|
|
||||||
|
assert not system_adopt_data_disk.auto
|
||||||
|
|
||||||
|
coresys.resolution.suggestions = Suggestion(
|
||||||
|
SuggestionType.ADOPT_DATA_DISK, ContextType.SYSTEM, reference="/dev/sda1"
|
||||||
|
)
|
||||||
|
coresys.resolution.issues = Issue(
|
||||||
|
IssueType.MULTIPLE_DATA_DISKS, ContextType.SYSTEM, reference="/dev/sda1"
|
||||||
|
)
|
||||||
|
udisks2_service.resolved_devices = [
|
||||||
|
"/org/freedesktop/UDisks2/block_devices/mmcblk1p3"
|
||||||
|
]
|
||||||
|
|
||||||
|
await system_adopt_data_disk()
|
||||||
|
|
||||||
|
assert mmcblk1p3_filesystem_service.SetLabel.calls == [
|
||||||
|
("hassos-data-old", {"auth.no_user_interaction": Variant("b", True)})
|
||||||
|
]
|
||||||
|
assert len(coresys.resolution.suggestions) == 0
|
||||||
|
assert len(coresys.resolution.issues) == 0
|
||||||
|
assert logind_service.Reboot.calls == [(False,)]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fixup_device_removed(
|
||||||
|
coresys: CoreSys,
|
||||||
|
mmcblk1p3_filesystem_service: FilesystemService,
|
||||||
|
udisks2_service: UDisks2ManagerService,
|
||||||
|
logind_service: LogindService,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
):
|
||||||
|
"""Test fixup when device removed."""
|
||||||
|
mmcblk1p3_filesystem_service.SetLabel.calls.clear()
|
||||||
|
logind_service.Reboot.calls.clear()
|
||||||
|
system_adopt_data_disk = FixupSystemAdoptDataDisk(coresys)
|
||||||
|
|
||||||
|
assert not system_adopt_data_disk.auto
|
||||||
|
|
||||||
|
coresys.resolution.suggestions = Suggestion(
|
||||||
|
SuggestionType.ADOPT_DATA_DISK, ContextType.SYSTEM, reference="/dev/sda1"
|
||||||
|
)
|
||||||
|
coresys.resolution.issues = Issue(
|
||||||
|
IssueType.MULTIPLE_DATA_DISKS, ContextType.SYSTEM, reference="/dev/sda1"
|
||||||
|
)
|
||||||
|
|
||||||
|
udisks2_service.resolved_devices = []
|
||||||
|
await system_adopt_data_disk()
|
||||||
|
|
||||||
|
assert len(coresys.resolution.suggestions) == 0
|
||||||
|
assert len(coresys.resolution.issues) == 0
|
||||||
|
assert "Data disk at /dev/sda1 with name conflict was removed" in caplog.text
|
||||||
|
assert mmcblk1p3_filesystem_service.SetLabel.calls == []
|
||||||
|
assert logind_service.Reboot.calls == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fixup_reboot_failed(
|
||||||
|
coresys: CoreSys,
|
||||||
|
mmcblk1p3_filesystem_service: FilesystemService,
|
||||||
|
udisks2_service: UDisks2ManagerService,
|
||||||
|
logind_service: LogindService,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
):
|
||||||
|
"""Test fixup when reboot fails."""
|
||||||
|
mmcblk1p3_filesystem_service.SetLabel.calls.clear()
|
||||||
|
logind_service.side_effect_reboot = DBusError(ErrorType.SERVICE_ERROR, "error")
|
||||||
|
system_adopt_data_disk = FixupSystemAdoptDataDisk(coresys)
|
||||||
|
|
||||||
|
assert not system_adopt_data_disk.auto
|
||||||
|
|
||||||
|
coresys.resolution.suggestions = Suggestion(
|
||||||
|
SuggestionType.ADOPT_DATA_DISK, ContextType.SYSTEM, reference="/dev/sda1"
|
||||||
|
)
|
||||||
|
coresys.resolution.issues = Issue(
|
||||||
|
IssueType.MULTIPLE_DATA_DISKS, ContextType.SYSTEM, reference="/dev/sda1"
|
||||||
|
)
|
||||||
|
udisks2_service.resolved_devices = [
|
||||||
|
"/org/freedesktop/UDisks2/block_devices/mmcblk1p3"
|
||||||
|
]
|
||||||
|
|
||||||
|
await system_adopt_data_disk()
|
||||||
|
|
||||||
|
assert mmcblk1p3_filesystem_service.SetLabel.calls == [
|
||||||
|
("hassos-data-old", {"auth.no_user_interaction": Variant("b", True)})
|
||||||
|
]
|
||||||
|
assert len(coresys.resolution.suggestions) == 1
|
||||||
|
assert (
|
||||||
|
Suggestion(SuggestionType.EXECUTE_REBOOT, ContextType.SYSTEM)
|
||||||
|
in coresys.resolution.suggestions
|
||||||
|
)
|
||||||
|
assert len(coresys.resolution.issues) == 1
|
||||||
|
assert (
|
||||||
|
Issue(IssueType.REBOOT_REQUIRED, ContextType.SYSTEM)
|
||||||
|
in coresys.resolution.issues
|
||||||
|
)
|
||||||
|
assert "Could not reboot host to finish data disk adoption" in caplog.text
|
Loading…
x
Reference in New Issue
Block a user