Compare commits

...

40 Commits
114 ... 122

Author SHA1 Message Date
Pascal Vizeli
6ac45a24fc Merge pull request #615 from home-assistant/dev
Release 122
2018-08-02 23:39:43 +02:00
Pascal Vizeli
9430b39042 Update uvloop version 0.11.1 (#614) 2018-08-02 23:18:40 +02:00
Pascal Vizeli
ae7466ccfe Fix UnicodeDecodeError with read json file (#613)
* Update json.py

* Update data.py
2018-08-02 21:48:50 +02:00
Simon Holzmayer
2c17fe5da8 Adapt regex validation to allow docker images from other registries (#608)
* Adapt regex validation to allow images from other registries than dockerhub

Issue #564

* Update validate.py
2018-07-30 12:34:42 +02:00
Pascal Vizeli
a0fb91af29 Use requirements.txt (#607)
* Create requirements.txt

* Update setup.py

* Update Dockerfile

* Update Dockerfile

* Update requirements.txt

* Update requirements.txt

* Update Dockerfile

* Update tox.ini
2018-07-27 16:34:47 +02:00
Pascal Vizeli
f626e31fd3 Bump version 122 2018-07-25 01:52:24 +02:00
Pascal Vizeli
0151a149fd Merge pull request #604 from home-assistant/dev
Release 121
2018-07-25 01:47:36 +02:00
Pascal Vizeli
9dea93142b Timeout shutdown (#603)
* Don't wait too long for shutdown

* Update log message

* Fix timeout

* Fast shudown
2018-07-25 01:46:54 +02:00
Pascal Vizeli
7f878bfac0 Bump version to 121 2018-07-25 01:39:33 +02:00
Pascal Vizeli
ebe9ae2341 Merge pull request #600 from home-assistant/dev
Release 120
2018-07-24 22:11:10 +02:00
Pascal Vizeli
e777bbd024 Fix bug with proxy (#599) 2018-07-24 22:00:46 +02:00
Pascal Vizeli
2116d56124 Bump version 120 2018-07-24 16:32:46 +02:00
Pascal Vizeli
0b6a82b018 Merge pull request #598 from home-assistant/dev
Release 119
2018-07-24 16:21:06 +02:00
Pascal Vizeli
b4ea28af4e Update uvloop to 0.11.0 (#597) 2018-07-24 16:16:26 +02:00
Pascal Vizeli
22f59712df Bump version 119 2018-07-23 13:01:51 +02:00
Pascal Vizeli
efe95f7bab Merge pull request #593 from home-assistant/dev
Release 118
2018-07-23 12:59:15 +02:00
Pascal Vizeli
200c68f67f Fix proxy data passthrougth (#592)
* Fix proxy data passthrougth

* Update homeassistant.py
2018-07-23 12:53:32 +02:00
Pascal Vizeli
dcefec7b99 Cleanup old stuff (#589) 2018-07-22 01:51:45 +02:00
Pascal Vizeli
5db798bcf8 Fix API for home-assistant (#588)
* Fix API for home-assistant

* Update API.md
2018-07-22 00:42:45 +02:00
Pascal Vizeli
70005296cc Bump version 118 2018-07-21 20:25:12 +02:00
Pascal Vizeli
f2bf8dea93 Merge pull request #585 from home-assistant/dev
Release 117
2018-07-21 20:21:18 +02:00
Pascal Vizeli
fee858c956 Fix exception is HomeAssistant allready running (#587) 2018-07-21 20:13:13 +02:00
Pascal Vizeli
e3ae48c8ff Remove geo ip (#586)
* Remove geo ip

* Update core.py

* Update dt.py
2018-07-21 19:45:11 +02:00
Pascal Vizeli
fa9e20385e Bugfix passwrod (#584) 2018-07-21 19:07:22 +02:00
Pascal Vizeli
f51c9704e0 Fix timeout on freegeoip (#581)
* Fix timeout on freegeoip

* Update updater.py

* Update supervisor.py

* Update dt.py

* Update hassos.py

* Update core.py

* Update hassos.py

* Update supervisor.py

* Update updater.py
2018-07-21 19:01:20 +02:00
Pascal Vizeli
57c58d81c0 Bump version 117 2018-07-21 00:06:34 +02:00
Pascal Vizeli
1ec1082068 Merge pull request #580 from home-assistant/dev
Release 116
2018-07-21 00:05:56 +02:00
Pascal Vizeli
35b7c2269c Support control of hassos-cli (#555)
* Support control of hassos-cli

* Update const.py

* Update validate.py

* Update supervisor.py

* Create hassos_cli.py

* Update hassos_cli.py

* Update hassos_cli.py

* Update hassos.py

* Update tasks.py

* Update hassos.py

* Update API.md

* Update API.md

* Update const.py

* Update hassos.py

* Update __init__.py

* Fix lint

* fix

* Fix logging

* change order

* Fix download
2018-07-20 23:45:36 +02:00
Pascal Vizeli
cc3e6ec6fd Fix stream error with aiohttp >= 3 (#579)
* Fix stream error with aiohttp >= 3

* Update proxy.py

* Update proxy.py

* Update proxy.py

* Update proxy.py

* Update proxy.py

* Update proxy.py
2018-07-20 22:28:56 +02:00
Paulus Schoutsen
4df42e054d Leverage access and refresh tokens if available (#575)
* Leverage access and refresh tokens if available

* Update homeassistant.py

* Update homeassistant.py

* Update proxy.py

* Migrate HomeAssistant to new exception layout

* Fix build for 3.7

* Cleanups

* Fix style

* fix log strings

* Fix new style

* Fix travis build

* python 3.7

* next try

* fix

* fix lint

* Fix lint p2

* Add logging

* Fix logging

* fix access

* Fix spell

* fix return

* Fix runtime

* Add to hass config
2018-07-20 16:55:48 +02:00
Pascal Vizeli
1b481e0b37 Fix small bugs (python37) (#577)
* Fix small bugs (python37)

* Update utils.py

* Update utils.py

* Update utils.py

* Update utils.py

* Update utils.py
2018-07-19 21:22:26 +02:00
Pascal Vizeli
3aa4cdf540 Fix remove data inside executor (#576) 2018-07-19 20:25:58 +02:00
Pascal Vizeli
029f277945 Reset readonly on remove data (#569)
* Reset readonly on remove data

* Update addon.py

* Update utils.py

* Fix lint

* Update utils.py

* Update utils.py

* Update utils.py

* Update utils.py

* Update addon.py
2018-07-19 12:44:16 +02:00
Pascal Vizeli
e7e0b9adda Fix-python7 compatibility (#573) 2018-07-19 01:18:43 +02:00
Pascal Vizeli
5fbff75da8 Support new base images (#571)
* Support new base images

* Update Dockerfile

* Update setup.py
2018-07-17 23:32:50 +02:00
Paulus Schoutsen
58299a0389 Add release drafter 2018-07-10 10:38:45 +02:00
Pascal Vizeli
1151d7e17b Bump version to 116 2018-07-06 13:10:16 +02:00
Pascal Vizeli
b56ed547e3 Merge pull request #559 from home-assistant/dev
Release 115
2018-07-06 13:09:32 +02:00
Pascal Vizeli
a71ebba940 Bugfix rollback if the hass instant is complete corrupt (#558) 2018-07-06 13:08:57 +02:00
Pascal Vizeli
4fcb516c75 Bump version to 115 2018-07-06 01:38:18 +02:00
34 changed files with 502 additions and 244 deletions

4
.github/release-drafter.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
template: |
## What's Changed
$CHANGES

View File

@@ -1,12 +1,6 @@
sudo: false sudo: true
matrix: dist: xenial
fast_finish: true
include:
- python: "3.6"
cache:
directories:
- $HOME/.cache/pip
install: pip install -U tox install: pip install -U tox
language: python language: python
python: 3.7
script: tox script: tox

10
API.md
View File

@@ -273,7 +273,9 @@ return:
```json ```json
{ {
"version": "2.3", "version": "2.3",
"version_cli": "7",
"version_latest": "2.4", "version_latest": "2.4",
"version_cli_latest": "8",
"board": "ova|rpi" "board": "ova|rpi"
} }
``` ```
@@ -285,6 +287,13 @@ return:
} }
``` ```
- POST `/hassos/update/cli`
```json
{
"version": "optional"
}
```
- POST `/hassos/config/sync` - POST `/hassos/config/sync`
Load host configs from a USB stick. Load host configs from a USB stick.
@@ -372,6 +381,7 @@ Output is the raw Docker log.
"port": "port for access hass", "port": "port for access hass",
"ssl": "bool", "ssl": "bool",
"password": "", "password": "",
"refresh_token": "",
"watchdog": "bool", "watchdog": "bool",
"startup_time": 600 "startup_time": 600
} }

View File

@@ -5,8 +5,8 @@ FROM $BUILD_FROM
ENV LANG C.UTF-8 ENV LANG C.UTF-8
# Setup base # Setup base
COPY requirements.txt /usr/src/
RUN apk add --no-cache \ RUN apk add --no-cache \
python3 \
git \ git \
socat \ socat \
glib \ glib \
@@ -14,13 +14,10 @@ RUN apk add --no-cache \
eudev-libs \ eudev-libs \
&& apk add --no-cache --virtual .build-dependencies \ && apk add --no-cache --virtual .build-dependencies \
make \ make \
python3-dev \
g++ \ g++ \
&& pip3 install --no-cache-dir \ && pip3 install -r /usr/src/requirements.txt \
uvloop==0.10.2 \ && apk del .build-dependencies \
cchardet==2.1.1 \ && rm -f /usr/src/requirements.txt
pycryptodome==3.4.11 \
&& apk del .build-dependencies
# Install HassIO # Install HassIO
COPY . /usr/src/hassio COPY . /usr/src/hassio

View File

@@ -4,7 +4,7 @@ from concurrent.futures import ThreadPoolExecutor
import logging import logging
import sys import sys
import hassio.bootstrap as bootstrap from hassio import bootstrap
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@@ -14,7 +14,7 @@ from voluptuous.humanize import humanize_error
from .validate import ( from .validate import (
validate_options, SCHEMA_ADDON_SNAPSHOT, RE_VOLUME, RE_SERVICE) validate_options, SCHEMA_ADDON_SNAPSHOT, RE_VOLUME, RE_SERVICE)
from .utils import check_installed from .utils import check_installed, remove_data
from ..const import ( from ..const import (
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP,
ATTR_OPTIONS, ATTR_PORTS, ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY, ATTR_OPTIONS, ATTR_PORTS, ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY,
@@ -636,7 +636,7 @@ class Addon(CoreSysAttributes):
if self.path_data.is_dir(): if self.path_data.is_dir():
_LOGGER.info( _LOGGER.info(
"Remove Home-Assistant addon data folder %s", self.path_data) "Remove Home-Assistant addon data folder %s", self.path_data)
shutil.rmtree(str(self.path_data)) await remove_data(self.path_data)
# Cleanup audio settings # Cleanup audio settings
if self.path_asound.exists(): if self.path_asound.exists():
@@ -856,12 +856,12 @@ class Addon(CoreSysAttributes):
# restore data # restore data
def _restore_data(): def _restore_data():
"""Restore data.""" """Restore data."""
if self.path_data.is_dir():
shutil.rmtree(str(self.path_data), ignore_errors=True)
shutil.copytree(str(Path(temp, "data")), str(self.path_data)) shutil.copytree(str(Path(temp, "data")), str(self.path_data))
_LOGGER.info("Restore data for addon %s", self._id)
if self.path_data.is_dir():
await remove_data(self.path_data)
try: try:
_LOGGER.info("Restore data for addon %s", self._id)
await self.sys_run_in_executor(_restore_data) await self.sys_run_in_executor(_restore_data)
except shutil.Error as err: except shutil.Error as err:
_LOGGER.error("Can't restore origin data: %s", err) _LOGGER.error("Can't restore origin data: %s", err)

View File

@@ -80,7 +80,7 @@ class AddonsData(JsonConfig, CoreSysAttributes):
read_json_file(repository_file) read_json_file(repository_file)
) )
except (OSError, json.JSONDecodeError): except (OSError, json.JSONDecodeError, UnicodeDecodeError):
_LOGGER.warning("Can't read repository information from %s", _LOGGER.warning("Can't read repository information from %s",
repository_file) repository_file)
return return

View File

@@ -1,4 +1,5 @@
"""Util addons functions.""" """Util addons functions."""
import asyncio
import hashlib import hashlib
import logging import logging
import re import re
@@ -33,3 +34,20 @@ def check_installed(method):
return await method(addon, *args, **kwargs) return await method(addon, *args, **kwargs)
return wrap_check return wrap_check
async def remove_data(folder):
"""Remove folder and reset privileged."""
try:
proc = await asyncio.create_subprocess_exec(
"rm", "-rf", str(folder),
stdout=asyncio.subprocess.DEVNULL
)
_, error_msg = await proc.communicate()
except OSError as err:
error_msg = str(err)
if proc.returncode == 0:
return
_LOGGER.error("Can't remove Add-on Data: %s", error_msg)

View File

@@ -129,7 +129,8 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [SCHEMA_ELEMENT]) vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [SCHEMA_ELEMENT])
})) }))
}), False), }), False),
vol.Optional(ATTR_IMAGE): vol.Match(r"^[\w{}]+/[\-\w{}]+$"), vol.Optional(ATTR_IMAGE):
vol.Match(r"^([a-zA-Z.:\d{}]+/)*?([\w{}]+)/([\-\w{}]+)$"),
vol.Optional(ATTR_TIMEOUT, default=10): vol.Optional(ATTR_TIMEOUT, default=10):
vol.All(vol.Coerce(int), vol.Range(min=10, max=120)), vol.All(vol.Coerce(int), vol.Range(min=10, max=120)),
}, extra=vol.REMOVE_EXTRA) }, extra=vol.REMOVE_EXTRA)

View File

@@ -76,6 +76,7 @@ class RestAPI(CoreSysAttributes):
self.webapp.add_routes([ self.webapp.add_routes([
web.get('/hassos/info', api_hassos.info), web.get('/hassos/info', api_hassos.info),
web.post('/hassos/update', api_hassos.update), web.post('/hassos/update', api_hassos.update),
web.post('/hassos/update/cli', api_hassos.update_cli),
web.post('/hassos/config/sync', api_hassos.config_sync), web.post('/hassos/config/sync', api_hassos.config_sync),
]) ])
@@ -234,13 +235,16 @@ class RestAPI(CoreSysAttributes):
async def start(self): async def start(self):
"""Run rest api webserver.""" """Run rest api webserver."""
await self._runner.setup() await self._runner.setup()
self._site = web.TCPSite(self._runner, "0.0.0.0", 80) self._site = web.TCPSite(
self._runner, host="0.0.0.0", port=80, shutdown_timeout=5)
try: try:
await self._site.start() await self._site.start()
except OSError as err: except OSError as err:
_LOGGER.fatal( _LOGGER.fatal(
"Failed to create HTTP server at 0.0.0.0:80 -> %s", err) "Failed to create HTTP server at 0.0.0.0:80 -> %s", err)
else:
_LOGGER.info("Start API on %s", self.sys_docker.network.supervisor)
async def stop(self): async def stop(self):
"""Stop rest api webserver.""" """Stop rest api webserver."""
@@ -250,3 +254,5 @@ class RestAPI(CoreSysAttributes):
# Shutdown running API # Shutdown running API
await self._site.stop() await self._site.stop()
await self._runner.cleanup() await self._runner.cleanup()
_LOGGER.info("Stop API on %s", self.sys_docker.network.supervisor)

View File

@@ -5,7 +5,9 @@ import logging
import voluptuous as vol import voluptuous as vol
from .utils import api_process, api_validate from .utils import api_process, api_validate
from ..const import ATTR_VERSION, ATTR_BOARD, ATTR_VERSION_LATEST from ..const import (
ATTR_VERSION, ATTR_BOARD, ATTR_VERSION_LATEST, ATTR_VERSION_CLI,
ATTR_VERSION_CLI_LATEST)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -23,7 +25,9 @@ class APIHassOS(CoreSysAttributes):
"""Return hassos information.""" """Return hassos information."""
return { return {
ATTR_VERSION: self.sys_hassos.version, ATTR_VERSION: self.sys_hassos.version,
ATTR_VERSION_CLI: self.sys_hassos.version_cli,
ATTR_VERSION_LATEST: self.sys_hassos.version_latest, ATTR_VERSION_LATEST: self.sys_hassos.version_latest,
ATTR_VERSION_CLI_LATEST: self.sys_hassos.version_cli_latest,
ATTR_BOARD: self.sys_hassos.board, ATTR_BOARD: self.sys_hassos.board,
} }
@@ -35,6 +39,14 @@ class APIHassOS(CoreSysAttributes):
await asyncio.shield(self.sys_hassos.update(version)) await asyncio.shield(self.sys_hassos.update(version))
@api_process
async def update_cli(self, request):
"""Update HassOS CLI."""
body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.sys_hassos.version_cli_latest)
await asyncio.shield(self.sys_hassos.update_cli(version))
@api_process @api_process
def config_sync(self, request): def config_sync(self, request):
"""Trigger config reload on HassOS.""" """Trigger config reload on HassOS."""

