Make homeassistant container constant (#808)

* Make homeassistant container constant

* Update homeassistant.py

* Update homeassistant.py

* Update interface.py

* Update homeassistant.py

* Fix handling

* add start function

* Add typing

* Fix lint

* Add API call

* Update logs

* Fix some issue with watchdog

* Fix lint
This commit is contained in:
Pascal Vizeli 2019-03-27 17:20:05 +01:00 committed by GitHub
parent 4eb02f474d
commit b52f90187b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 351 additions and 210 deletions

1
API.md
View File

@ -376,6 +376,7 @@ Output is the raw Docker log.
- POST `/homeassistant/check`
- POST `/homeassistant/start`
- POST `/homeassistant/stop`
- POST `/homeassistant/rebuild`
- POST `/homeassistant/options`

View File

@ -1,23 +1,24 @@
"""Init file for Hass.io RESTful API."""
import logging
from pathlib import Path
from typing import Optional
from aiohttp import web
from ..coresys import CoreSys, CoreSysAttributes
from .addons import APIAddons
from .auth import APIAuth
from .discovery import APIDiscovery
from .homeassistant import APIHomeAssistant
from .hardware import APIHardware
from .host import APIHost
from .hassos import APIHassOS
from .homeassistant import APIHomeAssistant
from .host import APIHost
from .info import APIInfo
from .proxy import APIProxy
from .supervisor import APISupervisor
from .snapshots import APISnapshots
from .services import APIServices
from .security import SecurityMiddleware
from ..coresys import CoreSysAttributes
from .services import APIServices
from .snapshots import APISnapshots
from .supervisor import APISupervisor
_LOGGER = logging.getLogger(__name__)
@ -25,18 +26,18 @@ _LOGGER = logging.getLogger(__name__)
class RestAPI(CoreSysAttributes):
"""Handle RESTful API for Hass.io."""
def __init__(self, coresys):
def __init__(self, coresys: CoreSys):
"""Initialize Docker base wrapper."""
self.coresys = coresys
self.security = SecurityMiddleware(coresys)
self.webapp = web.Application(
self.coresys: CoreSys = coresys
self.security: SecurityMiddleware = SecurityMiddleware(coresys)
self.webapp: web.Application = web.Application(
middlewares=[self.security.token_validation])
# service stuff
self._runner = web.AppRunner(self.webapp)
self._site = None
self._runner: web.AppRunner = web.AppRunner(self.webapp)
self._site: Optional[web.TCPSite] = None
async def load(self):
async def load(self) -> None:
"""Register REST API Calls."""
self._register_supervisor()
self._register_host()
@ -52,7 +53,7 @@ class RestAPI(CoreSysAttributes):
self._register_info()
self._register_auth()
def _register_host(self):
def _register_host(self) -> None:
"""Register hostcontrol functions."""
api_host = APIHost()
api_host.coresys = self.coresys
@ -72,7 +73,7 @@ class RestAPI(CoreSysAttributes):
api_host.service_reload),
])
def _register_hassos(self):
def _register_hassos(self) -> None:
"""Register HassOS functions."""
api_hassos = APIHassOS()
api_hassos.coresys = self.coresys
@ -84,7 +85,7 @@ class RestAPI(CoreSysAttributes):
web.post('/hassos/config/sync', api_hassos.config_sync),
])
def _register_hardware(self):
def _register_hardware(self) -> None:
"""Register hardware functions."""
api_hardware = APIHardware()
api_hardware.coresys = self.coresys
@ -94,7 +95,7 @@ class RestAPI(CoreSysAttributes):
web.get('/hardware/audio', api_hardware.audio),
])
def _register_info(self):
def _register_info(self) -> None:
"""Register info functions."""
api_info = APIInfo()
api_info.coresys = self.coresys
@ -103,7 +104,7 @@ class RestAPI(CoreSysAttributes):
web.get('/info', api_info.info),
])
def _register_auth(self):
def _register_auth(self) -> None:
"""Register auth functions."""
api_auth = APIAuth()
api_auth.coresys = self.coresys
@ -112,7 +113,7 @@ class RestAPI(CoreSysAttributes):
web.post('/auth', api_auth.auth),
])
def _register_supervisor(self):
def _register_supervisor(self) -> None:
"""Register Supervisor functions."""
api_supervisor = APISupervisor()
api_supervisor.coresys = self.coresys
@ -127,7 +128,7 @@ class RestAPI(CoreSysAttributes):
web.post('/supervisor/options', api_supervisor.options),
])
def _register_homeassistant(self):
def _register_homeassistant(self) -> None:
"""Register Home Assistant functions."""
api_hass = APIHomeAssistant()
api_hass.coresys = self.coresys
@ -142,9 +143,10 @@ class RestAPI(CoreSysAttributes):
web.post('/homeassistant/stop', api_hass.stop),
web.post('/homeassistant/start', api_hass.start),
web.post('/homeassistant/check', api_hass.check),
web.post('/homeassistant/rebuild', api_hass.rebuild),
])
def _register_proxy(self):
def _register_proxy(self) -> None:
"""Register Home Assistant API Proxy."""
api_proxy = APIProxy()
api_proxy.coresys = self.coresys
@ -158,7 +160,7 @@ class RestAPI(CoreSysAttributes):
web.get('/homeassistant/api/', api_proxy.api),
])
def _register_addons(self):
def _register_addons(self) -> None:
"""Register Add-on functions."""
api_addons = APIAddons()
api_addons.coresys = self.coresys
@ -184,7 +186,7 @@ class RestAPI(CoreSysAttributes):
web.get('/addons/{addon}/stats', api_addons.stats),
])
def _register_snapshots(self):
def _register_snapshots(self) -> None:
"""Register snapshots functions."""
api_snapshots = APISnapshots()
api_snapshots.coresys = self.coresys
@ -204,7 +206,7 @@ class RestAPI(CoreSysAttributes):
web.get('/snapshots/{snapshot}/download', api_snapshots.download),
])
def _register_services(self):
def _register_services(self) -> None:
"""Register services functions."""
api_services = APIServices()
api_services.coresys = self.coresys
@ -216,7 +218,7 @@ class RestAPI(CoreSysAttributes):
web.delete('/services/{service}', api_services.del_service),
])
def _register_discovery(self):
def _register_discovery(self) -> None:
"""Register discovery functions."""
api_discovery = APIDiscovery()
api_discovery.coresys = self.coresys
@ -228,7 +230,7 @@ class RestAPI(CoreSysAttributes):
web.post('/discovery', api_discovery.set_discovery),
])
def _register_panel(self):
def _register_panel(self) -> None:
"""Register panel for Home Assistant."""
panel_dir = Path(__file__).parent.joinpath("panel")
@ -256,7 +258,7 @@ class RestAPI(CoreSysAttributes):
# This route is for HA > 0.70
self.webapp.add_routes([web.static('/app', panel_dir)])
async def start(self):
async def start(self) -> None:
"""Run RESTful API webserver."""
await self._runner.setup()
self._site = web.TCPSite(
@ -270,7 +272,7 @@ class RestAPI(CoreSysAttributes):
else:
_LOGGER.info("Start API on %s", self.sys_docker.network.supervisor)
async def stop(self):
async def stop(self) -> None:
"""Stop RESTful API webserver."""
if not self._site:
return

View File

@ -1,15 +1,34 @@
"""Init file for Hass.io Home Assistant RESTful API."""
import asyncio
import logging
from typing import Coroutine, Dict, Any
import voluptuous as vol
from aiohttp import web
from ..const import (
ATTR_ARCH, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_BOOT, ATTR_CPU_PERCENT,
ATTR_CUSTOM, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_MACHINE, ATTR_MEMORY_LIMIT,
ATTR_MEMORY_USAGE, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_PASSWORD,
ATTR_PORT, ATTR_REFRESH_TOKEN, ATTR_SSL, ATTR_VERSION, ATTR_WAIT_BOOT,
ATTR_WATCHDOG, CONTENT_TYPE_BINARY)
ATTR_ARCH,
ATTR_BLK_READ,
ATTR_BLK_WRITE,
ATTR_BOOT,
ATTR_CPU_PERCENT,
ATTR_CUSTOM,
ATTR_IMAGE,
ATTR_LAST_VERSION,
ATTR_MACHINE,
ATTR_MEMORY_LIMIT,
ATTR_MEMORY_USAGE,
ATTR_NETWORK_RX,
ATTR_NETWORK_TX,
ATTR_PASSWORD,
ATTR_PORT,
ATTR_REFRESH_TOKEN,
ATTR_SSL,
ATTR_VERSION,
ATTR_WAIT_BOOT,
ATTR_WATCHDOG,
CONTENT_TYPE_BINARY,
)
from ..coresys import CoreSysAttributes
from ..exceptions import APIError
from ..validate import DOCKER_IMAGE, NETWORK_PORT
@ -18,37 +37,28 @@ from .utils import api_process, api_process_raw, api_validate
_LOGGER = logging.getLogger(__name__)
# pylint: disable=no-value-for-parameter
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_BOOT):
vol.Boolean(),
vol.Inclusive(ATTR_IMAGE, 'custom_hass'):
vol.Maybe(vol.Coerce(str)),
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'):
vol.Any(None, DOCKER_IMAGE),
vol.Optional(ATTR_PORT):
NETWORK_PORT,
vol.Optional(ATTR_PASSWORD):
vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_SSL):
vol.Boolean(),
vol.Optional(ATTR_WATCHDOG):
vol.Boolean(),
vol.Optional(ATTR_WAIT_BOOT):
vol.All(vol.Coerce(int), vol.Range(min=60)),
vol.Optional(ATTR_REFRESH_TOKEN):
vol.Maybe(vol.Coerce(str)),
})
SCHEMA_OPTIONS = vol.Schema(
{
vol.Optional(ATTR_BOOT): vol.Boolean(),
vol.Inclusive(ATTR_IMAGE, "custom_hass"): vol.Maybe(vol.Coerce(str)),
vol.Inclusive(ATTR_LAST_VERSION, "custom_hass"): vol.Any(None, DOCKER_IMAGE),
vol.Optional(ATTR_PORT): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_SSL): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
vol.Optional(ATTR_WAIT_BOOT): vol.All(vol.Coerce(int), vol.Range(min=60)),
vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(vol.Coerce(str)),
}
)
SCHEMA_VERSION = vol.Schema({
vol.Optional(ATTR_VERSION): vol.Coerce(str),
})
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
class APIHomeAssistant(CoreSysAttributes):
"""Handle RESTful API for Home Assistant functions."""
@api_process
async def info(self, request):
async def info(self, request: web.Request) -> Dict[str, Any]:
"""Return host information."""
return {
ATTR_VERSION: self.sys_homeassistant.version,
@ -65,7 +75,7 @@ class APIHomeAssistant(CoreSysAttributes):
}
@api_process
async def options(self, request):
async def options(self, request: web.Request) -> None:
"""Set Home Assistant options."""
body = await api_validate(SCHEMA_OPTIONS, request)
@ -81,6 +91,7 @@ class APIHomeAssistant(CoreSysAttributes):
if ATTR_PASSWORD in body:
self.sys_homeassistant.api_password = body[ATTR_PASSWORD]
self.sys_homeassistant.refresh_token = None
if ATTR_SSL in body:
self.sys_homeassistant.api_ssl = body[ATTR_SSL]
@ -97,7 +108,7 @@ class APIHomeAssistant(CoreSysAttributes):
self.sys_homeassistant.save_data()
@api_process
async def stats(self, request):
async def stats(self, request: web.Request) -> Dict[Any, str]:
"""Return resource information."""
stats = await self.sys_homeassistant.stats()
if not stats:
@ -114,7 +125,7 @@ class APIHomeAssistant(CoreSysAttributes):
}
@api_process
async def update(self, request):
async def update(self, request: web.Request) -> None:
"""Update Home Assistant."""
body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.sys_homeassistant.last_version)
@ -122,27 +133,32 @@ class APIHomeAssistant(CoreSysAttributes):
await asyncio.shield(self.sys_homeassistant.update(version))
@api_process
def stop(self, request):
def stop(self, request: web.Request) -> Coroutine:
"""Stop Home Assistant."""
return asyncio.shield(self.sys_homeassistant.stop())
@api_process
def start(self, request):
def start(self, request: web.Request) -> Coroutine:
"""Start Home Assistant."""
return asyncio.shield(self.sys_homeassistant.start())
@api_process
def restart(self, request):
def restart(self, request: web.Request) -> Coroutine:
"""Restart Home Assistant."""
return asyncio.shield(self.sys_homeassistant.restart())
@api_process
def rebuild(self, request: web.Request) -> Coroutine:
"""Rebuild Home Assistant."""
return asyncio.shield(self.sys_homeassistant.rebuild())
@api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request):
def logs(self, request: web.Request) -> Coroutine:
"""Return Home Assistant Docker logs."""
return self.sys_homeassistant.logs()
@api_process
async def check(self, request):
async def check(self, request: web.Request) -> None:
"""Check configuration of Home Assistant."""
result = await self.sys_homeassistant.check_config()
if not result.valid:

