Don't relay on latest with HA/Addons (#1175)

* Don't relay on latest with HA/Addons

* Fix latest on install

* Revert some options

* Fix attach

* migrate to new version handling

* Fix thread

* Fix is running

* Allow wait

* debug code

* Fix debug value

* Fix list

* Fix regex

* Some better log output

* Fix logic

* Improve cleanup handling

* Fix bug

* Cleanup old code

* Improve version handling

* Fix the way to attach
This commit is contained in:
Pascal Vizeli 2019-08-07 09:51:27 +02:00 committed by GitHub
parent 882586b246
commit 778bc46848
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 175 additions and 85 deletions

View File

@ -4,7 +4,8 @@
"context": "..", "context": "..",
"dockerFile": "Dockerfile", "dockerFile": "Dockerfile",
"runArgs": [ "runArgs": [
"-e", "GIT_EDTIOR='code --wait'" "-e",
"GIT_EDTIOR='code --wait'"
], ],
"extensions": [ "extensions": [
"ms-python.python" "ms-python.python"
@ -14,9 +15,13 @@
"python.linting.pylintEnabled": true, "python.linting.pylintEnabled": true,
"python.linting.enabled": true, "python.linting.enabled": true,
"python.formatting.provider": "black", "python.formatting.provider": "black",
"python.formatting.blackArgs": [
"--target--version",
"py37"
],
"editor.formatOnPaste": false, "editor.formatOnPaste": false,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.formatOnType": true, "editor.formatOnType": true,
"files.trimTrailingWhitespace": true "files.trimTrailingWhitespace": true
} }
} }

View File

@ -52,7 +52,7 @@ stages:
versionSpec: '3.7' versionSpec: '3.7'
- script: pip install black - script: pip install black
displayName: 'Install black' displayName: 'Install black'
- script: black --check hassio tests - script: black --target-version py37 --check hassio tests
displayName: 'Run Black' displayName: 'Run Black'
- job: 'JQ' - job: 'JQ'
pool: pool:

View File

@ -38,9 +38,10 @@ if __name__ == "__main__":
_LOGGER.info("Initialize Hass.io setup") _LOGGER.info("Initialize Hass.io setup")
coresys = loop.run_until_complete(bootstrap.initialize_coresys()) coresys = loop.run_until_complete(bootstrap.initialize_coresys())
loop.run_until_complete(coresys.core.connect())
bootstrap.migrate_system_env(coresys)
bootstrap.supervisor_debugger(coresys) bootstrap.supervisor_debugger(coresys)
bootstrap.migrate_system_env(coresys)
_LOGGER.info("Setup HassIO") _LOGGER.info("Setup HassIO")
loop.run_until_complete(coresys.core.setup()) loop.run_until_complete(coresys.core.setup())

View File

@ -130,6 +130,7 @@ class AddonManager(CoreSysAttributes):
raise AddonsError() from None raise AddonsError() from None
else: else:
self.local[slug] = addon self.local[slug] = addon
_LOGGER.info("Add-on '%s' successfully installed", slug)
async def uninstall(self, slug: str) -> None: async def uninstall(self, slug: str) -> None:
"""Remove an add-on.""" """Remove an add-on."""
@ -159,6 +160,8 @@ class AddonManager(CoreSysAttributes):
self.data.uninstall(addon) self.data.uninstall(addon)
self.local.pop(slug) self.local.pop(slug)
_LOGGER.info("Add-on '%s' successfully removed", slug)
async def update(self, slug: str) -> None: async def update(self, slug: str) -> None:
"""Update add-on.""" """Update add-on."""
if slug not in self.local: if slug not in self.local:
@ -184,9 +187,15 @@ class AddonManager(CoreSysAttributes):
last_state = await addon.state() last_state = await addon.state()
try: try:
await addon.instance.update(store.version, store.image) await addon.instance.update(store.version, store.image)
# Cleanup
with suppress(DockerAPIError):
await addon.instance.cleanup()
except DockerAPIError: except DockerAPIError:
raise AddonsError() from None raise AddonsError() from None
self.data.update(store) else:
self.data.update(store)
_LOGGER.info("Add-on '%s' successfully updated", slug)
# Setup/Fix AppArmor profile # Setup/Fix AppArmor profile
await addon.install_apparmor() await addon.install_apparmor()
@ -224,6 +233,7 @@ class AddonManager(CoreSysAttributes):
raise AddonsError() from None raise AddonsError() from None
else: else:
self.data.update(store) self.data.update(store)
_LOGGER.info("Add-on '%s' successfully rebuilded", slug)
# restore state # restore state
if last_state == STATE_STARTED: if last_state == STATE_STARTED:

View File

@ -75,7 +75,7 @@ class Addon(AddonModel):
async def load(self) -> None: async def load(self) -> None:
"""Async initialize of object.""" """Async initialize of object."""
with suppress(DockerAPIError): with suppress(DockerAPIError):
await self.instance.attach() await self.instance.attach(tag=self.version)
@property @property
def ip_address(self) -> IPv4Address: def ip_address(self) -> IPv4Address:

View File

@ -218,7 +218,7 @@ def reg_signal(loop):
def supervisor_debugger(coresys: CoreSys) -> None: def supervisor_debugger(coresys: CoreSys) -> None:
"""Setup debugger if needed.""" """Setup debugger if needed."""
if not coresys.config.debug or not coresys.dev: if not coresys.config.debug:
return return
import ptvsd import ptvsd
@ -226,4 +226,5 @@ def supervisor_debugger(coresys: CoreSys) -> None:
ptvsd.enable_attach(address=("0.0.0.0", 33333), redirect_output=True) ptvsd.enable_attach(address=("0.0.0.0", 33333), redirect_output=True)
if coresys.config.debug_block: if coresys.config.debug_block:
_LOGGER.info("Wait until debugger is attached")
ptvsd.wait_for_attach() ptvsd.wait_for_attach()

View File

@ -24,11 +24,12 @@ class HassIO(CoreSysAttributes):
"""Initialize Hass.io object.""" """Initialize Hass.io object."""
self.coresys = coresys self.coresys = coresys
async def setup(self): async def connect(self):
"""Setup HassIO orchestration.""" """Connect Supervisor container."""
# Load Supervisor
await self.sys_supervisor.load() await self.sys_supervisor.load()
async def setup(self):
"""Setup HassIO orchestration."""
# Load DBus # Load DBus
await self.sys_dbus.load() await self.sys_dbus.load()

View File

@ -50,15 +50,15 @@ class DockerAPI:
return self.docker.api return self.docker.api
def run( def run(
self, image: str, **kwargs: Dict[str, Any] self, image: str, version: str = "latest", **kwargs: Dict[str, Any]
) -> docker.models.containers.Container: ) -> docker.models.containers.Container:
""""Create a Docker container and run it. """"Create a Docker container and run it.
Need run inside executor. Need run inside executor.
""" """
name = kwargs.get("name", image) name: str = kwargs.get("name", image)
network_mode = kwargs.get("network_mode") network_mode: str = kwargs.get("network_mode")
hostname = kwargs.get("hostname") hostname: str = kwargs.get("hostname")
# Setup network # Setup network
kwargs["dns_search"] = ["."] kwargs["dns_search"] = ["."]
@ -71,7 +71,7 @@ class DockerAPI:
# Create container # Create container
try: try:
container = self.docker.containers.create( container = self.docker.containers.create(
image, use_config_proxy=False, **kwargs f"{image}:{version}", use_config_proxy=False, **kwargs
) )
except docker.errors.DockerException as err: except docker.errors.DockerException as err:
_LOGGER.error("Can't create container from %s: %s", name, err) _LOGGER.error("Can't create container from %s: %s", name, err)
@ -102,7 +102,11 @@ class DockerAPI:
return container return container
def run_command( def run_command(
self, image: str, command: Optional[str] = None, **kwargs: Dict[str, Any] self,
image: str,
version: str = "latest",
command: Optional[str] = None,
**kwargs: Dict[str, Any],
) -> CommandReturn: ) -> CommandReturn:
"""Create a temporary container and run command. """Create a temporary container and run command.
@ -114,11 +118,11 @@ class DockerAPI:
_LOGGER.info("Run command '%s' on %s", command, image) _LOGGER.info("Run command '%s' on %s", command, image)
try: try:
container = self.docker.containers.run( container = self.docker.containers.run(
image, f"{image}:{version}",
command=command, command=command,
network=self.network.name, network=self.network.name,
use_config_proxy=False, use_config_proxy=False,
**kwargs **kwargs,
) )
# wait until command is done # wait until command is done

View File

