Compare commits

...

25 Commits
135 ... 139

Author SHA1 Message Date
Pascal Vizeli
6e7cf5e4c9 Merge pull request #796 from home-assistant/dev
Release 139
2018-11-05 16:19:17 +01:00
Pascal Vizeli
11f8c97347 Fix discovery update (#795)
* Update discovery.py

* Update discovery.py

* Update discovery.py

* Update discovery.py

* Update discovery.py

* Update discovery.py

* Update discovery.py
2018-11-05 14:59:57 +01:00
Pascal Vizeli
a1461fd518 Update requirements.txt 2018-11-05 13:53:16 +01:00
Pascal Vizeli
fa5c2e37d3 Discovery default config (#793) 2018-11-05 07:45:28 +01:00
luca-simonetti
1f091b20ad fix: use a different convention to handle multiple devices on same card (#767)
* fix: use a different convention to handle multiple devices on same card

* fix: use a different convention to handle multiple devices on same card

* Update alsa.py

* Update alsa.py
2018-11-02 10:47:25 +01:00
Pascal Vizeli
d3b4a03851 Catch exception on watchdog for pretty log (#778)
* Catch exception on watchdog for pretty log

* Update tasks.py
2018-10-29 16:40:19 +01:00
Jorim Tielemans
fb12fee59b Expand add-on installation error message (#783)
* Expand error message

Since an add-on is only available for certain machine and architecture combination we should log both.

* Update addon.py
2018-10-27 15:24:56 +02:00
Pascal Vizeli
7a87d2334a flake8 update to 3.6.0 (#777)
* flake8 update to 3.6.0

* fix lint
2018-10-27 15:23:26 +02:00
Pascal Vizeli
9591e71138 Update auth.py (#771) 2018-10-24 14:02:16 +02:00
Ville Skyttä
cecad526a2 Grammar and spelling fixes (#772) 2018-10-24 14:01:28 +02:00
Pascal Vizeli
53dab4ee45 Bump version 139 2018-10-16 12:52:19 +02:00
Pascal Vizeli
8abbba46c7 Merge pull request #766 from home-assistant/dev
Release 138
2018-10-16 12:51:47 +02:00
Pascal Vizeli
0f01ac1b59 Fix syntax 2018-10-16 12:45:06 +02:00
Pascal Vizeli
aa8ab593c0 Rename login_backend to auth_api (#764)
* Update const.py

* Update validate.py

* Update addon.py

* Update auth.py

* Update addons.py

* Update API.md
2018-10-16 12:33:40 +02:00
Pascal Vizeli
84f791220e Don't clean cache on fake auth (#765)
* Don't clean cache on fake auth

* Update auth.py
2018-10-16 12:30:24 +02:00
Pascal Vizeli
cee2c5469f Bump version 138 2018-10-15 15:25:29 +02:00
Pascal Vizeli
6e75964a8b Merge pull request #761 from home-assistant/dev
Release 137
2018-10-15 15:25:05 +02:00
Pascal Vizeli
5ab5036504 Fix proxy handling with failing connection (#760)
* Fix proxy handling with failing connection

* fix lint

* Fix exception handling

* clenaup error handling

* Fix type error

* Fix event stream

* Fix stream handling

* Fix

* Fix lint

* Handle

* Update proxy.py

* fix lint
2018-10-15 13:01:52 +02:00
Pascal Vizeli
000a3c1f7e Bump to 137 2018-10-12 14:39:47 +02:00
Pascal Vizeli
8ea123eb94 Merge pull request #754 from home-assistant/dev
Release 136
2018-10-12 14:39:18 +02:00
Pascal Vizeli
571c42ef7d Create role for backup add-ons (#755)
* Create role for backup add-ons

* Update validate.py

* Update security.py
2018-10-12 12:48:12 +02:00
Pascal Vizeli
8443da0b9f Add-on SSO support with Home Assistant auth system (#752)
* Create auth.py

* Finish auth cache

* Add documentation

* Add valid schema

* Update auth.py

* Update auth.py

* Update security.py

* Create auth.py

* Update coresys.py

* Update bootstrap.py

* Update const.py

* Update validate.py

* Update const.py

* Update addon.py

* Update auth.py

* Update __init__.py

* Update auth.py

* Update auth.py

* Update auth.py

* Update const.py

* Update auth.py

* Update auth.py

* Update auth.py

* Update validate.py

* Update coresys.py

* Update auth.py

* Update auth.py

* more security

* Update API.md

* Update auth.py

* Update auth.py

* Update auth.py

* Update auth.py

* Update auth.py

* Update homeassistant.py

* Update homeassistant.py
2018-10-12 12:21:48 +02:00
Pascal Vizeli
7dbbcf24c8 Check exists hardware for audio/gpio devices (#753)
* Update hardware.py

* Update addon.py

* Update hardware.py

* Update addon.py
2018-10-12 10:22:58 +02:00
Pascal Vizeli
468cb0c36b Rename info (#750)
* Rename version to info

* fix security
2018-10-10 16:46:34 +02:00
Pascal Vizeli
78e093df96 Bump version 136 2018-10-09 17:10:25 +02:00
25 changed files with 330 additions and 80 deletions

18
API.md
View File

@@ -415,7 +415,7 @@ Proxy to real websocket instance.
### RESTful for API addons
If a add-on will call itself, you can use `/addons/self/...`.
If an add-on will call itself, you can use `/addons/self/...`.
- GET `/addons`
@@ -488,6 +488,7 @@ Get all available addons.
"hassio_api": "bool",
"hassio_role": "default|homeassistant|manager|admin",
"homeassistant_api": "bool",
"auth_api": "bool",
"full_access": "bool",
"protected": "bool",
"rating": "1-6",
@@ -663,14 +664,27 @@ return:
### Misc
- GET `/version`
- GET `/info`
```json
{
"supervisor": "version",
"homeassistant": "version",
"hassos": "null|version",
"hostname": "name",
"machine": "type",
"arch": "arch",
"channel": "stable|beta|dev"
}
```
### Auth / SSO API
You can use the user system on homeassistant. We handle this auth system on
supervisor.
You can call post `/auth`
We support:
- Json `{ "user|name": "...", "password": "..." }`
- application/x-www-form-urlencoded `user|name=...&password=...`
- BasicAuth

View File

@@ -28,7 +28,7 @@ from ..const import (
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES,
ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_FULL_ACCESS,
ATTR_PROTECTED, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE,
ATTR_MACHINE,
ATTR_MACHINE, ATTR_AUTH_API,
SECURITY_PROFILE, SECURITY_DISABLE, SECURITY_DEFAULT)
from ..coresys import CoreSysAttributes
from ..docker.addon import DockerAddon
@@ -411,6 +411,11 @@ class Addon(CoreSysAttributes):
"""Return True if the add-on read access to devicetree."""
return self._mesh[ATTR_DEVICETREE]
@property
def access_auth_api(self):
"""Return True if the add-on access to login/auth backend."""
return self._mesh[ATTR_AUTH_API]
@property
def with_audio(self):
"""Return True if the add-on access to audio."""
@@ -669,7 +674,8 @@ class Addon(CoreSysAttributes):
"""Install an add-on."""
if not self.available:
_LOGGER.error(
"Add-on %s not supported on %s", self._id, self.sys_arch)
"Add-on %s not supported on %s with %s architecture",
self._id, self.sys_machine, self.sys_arch)
return False
if self.is_installed:

View File

@@ -20,12 +20,13 @@ from ..const import (
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY,
ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_PROTECTED,
ATTR_FULL_ACCESS, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE,
ATTR_MACHINE,
ATTR_MACHINE, ATTR_AUTH_API,
PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO,
PRIVILEGED_IPC_LOCK, PRIVILEGED_SYS_TIME, PRIVILEGED_SYS_NICE,
PRIVILEGED_SYS_RESOURCE, PRIVILEGED_SYS_PTRACE, PRIVILEGED_DAC_READ_SEARCH,
ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_MANAGER, ROLE_ADMIN)
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE, UUID_MATCH
ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_MANAGER, ROLE_ADMIN, ROLE_BACKUP)
from ..validate import (
NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE, UUID_MATCH, SHA256)
from ..services.validate import DISCOVERY_SERVICES
_LOGGER = logging.getLogger(__name__)
@@ -84,6 +85,7 @@ PRIVILEGED_ALL = [
ROLE_ALL = [
ROLE_DEFAULT,
ROLE_HOMEASSISTANT,
ROLE_BACKUP,
ROLE_MANAGER,
ROLE_ADMIN,
]
@@ -143,6 +145,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Optional(ATTR_STDIN, default=False): vol.Boolean(),
vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(),
vol.Optional(ATTR_DOCKER_API, default=False): vol.Boolean(),
vol.Optional(ATTR_AUTH_API, default=False): vol.Boolean(),
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
vol.Optional(ATTR_DISCOVERY): [vol.In(DISCOVERY_SERVICES)],
vol.Required(ATTR_OPTIONS): dict,
@@ -187,7 +190,7 @@ SCHEMA_BUILD_CONFIG = vol.Schema({
SCHEMA_ADDON_USER = vol.Schema({
vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH,
vol.Optional(ATTR_ACCESS_TOKEN): vol.Match(r"^[0-9a-f]{64}$"),
vol.Optional(ATTR_ACCESS_TOKEN): SHA256,
vol.Optional(ATTR_OPTIONS, default=dict): dict,
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
vol.Optional(ATTR_BOOT):

View File

@@ -5,16 +5,17 @@ from pathlib import Path
from aiohttp import web
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 .info import APIInfo
from .proxy import APIProxy
from .supervisor import APISupervisor
from .snapshots import APISnapshots
from .services import APIServices
from .version import APIVersion
from .security import SecurityMiddleware
from ..coresys import CoreSysAttributes
@@ -48,7 +49,8 @@ class RestAPI(CoreSysAttributes):
self._register_snapshots()
self._register_discovery()
self._register_services()
self._register_version()
self._register_info()
self._register_auth()
def _register_host(self):
"""Register hostcontrol functions."""
@@ -92,13 +94,22 @@ class RestAPI(CoreSysAttributes):
web.get('/hardware/audio', api_hardware.audio),
])
def _register_version(self):
"""Register version functions."""
api_version = APIVersion()
api_version.coresys = self.coresys
def _register_info(self):
"""Register info functions."""
api_info = APIInfo()
api_info.coresys = self.coresys
self.webapp.add_routes([
web.get('/version', api_version.info),
web.get('/info', api_info.info),
])
def _register_auth(self):
"""Register auth functions."""
api_auth = APIAuth()
api_auth.coresys = self.coresys
self.webapp.add_routes([
web.post('/auth', api_auth.auth),
])
def _register_supervisor(self):

View File

@@ -20,7 +20,7 @@ from ..const import (
ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES,
ATTR_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API,
ATTR_FULL_ACCESS, ATTR_PROTECTED, ATTR_RATING, ATTR_HOST_PID,
ATTR_HASSIO_ROLE, ATTR_MACHINE, ATTR_AVAILABLE,
ATTR_HASSIO_ROLE, ATTR_MACHINE, ATTR_AVAILABLE, ATTR_AUTH_API,
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT, REQUEST_FROM)
from ..coresys import CoreSysAttributes
from ..validate import DOCKER_PORTS, ALSA_DEVICE
@@ -149,6 +149,7 @@ class APIAddons(CoreSysAttributes):
ATTR_STDIN: addon.with_stdin,
ATTR_HASSIO_API: addon.access_hassio_api,
ATTR_HASSIO_ROLE: addon.hassio_role,
ATTR_AUTH_API: addon.access_auth_api,
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
ATTR_GPIO: addon.with_gpio,
ATTR_DEVICETREE: addon.with_devicetree,

61
hassio/api/auth.py Normal file
View File

@@ -0,0 +1,61 @@
"""Init file for Hass.io auth/SSO RESTful API."""
import logging
from aiohttp import BasicAuth
from aiohttp.web_exceptions import HTTPUnauthorized
from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION, WWW_AUTHENTICATE
from .utils import api_process
from ..const import REQUEST_FROM, CONTENT_TYPE_JSON, CONTENT_TYPE_URL
from ..coresys import CoreSysAttributes
from ..exceptions import APIForbidden
_LOGGER = logging.getLogger(__name__)
class APIAuth(CoreSysAttributes):
"""Handle RESTful API for auth functions."""
def _process_basic(self, request, addon):
"""Process login request with basic auth.
Return a coroutine.
"""
auth = BasicAuth.decode(request.headers[AUTHORIZATION])
return self.sys_auth.check_login(addon, auth.login, auth.password)
def _process_dict(self, request, addon, data):
"""Process login with dict data.
Return a coroutine.
"""
username = data.get('username') or data.get('user')
password = data.get('password')
return self.sys_auth.check_login(addon, username, password)
@api_process
async def auth(self, request):
"""Process login request."""
addon = request[REQUEST_FROM]
if not addon.access_auth_api:
raise APIForbidden("Can't use Home Assistant auth!")
# BasicAuth
if AUTHORIZATION in request.headers:
return await self._process_basic(request, addon)
# Json
if request.headers.get(CONTENT_TYPE) == CONTENT_TYPE_JSON:
data = await request.json()
return await self._process_dict(request, addon, data)
# URL encoded
if request.headers.get(CONTENT_TYPE) == CONTENT_TYPE_URL:
data = await request.post()
return await self._process_dict(request, addon, data)
raise HTTPUnauthorized(headers={
WWW_AUTHENTICATE: "Basic realm=\"Hass.io Authentication\""
})

View File

@@ -1,4 +1,4 @@
"""Init file for Hass.io version RESTful API."""
"""Init file for Hass.io info RESTful API."""
import logging
from .utils import api_process
@@ -10,12 +10,12 @@ from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__)
class APIVersion(CoreSysAttributes):
"""Handle RESTful API for version functions."""
class APIInfo(CoreSysAttributes):
"""Handle RESTful API for info functions."""
@api_process
async def info(self, request):
"""Show version info."""
"""Show system info."""
return {
ATTR_SUPERVISOR: self.sys_supervisor.version,
ATTR_HOMEASSISTANT: self.sys_homeassistant.version,

View File

@@ -5,14 +5,15 @@ import logging
import aiohttp
from aiohttp import web
from aiohttp.web_exceptions import (
HTTPBadGateway, HTTPInternalServerError, HTTPUnauthorized)
from aiohttp.web_exceptions import HTTPBadGateway, HTTPUnauthorized
from aiohttp.client_exceptions import ClientConnectorError
from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION
import async_timeout
from ..const import HEADER_HA_ACCESS
from ..coresys import CoreSysAttributes
from ..exceptions import HomeAssistantAuthError, HomeAssistantAPIError
from ..exceptions import (
HomeAssistantAuthError, HomeAssistantAPIError, APIError)
_LOGGER = logging.getLogger(__name__)
@@ -82,19 +83,13 @@ class APIProxy(CoreSysAttributes):
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
async for data in client.content:
await response.write(data)
except aiohttp.ClientError:
except (aiohttp.ClientError, aiohttp.ClientPayloadError):
pass
finally:
client.close()
_LOGGER.info("Home Assistant EventStream close")
_LOGGER.info("Home Assistant EventStream close")
return response
async def api(self, request):
@@ -117,7 +112,7 @@ class APIProxy(CoreSysAttributes):
try:
client = await self.sys_websession_ssl.ws_connect(
url, heartbeat=60, verify_ssl=False)
url, heartbeat=30, verify_ssl=False)
# Handle authentication
data = await client.receive_json()
@@ -129,7 +124,7 @@ class APIProxy(CoreSysAttributes):
# Invalid protocol
_LOGGER.error(
"Got unexpected response from HA WebSocket: %s", data)
raise HTTPBadGateway()
raise APIError()
if self.sys_homeassistant.refresh_token:
await self.sys_homeassistant.ensure_access_token()
@@ -156,19 +151,19 @@ class APIProxy(CoreSysAttributes):
raise HomeAssistantAuthError()
except (RuntimeError, ValueError) as err:
except (RuntimeError, ValueError, ClientConnectorError) as err:
_LOGGER.error("Client error on WebSocket API %s.", err)
except HomeAssistantAuthError as err:
except HomeAssistantAuthError:
_LOGGER.error("Failed authentication to Home Assistant WebSocket")
raise HTTPBadGateway()
raise APIError()
async def websocket(self, request):
"""Initialize a WebSocket API connection."""
_LOGGER.info("Home Assistant WebSocket API request initialize")
# init server
server = web.WebSocketResponse(heartbeat=60)
server = web.WebSocketResponse(heartbeat=30)
await server.prepare(request)
# handle authentication
@@ -200,10 +195,13 @@ class APIProxy(CoreSysAttributes):
})
except (RuntimeError, ValueError) as err:
_LOGGER.error("Can't initialize handshake: %s", err)
raise HTTPInternalServerError() from None
return server
# init connection to hass
client = await self._websocket_client()
try:
client = await self._websocket_client()
except APIError:
return server
_LOGGER.info("Home Assistant WebSocket API request running")
try:
@@ -238,7 +236,7 @@ class APIProxy(CoreSysAttributes):
except asyncio.CancelledError:
pass
except RuntimeError as err:
except (RuntimeError, ConnectionError, TypeError) as err:
_LOGGER.info("Home Assistant WebSocket API error: %s", err)
finally:
@@ -248,8 +246,10 @@ class APIProxy(CoreSysAttributes):
server_read.cancel()
# close connections
await client.close()
await server.close()
if not client.closed:
await client.close()
if not server.closed:
await server.close()
_LOGGER.info("Home Assistant WebSocket API connection is closed")
return server

View File

@@ -7,7 +7,7 @@ from aiohttp.web_exceptions import HTTPUnauthorized, HTTPForbidden
from ..const import (
HEADER_TOKEN, REQUEST_FROM, ROLE_ADMIN, ROLE_DEFAULT, ROLE_HOMEASSISTANT,
ROLE_MANAGER)
ROLE_MANAGER, ROLE_BACKUP)
from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__)
@@ -33,9 +33,10 @@ NO_SECURITY_CHECK = re.compile(
ADDONS_API_BYPASS = re.compile(
r"^(?:"
r"|/addons/self/(?!security|update)[^/]+"
r"|/version"
r"|/info"
r"|/services.*"
r"|/discovery.*"
r"|/auth"
r")$"
)
@@ -52,6 +53,11 @@ ADDONS_ROLE_ACCESS = {
r"|/homeassistant/.+"
r")$"
),
ROLE_BACKUP: re.compile(
r"^(?:"
r"|/snapshots.*"
r")$"
),
ROLE_MANAGER: re.compile(
r"^(?:"
r"|/homeassistant/.+"

95
hassio/auth.py Normal file
View File

@@ -0,0 +1,95 @@
"""Manage SSO for Add-ons with Home Assistant user."""
import logging
import hashlib
from .const import (
FILE_HASSIO_AUTH, ATTR_PASSWORD, ATTR_USERNAME, ATTR_ADDON)
from .coresys import CoreSysAttributes
from .utils.json import JsonConfig
from .validate import SCHEMA_AUTH_CONFIG
from .exceptions import AuthError, HomeAssistantAPIError
_LOGGER = logging.getLogger(__name__)
class Auth(JsonConfig, CoreSysAttributes):
"""Manage SSO for Add-ons with Home Assistant user."""
def __init__(self, coresys):
"""Initialize updater."""
super().__init__(FILE_HASSIO_AUTH, SCHEMA_AUTH_CONFIG)
self.coresys = coresys
def _check_cache(self, username, password):
"""Check password in cache."""
username_h = _rehash(username)
password_h = _rehash(password, username)
if self._data.get(username_h) == password_h:
_LOGGER.info("Cache hit for %s", username)
return True
_LOGGER.warning("No cache hit for %s", username)
return False
def _update_cache(self, username, password):
"""Cache a username, password."""
username_h = _rehash(username)
password_h = _rehash(password, username)
if self._data.get(username_h) == password_h:
return
self._data[username_h] = password_h
self.save_data()
def _dismatch_cache(self, username, password):
"""Remove user from cache."""
username_h = _rehash(username)
password_h = _rehash(password, username)
if self._data.get(username_h) != password_h:
return
self._data.pop(username_h, None)
self.save_data()
async def check_login(self, addon, username, password):
"""Check username login."""
if password is None:
_LOGGER.error("None as password is not supported!")
raise AuthError()
_LOGGER.info("Auth request from %s for %s", addon.slug, username)
# Check API state
if not await self.sys_homeassistant.check_api_state():
_LOGGER.info("Home Assistant not running, check cache")
return self._check_cache(username, password)
try:
async with self.sys_homeassistant.make_request(
'post', 'api/hassio_auth', json={
ATTR_USERNAME: username,
ATTR_PASSWORD: password,
ATTR_ADDON: addon.slug,
}) as req:
if req.status == 200:
_LOGGER.info("Success login from %s", username)
self._update_cache(username, password)
return True
_LOGGER.warning("Wrong login from %s", username)
self._dismatch_cache(username, password)
return False
except HomeAssistantAPIError:
_LOGGER.error("Can't request auth on Home Assistant!")
raise AuthError()
def _rehash(value, salt2=""):
"""Rehash a value."""
for idx in range(1, 20):
value = hashlib.sha256(f"{value}{idx}{salt2}".encode()).hexdigest()
return value

View File

@@ -8,6 +8,7 @@ from pathlib import Path
from colorlog import ColoredFormatter
from .core import HassIO
from .auth import Auth
from .addons import AddonManager
from .api import RestAPI
from .const import SOCKET_DOCKER
@@ -38,6 +39,7 @@ def initialize_coresys(loop):
# Initialize core objects
coresys.core = HassIO(coresys)
coresys.auth = Auth(coresys)
coresys.updater = Updater(coresys)
coresys.api = RestAPI(coresys)
coresys.supervisor = Supervisor(coresys)

View File

@@ -2,7 +2,7 @@
from pathlib import Path
from ipaddress import ip_network
HASSIO_VERSION = '135'
HASSIO_VERSION = '139'
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
URL_HASSIO_VERSION = \
@@ -16,6 +16,7 @@ URL_HASSOS_OTA = (
HASSIO_DATA = Path("/data")
FILE_HASSIO_AUTH = Path(HASSIO_DATA, "auth.json")
FILE_HASSIO_ADDONS = Path(HASSIO_DATA, "addons.json")
FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json")
FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json")
@@ -50,6 +51,7 @@ CONTENT_TYPE_PNG = 'image/png'
CONTENT_TYPE_JSON = 'application/json'
CONTENT_TYPE_TEXT = 'text/plain'
CONTENT_TYPE_TAR = 'application/tar'
CONTENT_TYPE_URL = 'application/x-www-form-urlencoded'
HEADER_HA_ACCESS = 'x-ha-access'
HEADER_TOKEN = 'x-hassio-key'
@@ -184,6 +186,7 @@ ATTR_PROTECTED = 'protected'
ATTR_RATING = 'rating'
ATTR_HASSIO_ROLE = 'hassio_role'
ATTR_SUPERVISOR = 'supervisor'
ATTR_AUTH_API = 'auth_api'
SERVICE_MQTT = 'mqtt'
PROVIDE_SERVICE = 'provide'
@@ -253,5 +256,9 @@ FEATURES_SERVICES = 'services'
ROLE_DEFAULT = 'default'
ROLE_HOMEASSISTANT = 'homeassistant'
ROLE_BACKUP = 'backup'
ROLE_MANAGER = 'manager'
ROLE_ADMIN = 'admin'
CHAN_ID = 'chan_id'
CHAN_TYPE = 'chan_type'

View File

@@ -33,6 +33,7 @@ class CoreSys:
# Internal objects pointers
self._core = None
self._auth = None
self._homeassistant = None
self._supervisor = None
self._addons = None
@@ -122,6 +123,18 @@ class CoreSys:
raise RuntimeError("Hass.io already set!")
self._core = value
@property
def auth(self):
"""Return Auth object."""
return self._auth
@auth.setter
def auth(self, value):
"""Set a Auth object."""
if self._auth:
raise RuntimeError("Auth already set!")
self._auth = value
@property
def homeassistant(self):
"""Return Home Assistant object."""

View File

@@ -36,6 +36,7 @@ class Discovery(CoreSysAttributes, JsonConfig):
discovery = Message(**message)
messages[discovery.uuid] = discovery
_LOGGER.info("Load %d messages", len(messages))
self.message_obj = messages
def save(self):
@@ -60,7 +61,7 @@ class Discovery(CoreSysAttributes, JsonConfig):
def send(self, addon, service, config):
"""Send a discovery message to Home Assistant."""
try:
DISCOVERY_SERVICES[service](config)
config = DISCOVERY_SERVICES[service](config)
except vol.Invalid as err:
_LOGGER.error(
"Invalid discovery %s config", humanize_error(config, err))
@@ -115,7 +116,7 @@ class Discovery(CoreSysAttributes, JsonConfig):
@attr.s
class Message:
"""Represent a single Discovery message."""
uuid = attr.ib(factory=lambda: uuid4().hex, cmp=False, init=False)
addon = attr.ib()
service = attr.ib()
config = attr.ib(cmp=False)
uuid = attr.ib(factory=lambda: uuid4().hex, cmp=False)

View File

@@ -1,7 +1,6 @@
"""Init file for Hass.io add-on Docker object."""
import logging
import os
from pathlib import Path
import docker
import requests
@@ -101,7 +100,7 @@ class DockerAddon(DockerInterface):
devices = self.addon.devices or []
# Use audio devices
if self.addon.with_audio and AUDIO_DEVICE not in devices:
if self.addon.with_audio and self.sys_hardware.support_audio:
devices.append(AUDIO_DEVICE)
# Auto mapping UART devices
@@ -216,10 +215,8 @@ class DockerAddon(DockerInterface):
# Init other hardware mappings
# GPIO support
if self.addon.with_gpio:
if self.addon.with_gpio and self.sys_hardware.support_gpio:
for gpio_path in ("/sys/class/gpio", "/sys/devices/platform/soc"):
if not Path(gpio_path).exists():
continue
volumes.update({
gpio_path: {
'bind': gpio_path, 'mode': 'rw'

View File

@@ -57,6 +57,13 @@ class HassioUpdaterError(HassioError):
pass
# Auth
class AuthError(HassioError):
"""Auth errors."""
pass
# Host
class HostError(HassioError):

View File

@@ -66,7 +66,7 @@ class HassOS(CoreSysAttributes):
return self._board
def _check_host(self):
"""Check if HassOS is availabe."""
"""Check if HassOS is available."""
if not self.available:
_LOGGER.error("No HassOS available")
raise HassOSNotSupportedError()

View File

@@ -191,7 +191,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
@property
def hassio_token(self):
"""Return a access token for the Hass.io API."""
"""Return an access token for the Hass.io API."""
return self._data.get(ATTR_ACCESS_TOKEN)
@property
@@ -260,7 +260,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
_LOGGER.warning("Version %s is already installed", version)
return HomeAssistantUpdateError()
# process a update
# process an update
async def _update(to_version):
"""Run Home Assistant update."""
try:
@@ -442,9 +442,9 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
async with self.make_request('get', 'api/') as resp:
if resp.status in (200, 201):
return True
err = resp.status
status = resp.status
_LOGGER.warning("Home Assistant API config mismatch: %s", status)
_LOGGER.warning("Home Assistant API config mismatch: %d", err)
return False
async def _block_till_run(self):

View File

@@ -6,7 +6,8 @@ from string import Template
import attr
from ..const import ATTR_INPUT, ATTR_OUTPUT, ATTR_DEVICES, ATTR_NAME
from ..const import (
ATTR_INPUT, ATTR_OUTPUT, ATTR_DEVICES, ATTR_NAME, CHAN_ID, CHAN_TYPE)
from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__)
@@ -58,7 +59,9 @@ class AlsaAudio(CoreSysAttributes):
# Process devices
for dev_id, dev_data in self.sys_hardware.audio_devices.items():
for chan_id, chan_type in dev_data[ATTR_DEVICES].items():
for chan_info in dev_data[ATTR_DEVICES]:
chan_id = chan_info[CHAN_ID]
chan_type = chan_info[CHAN_TYPE]
alsa_id = f"{dev_id},{chan_id}"
dev_name = dev_data[ATTR_NAME]

View File

@@ -6,7 +6,7 @@ import re
import pyudev
from ..const import ATTR_NAME, ATTR_TYPE, ATTR_DEVICES
from ..const import ATTR_NAME, ATTR_TYPE, ATTR_DEVICES, CHAN_ID, CHAN_TYPE
_LOGGER = logging.getLogger(__name__)
@@ -20,6 +20,7 @@ PROC_STAT = Path("/proc/stat")
RE_BOOT_TIME = re.compile(r"btime (\d+)")
GPIO_DEVICES = Path("/sys/class/gpio")
SOC_DEVICES = Path("/sys/devices/platform/soc")
RE_TTY = re.compile(r"tty[A-Z]+")
@@ -60,6 +61,11 @@ class Hardware:
return dev_list
@property
def support_audio(self):
"""Return True if the system have audio support."""
return bool(self.audio_devices)
@property
def audio_devices(self):
"""Return all available audio interfaces."""
@@ -68,10 +74,8 @@ class Hardware:
return {}
try:
with ASOUND_CARDS.open('r') as cards_file:
cards = cards_file.read()
with ASOUND_DEVICES.open('r') as devices_file:
devices = devices_file.read()
cards = ASOUND_CARDS.read_text()
devices = ASOUND_DEVICES.read_text()
except OSError as err:
_LOGGER.error("Can't read asound data: %s", err)
return {}
@@ -83,20 +87,27 @@ class Hardware:
audio_list[match.group(1)] = {
ATTR_NAME: match.group(3),
ATTR_TYPE: match.group(2),
ATTR_DEVICES: {},
ATTR_DEVICES: [],
}
# parse devices
for match in RE_DEVICES.finditer(devices):
try:
audio_list[match.group(1)][ATTR_DEVICES][match.group(2)] = \
match.group(3)
audio_list[match.group(1)][ATTR_DEVICES].append({
CHAN_ID: match.group(2),
CHAN_TYPE: match.group(3)
})
except KeyError:
_LOGGER.warning("Wrong audio device found %s", match.group(0))
continue
return audio_list
@property
def support_gpio(self):
"""Return True if device support GPIOs."""
return SOC_DEVICES.exists() and GPIO_DEVICES.exists()
@property
def gpio_devices(self):
"""Return list of GPIO interface on device."""

View File

@@ -118,7 +118,7 @@ class SnapshotManager(CoreSysAttributes):
async def do_snapshot_full(self, name="", password=None):
"""Create a full snapshot."""
if self.lock.locked():
_LOGGER.error("It is already a snapshot/restore process running")
_LOGGER.error("A snapshot/restore process is already running")
return None
snapshot = self._create_snapshot(name, SNAPSHOT_FULL, password)
@@ -153,7 +153,7 @@ class SnapshotManager(CoreSysAttributes):
password=None):
"""Create a partial snapshot."""
if self.lock.locked():
_LOGGER.error("It is already a snapshot/restore process running")
_LOGGER.error("A snapshot/restore process is already running")
return None
addons = addons or []
@@ -201,7 +201,7 @@ class SnapshotManager(CoreSysAttributes):
async def do_restore_full(self, snapshot, password=None):
"""Restore a snapshot."""
if self.lock.locked():
_LOGGER.error("It is already a snapshot/restore process running")
_LOGGER.error("A snapshot/restore process is already running")
return False
if snapshot.sys_type != SNAPSHOT_FULL:
@@ -274,7 +274,7 @@ class SnapshotManager(CoreSysAttributes):
addons=None, folders=None, password=None):
"""Restore a snapshot."""
if self.lock.locked():
_LOGGER.error("It is already a snapshot/restore process running")
_LOGGER.error("A snapshot/restore process is already running")
return False
if snapshot.protected and not snapshot.set_password(password):

View File

@@ -3,6 +3,7 @@ import asyncio
import logging
from .coresys import CoreSysAttributes
from .exceptions import HomeAssistantError
_LOGGER = logging.getLogger(__name__)
@@ -104,7 +105,10 @@ class Tasks(CoreSysAttributes):
return
_LOGGER.warning("Watchdog found a problem with Home Assistant Docker!")
await self.sys_homeassistant.start()
try:
await self.sys_homeassistant.start()
except HomeAssistantError:
_LOGGER.error("Watchdog Home Assistant reanimation fails!")
async def _watchdog_homeassistant_api(self):
"""Create scheduler task for monitoring running state of API.
@@ -136,6 +140,8 @@ class Tasks(CoreSysAttributes):
_LOGGER.error("Watchdog found a problem with Home Assistant API!")
try:
await self.sys_homeassistant.restart()
except HomeAssistantError:
_LOGGER.error("Watchdog Home Assistant reanimation fails!")
finally:
self._cache[HASS_WATCHDOG_API] = 0

View File

@@ -23,6 +23,7 @@ DOCKER_IMAGE = vol.Match(r"^[\w{}]+/[\-\w{}]+$")
ALSA_DEVICE = vol.Maybe(vol.Match(r"\d+,\d+"))
CHANNELS = vol.In([CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV])
UUID_MATCH = vol.Match(r"^[0-9a-f]{32}$")
SHA256 = vol.Match(r"^[0-9a-f]{64}$")
SERVICE_ALL = vol.In([SERVICE_MQTT])
@@ -74,7 +75,7 @@ DOCKER_PORTS = vol.Schema({
# pylint: disable=no-value-for-parameter
SCHEMA_HASS_CONFIG = vol.Schema({
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH,
vol.Optional(ATTR_ACCESS_TOKEN): vol.Match(r"^[0-9a-f]{64}$"),
vol.Optional(ATTR_ACCESS_TOKEN): SHA256,
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE,
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
@@ -120,3 +121,8 @@ SCHEMA_DISCOVERY = vol.Schema([
SCHEMA_DISCOVERY_CONFIG = vol.Schema({
vol.Optional(ATTR_DISCOVERY, default=list): schema_or(SCHEMA_DISCOVERY),
}, extra=vol.REMOVE_EXTRA)
SCHEMA_AUTH_CONFIG = vol.Schema({
SHA256: SHA256
})

View File

@@ -1,11 +1,11 @@
attr==0.3.1
async_timeout==3.0.0
aiohttp==3.4.0
attrs==18.2.0
async_timeout==3.0.1
aiohttp==3.4.4
docker==3.5.0
colorlog==3.1.2
colorlog==3.1.4
voluptuous==0.11.5
gitpython==2.1.10
pytz==2018.4
pytz==2018.5
pyudev==0.21.0
pycryptodome==3.6.6
cpe==1.2.1

View File

@@ -3,7 +3,7 @@ envlist = lint
[testenv]
deps =
flake8==3.5.0
flake8==3.6.0
pylint==2.1.1
-r{toxinidir}/requirements.txt