View File

@ -6,8 +6,12 @@ import logging
import async_timeout
from .coresys import CoreSysAttributes
from .const import (STARTUP_SYSTEM, STARTUP_SERVICES, STARTUP_APPLICATION,
STARTUP_INITIALIZE)
from .const import (
STARTUP_SYSTEM,
STARTUP_SERVICES,
STARTUP_APPLICATION,
STARTUP_INITIALIZE,
)
from .exceptions import HassioError, HomeAssistantError
_LOGGER = logging.getLogger(__name__)
@ -108,7 +112,7 @@ class HassIO(CoreSysAttributes):
await self.sys_tasks.load()
# If landingpage / run upgrade in background
if self.sys_homeassistant.version == 'landingpage':
if self.sys_homeassistant.version == "landingpage":
self.sys_create_task(self.sys_homeassistant.install())
_LOGGER.info("Hass.io is up and running")
@ -121,12 +125,14 @@ class HassIO(CoreSysAttributes):
# process async stop tasks
try:
with async_timeout.timeout(10):
await asyncio.wait([
self.sys_api.stop(),
self.sys_dns.stop(),
self.sys_websession.close(),
self.sys_websession_ssl.close()
])
await asyncio.wait(
[
self.sys_api.stop(),
self.sys_dns.stop(),
self.sys_websession.close(),
self.sys_websession_ssl.close(),
]
)
except asyncio.TimeoutError:
_LOGGER.warning("Force Shutdown!")