@ -327,6 +327,7 @@ class DockerAddon(DockerInterface):
# Create & Run container # Create & Run container
docker_container = self.sys_docker.run( docker_container = self.sys_docker.run(
self.image, self.image,
version=self.addon.version,
name=self.name, name=self.name,
hostname=self.hostname, hostname=self.hostname,
detach=True, detach=True,
@ -346,10 +347,12 @@ class DockerAddon(DockerInterface):
tmpfs=self.tmpfs, tmpfs=self.tmpfs,
) )
_LOGGER.info("Start Docker add-on %s with version %s", self.image, self.version)
self._meta = docker_container.attrs self._meta = docker_container.attrs
_LOGGER.info("Start Docker add-on %s with version %s", self.image, self.version)
def _install(self, tag: str, image: Optional[str] = None) -> None: def _install(
self, tag: str, image: Optional[str] = None, latest: bool = False
) -> None:
"""Pull Docker image or build it. """Pull Docker image or build it.
Need run inside executor. Need run inside executor.
@ -357,7 +360,7 @@ class DockerAddon(DockerInterface):
if self.addon.need_build: if self.addon.need_build:
self._build(tag) self._build(tag)
else: else:
super()._install(tag, image) super()._install(tag, image, latest)
def _build(self, tag: str) -> None: def _build(self, tag: str) -> None:
"""Build a Docker container. """Build a Docker container.
@ -373,7 +376,6 @@ class DockerAddon(DockerInterface):
) )
_LOGGER.debug("Build %s:%s done: %s", self.image, tag, log) _LOGGER.debug("Build %s:%s done: %s", self.image, tag, log)
image.tag(self.image, tag="latest")
# Update meta data # Update meta data
self._meta = image.attrs self._meta = image.attrs

View File

@ -21,12 +21,12 @@ class DockerHassOSCli(DockerInterface, CoreSysAttributes):
"""Don't need stop.""" """Don't need stop."""
return True return True
def _attach(self): def _attach(self, tag: str):
"""Attach to running Docker container. """Attach to running Docker container.
Need run inside executor. Need run inside executor.
""" """
try: try:
image = self.sys_docker.images.get(self.image) image = self.sys_docker.images.get(f"{self.image}:{tag}")
except docker.errors.DockerException: except docker.errors.DockerException:
_LOGGER.warning("Can't find a HassOS CLI %s", self.image) _LOGGER.warning("Can't find a HassOS CLI %s", self.image)

View File