View File

@@ -10,7 +10,7 @@ from ..const import (
ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG, ATTR_CPU_PERCENT, ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG, ATTR_CPU_PERCENT,
ATTR_MEMORY_USAGE, ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_MEMORY_USAGE, ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX,
ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_WAIT_BOOT, ATTR_MACHINE, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_WAIT_BOOT, ATTR_MACHINE,
CONTENT_TYPE_BINARY) ATTR_REFRESH_TOKEN, CONTENT_TYPE_BINARY)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..validate import NETWORK_PORT, DOCKER_IMAGE from ..validate import NETWORK_PORT, DOCKER_IMAGE
@@ -30,6 +30,8 @@ SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_WATCHDOG): vol.Boolean(), vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
vol.Optional(ATTR_WAIT_BOOT): vol.Optional(ATTR_WAIT_BOOT):
vol.All(vol.Coerce(int), vol.Range(min=60)), vol.All(vol.Coerce(int), vol.Range(min=60)),
# Required once we enforce user system
vol.Optional(ATTR_REFRESH_TOKEN): str,
}) })
SCHEMA_VERSION = vol.Schema({ SCHEMA_VERSION = vol.Schema({
@@ -83,8 +85,10 @@ class APIHomeAssistant(CoreSysAttributes):
if ATTR_WAIT_BOOT in body: if ATTR_WAIT_BOOT in body:
self.sys_homeassistant.wait_boot = body[ATTR_WAIT_BOOT] self.sys_homeassistant.wait_boot = body[ATTR_WAIT_BOOT]
if ATTR_REFRESH_TOKEN in body:
self.sys_homeassistant.refresh_token = body[ATTR_REFRESH_TOKEN]
self.sys_homeassistant.save_data() self.sys_homeassistant.save_data()
return True
@api_process @api_process
async def stats(self, request): async def stats(self, request):
@@ -109,11 +113,7 @@ class APIHomeAssistant(CoreSysAttributes):
body = await api_validate(SCHEMA_VERSION, request) body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.sys_homeassistant.last_version) version = body.get(ATTR_VERSION, self.sys_homeassistant.last_version)
if version == self.sys_homeassistant.version: await asyncio.shield(self.sys_homeassistant.update(version))
raise RuntimeError("Version {} is already in use".format(version))
return await asyncio.shield(
self.sys_homeassistant.update(version))
@api_process @api_process
def stop(self, request): def stop(self, request):

View File

@@ -1,15 +1,18 @@
"""Utils for HomeAssistant Proxy.""" """Utils for HomeAssistant Proxy."""
import asyncio import asyncio
from contextlib import asynccontextmanager
import logging import logging
import aiohttp import aiohttp
from aiohttp import web from aiohttp import web
from aiohttp.web_exceptions import HTTPBadGateway, HTTPInternalServerError from aiohttp.web_exceptions import (
HTTPBadGateway, HTTPInternalServerError, HTTPUnauthorized)
from aiohttp.hdrs import CONTENT_TYPE from aiohttp.hdrs import CONTENT_TYPE
import async_timeout import async_timeout
from ..const import HEADER_HA_ACCESS from ..const import HEADER_HA_ACCESS
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import HomeAssistantAuthError, HomeAssistantAPIError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -23,49 +26,43 @@ class APIProxy(CoreSysAttributes):
addon = self.sys_addons.from_uuid(hassio_token) addon = self.sys_addons.from_uuid(hassio_token)
if not addon: if not addon:
_LOGGER.warning("Unknown Home-Assistant API access!") _LOGGER.warning("Unknown HomeAssistant API access!")
elif not addon.access_homeassistant_api:
_LOGGER.warning("Not permitted API access: %s", addon.slug)
else: else:
_LOGGER.info("%s access from %s", request.path, addon.slug) _LOGGER.info("%s access from %s", request.path, addon.slug)
return
raise HTTPUnauthorized()
@asynccontextmanager
async def _api_client(self, request, path, timeout=300): async def _api_client(self, request, path, timeout=300):
"""Return a client request with proxy origin for Home-Assistant.""" """Return a client request with proxy origin for Home-Assistant."""
url = f"{self.sys_homeassistant.api_url}/api/{path}"
try: try:
data = None
headers = {}
method = getattr(self.sys_websession_ssl, request.method.lower())
params = request.query or None
# read data # read data
with async_timeout.timeout(30): with async_timeout.timeout(30):
data = await request.read() data = await request.read()
if data: if data:
headers.update({CONTENT_TYPE: request.content_type}) content_type = request.content_type
else:
content_type = None
# need api password? async with self.sys_homeassistant.make_request(
if self.sys_homeassistant.api_password: request.method.lower(), f'api/{path}',
headers = { content_type=content_type,
HEADER_HA_ACCESS: self.sys_homeassistant.api_password, data=data,
} timeout=timeout,
) as resp:
# reset headers yield resp
if not headers: return
headers = None
client = await method(
url, data=data, headers=headers, timeout=timeout,
params=params
)
return client
except HomeAssistantAuthError:
_LOGGER.error("Authenticate error on API for request %s", path)
except aiohttp.ClientError as err: except aiohttp.ClientError as err:
_LOGGER.error("Client error on API %s request %s.", path, err) _LOGGER.error("Client error on API %s request %s", path, err)
except asyncio.TimeoutError: except asyncio.TimeoutError:
_LOGGER.error("Client timeout error on API request %s.", path) _LOGGER.error("Client timeout error on API request %s", path)
raise HTTPBadGateway() raise HTTPBadGateway()
@@ -74,30 +71,25 @@ class APIProxy(CoreSysAttributes):
self._check_access(request) self._check_access(request)
_LOGGER.info("Home-Assistant EventStream start") _LOGGER.info("Home-Assistant EventStream start")
client = await self._api_client(request, 'stream', timeout=None) async with self._api_client(request, 'stream', timeout=None) as client:
response = web.StreamResponse()
response.content_type = request.headers.get(CONTENT_TYPE)
try:
await response.prepare(request)
while True:
data = await client.content.read(10)
if not data:
break
await response.write(data)
response = web.StreamResponse() except aiohttp.ClientError:
response.content_type = request.headers.get(CONTENT_TYPE) pass
try:
await response.prepare(request)
while True:
data = await client.content.read(10)
if not data:
await response.write_eof()
break
await response.write(data)
except aiohttp.ClientError: finally:
await response.write_eof() client.close()
_LOGGER.info("Home-Assistant EventStream close")
except asyncio.CancelledError: return response
pass
finally:
client.close()
_LOGGER.info("Home-Assistant EventStream close")
return response
async def api(self, request): async def api(self, request):
"""Proxy HomeAssistant API Requests.""" """Proxy HomeAssistant API Requests."""
@@ -105,14 +97,13 @@ class APIProxy(CoreSysAttributes):
# Normal request # Normal request
path = request.match_info.get('path', '') path = request.match_info.get('path', '')
client = await self._api_client(request, path) async with self._api_client(request, path) as client:
data = await client.read()
data = await client.read() return web.Response(
return web.Response( body=data,
body=data, status=client.status,
status=client.status, content_type=client.content_type
content_type=client.content_type )
)
async def _websocket_client(self): async def _websocket_client(self):
"""Initialize a websocket api connection.""" """Initialize a websocket api connection."""
@@ -123,19 +114,44 @@ class APIProxy(CoreSysAttributes):
url, heartbeat=60, verify_ssl=False) url, heartbeat=60, verify_ssl=False)
# handle authentication # handle authentication
for _ in range(2): data = await client.receive_json()
data = await client.receive_json()
if data.get('type') == 'auth_ok':
return client
elif data.get('type') == 'auth_required':
await client.send_json({
'type': 'auth',
'api_password': self.sys_homeassistant.api_password,
})
_LOGGER.error("Authentication to Home-Assistant websocket") if data.get('type') == 'auth_ok':
return client
except (aiohttp.ClientError, RuntimeError) as err: if data.get('type') != 'auth_required':
# Invalid protocol
_LOGGER.error(
'Got unexpected response from HA websocket: %s', data)
raise HTTPBadGateway()
if self.sys_homeassistant.refresh_token:
await self.sys_homeassistant.ensure_access_token()
await client.send_json({
'type': 'auth',
'access_token': self.sys_homeassistant.access_token,
})
else:
await client.send_json({
'type': 'auth',
'api_password': self.sys_homeassistant.api_password,
})
data = await client.receive_json()
if data.get('type') == 'auth_ok':
return client
# Renew the Token is invalid
if (data.get('type') == 'invalid_auth' and
self.sys_homeassistant.refresh_token):
self.sys_homeassistant.access_token = None
return await self._websocket_client()
_LOGGER.error(
"Failed authentication to Home-Assistant websocket: %s", data)
except (RuntimeError, HomeAssistantAPIError) as err:
_LOGGER.error("Client error on websocket API %s.", err) _LOGGER.error("Client error on websocket API %s.", err)
raise HTTPBadGateway() raise HTTPBadGateway()
@@ -157,13 +173,19 @@ class APIProxy(CoreSysAttributes):
# Check API access # Check API access
response = await server.receive_json() response = await server.receive_json()
hassio_token = response.get('api_password') hassio_token = (response.get('api_password') or
response.get('access_token'))
addon = self.sys_addons.from_uuid(hassio_token) addon = self.sys_addons.from_uuid(hassio_token)
if not addon: if not addon or not addon.access_homeassistant_api:
_LOGGER.warning("Unauthorized websocket access!") _LOGGER.warning("Unauthorized websocket access!")
else: await server.send_json({
_LOGGER.info("Websocket access from %s", addon.slug) 'type': 'auth_invalid',
'message': 'Invalid access',
})
return server
_LOGGER.info("Websocket access from %s", addon.slug)
await server.send_json({ await server.send_json({
'type': 'auth_ok', 'type': 'auth_ok',

View File

@@ -1,6 +1,5 @@
"""Init file for HassIO util for rest api.""" """Init file for HassIO util for rest api."""
import json import json
import hashlib
import logging import logging
from aiohttp import web from aiohttp import web
@@ -94,9 +93,3 @@ async def api_validate(schema, request):
raise RuntimeError(humanize_error(data, ex)) from None raise RuntimeError(humanize_error(data, ex)) from None
return data return data
def hash_password(password):
"""Hash and salt our passwords."""
key = ")*()*SALT_HASSIO2123{}6554547485HSKA!!*JSLAfdasda$".format(password)
return hashlib.sha256(key.encode()).hexdigest()

View File

@@ -2,7 +2,7 @@
from pathlib import Path from pathlib import Path
from ipaddress import ip_network from ipaddress import ip_network
HASSIO_VERSION = '114' HASSIO_VERSION = '122'
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons" URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
URL_HASSIO_VERSION = \ URL_HASSIO_VERSION = \
@@ -50,7 +50,7 @@ CONTENT_TYPE_JSON = 'application/json'
CONTENT_TYPE_TEXT = 'text/plain' CONTENT_TYPE_TEXT = 'text/plain'
CONTENT_TYPE_TAR = 'application/tar' CONTENT_TYPE_TAR = 'application/tar'
HEADER_HA_ACCESS = 'x-ha-access' HEADER_HA_ACCESS = 'x-ha-access'
HEADER_TOKEN = 'X-HASSIO-KEY' HEADER_TOKEN = 'x-hassio-key'
ENV_TOKEN = 'HASSIO_TOKEN' ENV_TOKEN = 'HASSIO_TOKEN'
ENV_TIME = 'TZ' ENV_TIME = 'TZ'
@@ -174,6 +174,10 @@ ATTR_DEVICETREE = 'devicetree'
ATTR_CPE = 'cpe' ATTR_CPE = 'cpe'
ATTR_BOARD = 'board' ATTR_BOARD = 'board'
ATTR_HASSOS = 'hassos' ATTR_HASSOS = 'hassos'
ATTR_HASSOS_CLI = 'hassos_cli'
ATTR_VERSION_CLI = 'version_cli'
ATTR_VERSION_CLI_LATEST = 'version_cli_latest'
ATTR_REFRESH_TOKEN = 'refresh_token'
SERVICE_MQTT = 'mqtt' SERVICE_MQTT = 'mqtt'

View File

@@ -3,11 +3,12 @@ from contextlib import suppress
import asyncio import asyncio
import logging import logging
import async_timeout
from .coresys import CoreSysAttributes from .coresys import CoreSysAttributes
from .const import ( from .const import (
STARTUP_SYSTEM, STARTUP_SERVICES, STARTUP_APPLICATION, STARTUP_INITIALIZE) STARTUP_SYSTEM, STARTUP_SERVICES, STARTUP_APPLICATION, STARTUP_INITIALIZE)
from .exceptions import HassioError from .exceptions import HassioError, HomeAssistantError
from .utils.dt import fetch_timezone
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -21,10 +22,8 @@ class HassIO(CoreSysAttributes):
async def setup(self): async def setup(self):
"""Setup HassIO orchestration.""" """Setup HassIO orchestration."""
# update timezone # Load Supervisor
if self.sys_config.timezone == 'UTC': await self.sys_supervisor.load()
self.sys_config.timezone = \
await fetch_timezone(self.sys_websession)
# Load DBus # Load DBus
await self.sys_dbus.load() await self.sys_dbus.load()
@@ -35,9 +34,6 @@ class HassIO(CoreSysAttributes):
# Load HassOS # Load HassOS
await self.sys_hassos.load() await self.sys_hassos.load()
# Load Supervisor
await self.sys_supervisor.load()
# Load Home Assistant # Load Home Assistant
await self.sys_homeassistant.load() await self.sys_homeassistant.load()
@@ -71,7 +67,6 @@ class HassIO(CoreSysAttributes):
# start api # start api
await self.sys_api.start() await self.sys_api.start()
_LOGGER.info("Start API on %s", self.sys_docker.network.supervisor)
# start addon mark as initialize # start addon mark as initialize
await self.sys_addons.boot(STARTUP_INITIALIZE) await self.sys_addons.boot(STARTUP_INITIALIZE)
@@ -93,7 +88,8 @@ class HassIO(CoreSysAttributes):
# run HomeAssistant # run HomeAssistant
if self.sys_homeassistant.boot: if self.sys_homeassistant.boot:
await self.sys_homeassistant.start() with suppress(HomeAssistantError):
await self.sys_homeassistant.start()
# start addon mark as application # start addon mark as application
await self.sys_addons.boot(STARTUP_APPLICATION) await self.sys_addons.boot(STARTUP_APPLICATION)
@@ -118,12 +114,18 @@ class HassIO(CoreSysAttributes):
self.sys_scheduler.suspend = True self.sys_scheduler.suspend = True
# process async stop tasks # process async stop tasks
await asyncio.wait([ try:
self.sys_api.stop(), with async_timeout.timeout(10):
self.sys_dns.stop(), await asyncio.wait([
self.sys_websession.close(), self.sys_api.stop(),
self.sys_websession_ssl.close() self.sys_dns.stop(),
]) self.sys_websession.close(),
self.sys_websession_ssl.close()
])
except asyncio.TimeoutError:
_LOGGER.warning("Force Shutdown!")
_LOGGER.info("Hass.io is down")
async def shutdown(self): async def shutdown(self):
"""Shutdown all running containers in correct order.""" """Shutdown all running containers in correct order."""

View File

@@ -0,0 +1,37 @@
"""HassOS Cli docker object."""
import logging
import docker
from .interface import DockerInterface
from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__)
class DockerHassOSCli(DockerInterface, CoreSysAttributes):
"""Docker hassio wrapper for HassOS Cli."""
@property
def image(self):
"""Return name of HassOS cli image."""
return f"homeassistant/{self.sys_arch}-hassio-cli"
def _stop(self):
"""Don't need stop."""
return True
def _attach(self):
"""Attach to running docker container.
Need run inside executor.
"""
try:
image = self.sys_docker.images.get(self.image)
except docker.errors.DockerException:
_LOGGER.warning("Can't find a HassOS cli %s", self.image)
else:
self._meta = image.attrs
_LOGGER.info("Found HassOS cli %s with version %s",
self.image, self.version)

View File

@@ -11,7 +11,7 @@ _LOGGER = logging.getLogger(__name__)
class DockerSupervisor(DockerInterface, CoreSysAttributes): class DockerSupervisor(DockerInterface, CoreSysAttributes):
"""Docker hassio wrapper for HomeAssistant.""" """Docker hassio wrapper for Supervisor."""
@property @property
def name(self): def name(self):

View File

@@ -1,4 +1,7 @@
"""Core Exceptions.""" """Core Exceptions."""
import asyncio
import aiohttp
class HassioError(Exception): class HassioError(Exception):
@@ -11,6 +14,29 @@ class HassioNotSupportedError(HassioError):
pass pass
# HomeAssistant
class HomeAssistantError(HassioError):
"""Home Assistant exception."""
pass
class HomeAssistantUpdateError(HomeAssistantError):
"""Error on update of a Home Assistant."""
pass
class HomeAssistantAuthError(HomeAssistantError):
"""Home Assistant Auth API exception."""
pass
class HomeAssistantAPIError(
HomeAssistantAuthError, asyncio.TimeoutError, aiohttp.ClientError):
"""Home Assistant API exception."""
pass
# HassOS # HassOS
class HassOSError(HassioError): class HassOSError(HassioError):

View File

@@ -1,4 +1,5 @@
"""HassOS support on supervisor.""" """HassOS support on supervisor."""
import asyncio
import logging import logging
from pathlib import Path from pathlib import Path
@@ -7,6 +8,7 @@ from cpe import CPE
from .coresys import CoreSysAttributes from .coresys import CoreSysAttributes
from .const import URL_HASSOS_OTA from .const import URL_HASSOS_OTA
from .docker.hassos_cli import DockerHassOSCli
from .exceptions import HassOSNotSupportedError, HassOSUpdateError, DBusError from .exceptions import HassOSNotSupportedError, HassOSUpdateError, DBusError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -18,6 +20,7 @@ class HassOS(CoreSysAttributes):
def __init__(self, coresys): def __init__(self, coresys):
"""Initialize HassOS handler.""" """Initialize HassOS handler."""
self.coresys = coresys self.coresys = coresys
self.instance = DockerHassOSCli(coresys)
self._available = False self._available = False
self._version = None self._version = None
self._board = None self._board = None
@@ -32,11 +35,31 @@ class HassOS(CoreSysAttributes):
"""Return version of HassOS.""" """Return version of HassOS."""
return self._version return self._version
@property
def version_cli(self):
"""Return version of HassOS cli."""
return self.instance.version
@property @property
def version_latest(self): def version_latest(self):
"""Return version of HassOS.""" """Return version of HassOS."""
return self.sys_updater.version_hassos return self.sys_updater.version_hassos
@property
def version_cli_latest(self):
"""Return version of HassOS."""
return self.sys_updater.version_hassos_cli
@property
def need_update(self):
"""Return true if a HassOS update is available."""
return self.version != self.version_latest
@property
def need_cli_update(self):
"""Return true if a HassOS cli update is available."""
return self.version_cli != self.version_cli_latest
@property @property
def board(self): def board(self):
"""Return board name.""" """Return board name."""
@@ -56,6 +79,10 @@ class HassOS(CoreSysAttributes):
try: try:
_LOGGER.info("Fetch OTA update from %s", url) _LOGGER.info("Fetch OTA update from %s", url)
async with self.sys_websession.get(url) as request: async with self.sys_websession.get(url) as request:
if request.status != 200:
raise HassOSUpdateError()
# Download RAUCB file
with raucb.open('wb') as ota_file: with raucb.open('wb') as ota_file:
while True: while True:
chunk = await request.content.read(1048576) chunk = await request.content.read(1048576)
@@ -66,7 +93,7 @@ class HassOS(CoreSysAttributes):
_LOGGER.info("OTA update is downloaded on %s", raucb) _LOGGER.info("OTA update is downloaded on %s", raucb)
return raucb return raucb
except aiohttp.ClientError as err: except (aiohttp.ClientError, asyncio.TimeoutError) as err:
_LOGGER.warning("Can't fetch versions from %s: %s", url, err) _LOGGER.warning("Can't fetch versions from %s: %s", url, err)
except OSError as err: except OSError as err:
@@ -86,7 +113,7 @@ class HassOS(CoreSysAttributes):
cpe = CPE(self.sys_host.info.cpe) cpe = CPE(self.sys_host.info.cpe)
assert cpe.get_product()[0] == 'hassos' assert cpe.get_product()[0] == 'hassos'
except (AssertionError, NotImplementedError): except (AssertionError, NotImplementedError):
_LOGGER.debug("Ignore HassOS") _LOGGER.debug("Found no HassOS")
return return
# Store meta data # Store meta data
@@ -95,6 +122,7 @@ class HassOS(CoreSysAttributes):
self._board = cpe.get_target_hardware()[0] self._board = cpe.get_target_hardware()[0]
_LOGGER.info("Detect HassOS %s on host system", self.version) _LOGGER.info("Detect HassOS %s on host system", self.version)
await self.instance.attach()
def config_sync(self): def config_sync(self):
"""Trigger a host config reload from usb. """Trigger a host config reload from usb.
@@ -142,3 +170,17 @@ class HassOS(CoreSysAttributes):
_LOGGER.error( _LOGGER.error(
"HassOS update fails with: %s", rauc_status.get('LastError')) "HassOS update fails with: %s", rauc_status.get('LastError'))
raise HassOSUpdateError() raise HassOSUpdateError()
async def update_cli(self, version=None):
"""Update local HassOS cli."""
version = version or self.version_cli_latest
if version == self.version_cli:
_LOGGER.warning("Version %s is already installed for CLI", version)
raise HassOSUpdateError()
if await self.instance.update(version):
return
_LOGGER.error("HassOS CLI update fails.")
raise HassOSUpdateError()

View File

@@ -1,5 +1,6 @@
"""HomeAssistant control object.""" """HomeAssistant control object."""
import asyncio import asyncio
from contextlib import asynccontextmanager, suppress
import logging import logging
import os import os
import re import re
@@ -7,15 +8,19 @@ import socket
import time import time
import aiohttp import aiohttp
from aiohttp.hdrs import CONTENT_TYPE from aiohttp import hdrs
import attr import attr
from .const import ( from .const import (
FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_UUID, FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_UUID,
ATTR_BOOT, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG, ATTR_BOOT, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG,
ATTR_WAIT_BOOT, HEADER_HA_ACCESS, CONTENT_TYPE_JSON) ATTR_WAIT_BOOT, ATTR_REFRESH_TOKEN,
HEADER_HA_ACCESS)
from .coresys import CoreSysAttributes from .coresys import CoreSysAttributes
from .docker.homeassistant import DockerHomeAssistant from .docker.homeassistant import DockerHomeAssistant
from .exceptions import (
HomeAssistantUpdateError, HomeAssistantError, HomeAssistantAPIError,
HomeAssistantAuthError)
from .utils import convert_to_ascii, process_lock from .utils import 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
@@ -25,7 +30,7 @@ _LOGGER = logging.getLogger(__name__)
RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml") RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml")
# pylint: disable=invalid-name # pylint: disable=invalid-name
ConfigResult = attr.make_class('ConfigResult', ['valid', 'log']) ConfigResult = attr.make_class('ConfigResult', ['valid', 'log'], frozen=True)
class HomeAssistant(JsonConfig, CoreSysAttributes): class HomeAssistant(JsonConfig, CoreSysAttributes):
@@ -38,6 +43,8 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
self.instance = DockerHomeAssistant(coresys) self.instance = DockerHomeAssistant(coresys)
self.lock = asyncio.Lock(loop=coresys.loop) self.lock = asyncio.Lock(loop=coresys.loop)
self._error_state = False self._error_state = False
# We don't persist access tokens. Instead we fetch new ones when needed
self.access_token = None
async def load(self): async def load(self):
"""Prepare HomeAssistant object.""" """Prepare HomeAssistant object."""
@@ -175,6 +182,16 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
"""Return a UUID of this HomeAssistant.""" """Return a UUID of this HomeAssistant."""
return self._data[ATTR_UUID] return self._data[ATTR_UUID]
@property
def refresh_token(self):
"""Return the refresh token to authenticate with HomeAssistant."""
return self._data.get(ATTR_REFRESH_TOKEN)
@refresh_token.setter
def refresh_token(self, value):
"""Set Home Assistant refresh_token."""
self._data[ATTR_REFRESH_TOKEN] = value
@process_lock @process_lock
async def install_landingpage(self): async def install_landingpage(self):
"""Install a landingpage.""" """Install a landingpage."""
@@ -186,7 +203,11 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
await asyncio.sleep(60) await asyncio.sleep(60)
# Run landingpage after installation # Run landingpage after installation
await self._start() _LOGGER.info("Start landingpage")
try:
await self._start()
except HomeAssistantError:
_LOGGER.warning("Can't start landingpage")
@process_lock @process_lock
async def install(self): async def install(self):
@@ -205,46 +226,57 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
# finishing # finishing
_LOGGER.info("HomeAssistant docker now installed") _LOGGER.info("HomeAssistant docker now installed")
if self.boot: try:
if not self.boot:
return
_LOGGER.info("Start HomeAssistant")
await self._start() await self._start()
await self.instance.cleanup() except HomeAssistantError:
_LOGGER.error("Can't start HomeAssistant!")
finally:
await self.instance.cleanup()
@process_lock @process_lock
async def update(self, version=None): async def update(self, version=None):
"""Update HomeAssistant version.""" """Update HomeAssistant version."""
version = version or self.last_version version = version or self.last_version
rollback = self.version rollback = self.version if not self.error_state else None
running = await self.instance.is_running() running = await self.instance.is_running()
exists = await self.instance.exists() exists = await self.instance.exists()
if exists and version == self.instance.version: if exists and version == self.instance.version:
_LOGGER.warning("Version %s is already installed", version) _LOGGER.warning("Version %s is already installed", version)
return False return HomeAssistantUpdateError()
# process a update # process a update
async def _update(to_version): async def _update(to_version):
"""Run Home Assistant update.""" """Run Home Assistant update."""
try: try:
return await self.instance.update(to_version) _LOGGER.info("Update HomeAssistant to version %s", to_version)
if not await self.instance.update(to_version):
raise HomeAssistantUpdateError()
finally: finally:
if running: if running:
await self._start() await self._start()
_LOGGER.info("Successfull run HomeAssistant %s", to_version)
# Update Home Assistant # Update Home Assistant
ret = await _update(version) with suppress(HomeAssistantError):
await _update(version)
return
# Update going wrong, revert it # Update going wrong, revert it
if self.error_state and rollback: if self.error_state and rollback:
_LOGGER.fatal("Home Assistant update fails -> rollback!") _LOGGER.fatal("HomeAssistant update fails -> rollback!")
ret = await _update(rollback) await _update(rollback)
else:
return ret raise HomeAssistantUpdateError()
async def _start(self): async def _start(self):
"""Start HomeAssistant docker & wait.""" """Start HomeAssistant docker & wait."""
if not await self.instance.run(): if not await self.instance.run():
return False raise HomeAssistantError()
return await self._block_till_run() await self._block_till_run()
@process_lock @process_lock
def start(self): def start(self):
@@ -266,7 +298,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
async def restart(self): async def restart(self):
"""Restart HomeAssistant docker.""" """Restart HomeAssistant docker."""
await self.instance.stop() await self.instance.stop()
return await self._start() await self._start()
def logs(self): def logs(self):
"""Get HomeAssistant docker logs. """Get HomeAssistant docker logs.
@@ -309,7 +341,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
# if not valid # if not valid
if result.exit_code is None: if result.exit_code is None:
return ConfigResult(False, "") raise HomeAssistantError()
# parse output # parse output
log = convert_to_ascii(result.output) log = convert_to_ascii(result.output)
@@ -317,51 +349,86 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
return ConfigResult(False, log) return ConfigResult(False, log)
return ConfigResult(True, log) return ConfigResult(True, log)
async def check_api_state(self): async def ensure_access_token(self):
"""Check if Home-Assistant up and running.""" """Ensures there is an access token."""
url = f"{self.api_url}/api/" if self.access_token is not None:
header = {CONTENT_TYPE: CONTENT_TYPE_JSON} return
if self.api_password: with suppress(asyncio.TimeoutError, aiohttp.ClientError):
header.update({HEADER_HA_ACCESS: self.api_password})
try:
# pylint: disable=bad-continuation
async with self.sys_websession_ssl.get( async with self.sys_websession_ssl.get(
url, headers=header, timeout=30) as request: f"{self.api_url}/auth/token",
status = request.status timeout=30,
data={
"grant_type": "refresh_token",
"refresh_token": self.refresh_token
}
) as resp:
if resp.status != 200:
_LOGGER.error("Authenticate problem with HomeAssistant!")
raise HomeAssistantAuthError()
tokens = await resp.json()
self.access_token = tokens['access_token']
return
except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Can't update HomeAssistant access token!")
return False raise HomeAssistantAPIError()
if status not in (200, 201): @asynccontextmanager
_LOGGER.warning("Home-Assistant API config missmatch") async def make_request(self, method, path, json=None, content_type=None,
return True data=None, timeout=30):
"""Async context manager to make a request with right auth."""
url = f"{self.api_url}/{path}"
headers = {}
# Passthrough content type
if content_type is not None:
headers[hdrs.CONTENT_TYPE] = content_type
# Set old API Password
if self.api_password:
headers[HEADER_HA_ACCESS] = self.api_password
for _ in (1, 2):
# Prepare Access token
if self.refresh_token:
await self.ensure_access_token()
headers[hdrs.AUTHORIZATION] = f'Bearer {self.access_token}'
async with getattr(self.sys_websession_ssl, method)(
url, data=data, timeout=timeout, json=json, headers=headers
) as resp:
# Access token expired
if resp.status == 401 and self.refresh_token:
self.access_token = None
continue
yield resp
return
raise HomeAssistantAPIError()
async def check_api_state(self):
"""Return True if Home-Assistant up and running."""
with suppress(HomeAssistantAPIError):
async with self.make_request('get', 'api/') as resp:
if resp.status in (200, 201):
return True
err = resp.status
_LOGGER.warning("Home-Assistant API config missmatch: %d", err)
return False
async def send_event(self, event_type, event_data=None): async def send_event(self, event_type, event_data=None):
"""Send event to Home-Assistant.""" """Send event to Home-Assistant."""
url = f"{self.api_url}/api/events/{event_type}" with suppress(HomeAssistantAPIError):
header = {CONTENT_TYPE: CONTENT_TYPE_JSON} async with self.make_request(
'get', f'api/events/{event_type}'
) as resp:
if resp.status in (200, 201):
return
err = resp.status
if self.api_password: _LOGGER.warning("HomeAssistant event %s fails: %s", event_type, err)
header.update({HEADER_HA_ACCESS: self.api_password}) return HomeAssistantError()
try:
# pylint: disable=bad-continuation
async with self.sys_websession_ssl.post(
url, headers=header, timeout=30,
json=event_data) as request:
status = request.status
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
_LOGGER.warning(
"Home-Assistant event %s fails: %s", event_type, err)
return False
if status not in (200, 201):
_LOGGER.warning("Home-Assistant event %s fails", event_type)
return False
return True
async def _block_till_run(self): async def _block_till_run(self):
"""Block until Home-Assistant is booting up or startup timeout.""" """Block until Home-Assistant is booting up or startup timeout."""
@@ -374,27 +441,28 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
result = sock.connect_ex((str(self.api_ip), self.api_port)) result = sock.connect_ex((str(self.api_ip), self.api_port))
sock.close() sock.close()
# Check if the port is available
if result == 0: if result == 0:
return True return True
return False
except OSError: except OSError:
pass pass
return False
while time.monotonic() - start_time < self.wait_boot: while time.monotonic() - start_time < self.wait_boot:
# Check if API response # Check if API response
if await self.sys_run_in_executor(check_port): if await self.sys_run_in_executor(check_port):
_LOGGER.info("Detect a running Home-Assistant instance") _LOGGER.info("Detect a running HomeAssistant instance")
self._error_state = False self._error_state = False
return True return
# wait and don't hit the system
await asyncio.sleep(10)
# Check if Container is is_running # Check if Container is is_running
if not await self.instance.is_running(): if not await self.instance.is_running():
_LOGGER.error("Home Assistant is crashed!") _LOGGER.error("Home Assistant is crashed!")
break break
# wait and don't hit the system _LOGGER.warning("Don't wait anymore of HomeAssistant startup!")
await asyncio.sleep(10)
_LOGGER.warning("Don't wait anymore of Home-Assistant startup!")
self._error_state = True self._error_state = True
return False raise HomeAssistantError()

View File

@@ -3,6 +3,8 @@ import asyncio
import logging import logging
import shlex import shlex
import async_timeout
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
COMMAND = "socat UDP-RECVFROM:53,fork UDP-SENDTO:127.0.0.11:53" COMMAND = "socat UDP-RECVFROM:53,fork UDP-SENDTO:127.0.0.11:53"
@@ -38,5 +40,10 @@ class DNSForward:
return return
self.proc.kill() self.proc.kill()
await self.proc.wait() try:
with async_timeout.timeout(5):
await self.proc.wait()
except asyncio.TimeoutError:
_LOGGER.warning("Stop waiting for DNS shutdown")
_LOGGER.info("Stop DNS forwarding") _LOGGER.info("Stop DNS forwarding")

View File

@@ -16,7 +16,7 @@ ALL_FOLDERS = [FOLDER_HOMEASSISTANT, FOLDER_SHARE, FOLDER_ADDONS, FOLDER_SSL]
def unique_addons(addons_list): def unique_addons(addons_list):
"""Validate that an add-on is unique.""" """Validate that an add-on is unique."""
single = set([addon[ATTR_SLUG] for addon in addons_list]) single = set(addon[ATTR_SLUG] for addon in addons_list)
if len(single) != len(addons_list): if len(single) != len(addons_list):
raise vol.Invalid("Invalid addon list on snapshot!") raise vol.Invalid("Invalid addon list on snapshot!")

View File

@@ -1,4 +1,5 @@
"""HomeAssistant control object.""" """HomeAssistant control object."""
import asyncio
import logging import logging
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
@@ -60,7 +61,7 @@ class Supervisor(CoreSysAttributes):
async with self.sys_websession.get(url, timeout=10) as request: async with self.sys_websession.get(url, timeout=10) as request:
data = await request.text() data = await request.text()
except aiohttp.ClientError as err: except (aiohttp.ClientError, asyncio.TimeoutError) as err:
_LOGGER.warning("Can't fetch AppArmor profile: %s", err) _LOGGER.warning("Can't fetch AppArmor profile: %s", err)
return return

View File

@@ -10,6 +10,7 @@ HASS_WATCHDOG_API = 'HASS_WATCHDOG_API'
RUN_UPDATE_SUPERVISOR = 29100 RUN_UPDATE_SUPERVISOR = 29100
RUN_UPDATE_ADDONS = 57600 RUN_UPDATE_ADDONS = 57600
RUN_UPDATE_HASSOSCLI = 29100
RUN_RELOAD_ADDONS = 21600 RUN_RELOAD_ADDONS = 21600
RUN_RELOAD_SNAPSHOTS = 72000 RUN_RELOAD_SNAPSHOTS = 72000
@@ -35,6 +36,8 @@ class Tasks(CoreSysAttributes):
self._update_addons, RUN_UPDATE_ADDONS)) self._update_addons, RUN_UPDATE_ADDONS))
self.jobs.add(self.sys_scheduler.register_task( self.jobs.add(self.sys_scheduler.register_task(
self._update_supervisor, RUN_UPDATE_SUPERVISOR)) self._update_supervisor, RUN_UPDATE_SUPERVISOR))
self.jobs.add(self.sys_scheduler.register_task(
self._update_hassos_cli, RUN_UPDATE_HASSOSCLI))
self.jobs.add(self.sys_scheduler.register_task( self.jobs.add(self.sys_scheduler.register_task(
self.sys_addons.reload, RUN_RELOAD_ADDONS)) self.sys_addons.reload, RUN_RELOAD_ADDONS))
@@ -79,7 +82,7 @@ class Tasks(CoreSysAttributes):
if not self.sys_supervisor.need_update: if not self.sys_supervisor.need_update:
return return
# don't perform an update on beta/dev channel # don't perform an update on dev channel
if self.sys_dev: if self.sys_dev:
_LOGGER.warning("Ignore Hass.io update on dev channel!") _LOGGER.warning("Ignore Hass.io update on dev channel!")
return return
@@ -131,5 +134,20 @@ class Tasks(CoreSysAttributes):
return return
_LOGGER.error("Watchdog found a problem with Home-Assistant API!") _LOGGER.error("Watchdog found a problem with Home-Assistant API!")
await self.sys_homeassistant.restart() try:
self._cache[HASS_WATCHDOG_API] = 0 await self.sys_homeassistant.restart()
finally:
self._cache[HASS_WATCHDOG_API] = 0
async def _update_hassos_cli(self):
"""Check and run update of HassOS CLI."""
if not self.sys_hassos.need_cli_update:
return
# don't perform an update on dev channel
if self.sys_dev:
_LOGGER.warning("Ignore HassOS CLI update on dev channel!")
return
_LOGGER.info("Found new HassOS CLI version")
await self.sys_hassos.update_cli()