View File

@ -17,7 +17,7 @@ class DockerHassOSCli(DockerInterface, CoreSysAttributes):
"""Return name of HassOS CLI image."""
return f"homeassistant/{self.sys_arch.supervisor}-hassio-cli"
def _stop(self):
def _stop(self, remove_container=True):
"""Don't need stop."""
return True
@ -33,5 +33,6 @@ class DockerHassOSCli(DockerInterface, CoreSysAttributes):
else:
self._meta = image.attrs
_LOGGER.info("Found HassOS CLI %s with version %s", self.image,
self.version)
_LOGGER.info(
"Found HassOS CLI %s with version %s", self.image, self.version
)

View File

@ -8,7 +8,7 @@ from ..const import ENV_TOKEN, ENV_TIME, LABEL_MACHINE
_LOGGER = logging.getLogger(__name__)
HASS_DOCKER_NAME = 'homeassistant'
HASS_DOCKER_NAME = "homeassistant"
class DockerHomeAssistant(DockerInterface):
@ -17,8 +17,8 @@ class DockerHomeAssistant(DockerInterface):
@property
def machine(self):
"""Return machine of Home Assistant Docker image."""
if self._meta and LABEL_MACHINE in self._meta['Config']['Labels']:
return self._meta['Config']['Labels'][LABEL_MACHINE]
if self._meta and LABEL_MACHINE in self._meta["Config"]["Labels"]:
return self._meta["Config"]["Labels"][LABEL_MACHINE]
return None
@property
@ -58,25 +58,29 @@ class DockerHomeAssistant(DockerInterface):
privileged=True,
init=True,
devices=self.devices,
network_mode='host',
network_mode="host",
environment={
'HASSIO': self.sys_docker.network.supervisor,
"HASSIO": self.sys_docker.network.supervisor,
ENV_TIME: self.sys_timezone,
ENV_TOKEN: self.sys_homeassistant.hassio_token,
},
volumes={
str(self.sys_config.path_extern_homeassistant):
{'bind': '/config', 'mode': 'rw'},
str(self.sys_config.path_extern_ssl):
{'bind': '/ssl', 'mode': 'ro'},
str(self.sys_config.path_extern_share):
{'bind': '/share', 'mode': 'rw'},
}
str(self.sys_config.path_extern_homeassistant): {
"bind": "/config",
"mode": "rw",
},
str(self.sys_config.path_extern_ssl): {"bind": "/ssl", "mode": "ro"},
str(self.sys_config.path_extern_share): {
"bind": "/share",
"mode": "rw",
},
},
)
if ret:
_LOGGER.info("Start homeassistant %s with version %s",
self.image, self.version)
_LOGGER.info(
"Start homeassistant %s with version %s", self.image, self.version
)
return ret
@ -94,17 +98,18 @@ class DockerHomeAssistant(DockerInterface):
detach=True,
stdout=True,
stderr=True,
environment={
ENV_TIME: self.sys_timezone,
},
environment={ENV_TIME: self.sys_timezone},
volumes={
str(self.sys_config.path_extern_homeassistant):
{'bind': '/config', 'mode': 'rw'},
str(self.sys_config.path_extern_ssl):
{'bind': '/ssl', 'mode': 'ro'},
str(self.sys_config.path_extern_share):
{'bind': '/share', 'mode': 'ro'},
}
str(self.sys_config.path_extern_homeassistant): {
"bind": "/config",
"mode": "rw",
},
str(self.sys_config.path_extern_ssl): {"bind": "/ssl", "mode": "ro"},
str(self.sys_config.path_extern_share): {
"bind": "/share",
"mode": "ro",
},
},
)
def is_initialize(self):
@ -117,8 +122,13 @@ class DockerHomeAssistant(DockerInterface):
Need run inside executor.
"""
try:
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:
return False
# we run on an old image, stop and start it
if docker_container.image.id != docker_image.id:
return False
return True

View File

@ -5,10 +5,10 @@ import logging
import docker
from .stats import DockerStats
from ..const import LABEL_VERSION, LABEL_ARCH
from ..const import LABEL_ARCH, LABEL_VERSION
from ..coresys import CoreSysAttributes
from ..utils import process_lock
from .stats import DockerStats
_LOGGER = logging.getLogger(__name__)
@ -37,17 +37,17 @@ class DockerInterface(CoreSysAttributes):
"""Return meta data of configuration for container/image."""
if not self._meta:
return {}
return self._meta.get('Config', {})
return self._meta.get("Config", {})
@property
def meta_labels(self):
"""Return meta data of labels for container/image."""
return self.meta_config.get('Labels') or {}
return self.meta_config.get("Labels") or {}
@property
def image(self):
"""Return name of Docker image."""
return self.meta_config.get('Image')
return self.meta_config.get("Image")
@property
def version(self):
@ -80,7 +80,7 @@ class DockerInterface(CoreSysAttributes):
_LOGGER.info("Pull image %s tag %s.", image, tag)
docker_image = self.sys_docker.images.pull(f"{image}:{tag}")
docker_image.tag(image, tag='latest')
docker_image.tag(image, tag="latest")
self._meta = docker_image.attrs
except docker.errors.APIError as err:
_LOGGER.error("Can't install %s:%s -> %s.", image, tag, err)
@ -125,7 +125,7 @@ class DockerInterface(CoreSysAttributes):
return False
# container is not running
if docker_container.status != 'running':
if docker_container.status != "running":
return False
# we run on an old image, stop and start it
@ -152,8 +152,7 @@ class DockerInterface(CoreSysAttributes):
except docker.errors.DockerException:
return False
_LOGGER.info("Attach to image %s with version %s", self.image,
self.version)
_LOGGER.info("Attach to image %s with version %s", self.image, self.version)
return True
@ -170,12 +169,12 @@ class DockerInterface(CoreSysAttributes):
raise NotImplementedError()
@process_lock
def stop(self):
def stop(self, remove_container=True):
"""Stop/remove Docker container."""
return self.sys_run_in_executor(self._stop)
return self.sys_run_in_executor(self._stop, remove_container)
def _stop(self):
"""Stop/remove and remove docker container.
def _stop(self, remove_container=True):
"""Stop/remove Docker container.
Need run inside executor.
"""
@ -184,14 +183,39 @@ class DockerInterface(CoreSysAttributes):
except docker.errors.DockerException:
return False
if docker_container.status == 'running':
if docker_container.status == "running":
_LOGGER.info("Stop %s Docker application", self.image)
with suppress(docker.errors.DockerException):
docker_container.stop(timeout=self.timeout)
with suppress(docker.errors.DockerException):
_LOGGER.info("Clean %s Docker application", self.image)
docker_container.remove(force=True)
if remove_container:
with suppress(docker.errors.DockerException):
_LOGGER.info("Clean %s Docker application", self.image)
docker_container.remove(force=True)
return True
@process_lock
def start(self):
"""Start Docker container."""
return self.sys_run_in_executor(self._start)
def _start(self):
"""Start docker container.
Need run inside executor.
"""
try:
docker_container = self.sys_docker.containers.get(self.name)
except docker.errors.DockerException:
return False
_LOGGER.info("Start %s", self.image)
try:
docker_container.start()
except docker.errors.DockerException as err:
_LOGGER.error("Can't start %s: %s", self.image, err)
return False
return True
@ -208,17 +232,16 @@ class DockerInterface(CoreSysAttributes):
# Cleanup container
self._stop()
_LOGGER.info("Remove Docker %s with latest and %s", self.image,
self.version)
_LOGGER.info("Remove Docker %s with latest and %s", self.image, self.version)
try:
with suppress(docker.errors.ImageNotFound):
self.sys_docker.images.remove(
image=f"{self.image}:latest", force=True)
self.sys_docker.images.remove(image=f"{self.image}:latest", force=True)
with suppress(docker.errors.ImageNotFound):
self.sys_docker.images.remove(
image=f"{self.image}:{self.version}", force=True)
image=f"{self.image}:{self.version}", force=True
)
except docker.errors.DockerException as err:
_LOGGER.warning("Can't remove image %s: %s", self.image, err)
@ -239,8 +262,9 @@ class DockerInterface(CoreSysAttributes):
"""
image = image or self.image
_LOGGER.info("Update Docker %s:%s to %s:%s", self.image, self.version,
image, tag)
_LOGGER.info(
"Update Docker %s:%s to %s:%s", self.image, self.version, image, tag
)
# Update docker image
if not self._install(tag, image):
@ -300,6 +324,29 @@ class DockerInterface(CoreSysAttributes):
return True
@process_lock
def restart(self):
"""Restart docker container."""
return self.sys_loop.run_in_executor(None, self._restart)
def _restart(self):
"""Restart docker container.
Need run inside executor.
"""
try:
container = self.sys_docker.containers.get(self.name)
except docker.errors.DockerException:
return False
_LOGGER.info("Restart %s", self.image)
try:
container.restart(timeout=self.timeout)
except docker.errors.DockerException as err:
_LOGGER.warning("Can't restart %s: %s", self.image, err)
return False
return True
@process_lock
def execute_command(self, command):
"""Create a temporary container and run command."""
@ -332,3 +379,30 @@ class DockerInterface(CoreSysAttributes):
except docker.errors.DockerException as err:
_LOGGER.error("Can't read stats from %s: %s", self.name, err)
return None
def is_fails(self):
"""Return True if Docker is failing state.
Return a Future.
"""
return self.sys_run_in_executor(self._is_fails)
def _is_fails(self):
"""Return True if Docker is failing state.
Need run inside executor.
"""
try:
docker_container = self.sys_docker.containers.get(self.name)
except docker.errors.DockerException:
return False
# container is not running
if docker_container.status != "exited":
return False
# Check return value
if int(docker_container.attrs["State"]["ExitCode"]) != 0:
return True
return False

View File

@ -2,26 +2,44 @@
import asyncio
from contextlib import asynccontextmanager, suppress
from datetime import datetime, timedelta
from ipaddress import IPv4Address
import logging
import os
import re
from pathlib import Path
import re
import socket
import time
from typing import Any, AsyncContextManager, Coroutine, Dict, Optional
from uuid import UUID
import aiohttp
from aiohttp import hdrs
import attr
from .const import (FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION,
ATTR_UUID, ATTR_BOOT, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL,
ATTR_WATCHDOG, ATTR_WAIT_BOOT, ATTR_REFRESH_TOKEN,
ATTR_ACCESS_TOKEN, HEADER_HA_ACCESS)
from .coresys import CoreSysAttributes
from .const import (
ATTR_ACCESS_TOKEN,
ATTR_BOOT,
ATTR_IMAGE,
ATTR_LAST_VERSION,
ATTR_PASSWORD,
ATTR_PORT,
ATTR_REFRESH_TOKEN,
ATTR_SSL,
ATTR_UUID,
ATTR_WAIT_BOOT,
ATTR_WATCHDOG,
FILE_HASSIO_HOMEASSISTANT,
HEADER_HA_ACCESS,
)
from .coresys import CoreSys, CoreSysAttributes
from .docker.homeassistant import DockerHomeAssistant
from .exceptions import (HomeAssistantUpdateError, HomeAssistantError,
HomeAssistantAPIError, HomeAssistantAuthError)
from .utils import convert_to_ascii, process_lock, create_token
from .exceptions import (
HomeAssistantAPIError,
HomeAssistantAuthError,
HomeAssistantError,
HomeAssistantUpdateError,
)
from .utils import convert_to_ascii, create_token, process_lock
from .utils.json import JsonConfig
from .validate import SCHEMA_HASS_CONFIG
@ -40,18 +58,19 @@ class ConfigResult:
class HomeAssistant(JsonConfig, CoreSysAttributes):
"""Home Assistant core object for handle it."""
def __init__(self, coresys):
def __init__(self, coresys: CoreSys):
"""Initialize Home Assistant object."""
super().__init__(FILE_HASSIO_HOMEASSISTANT, SCHEMA_HASS_CONFIG)
self.coresys = coresys
self.instance = DockerHomeAssistant(coresys)
self.lock = asyncio.Lock(loop=coresys.loop)
self._error_state = False
# We don't persist access tokens. Instead we fetch new ones when needed
self.access_token = None
self._access_token_expires = None
self.coresys: CoreSys = coresys
self.instance: DockerHomeAssistant = DockerHomeAssistant(coresys)
self.lock: asyncio.Lock = asyncio.Lock(loop=coresys.loop)
self._error_state: bool = False
async def load(self):
# We don't persist access tokens. Instead we fetch new ones when needed
self.access_token: Optional[str] = None
self._access_token_expires: Optional[datetime] = None
async def load(self) -> None:
"""Prepare Home Assistant object."""
if await self.instance.attach():
return
@ -60,95 +79,95 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
await self.install_landingpage()
@property
def machine(self):
def machine(self) -> str:
"""Return the system machines."""
return self.instance.machine
@property
def arch(self):
def arch(self) -> str:
"""Return arch of running Home Assistant."""
return self.instance.arch
@property
def error_state(self):
def error_state(self) -> bool:
"""Return True if system is in error."""
return self._error_state
@property
def api_ip(self):
def api_ip(self) -> IPv4Address:
"""Return IP of Home Assistant instance."""
return self.sys_docker.network.gateway
@property
def api_port(self):
def api_port(self) -> int:
"""Return network port to Home Assistant instance."""
return self._data[ATTR_PORT]
@api_port.setter
def api_port(self, value):
def api_port(self, value: int) -> None:
"""Set network port for Home Assistant instance."""
self._data[ATTR_PORT] = value
@property
def api_password(self):
def api_password(self) -> str:
"""Return password for Home Assistant instance."""
return self._data.get(ATTR_PASSWORD)
@api_password.setter
def api_password(self, value):
def api_password(self, value: str):
"""Set password for Home Assistant instance."""
self._data[ATTR_PASSWORD] = value
@property
def api_ssl(self):
def api_ssl(self) -> bool:
"""Return if we need ssl to Home Assistant instance."""
return self._data[ATTR_SSL]
@api_ssl.setter
def api_ssl(self, value):
def api_ssl(self, value: bool):
"""Set SSL for Home Assistant instance."""
self._data[ATTR_SSL] = value
@property
def api_url(self):
def api_url(self) -> str:
"""Return API url to Home Assistant."""
return "{}://{}:{}".format('https' if self.api_ssl else 'http',
self.api_ip, self.api_port)
@property
def watchdog(self):
def watchdog(self) -> bool:
"""Return True if the watchdog should protect Home Assistant."""
return self._data[ATTR_WATCHDOG]
@watchdog.setter
def watchdog(self, value):
def watchdog(self, value: bool):
"""Return True if the watchdog should protect Home Assistant."""
self._data[ATTR_WATCHDOG] = value
@property
def wait_boot(self):
def wait_boot(self) -> int:
"""Return time to wait for Home Assistant startup."""
return self._data[ATTR_WAIT_BOOT]
@wait_boot.setter
def wait_boot(self, value):
def wait_boot(self, value: int):
"""Set time to wait for Home Assistant startup."""
self._data[ATTR_WAIT_BOOT] = value
@property
def version(self):
def version(self) -> str:
"""Return version of running Home Assistant."""
return self.instance.version
@property
def last_version(self):
def last_version(self) -> str:
"""Return last available version of Home Assistant."""
if self.is_custom_image:
return self._data.get(ATTR_LAST_VERSION)
return self.sys_updater.version_homeassistant
@last_version.setter
def last_version(self, value):
def last_version(self, value: str):
"""Set last available version of Home Assistant."""
if value:
self._data[ATTR_LAST_VERSION] = value
@ -156,14 +175,14 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
self._data.pop(ATTR_LAST_VERSION, None)
@property
def image(self):
def image(self) -> str:
"""Return image name of the Home Assistant container."""
if self._data.get(ATTR_IMAGE):
return self._data[ATTR_IMAGE]
return os.environ['HOMEASSISTANT_REPOSITORY']
@image.setter
def image(self, value):
def image(self, value: str):
"""Set image name of Home Assistant container."""
if value:
self._data[ATTR_IMAGE] = value
@ -171,43 +190,43 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
self._data.pop(ATTR_IMAGE, None)
@property
def is_custom_image(self):
def is_custom_image(self) -> bool:
"""Return True if a custom image is used."""
return all(
attr in self._data for attr in (ATTR_IMAGE, ATTR_LAST_VERSION))
@property
def boot(self):
def boot(self) -> bool:
"""Return True if Home Assistant boot is enabled."""
return self._data[ATTR_BOOT]
@boot.setter
def boot(self, value):
def boot(self, value: bool):
"""Set Home Assistant boot options."""
self._data[ATTR_BOOT] = value
@property
def uuid(self):
def uuid(self) -> UUID:
"""Return a UUID of this Home Assistant instance."""
return self._data[ATTR_UUID]
@property
def hassio_token(self):
def hassio_token(self) -> str:
"""Return an access token for the Hass.io API."""
return self._data.get(ATTR_ACCESS_TOKEN)
@property
def refresh_token(self):
def refresh_token(self) -> str:
"""Return the refresh token to authenticate with Home Assistant."""
return self._data.get(ATTR_REFRESH_TOKEN)
@refresh_token.setter
def refresh_token(self, value):
def refresh_token(self, value: str):
"""Set Home Assistant refresh_token."""
self._data[ATTR_REFRESH_TOKEN] = value
@process_lock
async def install_landingpage(self):
async def install_landingpage(self) -> None:
"""Install a landing page."""
_LOGGER.info("Setup HomeAssistant landingpage")
while True:
@ -217,7 +236,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
await asyncio.sleep(30)
@process_lock
async def install(self):
async def install(self) -> None:
"""Install a landing page."""
_LOGGER.info("Setup Home Assistant")
while True:
@ -244,7 +263,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
await self.instance.cleanup()
@process_lock
async def update(self, version=None):
async def update(self, version=None) -> None:
"""Update HomeAssistant version."""
version = version or self.last_version
rollback = self.version if not self.error_state else None
@ -258,14 +277,13 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
# process an update
async def _update(to_version):
"""Run Home Assistant update."""
try:
_LOGGER.info("Update Home Assistant to version %s", to_version)
if not await self.instance.update(to_version):
raise HomeAssistantUpdateError()
finally:
if running:
await self._start()
_LOGGER.info("Successful run Home Assistant %s", to_version)
_LOGGER.info("Update Home Assistant to version %s", to_version)
if not await self.instance.update(to_version):
raise HomeAssistantUpdateError()
if running:
await self._start()
_LOGGER.info("Successful run Home Assistant %s", to_version)
# Update Home Assistant
with suppress(HomeAssistantError):
@ -279,7 +297,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
else:
raise HomeAssistantUpdateError()
async def _start(self):
async def _start(self) -> None:
"""Start Home Assistant Docker & wait."""
if await self.instance.is_running():
_LOGGER.warning("Home Assistant is already running!")
@ -294,61 +312,74 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
await self._block_till_run()
@process_lock
def start(self):
"""Run Home Assistant docker.
async def start(self) -> None:
"""Run Home Assistant docker."""
if await self.instance.is_running():
await self.instance.restart()
elif await self.instance.is_initialize():
await self.instance.start()
else:
await self._start()
return
Return a coroutine.
"""
return self._start()
await self._block_till_run()
@process_lock
def stop(self):
def stop(self) -> Coroutine:
"""Stop Home Assistant Docker.
Return a coroutine.
"""
return self.instance.stop()
return self.instance.stop(remove_container=False)
@process_lock
async def restart(self):
async def restart(self) -> None:
"""Restart Home Assistant Docker."""
if not await self.instance.restart():
raise HomeAssistantError()
await self._block_till_run()
@process_lock
async def rebuild(self) -> None:
"""Rebuild Home Assistant Docker container."""
await self.instance.stop()
await self._start()
def logs(self):
def logs(self) -> Coroutine:
"""Get HomeAssistant docker logs.
Return a coroutine.
"""
return self.instance.logs()
def stats(self):
def stats(self) -> Coroutine:
"""Return stats of Home Assistant.
Return a coroutine.
"""
return self.instance.stats()
def is_running(self):
def is_running(self) -> Coroutine:
"""Return True if Docker container is running.
Return a coroutine.
"""
return self.instance.is_running()
def is_initialize(self):
"""Return True if a Docker container is exists.
def is_fails(self) -> Coroutine:
"""Return True if a Docker container is fails state.
Return a coroutine.
"""
return self.instance.is_initialize()
return self.instance.is_fails()
@property
def in_progress(self):
def in_progress(self) -> bool:
"""Return True if a task is in progress."""
return self.instance.in_progress or self.lock.locked()
async def check_config(self):
async def check_config(self) -> ConfigResult:
"""Run Home Assistant config check."""
result = await self.instance.execute_command(
"python3 -m homeassistant -c /config --script check_config")
@ -367,7 +398,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
_LOGGER.info("Home Assistant config is valid")
return ConfigResult(True, log)
async def ensure_access_token(self):
async def ensure_access_token(self) -> None:
"""Ensures there is an access token."""
if self.access_token is not None and self._access_token_expires > datetime.utcnow():
return
@ -392,12 +423,12 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
@asynccontextmanager
async def make_request(self,
method,
path,
json=None,
content_type=None,
data=None,
timeout=30):
method: str,
path: str,
json: Optional[Dict[str, Any]] = None,
content_type: Optional[str] = None,
data: Optional[bytes] = None,
timeout=30) -> AsyncContextManager[aiohttp.ClientResponse]:
"""Async context manager to make a request with right auth."""
url = f"{self.api_url}/{path}"
headers = {}
@ -432,7 +463,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
raise HomeAssistantAPIError()
async def check_api_state(self):
async def check_api_state(self) -> bool:
"""Return True if Home Assistant up and running."""
with suppress(HomeAssistantAPIError):
async with self.make_request('get', 'api/') as resp:
@ -443,7 +474,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
return False
async def _block_till_run(self):
async def _block_till_run(self) -> None:
"""Block until Home-Assistant is booting up or startup timeout."""
start_time = time.monotonic()
migration_progress = False

View File

@ -94,7 +94,7 @@ class Tasks(CoreSysAttributes):
async def _watchdog_homeassistant_docker(self):
"""Check running state of Docker and start if they is close."""
# if Home Assistant is active
if not await self.sys_homeassistant.is_initialize() or \
if not await self.sys_homeassistant.is_fails() or \
not self.sys_homeassistant.watchdog or \
self.sys_homeassistant.error_state:
return
@ -117,7 +117,7 @@ class Tasks(CoreSysAttributes):
a delay in our system.
"""
# If Home-Assistant is active
if not await self.sys_homeassistant.is_initialize() or \
if not await self.sys_homeassistant.is_fails() or \
not self.sys_homeassistant.watchdog or \
self.sys_homeassistant.error_state:
return