@ -1,8 +1,10 @@
"""Init file for Hass.io Docker object.""" """Init file for Hass.io Docker object."""
from distutils.version import StrictVersion
from contextlib import suppress from contextlib import suppress
from ipaddress import IPv4Address from ipaddress import IPv4Address
import logging import logging
from typing import Awaitable import re
from typing import Awaitable, List, Optional
import docker import docker
@ -13,30 +15,31 @@ from .interface import CommandReturn, DockerInterface
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
HASS_DOCKER_NAME = "homeassistant" HASS_DOCKER_NAME = "homeassistant"
RE_VERSION = re.compile(r"(?P<version>\d+\.\d+\.\d+(?:b\d+|d\d+)?)")
class DockerHomeAssistant(DockerInterface): class DockerHomeAssistant(DockerInterface):
"""Docker Hass.io wrapper for Home Assistant.""" """Docker Hass.io wrapper for Home Assistant."""
@property @property
def machine(self): def machine(self) -> Optional[str]:
"""Return machine of Home Assistant Docker image.""" """Return machine of Home Assistant Docker image."""
if self._meta and LABEL_MACHINE in self._meta["Config"]["Labels"]: if self._meta and LABEL_MACHINE in self._meta["Config"]["Labels"]:
return self._meta["Config"]["Labels"][LABEL_MACHINE] return self._meta["Config"]["Labels"][LABEL_MACHINE]
return None return None
@property @property
def image(self): def image(self) -> str:
"""Return name of Docker image.""" """Return name of Docker image."""
return self.sys_homeassistant.image return self.sys_homeassistant.image
@property @property
def name(self): def name(self) -> str:
"""Return name of Docker container.""" """Return name of Docker container."""
return HASS_DOCKER_NAME return HASS_DOCKER_NAME
@property @property
def timeout(self) -> str: def timeout(self) -> int:
"""Return timeout for Docker actions.""" """Return timeout for Docker actions."""
return 60 return 60
@ -60,6 +63,7 @@ class DockerHomeAssistant(DockerInterface):
# Create & Run container # Create & Run container
docker_container = self.sys_docker.run( docker_container = self.sys_docker.run(
self.image, self.image,
version=self.sys_homeassistant.version,
name=self.name, name=self.name,
hostname=self.name, hostname=self.name,
detach=True, detach=True,
@ -84,8 +88,8 @@ class DockerHomeAssistant(DockerInterface):
}, },
) )
_LOGGER.info("Start homeassistant %s with version %s", self.image, self.version)
self._meta = docker_container.attrs self._meta = docker_container.attrs
_LOGGER.info("Start homeassistant %s with version %s", self.image, self.version)
def _execute_command(self, command: str) -> CommandReturn: def _execute_command(self, command: str) -> CommandReturn:
"""Create a temporary container and run command. """Create a temporary container and run command.
@ -94,7 +98,8 @@ class DockerHomeAssistant(DockerInterface):
""" """
return self.sys_docker.run_command( return self.sys_docker.run_command(
self.image, self.image,
command, version=self.sys_homeassistant.version,
command=command,
privileged=True, privileged=True,
init=True, init=True,
detach=True, detach=True,
@ -134,3 +139,33 @@ class DockerHomeAssistant(DockerInterface):
return False return False
return True return True
def get_latest_version(self) -> Awaitable[str]:
"""Return latest version of local Home Asssistant image."""
return self.sys_run_in_executor(self._get_latest_version)
def _get_latest_version(self) -> str:
"""Return latest version of local Home Asssistant image.
Need run inside executor.
"""
available_version: List[str] = []
try:
for image in self.sys_docker.images.list(self.image):
for tag in image.tags:
match = RE_VERSION.search(tag)
if not match:
continue
available_version.append(match.group("version"))
assert available_version
except (docker.errors.DockerException, AssertionError):
_LOGGER.warning("No local HA version found")
raise DockerAPIError()
else:
_LOGGER.debug("Found HA versions: %s", available_version)
# Sort version and return latest version
available_version.sort(key=StrictVersion, reverse=True)
return available_version[0]

View File

@ -68,11 +68,13 @@ class DockerInterface(CoreSysAttributes):
return self.lock.locked() return self.lock.locked()
@process_lock @process_lock
def install(self, tag: str, image: Optional[str] = None): def install(self, tag: str, image: Optional[str] = None, latest: bool = False):
"""Pull docker image.""" """Pull docker image."""
return self.sys_run_in_executor(self._install, tag, image) return self.sys_run_in_executor(self._install, tag, image, latest)
def _install(self, tag: str, image: Optional[str] = None) -> None: def _install(
self, tag: str, image: Optional[str] = None, latest: bool = False
) -> None:
"""Pull Docker image. """Pull Docker image.
Need run inside executor. Need run inside executor.
@ -80,12 +82,12 @@ class DockerInterface(CoreSysAttributes):
image = image or self.image image = image or self.image
image = image.partition(":")[0] # remove potential tag image = image.partition(":")[0] # remove potential tag
_LOGGER.info("Pull image %s tag %s.", image, tag)
try: try:
_LOGGER.info("Pull image %s tag %s.", image, tag)
docker_image = self.sys_docker.images.pull(f"{image}:{tag}") docker_image = self.sys_docker.images.pull(f"{image}:{tag}")
if latest:
_LOGGER.info("Tag image %s with version %s as latest", image, tag) _LOGGER.info("Tag image %s with version %s as latest", image, tag)
docker_image.tag(image, tag="latest") docker_image.tag(image, tag="latest")
except docker.errors.APIError as err: except docker.errors.APIError as err:
_LOGGER.error("Can't install %s:%s -> %s.", image, tag, err) _LOGGER.error("Can't install %s:%s -> %s.", image, tag, err)
raise DockerAPIError() from None raise DockerAPIError() from None
@ -123,7 +125,6 @@ class DockerInterface(CoreSysAttributes):
""" """
try: try:
docker_container = self.sys_docker.containers.get(self.name) docker_container = self.sys_docker.containers.get(self.name)
docker_image = self.sys_docker.images.get(self.image)
except docker.errors.DockerException: except docker.errors.DockerException:
return False return False
@ -131,28 +132,24 @@ class DockerInterface(CoreSysAttributes):
if docker_container.status != "running": if docker_container.status != "running":
return False return False
# we run on an old image, stop and start it
if docker_container.image.id != docker_image.id:
return False
return True return True
@process_lock @process_lock
def attach(self): def attach(self, tag: str):
"""Attach to running Docker container.""" """Attach to running Docker container."""
return self.sys_run_in_executor(self._attach) return self.sys_run_in_executor(self._attach, tag)
def _attach(self) -> None: def _attach(self, tag: str) -> None:
"""Attach to running docker container. """Attach to running docker container.
Need run inside executor. Need run inside executor.
""" """
try: with suppress(docker.errors.DockerException):
if self.image:
self._meta = self.sys_docker.images.get(self.image).attrs
self._meta = self.sys_docker.containers.get(self.name).attrs self._meta = self.sys_docker.containers.get(self.name).attrs
except docker.errors.DockerException:
pass with suppress(docker.errors.DockerException):
if not self._meta and self.image:
self._meta = self.sys_docker.images.get(f"{self.image}:{tag}").attrs
# Successfull? # Successfull?
if not self._meta: if not self._meta:
@ -250,11 +247,15 @@ class DockerInterface(CoreSysAttributes):
self._meta = None self._meta = None
@process_lock @process_lock
def update(self, tag: str, image: Optional[str] = None) -> Awaitable[None]: def update(
self, tag: str, image: Optional[str] = None, latest: bool = False
) -> Awaitable[None]:
"""Update a Docker image.""" """Update a Docker image."""
return self.sys_run_in_executor(self._update, tag, image) return self.sys_run_in_executor(self._update, tag, image)
def _update(self, tag: str, image: Optional[str] = None) -> None: def _update(
self, tag: str, image: Optional[str] = None, latest: bool = False
) -> None:
"""Update a docker image. """Update a docker image.
Need run inside executor. Need run inside executor.
@ -266,14 +267,11 @@ class DockerInterface(CoreSysAttributes):
) )
# Update docker image # Update docker image
self._install(tag, image) self._install(tag, image, latest)
# Stop container & cleanup # Stop container & cleanup
with suppress(DockerAPIError): with suppress(DockerAPIError):
try: self._stop()
self._stop()
finally:
self._cleanup()
def logs(self) -> Awaitable[bytes]: def logs(self) -> Awaitable[bytes]:
"""Return Docker logs of container. """Return Docker logs of container.
@ -308,13 +306,13 @@ class DockerInterface(CoreSysAttributes):
Need run inside executor. Need run inside executor.
""" """
try: try:
latest = self.sys_docker.images.get(self.image) origin = self.sys_docker.images.get(f"{self.image}:{self.version}")
except docker.errors.DockerException: except docker.errors.DockerException:
_LOGGER.warning("Can't find %s for cleanup", self.image) _LOGGER.warning("Can't find %s for cleanup", self.image)
raise DockerAPIError() from None raise DockerAPIError() from None
for image in self.sys_docker.images.list(name=self.image): for image in self.sys_docker.images.list(name=self.image):
if latest.id == image.id: if origin.id == image.id:
continue continue
with suppress(docker.errors.DockerException): with suppress(docker.errors.DockerException):

