mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-25 18:16:32 +00:00
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:
parent
882586b246
commit
778bc46848
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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:
|
||||||
|
@ -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())
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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]
|
||||||
|
@ -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):
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user