View File

@@ -1,4 +1,5 @@
"""Fetch last versions from webserver.""" """Fetch last versions from webserver."""
import asyncio
from contextlib import suppress from contextlib import suppress
from datetime import timedelta from datetime import timedelta
import json import json
@@ -8,7 +9,7 @@ import aiohttp
from .const import ( from .const import (
URL_HASSIO_VERSION, FILE_HASSIO_UPDATER, ATTR_HOMEASSISTANT, ATTR_HASSIO, URL_HASSIO_VERSION, FILE_HASSIO_UPDATER, ATTR_HOMEASSISTANT, ATTR_HASSIO,
ATTR_CHANNEL, ATTR_HASSOS) ATTR_CHANNEL, ATTR_HASSOS, ATTR_HASSOS_CLI)
from .coresys import CoreSysAttributes from .coresys import CoreSysAttributes
from .utils import AsyncThrottle from .utils import AsyncThrottle
from .utils.json import JsonConfig from .utils.json import JsonConfig
@@ -51,6 +52,11 @@ class Updater(JsonConfig, CoreSysAttributes):
"""Return last version of hassos.""" """Return last version of hassos."""
return self._data.get(ATTR_HASSOS) return self._data.get(ATTR_HASSOS)
@property
def version_hassos_cli(self):
"""Return last version of hassos cli."""
return self._data.get(ATTR_HASSOS_CLI)
@property @property
def channel(self): def channel(self):
"""Return upstream channel of hassio instance.""" """Return upstream channel of hassio instance."""
@@ -76,7 +82,7 @@ class Updater(JsonConfig, CoreSysAttributes):
async with self.sys_websession.get(url, timeout=10) as request: async with self.sys_websession.get(url, timeout=10) as request:
data = await request.json(content_type=None) data = await request.json(content_type=None)
except aiohttp.ClientError as err: except (aiohttp.ClientError, asyncio.TimeoutError) as err:
_LOGGER.warning("Can't fetch versions from %s: %s", url, err) _LOGGER.warning("Can't fetch versions from %s: %s", url, err)
raise HassioUpdaterError() from None raise HassioUpdaterError() from None
@@ -99,6 +105,7 @@ class Updater(JsonConfig, CoreSysAttributes):
# update hassos version # update hassos version
if self.sys_hassos.available and board: if self.sys_hassos.available and board:
self._data[ATTR_HASSOS] = data['hassos'][board] self._data[ATTR_HASSOS] = data['hassos'][board]
self._data[ATTR_HASSOS_CLI] = data['hassos-cli']
except KeyError as err: except KeyError as err:
_LOGGER.warning("Can't process version data: %s", err) _LOGGER.warning("Can't process version data: %s", err)