View File

@ -25,7 +25,7 @@ class DockerSupervisor(DockerInterface, CoreSysAttributes):
"""Return IP address of this container.""" """Return IP address of this container."""
return self.sys_docker.network.supervisor return self.sys_docker.network.supervisor
def _attach(self) -> None: def _attach(self, tag: str) -> None:
"""Attach to running docker container. """Attach to running docker container.
Need run inside executor. Need run inside executor.

View File

@ -130,7 +130,7 @@ class HassOS(CoreSysAttributes):
_LOGGER.info("Detect HassOS %s on host system", self.version) _LOGGER.info("Detect HassOS %s on host system", self.version)
with suppress(DockerAPIError): with suppress(DockerAPIError):
await self.instance.attach() await self.instance.attach(tag="latest")
def config_sync(self) -> Awaitable[None]: def config_sync(self) -> Awaitable[None]:
"""Trigger a host config reload from usb. """Trigger a host config reload from usb.
@ -187,7 +187,11 @@ class HassOS(CoreSysAttributes):
return return
try: try:
await self.instance.update(version) await self.instance.update(version, latest=True)
# Cleanup
with suppress(DockerAPIError):
await self.instance.cleanup()
except DockerAPIError: except DockerAPIError:
_LOGGER.error("HassOS CLI update fails") _LOGGER.error("HassOS CLI update fails")
raise HassOSUpdateError() from None raise HassOSUpdateError() from None