View File

@@ -3,7 +3,6 @@ from datetime import datetime, timedelta, timezone
import logging import logging
import re import re
import aiohttp
import pytz import pytz
UTC = pytz.utc UTC = pytz.utc
@@ -23,22 +22,6 @@ DATETIME_RE = re.compile(
) )
async def fetch_timezone(websession):
"""Read timezone from freegeoip."""
data = {}
try:
async with websession.get(FREEGEOIP_URL, timeout=10) as request:
data = await request.json()
except aiohttp.ClientError as err:
_LOGGER.warning("Can't fetch freegeoip data: %s", err)
except ValueError as err:
_LOGGER.warning("Error on parse freegeoip data: %s", err)
return data.get('time_zone', 'UTC')
# Copyright (c) Django Software Foundation and individual contributors. # Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved. # All rights reserved.
# https://github.com/django/django/blob/master/LICENSE # https://github.com/django/django/blob/master/LICENSE

View File

@@ -247,7 +247,7 @@ class DBusSignalWrapper:
self._proc.send_signal(SIGINT) self._proc.send_signal(SIGINT)
await self._proc.communicate() await self._proc.communicate()
async def __aiter__(self): def __aiter__(self):
"""Start Iteratation.""" """Start Iteratation."""
return self return self

View File

@@ -45,7 +45,7 @@ class JsonConfig:
if self._file.is_file(): if self._file.is_file():
try: try:
self._data = read_json_file(self._file) self._data = read_json_file(self._file)
except (OSError, json.JSONDecodeError): except (OSError, json.JSONDecodeError, UnicodeDecodeError):
_LOGGER.warning("Can't read %s", self._file) _LOGGER.warning("Can't read %s", self._file)
self._data = {} self._data = {}

View File

@@ -9,7 +9,8 @@ from .const import (
ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_CHANNEL, ATTR_TIMEZONE, ATTR_HASSOS, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_CHANNEL, ATTR_TIMEZONE, ATTR_HASSOS,
ATTR_ADDONS_CUSTOM_LIST, ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO, ATTR_ADDONS_CUSTOM_LIST, ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO,
ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG,
ATTR_WAIT_BOOT, ATTR_UUID, CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV) ATTR_WAIT_BOOT, ATTR_UUID, ATTR_REFRESH_TOKEN, ATTR_HASSOS_CLI,
CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV)
RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$") RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$")
@@ -88,6 +89,7 @@ SCHEMA_HASS_CONFIG = vol.Schema({
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str), vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT, vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)), vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_SSL, default=False): vol.Boolean(), vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(), vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
vol.Optional(ATTR_WAIT_BOOT, default=600): vol.Optional(ATTR_WAIT_BOOT, default=600):
@@ -100,6 +102,7 @@ SCHEMA_UPDATER_CONFIG = vol.Schema({
vol.Optional(ATTR_HOMEASSISTANT): vol.Coerce(str), vol.Optional(ATTR_HOMEASSISTANT): vol.Coerce(str),
vol.Optional(ATTR_HASSIO): vol.Coerce(str), vol.Optional(ATTR_HASSIO): vol.Coerce(str),
vol.Optional(ATTR_HASSOS): vol.Coerce(str), vol.Optional(ATTR_HASSOS): vol.Coerce(str),
vol.Optional(ATTR_HASSOS_CLI): vol.Coerce(str),
}, extra=vol.REMOVE_EXTRA) }, extra=vol.REMOVE_EXTRA)