View File

@ -26,6 +26,7 @@ from .const import (
ATTR_REFRESH_TOKEN, ATTR_REFRESH_TOKEN,
ATTR_SSL, ATTR_SSL,
ATTR_UUID, ATTR_UUID,
ATTR_VERSION,
ATTR_WAIT_BOOT, ATTR_WAIT_BOOT,
ATTR_WATCHDOG, ATTR_WATCHDOG,
FILE_HASSIO_HOMEASSISTANT, FILE_HASSIO_HOMEASSISTANT,
@ -41,7 +42,7 @@ from .exceptions import (
HomeAssistantError, HomeAssistantError,
HomeAssistantUpdateError, HomeAssistantUpdateError,
) )
from .utils import convert_to_ascii, process_lock, check_port from .utils import check_port, convert_to_ascii, process_lock
from .utils.json import JsonConfig from .utils.json import JsonConfig
from .validate import SCHEMA_HASS_CONFIG from .validate import SCHEMA_HASS_CONFIG
@ -76,7 +77,15 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
async def load(self) -> None: async def load(self) -> None:
"""Prepare Home Assistant object.""" """Prepare Home Assistant object."""
with suppress(DockerAPIError): with suppress(DockerAPIError):
await self.instance.attach() # Evaluate Version if we lost this information
if not self.version:
if await self.instance.is_running():
self.version = self.instance.version
else:
self.version = await self.instance.get_latest_version()
self.save_data()
await self.instance.attach(tag=self.version)
return return
_LOGGER.info("No Home Assistant Docker image %s found.", self.image) _LOGGER.info("No Home Assistant Docker image %s found.", self.image)
@ -159,11 +168,6 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
"""Set time to wait for Home Assistant startup.""" """Set time to wait for Home Assistant startup."""
self._data[ATTR_WAIT_BOOT] = value self._data[ATTR_WAIT_BOOT] = value
@property
def version(self) -> str:
"""Return version of running Home Assistant."""
return self.instance.version
@property @property
def latest_version(self) -> str: def latest_version(self) -> str:
"""Return last available version of Home Assistant.""" """Return last available version of Home Assistant."""
@ -199,6 +203,16 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
"""Return True if a custom image is used.""" """Return True if a custom image is used."""
return all(attr in self._data for attr in (ATTR_IMAGE, ATTR_LAST_VERSION)) return all(attr in self._data for attr in (ATTR_IMAGE, ATTR_LAST_VERSION))
@property
def version(self) -> Optional[str]:
"""Return version of local version."""
return self._data.get(ATTR_VERSION)
@version.setter
def version(self, value: str) -> None:
"""Set installed version."""
self._data[ATTR_VERSION] = value
@property @property
def boot(self) -> bool: def boot(self) -> bool:
"""Return True if Home Assistant boot is enabled.""" """Return True if Home Assistant boot is enabled."""
@ -234,11 +248,16 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
"""Install a landing page.""" """Install a landing page."""
_LOGGER.info("Setup HomeAssistant landingpage") _LOGGER.info("Setup HomeAssistant landingpage")
while True: while True:
with suppress(DockerAPIError): try:
await self.instance.install("landingpage") await self.instance.install("landingpage")
return except DockerAPIError:
_LOGGER.warning("Fails install landingpage, retry after 30sec") _LOGGER.warning("Fails install landingpage, retry after 30sec")
await asyncio.sleep(30) await asyncio.sleep(30)
else:
break
self.version = self.instance.version
self.save_data()
@process_lock @process_lock
async def install(self) -> None: async def install(self) -> None:
@ -257,21 +276,23 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
_LOGGER.warning("Error on install Home Assistant. Retry in 30sec") _LOGGER.warning("Error on install Home Assistant. Retry in 30sec")
await asyncio.sleep(30) await asyncio.sleep(30)
# finishing
_LOGGER.info("Home Assistant docker now installed") _LOGGER.info("Home Assistant docker now installed")
self.version = self.instance.version
self.save_data()
# finishing
try: try:
if not self.boot:
return
_LOGGER.info("Start Home Assistant") _LOGGER.info("Start Home Assistant")
await self._start() await self._start()
except HomeAssistantError: except HomeAssistantError:
_LOGGER.error("Can't start Home Assistant!") _LOGGER.error("Can't start Home Assistant!")
finally:
with suppress(DockerAPIError): # Cleanup
await self.instance.cleanup() with suppress(DockerAPIError):
await self.instance.cleanup()
@process_lock @process_lock
async def update(self, version=None) -> None: async def update(self, version: Optional[str] = None) -> None:
"""Update HomeAssistant version.""" """Update HomeAssistant version."""
version = version or self.latest_version version = version or self.latest_version
rollback = self.version if not self.error_state else None rollback = self.version if not self.error_state else None
@ -283,7 +304,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
return return
# process an update # process an update
async def _update(to_version): async def _update(to_version: str) -> None:
"""Run Home Assistant update.""" """Run Home Assistant update."""
_LOGGER.info("Update Home Assistant to version %s", to_version) _LOGGER.info("Update Home Assistant to version %s", to_version)
try: try:
@ -291,10 +312,16 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
except DockerAPIError: except DockerAPIError:
_LOGGER.warning("Update Home Assistant image fails") _LOGGER.warning("Update Home Assistant image fails")
raise HomeAssistantUpdateError() from None raise HomeAssistantUpdateError() from None
else:
self.version = self.instance.version
if running: if running:
await self._start() await self._start()
_LOGGER.info("Successful run Home Assistant %s", to_version) _LOGGER.info("Successful run Home Assistant %s", to_version)
self.save_data()
with suppress(DockerAPIError):
await self.instance.cleanup()
# Update Home Assistant # Update Home Assistant
with suppress(HomeAssistantError): with suppress(HomeAssistantError):