View File

@@ -21,7 +21,6 @@ disable=
abstract-class-little-used, abstract-class-little-used,
abstract-class-not-used, abstract-class-not-used,
unused-argument, unused-argument,
global-statement,
redefined-variable-type, redefined-variable-type,
too-many-arguments, too-many-arguments,
too-many-branches, too-many-branches,
@@ -32,7 +31,10 @@ disable=
too-many-statements, too-many-statements,
too-many-lines, too-many-lines,
too-few-public-methods, too-few-public-methods,
abstract-method abstract-method,
no-else-return,
useless-return,
not-async-context-manager
[EXCEPTIONS] [EXCEPTIONS]
overgeneral-exceptions=Exception,HomeAssistantError overgeneral-exceptions=Exception,HomeAssistantError

13
requirements.txt Normal file
View File

@@ -0,0 +1,13 @@
attr==0.3.1
async_timeout==3.0.0
aiohttp==3.3.2
docker==3.4.0
colorlog==3.1.2
voluptuous==0.11.1
gitpython==2.1.10
pytz==2018.4
pyudev==0.21.0
pycryptodome==3.6.4
cpe==1.2.1
uvloop==0.11.1
cchardet==2.1.1

View File

@@ -38,18 +38,5 @@ setup(
'hassio.utils', 'hassio.utils',
'hassio.snapshots' 'hassio.snapshots'
], ],
include_package_data=True, include_package_data=True
install_requires=[
'attr==0.3.1',
'async_timeout==3.0.0',
'aiohttp==3.3.2',
'docker==3.3.0',
'colorlog==3.1.2',
'voluptuous==0.11.1',
'gitpython==2.1.10',
'pytz==2018.4',
'pyudev==0.21.0',
'pycryptodome==3.4.11',
"cpe==1.2.1"
]
) )

View File

@@ -3,8 +3,9 @@ envlist = lint
[testenv] [testenv]
deps = deps =
flake8 flake8==3.5.0
pylint pylint==2.0.0
-r{toxinidir}/requirements.txt
[testenv:lint] [testenv:lint]
basepython = python3 basepython = python3