View File

@ -24,7 +24,7 @@ class DNSForward:
*shlex.split(COMMAND), *shlex.split(COMMAND),
stdin=asyncio.subprocess.DEVNULL, stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.DEVNULL, stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL stderr=asyncio.subprocess.DEVNULL,
) )
except OSError as err: except OSError as err:
_LOGGER.error("Can't start DNS forwarding: %s", err) _LOGGER.error("Can't start DNS forwarding: %s", err)

View File

@ -34,7 +34,7 @@ class Supervisor(CoreSysAttributes):
async def load(self) -> None: async def load(self) -> None:
"""Prepare Home Assistant object.""" """Prepare Home Assistant object."""
try: try:
await self.instance.attach() await self.instance.attach(tag="latest")
except DockerAPIError: except DockerAPIError:
_LOGGER.fatal("Can't setup Supervisor Docker container!") _LOGGER.fatal("Can't setup Supervisor Docker container!")
@ -109,7 +109,7 @@ class Supervisor(CoreSysAttributes):
_LOGGER.info("Update Supervisor to version %s", version) _LOGGER.info("Update Supervisor to version %s", version)
try: try:
await self.instance.install(version) await self.instance.install(version, latest=True)
except DockerAPIError: except DockerAPIError:
_LOGGER.error("Update of Hass.io fails!") _LOGGER.error("Update of Hass.io fails!")
raise SupervisorUpdateError() from None raise SupervisorUpdateError() from None

View File

@ -27,6 +27,7 @@ from .const import (
ATTR_SSL, ATTR_SSL,
ATTR_TIMEZONE, ATTR_TIMEZONE,
ATTR_UUID, ATTR_UUID,
ATTR_VERSION,
ATTR_WAIT_BOOT, ATTR_WAIT_BOOT,
ATTR_WATCHDOG, ATTR_WATCHDOG,
CHANNEL_BETA, CHANNEL_BETA,
@ -82,6 +83,7 @@ DOCKER_PORTS_DESCRIPTION = vol.Schema(
SCHEMA_HASS_CONFIG = vol.Schema( SCHEMA_HASS_CONFIG = vol.Schema(
{ {
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH, vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH,
vol.Optional(ATTR_VERSION): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_ACCESS_TOKEN): TOKEN, vol.Optional(ATTR_ACCESS_TOKEN): TOKEN,
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(), vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
vol.Inclusive(ATTR_IMAGE, "custom_hass"): DOCKER_IMAGE, vol.Inclusive(ATTR_IMAGE, "custom_hass"): DOCKER_IMAGE,