Merge pull request #1602 from home-assistant/dev

Release 210
This commit is contained in:
Pascal Vizeli 2020-03-27 17:46:51 +01:00 committed by GitHub
commit fa783a0d2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 617 additions and 506 deletions

180
.vscode/tasks.json vendored
View File

@ -1,92 +1,90 @@
{ {
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{ {
"label": "Run Testenv", "label": "Run Testenv",
"type": "shell", "type": "shell",
"command": "./scripts/test_env.sh", "command": "./scripts/test_env.sh",
"group": { "group": {
"kind": "test", "kind": "test",
"isDefault": true, "isDefault": true
}, },
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new" "panel": "new"
}, },
"problemMatcher": [] "problemMatcher": []
}, },
{ {
"label": "Run Testenv CLI", "label": "Run Testenv CLI",
"type": "shell", "type": "shell",
"command": "docker run --rm -ti -v /etc/machine-id:/etc/machine-id --network=hassio --add-host hassio:172.30.32.2 homeassistant/amd64-hassio-cli:dev", "command": "docker exec -ti hassio_cli /usr/bin/cli.sh",
"group": { "group": {
"kind": "test", "kind": "test",
"isDefault": true, "isDefault": true
}, },
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new" "panel": "new"
}, },
"problemMatcher": [] "problemMatcher": []
}, },
{ {
"label": "Update UI", "label": "Update UI",
"type": "shell", "type": "shell",
"command": "./scripts/update-frontend.sh", "command": "./scripts/update-frontend.sh",
"group": { "group": {
"kind": "build", "kind": "build",
"isDefault": true "isDefault": true
}, },
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new" "panel": "new"
}, },
"problemMatcher": [] "problemMatcher": []
}, },
{ {
"label": "Pytest", "label": "Pytest",
"type": "shell", "type": "shell",
"command": "pytest --timeout=10 tests", "command": "pytest --timeout=10 tests",
"group": { "group": {
"kind": "test", "kind": "test",
"isDefault": true, "isDefault": true
}, },
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new" "panel": "new"
}, },
"problemMatcher": [] "problemMatcher": []
}, },
{ {
"label": "Flake8", "label": "Flake8",
"type": "shell", "type": "shell",
"command": "flake8 hassio tests", "command": "flake8 hassio tests",
"group": { "group": {
"kind": "test", "kind": "test",
"isDefault": true, "isDefault": true
}, },
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new" "panel": "new"
}, },
"problemMatcher": [] "problemMatcher": []
}, },
{ {
"label": "Pylint", "label": "Pylint",
"type": "shell", "type": "shell",
"command": "pylint hassio", "command": "pylint hassio",
"dependsOn": [ "dependsOn": ["Install all Requirements"],
"Install all Requirements" "group": {
], "kind": "test",
"group": { "isDefault": true
"kind": "test", },
"isDefault": true, "presentation": {
}, "reveal": "always",
"presentation": { "panel": "new"
"reveal": "always", },
"panel": "new" "problemMatcher": []
}, }
"problemMatcher": [] ]
} }
]
}

182
API.md
View File

@ -291,9 +291,7 @@ 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",
"boot": "rauc boot slot" "boot": "rauc boot slot"
} }
@ -307,14 +305,6 @@ return:
} }
``` ```
- POST `/os/update/cli`
```json
{
"version": "optional"
}
```
- POST `/os/config/sync` - POST `/os/config/sync`
Load host configs from a USB stick. Load host configs from a USB stick.
@ -857,90 +847,29 @@ return:
} }
``` ```
### Audio ### DNS
- GET `/audio/info` - GET `/dns/info`
```json ```json
{ {
"host": "ip-address", "host": "ip-address",
"version": "1", "version": "1",
"latest_version": "2", "latest_version": "2",
"audio": { "servers": ["dns://8.8.8.8"],
"card": [ "locals": ["dns://xy"]
{
"name": "...",
"index": 1,
"driver": "...",
"profiles": [
{
"name": "...",
"description": "...",
"active": false
}
]
}
],
"input": [
{
"name": "...",
"index": 0,
"description": "...",
"volume": 0.3,
"mute": false,
"default": false,
"card": "null|int",
"applications": [
{
"name": "...",
"index": 0,
"stream_index": 0,
"stream_type": "INPUT",
"volume": 0.3,
"mute": false,
"addon": ""
}
]
}
],
"output": [
{
"name": "...",
"index": 0,
"description": "...",
"volume": 0.3,
"mute": false,
"default": false,
"card": "null|int",
"applications": [
{
"name": "...",
"index": 0,
"stream_index": 0,
"stream_type": "OUTPUT",
"volume": 0.3,
"mute": false,
"addon": ""
}
]
}
],
"application": [
{
"name": "...",
"index": 0,
"stream_index": 0,
"stream_type": "OUTPUT",
"volume": 0.3,
"mute": false,
"addon": ""
}
]
}
} }
``` ```
- POST `/audio/update` - POST `/dns/options`
```json
{
"servers": ["dns://8.8.8.8"]
}
```
- POST `/dns/update`
```json ```json
{ {
@ -948,92 +877,47 @@ return:
} }
``` ```
- POST `/audio/restart` - POST `/dns/restart`
- POST `/audio/reload` - POST `/dns/reset`
- GET `/audio/logs` - GET `/dns/logs`
- POST `/audio/volume/input` - GET `/dns/stats`
```json ```json
{ {
"index": "...", "cpu_percent": 0.0,
"volume": 0.5 "memory_usage": 283123,
"memory_limit": 329392,
"memory_percent": 1.4,
"network_tx": 0,
"network_rx": 0,
"blk_read": 0,
"blk_write": 0
} }
``` ```
- POST `/audio/volume/output` ### CLI
- GET `/cli/info`
```json ```json
{ {
"index": "...", "version": "1",
"volume": 0.5 "version_latest": "2"
} }
``` ```
- POST `/audio/volume/{output|input}/application` - POST `/cli/update`
```json ```json
{ {
"index": "...", "version": "VERSION"
"volume": 0.5
} }
``` ```
- POST `/audio/mute/input` - GET `/cli/stats`
```json
{
"index": "...",
"active": false
}
```
- POST `/audio/mute/output`
```json
{
"index": "...",
"active": false
}
```
- POST `/audio/mute/{output|input}/application`
```json
{
"index": "...",
"active": false
}
```
- POST `/audio/default/input`
```json
{
"name": "..."
}
```
- POST `/audio/default/output`
```json
{
"name": "..."
}
```
- POST `/audio/profile`
```json
{
"card": "...",
"name": "..."
}
```
- GET `/audio/stats`
```json ```json
{ {

View File

@ -8,53 +8,19 @@ trigger:
pr: none pr: none
variables: variables:
- name: versionWheels - name: versionWheels
value: "1.6-3.7-alpine3.11" value: '1.6.1-3.7-alpine3.11'
- group: wheels resources:
repositories:
- repository: azure
type: github
name: 'home-assistant/ci-azure'
endpoint: 'home-assistant'
jobs: jobs:
- job: "Wheels" - template: templates/azp-job-wheels.yaml@azure
timeoutInMinutes: 360 parameters:
pool: builderVersion: '$(versionWheels)'
vmImage: "ubuntu-latest" builderApk: 'build-base;libffi-dev;openssl-dev'
strategy: builderPip: 'Cython'
maxParallel: 5 wheelsRequirement: 'requirements.txt'
matrix:
amd64:
buildArch: "amd64"
i386:
buildArch: "i386"
armhf:
buildArch: "armhf"
armv7:
buildArch: "armv7"
aarch64:
buildArch: "aarch64"
steps:
- script: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
qemu-user-static \
binfmt-support \
curl
sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
sudo update-binfmts --enable qemu-arm
sudo update-binfmts --enable qemu-aarch64
displayName: "Initial cross build"
- script: |
mkdir -p .ssh
echo -e "-----BEGIN RSA PRIVATE KEY-----\n$(wheelsSSH)\n-----END RSA PRIVATE KEY-----" >> .ssh/id_rsa
ssh-keyscan -H $(wheelsHost) >> .ssh/known_hosts
chmod 600 .ssh/*
displayName: "Install ssh key"
- script: sudo docker pull homeassistant/$(buildArch)-wheels:$(versionWheels)
displayName: "Install wheels builder"
- script: |
sudo docker run --rm -v $(pwd):/data:ro -v $(pwd)/.ssh:/root/.ssh:rw \
homeassistant/$(buildArch)-wheels:$(versionWheels) \
--apk "build-base;libffi-dev;openssl-dev" \
--index $(wheelsIndex) \
--requirement requirements.txt \
--upload rsync \
--remote wheels@$(wheelsHost):/opt/wheels
displayName: "Run wheels build"

View File

@ -1,7 +1,7 @@
aiohttp==3.6.1 aiohttp==3.6.1
async_timeout==3.0.1 async_timeout==3.0.1
attrs==19.3.0 attrs==19.3.0
cchardet==2.1.5 cchardet==2.1.6
colorlog==4.1.0 colorlog==4.1.0
cpe==1.2.1 cpe==1.2.1
cryptography==2.8 cryptography==2.8

View File

@ -1,6 +1,6 @@
flake8==3.7.9 flake8==3.7.9
pylint==2.4.4 pylint==2.4.4
pytest==5.3.5 pytest==5.4.1
pytest-timeout==1.3.4 pytest-timeout==1.3.4
pytest-aiohttp==0.3.0 pytest-aiohttp==0.3.0
black==19.10b0 black==19.10b0

View File

@ -2,4 +2,6 @@
# ============================================================================== # ==============================================================================
# Start Service service # Start Service service
# ============================================================================== # ==============================================================================
export LD_PRELOAD="/usr/local/lib/libjemalloc.so.2"
exec python3 -m supervisor exec python3 -m supervisor

View File

@ -81,11 +81,6 @@ function cleanup_docker() {
} }
function install_cli() {
docker pull homeassistant/amd64-hassio-cli:dev
}
function setup_test_env() { function setup_test_env() {
mkdir -p /workspaces/test_supervisor mkdir -p /workspaces/test_supervisor
@ -131,7 +126,6 @@ start_docker
trap "stop_docker" ERR trap "stop_docker" ERR
build_supervisor build_supervisor
install_cli
cleanup_lastboot cleanup_lastboot
cleanup_docker cleanup_docker
init_dbus init_dbus

View File

@ -59,7 +59,7 @@ class AddonManager(CoreSysAttributes):
def from_token(self, token: str) -> Optional[Addon]: def from_token(self, token: str) -> Optional[Addon]:
"""Return an add-on from Supervisor token.""" """Return an add-on from Supervisor token."""
for addon in self.installed: for addon in self.installed:
if token == addon.hassio_token: if token == addon.supervisor_token:
return addon return addon
return None return None

View File

@ -164,7 +164,7 @@ class Addon(AddonModel):
return self.persist[ATTR_UUID] return self.persist[ATTR_UUID]
@property @property
def hassio_token(self) -> Optional[str]: def supervisor_token(self) -> Optional[str]:
"""Return access token for Supervisor API.""" """Return access token for Supervisor API."""
return self.persist.get(ATTR_ACCESS_TOKEN) return self.persist.get(ATTR_ACCESS_TOKEN)

View File

@ -137,7 +137,7 @@ class AddonModel(CoreSysAttributes):
return None return None
@property @property
def hassio_token(self) -> Optional[str]: def supervisor_token(self) -> Optional[str]:
"""Return access token for Supervisor API.""" """Return access token for Supervisor API."""
return None return None

View File

@ -261,7 +261,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
), ),
vol.Optional(ATTR_IMAGE): vol.Match(RE_DOCKER_IMAGE), vol.Optional(ATTR_IMAGE): vol.Match(RE_DOCKER_IMAGE),
vol.Optional(ATTR_TIMEOUT, default=10): vol.All( vol.Optional(ATTR_TIMEOUT, default=10): vol.All(
vol.Coerce(int), vol.Range(min=10, max=120) vol.Coerce(int), vol.Range(min=10, max=300)
), ),
}, },
extra=vol.REMOVE_EXTRA, extra=vol.REMOVE_EXTRA,

View File

@ -7,11 +7,13 @@ from aiohttp import web
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from .addons import APIAddons from .addons import APIAddons
from .audio import APIAudio
from .auth import APIAuth from .auth import APIAuth
from .cli import APICli
from .discovery import APIDiscovery from .discovery import APIDiscovery
from .dns import APICoreDNS from .dns import APICoreDNS
from .hardware import APIHardware from .hardware import APIHardware
from .hassos import APIHassOS from .os import APIOS
from .homeassistant import APIHomeAssistant from .homeassistant import APIHomeAssistant
from .host import APIHost from .host import APIHost
from .info import APIInfo from .info import APIInfo
@ -21,7 +23,6 @@ from .security import SecurityMiddleware
from .services import APIServices from .services import APIServices
from .snapshots import APISnapshots from .snapshots import APISnapshots
from .supervisor import APISupervisor from .supervisor import APISupervisor
from .audio import APIAudio
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -49,7 +50,8 @@ class RestAPI(CoreSysAttributes):
"""Register REST API Calls.""" """Register REST API Calls."""
self._register_supervisor() self._register_supervisor()
self._register_host() self._register_host()
self._register_hassos() self._register_os()
self._register_cli()
self._register_hardware() self._register_hardware()
self._register_homeassistant() self._register_homeassistant()
self._register_proxy() self._register_proxy()
@ -84,22 +86,29 @@ class RestAPI(CoreSysAttributes):
] ]
) )
def _register_hassos(self) -> None: def _register_os(self) -> None:
"""Register HassOS functions.""" """Register OS functions."""
api_hassos = APIHassOS() api_os = APIOS()
api_hassos.coresys = self.coresys api_os.coresys = self.coresys
self.webapp.add_routes( self.webapp.add_routes(
[ [
web.get("/os/info", api_hassos.info), web.get("/os/info", api_os.info),
web.post("/os/update", api_hassos.update), web.post("/os/update", api_os.update),
web.post("/os/update/cli", api_hassos.update_cli), web.post("/os/config/sync", api_os.config_sync),
web.post("/os/config/sync", api_hassos.config_sync), ]
# Remove with old Supervisor fallback )
web.get("/hassos/info", api_hassos.info),
web.post("/hassos/update", api_hassos.update), def _register_cli(self) -> None:
web.post("/hassos/update/cli", api_hassos.update_cli), """Register HA cli functions."""
web.post("/hassos/config/sync", api_hassos.config_sync), api_cli = APICli()
api_cli.coresys = self.coresys
self.webapp.add_routes(
[
web.get("/cli/info", api_cli.info),
web.get("/cli/stats", api_cli.stats),
web.post("/cli/update", api_cli.update),
] ]
) )

View File

@ -166,5 +166,5 @@ class APIAudio(CoreSysAttributes):
body = await api_validate(SCHEMA_PROFILE, request) body = await api_validate(SCHEMA_PROFILE, request)
await asyncio.shield( await asyncio.shield(
self.sys_host.sound.set_profile(body[ATTR_CARD], body[ATTR_NAME]) self.sys_host.sound.ativate_profile(body[ATTR_CARD], body[ATTR_NAME])
) )

62
supervisor/api/cli.py Normal file
View File

@ -0,0 +1,62 @@
"""Init file for Supervisor HA cli RESTful API."""
import asyncio
import logging
from typing import Any, Dict
from aiohttp import web
import voluptuous as vol
from ..const import (
ATTR_VERSION,
ATTR_VERSION_LATEST,
ATTR_BLK_READ,
ATTR_BLK_WRITE,
ATTR_CPU_PERCENT,
ATTR_MEMORY_LIMIT,
ATTR_MEMORY_PERCENT,
ATTR_MEMORY_USAGE,
ATTR_NETWORK_RX,
ATTR_NETWORK_TX,
)
from ..coresys import CoreSysAttributes
from .utils import api_process, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__)
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
class APICli(CoreSysAttributes):
"""Handle RESTful API for HA Cli functions."""
@api_process
async def info(self, request: web.Request) -> Dict[str, Any]:
"""Return HA cli information."""
return {
ATTR_VERSION: self.sys_cli.version,
ATTR_VERSION_LATEST: self.sys_cli.latest_version,
}
@api_process
async def stats(self, request: web.Request) -> Dict[str, Any]:
"""Return resource information."""
stats = await self.sys_cli.stats()
return {
ATTR_CPU_PERCENT: stats.cpu_percent,
ATTR_MEMORY_USAGE: stats.memory_usage,
ATTR_MEMORY_LIMIT: stats.memory_limit,
ATTR_MEMORY_PERCENT: stats.memory_percent,
ATTR_NETWORK_RX: stats.network_rx,
ATTR_NETWORK_TX: stats.network_tx,
ATTR_BLK_READ: stats.blk_read,
ATTR_BLK_WRITE: stats.blk_write,
}
@api_process
async def update(self, request: web.Request) -> None:
"""Update HA CLI."""
body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.sys_cli.latest_version)
await asyncio.shield(self.sys_cli.update(version))

View File

@ -10,8 +10,6 @@ from ..const import (
ATTR_BOARD, ATTR_BOARD,
ATTR_BOOT, ATTR_BOOT,
ATTR_VERSION, ATTR_VERSION,
ATTR_VERSION_CLI,
ATTR_VERSION_CLI_LATEST,
ATTR_VERSION_LATEST, ATTR_VERSION_LATEST,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
@ -22,38 +20,28 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)}) SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
class APIHassOS(CoreSysAttributes): class APIOS(CoreSysAttributes):
"""Handle RESTful API for HassOS functions.""" """Handle RESTful API for OS functions."""
@api_process @api_process
async def info(self, request: web.Request) -> Dict[str, Any]: async def info(self, request: web.Request) -> Dict[str, Any]:
"""Return HassOS information.""" """Return OS 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.latest_version,
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,
ATTR_BOOT: self.sys_dbus.rauc.boot_slot, ATTR_BOOT: self.sys_dbus.rauc.boot_slot,
} }
@api_process @api_process
async def update(self, request: web.Request) -> None: async def update(self, request: web.Request) -> None:
"""Update HassOS.""" """Update OS."""
body = await api_validate(SCHEMA_VERSION, request) body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.sys_hassos.version_latest) version = body.get(ATTR_VERSION, self.sys_hassos.latest_version)
await asyncio.shield(self.sys_hassos.update(version)) await asyncio.shield(self.sys_hassos.update(version))
@api_process
async def update_cli(self, request: web.Request) -> None:
"""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: web.Request) -> Awaitable[None]: def config_sync(self, request: web.Request) -> Awaitable[None]:
"""Trigger config reload on HassOS.""" """Trigger config reload on OS."""
return asyncio.shield(self.sys_hassos.config_sync()) return asyncio.shield(self.sys_hassos.config_sync())

View File

@ -26,11 +26,11 @@ class APIProxy(CoreSysAttributes):
"""Check the Supervisor token.""" """Check the Supervisor token."""
if AUTHORIZATION in request.headers: if AUTHORIZATION in request.headers:
bearer = request.headers[AUTHORIZATION] bearer = request.headers[AUTHORIZATION]
hassio_token = bearer.split(" ")[-1] supervisor_token = bearer.split(" ")[-1]
else: else:
hassio_token = request.headers.get(HEADER_HA_ACCESS) supervisor_token = request.headers.get(HEADER_HA_ACCESS)
addon = self.sys_addons.from_token(hassio_token) addon = self.sys_addons.from_token(supervisor_token)
if not addon: if not addon:
_LOGGER.warning("Unknown Home Assistant API access!") _LOGGER.warning("Unknown Home Assistant API access!")
elif not addon.access_homeassistant_api: elif not addon.access_homeassistant_api:
@ -177,8 +177,10 @@ 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") or response.get("access_token") supervisor_token = response.get("api_password") or response.get(
addon = self.sys_addons.from_token(hassio_token) "access_token"
)
addon = self.sys_addons.from_token(supervisor_token)
if not addon or not addon.access_homeassistant_api: if not addon or not addon.access_homeassistant_api:
_LOGGER.warning("Unauthorized WebSocket access!") _LOGGER.warning("Unauthorized WebSocket access!")

View File

@ -75,6 +75,7 @@ ADDONS_ROLE_ACCESS = {
r"^(?:" r"^(?:"
r"|/audio/.*" r"|/audio/.*"
r"|/dns/.*" r"|/dns/.*"
r"|/cli/.*"
r"|/core/.+" r"|/core/.+"
r"|/homeassistant/.+" r"|/homeassistant/.+"
r"|/host/.+" r"|/host/.+"
@ -123,12 +124,13 @@ class SecurityMiddleware(CoreSysAttributes):
raise HTTPUnauthorized() raise HTTPUnauthorized()
# Home-Assistant # Home-Assistant
if supervisor_token == self.sys_homeassistant.hassio_token: if supervisor_token == self.sys_homeassistant.supervisor_token:
_LOGGER.debug("%s access from Home Assistant", request.path) _LOGGER.debug("%s access from Home Assistant", request.path)
request_from = self.sys_homeassistant request_from = self.sys_homeassistant
# Host # Host
if supervisor_token == self.sys_machine_id: # Remove machine_id handling later if all use new CLI
if supervisor_token in (self.sys_machine_id, self.sys_cli.supervisor_token):
_LOGGER.debug("%s access from Host", request.path) _LOGGER.debug("%s access from Host", request.path)
request_from = self.sys_host request_from = self.sys_host

View File

@ -49,7 +49,7 @@ class Audio(JsonConfig, CoreSysAttributes):
@version.setter @version.setter
def version(self, value: str) -> None: def version(self, value: str) -> None:
"""Return current version of Audio.""" """Set current version of Audio."""
self._data[ATTR_VERSION] = value self._data[ATTR_VERSION] = value
@property @property

View File

@ -14,6 +14,7 @@ from .auth import Auth
from .audio import Audio from .audio import Audio
from .const import SOCKET_DOCKER, UpdateChannels from .const import SOCKET_DOCKER, UpdateChannels
from .core import Core from .core import Core
from .cli import HaCli
from .coresys import CoreSys from .coresys import CoreSys
from .dbus import DBusManager from .dbus import DBusManager
from .discovery import Discovery from .discovery import Discovery
@ -67,6 +68,7 @@ async def initialize_coresys():
coresys.dbus = DBusManager(coresys) coresys.dbus = DBusManager(coresys)
coresys.hassos = HassOS(coresys) coresys.hassos = HassOS(coresys)
coresys.secrets = SecretsManager(coresys) coresys.secrets = SecretsManager(coresys)
coresys.cli = HaCli(coresys)
# bootstrap config # bootstrap config
initialize_system_data(coresys) initialize_system_data(coresys)

156
supervisor/cli.py Normal file
View File

@ -0,0 +1,156 @@
"""CLI support on supervisor."""
import asyncio
from contextlib import suppress
import logging
import secrets
from typing import Awaitable, Optional
from .const import ATTR_ACCESS_TOKEN, ATTR_VERSION, FILE_HASSIO_CLI
from .coresys import CoreSys, CoreSysAttributes
from .docker.cli import DockerCli
from .docker.stats import DockerStats
from .exceptions import CliError, CliUpdateError, DockerAPIError
from .utils.json import JsonConfig
from .validate import SCHEMA_CLI_CONFIG
_LOGGER: logging.Logger = logging.getLogger(__name__)
class HaCli(CoreSysAttributes, JsonConfig):
"""HA cli interface inside supervisor."""
def __init__(self, coresys: CoreSys):
"""Initialize cli handler."""
super().__init__(FILE_HASSIO_CLI, SCHEMA_CLI_CONFIG)
self.coresys: CoreSys = coresys
self.instance: DockerCli = DockerCli(coresys)
@property
def version(self) -> Optional[str]:
"""Return version of cli."""
return self._data.get(ATTR_VERSION)
@version.setter
def version(self, value: str) -> None:
"""Set current version of cli."""
self._data[ATTR_VERSION] = value
@property
def latest_version(self) -> str:
"""Return version of latest cli."""
return self.sys_updater.version_cli
@property
def need_update(self) -> bool:
"""Return true if a cli update is available."""
return self.version != self.latest_version
@property
def supervisor_token(self) -> str:
"""Return an access token for the Supervisor API."""
return self._data.get(ATTR_ACCESS_TOKEN)
@property
def in_progress(self) -> bool:
"""Return True if a task is in progress."""
return self.instance.in_progress
async def load(self) -> None:
"""Load cli setup."""
# Check cli state
try:
# Evaluate Version if we lost this information
if not self.version:
self.version = await self.instance.get_latest_version(key=int)
await self.instance.attach(tag=self.version)
except DockerAPIError:
_LOGGER.info("No Audio plugin Docker image %s found.", self.instance.image)
# Install cli
with suppress(CliError):
await self.install()
else:
self.version = self.instance.version
self.save_data()
# Run PulseAudio
with suppress(CliError):
if not await self.instance.is_running():
await self.start()
async def install(self) -> None:
"""Install cli."""
_LOGGER.info("Setup cli plugin")
while True:
# read audio tag and install it
if not self.latest_version:
await self.sys_updater.reload()
if self.latest_version:
with suppress(DockerAPIError):
await self.instance.install(self.latest_version)
break
_LOGGER.warning("Error on install cli plugin. Retry in 30sec")
await asyncio.sleep(30)
_LOGGER.info("cli plugin now installed")
self.version = self.instance.version
self.save_data()
async def update(self, version: Optional[str] = None) -> None:
"""Update local HA cli."""
version = version or self.latest_version
if version == self.version:
_LOGGER.warning("Version %s is already installed for cli", version)
return
try:
await self.instance.update(version, latest=True)
except DockerAPIError:
_LOGGER.error("HA cli update fails")
raise CliUpdateError() from None
else:
# Cleanup
with suppress(DockerAPIError):
await self.instance.cleanup()
async def start(self) -> None:
"""Run cli."""
# Create new API token
self._data[ATTR_ACCESS_TOKEN] = secrets.token_hex(56)
self.save_data()
# Start Instance
_LOGGER.info("Start cli plugin")
try:
await self.instance.run()
except DockerAPIError:
_LOGGER.error("Can't start cli plugin")
raise CliError() from None
async def stats(self) -> DockerStats:
"""Return stats of cli."""
try:
return await self.instance.stats()
except DockerAPIError:
raise CliError() from None
def is_running(self) -> Awaitable[bool]:
"""Return True if Docker container is running.
Return a coroutine.
"""
return self.instance.is_running()
async def repair(self) -> None:
"""Repair cli container."""
if await self.instance.exists():
return
_LOGGER.info("Repair HA cli %s", self.version)
try:
await self.instance.install(self.version, latest=True)
except DockerAPIError:
_LOGGER.error("Repairing of HA cli fails")

View File

@ -3,7 +3,7 @@ from enum import Enum
from ipaddress import ip_network from ipaddress import ip_network
from pathlib import Path from pathlib import Path
SUPERVISOR_VERSION = "209" SUPERVISOR_VERSION = "210"
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons" URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
@ -27,6 +27,7 @@ FILE_HASSIO_DISCOVERY = Path(SUPERVISOR_DATA, "discovery.json")
FILE_HASSIO_INGRESS = Path(SUPERVISOR_DATA, "ingress.json") FILE_HASSIO_INGRESS = Path(SUPERVISOR_DATA, "ingress.json")
FILE_HASSIO_DNS = Path(SUPERVISOR_DATA, "dns.json") FILE_HASSIO_DNS = Path(SUPERVISOR_DATA, "dns.json")
FILE_HASSIO_AUDIO = Path(SUPERVISOR_DATA, "audio.json") FILE_HASSIO_AUDIO = Path(SUPERVISOR_DATA, "audio.json")
FILE_HASSIO_CLI = Path(SUPERVISOR_DATA, "cli.json")
SOCKET_DOCKER = Path("/run/docker.sock") SOCKET_DOCKER = Path("/run/docker.sock")
@ -34,7 +35,6 @@ DOCKER_NETWORK = "hassio"
DOCKER_NETWORK_MASK = ip_network("172.30.32.0/23") DOCKER_NETWORK_MASK = ip_network("172.30.32.0/23")
DOCKER_NETWORK_RANGE = ip_network("172.30.33.0/24") DOCKER_NETWORK_RANGE = ip_network("172.30.33.0/24")
DNS_SERVERS = ["dns://1.1.1.1", "dns://9.9.9.9"]
DNS_SUFFIX = "local.hass.io" DNS_SUFFIX = "local.hass.io"
LABEL_VERSION = "io.hass.version" LABEL_VERSION = "io.hass.version"
@ -190,9 +190,6 @@ 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" ATTR_REFRESH_TOKEN = "refresh_token"
ATTR_ACCESS_TOKEN = "access_token" ATTR_ACCESS_TOKEN = "access_token"
ATTR_DOCKER_API = "docker_api" ATTR_DOCKER_API = "docker_api"

View File

@ -41,7 +41,9 @@ class Core(CoreSysAttributes):
await self.sys_host.load() await self.sys_host.load()
# Load Plugins container # Load Plugins container
await asyncio.wait([self.sys_dns.load(), self.sys_audio.load()]) await asyncio.wait(
[self.sys_dns.load(), self.sys_audio.load(), self.sys_cli.load()]
)
# Load Home Assistant # Load Home Assistant
await self.sys_homeassistant.load() await self.sys_homeassistant.load()
@ -203,13 +205,11 @@ class Core(CoreSysAttributes):
# Restore core functionality # Restore core functionality
await self.sys_dns.repair() await self.sys_dns.repair()
await self.sys_audio.repair()
await self.sys_cli.repair()
await self.sys_addons.repair() await self.sys_addons.repair()
await self.sys_homeassistant.repair() await self.sys_homeassistant.repair()
# Fix HassOS specific
if self.sys_hassos.available:
await self.sys_hassos.repair_cli()
# Tag version for latest # Tag version for latest
await self.sys_supervisor.repair() await self.sys_supervisor.repair()
_LOGGER.info("Finished repairing of Supervisor Environment") _LOGGER.info("Finished repairing of Supervisor Environment")

View File

@ -18,6 +18,7 @@ if TYPE_CHECKING:
from .audio import Audio from .audio import Audio
from .auth import Auth from .auth import Auth
from .core import Core from .core import Core
from .cli import HaCli
from .dbus import DBusManager from .dbus import DBusManager
from .discovery import Discovery from .discovery import Discovery
from .dns import CoreDNS from .dns import CoreDNS
@ -62,6 +63,7 @@ class CoreSys:
self._audio: Optional[Audio] = None self._audio: Optional[Audio] = None
self._auth: Optional[Auth] = None self._auth: Optional[Auth] = None
self._dns: Optional[CoreDNS] = None self._dns: Optional[CoreDNS] = None
self._cli: Optional[HaCli] = None
self._homeassistant: Optional[HomeAssistant] = None self._homeassistant: Optional[HomeAssistant] = None
self._supervisor: Optional[Supervisor] = None self._supervisor: Optional[Supervisor] = None
self._addons: Optional[AddonManager] = None self._addons: Optional[AddonManager] = None
@ -143,6 +145,18 @@ class CoreSys:
raise RuntimeError("Core already set!") raise RuntimeError("Core already set!")
self._core = value self._core = value
@property
def cli(self) -> HaCli:
"""Return HaCli object."""
return self._cli
@cli.setter
def cli(self, value: HaCli):
"""Set a HaCli object."""
if self._cli:
raise RuntimeError("HaCli already set!")
self._cli = value
@property @property
def arch(self) -> CpuArch: def arch(self) -> CpuArch:
"""Return CpuArch object.""" """Return CpuArch object."""
@ -449,6 +463,11 @@ class CoreSysAttributes:
"""Return core object.""" """Return core object."""
return self.coresys.core return self.coresys.core
@property
def sys_cli(self) -> HaCli:
"""Return HaCli object."""
return self.coresys.cli
@property @property
def sys_arch(self) -> CpuArch: def sys_arch(self) -> CpuArch:
"""Return CpuArch object.""" """Return CpuArch object."""

View File

@ -1,15 +1,31 @@
.:53 { .:53 {
log log
errors errors
loop
hosts /config/hosts { hosts /config/hosts {
fallthrough fallthrough
} }
template ANY AAAA local.hass.io hassio { template ANY AAAA local.hass.io hassio {
rcode NOERROR rcode NOERROR
} }
forward . $servers { forward . {{ locals | join(" ") }} dns://127.0.0.1:5353 {
except local.hass.io except local.hass.io
policy sequential policy sequential
health_check 5s
}
fallback REFUSED . dns://127.0.0.1:5353
fallback SERVFAIL . dns://127.0.0.1:5353
fallback NXDOMAIN . dns://127.0.0.1:5353
cache 10
}
.:5353 {
log
errors
forward . tls://1.1.1.1 tls://1.0.0.1 {
tls_servername cloudflare-dns.com
except local.hass.io
health_check 10s health_check 10s
} }
cache 30
} }

View File

@ -1,11 +0,0 @@
"""Discovery service for Home Panel."""
import voluptuous as vol
from supervisor.validate import network_port
from ..const import ATTR_HOST, ATTR_PORT
SCHEMA = vol.Schema(
{vol.Required(ATTR_HOST): vol.Coerce(str), vol.Required(ATTR_PORT): network_port}
)

View File

@ -4,13 +4,13 @@ from contextlib import suppress
from ipaddress import IPv4Address from ipaddress import IPv4Address
import logging import logging
from pathlib import Path from pathlib import Path
from string import Template
from typing import Awaitable, List, Optional from typing import Awaitable, List, Optional
import attr import attr
import jinja2
import voluptuous as vol import voluptuous as vol
from .const import ATTR_SERVERS, ATTR_VERSION, DNS_SERVERS, DNS_SUFFIX, FILE_HASSIO_DNS from .const import ATTR_SERVERS, ATTR_VERSION, DNS_SUFFIX, FILE_HASSIO_DNS
from .coresys import CoreSys, CoreSysAttributes from .coresys import CoreSys, CoreSysAttributes
from .docker.dns import DockerDNS from .docker.dns import DockerDNS
from .docker.stats import DockerStats from .docker.stats import DockerStats
@ -42,8 +42,10 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
self.coresys: CoreSys = coresys self.coresys: CoreSys = coresys
self.instance: DockerDNS = DockerDNS(coresys) self.instance: DockerDNS = DockerDNS(coresys)
self.forwarder: DNSForward = DNSForward() self.forwarder: DNSForward = DNSForward()
self.coredns_template: Optional[jinja2.Template] = None
self._hosts: List[HostEntry] = [] self._hosts: List[HostEntry] = []
self._loop: bool = False
@property @property
def corefile(self) -> Path: def corefile(self) -> Path:
@ -116,6 +118,12 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
# Start DNS forwarder # Start DNS forwarder
self.sys_create_task(self.forwarder.start(self.sys_docker.network.dns)) self.sys_create_task(self.forwarder.start(self.sys_docker.network.dns))
# Initialize CoreDNS Template
try:
self.coredns_template = jinja2.Template(COREDNS_TMPL.read_text())
except OSError as err:
_LOGGER.error("Can't read coredns.tmpl: %s", err)
# Run CoreDNS # Run CoreDNS
with suppress(CoreDNSError): with suppress(CoreDNSError):
if await self.instance.is_running(): if await self.instance.is_running():
@ -202,30 +210,43 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
self.hosts.unlink() self.hosts.unlink()
self._init_hosts() self._init_hosts()
# Reset loop protection
self._loop = False
await self.sys_addons.sync_dns() await self.sys_addons.sync_dns()
async def loop_detection(self) -> None:
"""Check if there was a loop found."""
log = await self.instance.logs()
# Check the log for loop plugin output
if b"plugin/loop: Loop" in log:
_LOGGER.error("Detect a DNS loop in local Network!")
self._loop = True
else:
self._loop = False
def _write_corefile(self) -> None: def _write_corefile(self) -> None:
"""Write CoreDNS config.""" """Write CoreDNS config."""
dns_servers: List[str] = [] dns_servers: List[str] = []
local_dns: List[str] = []
# Load Template servers: List[str] = []
try:
corefile_template: Template = Template(COREDNS_TMPL.read_text())
except OSError as err:
_LOGGER.error("Can't read coredns template file: %s", err)
raise CoreDNSError() from None
# Prepare DNS serverlist: Prio 1 Manual, Prio 2 Local, Prio 3 Fallback # Prepare DNS serverlist: Prio 1 Manual, Prio 2 Local, Prio 3 Fallback
local_dns: List[str] = self.sys_host.network.dns_servers or ["dns://127.0.0.11"] if not self._loop:
servers: List[str] = self.servers + local_dns + DNS_SERVERS local_dns = self.sys_host.network.dns_servers or ["dns://127.0.0.11"]
servers = self.servers + local_dns
else:
_LOGGER.warning("Ignore user DNS settings because of loop")
# Print some usefully debug data
_LOGGER.debug( _LOGGER.debug(
"config-dns = %s, local-dns = %s , backup-dns = %s", "config-dns = %s, local-dns = %s , backup-dns = CloudFlare DoT",
self.servers, self.servers,
local_dns, local_dns,
DNS_SERVERS,
) )
# Make sure, they are valid
for server in servers: for server in servers:
try: try:
dns_url(server) dns_url(server)
@ -235,7 +256,7 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
_LOGGER.warning("Ignore invalid DNS Server: %s", server) _LOGGER.warning("Ignore invalid DNS Server: %s", server)
# Generate config file # Generate config file
data = corefile_template.safe_substitute(servers=" ".join(dns_servers)) data = self.coredns_template.render(locals=dns_servers)
try: try:
self.corefile.write_text(data) self.corefile.write_text(data)
@ -339,7 +360,6 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
def is_fails(self) -> Awaitable[bool]: def is_fails(self) -> Awaitable[bool]:
"""Return True if a Docker container is fails state. """Return True if a Docker container is fails state.
Return a coroutine. Return a coroutine.
""" """
return self.instance.is_fails() return self.instance.is_fails()

View File

@ -117,8 +117,8 @@ class DockerAddon(DockerInterface):
return { return {
**addon_env, **addon_env,
ENV_TIME: self.sys_timezone, ENV_TIME: self.sys_timezone,
ENV_TOKEN: self.addon.hassio_token, ENV_TOKEN: self.addon.supervisor_token,
ENV_TOKEN_OLD: self.addon.hassio_token, ENV_TOKEN_OLD: self.addon.supervisor_token,
} }
@property @property

64
supervisor/docker/cli.py Normal file
View File

@ -0,0 +1,64 @@
"""HA Cli docker object."""
from contextlib import suppress
import logging
from ..coresys import CoreSysAttributes
from ..exceptions import DockerAPIError
from .interface import DockerInterface
from ..const import ENV_TIME, ENV_TOKEN
_LOGGER: logging.Logger = logging.getLogger(__name__)
CLI_DOCKER_NAME: str = "hassio_cli"
class DockerCli(DockerInterface, CoreSysAttributes):
"""Docker Supervisor wrapper for HA cli."""
@property
def image(self):
"""Return name of HA cli image."""
return f"homeassistant/{self.sys_arch.supervisor}-hassio-cli"
@property
def name(self) -> str:
"""Return name of Docker container."""
return CLI_DOCKER_NAME
def _run(self) -> None:
"""Run Docker image.
Need run inside executor.
"""
if self._is_running():
return
# Cleanup
with suppress(DockerAPIError):
self._stop()
# Create & Run container
docker_container = self.sys_docker.run(
self.image,
entrypoint=["/init"],
command=["/bin/bash", "-c", "sleep infinity"],
version=self.sys_cli.version,
init=False,
ipv4=self.sys_docker.network.cli,
name=self.name,
hostname=self.name.replace("_", "-"),
detach=True,
extra_hosts={"supervisor": self.sys_docker.network.supervisor},
environment={
ENV_TIME: self.sys_timezone,
ENV_TOKEN: self.sys_cli.supervisor_token,
},
)
self._meta = docker_container.attrs
_LOGGER.info(
"Start CLI %s with version %s - %s",
self.image,
self.version,
self.sys_docker.network.audio,
)

View File

@ -1,38 +0,0 @@
"""HassOS Cli docker object."""
import logging
import docker
from ..coresys import CoreSysAttributes
from .interface import DockerInterface
_LOGGER: logging.Logger = logging.getLogger(__name__)
class DockerHassOSCli(DockerInterface, CoreSysAttributes):
"""Docker Supervisor wrapper for HassOS Cli."""
@property
def image(self):
"""Return name of HassOS CLI image."""
return f"homeassistant/{self.sys_arch.supervisor}-hassio-cli"
def _stop(self, remove_container=True):
"""Don't need stop."""
return True
def _attach(self, tag: str):
"""Attach to running Docker container.
Need run inside executor.
"""
try:
image = self.sys_docker.images.get(f"{self.image}:{tag}")
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

@ -112,8 +112,8 @@ class DockerHomeAssistant(DockerInterface):
"HASSIO": self.sys_docker.network.supervisor, "HASSIO": self.sys_docker.network.supervisor,
"SUPERVISOR": self.sys_docker.network.supervisor, "SUPERVISOR": self.sys_docker.network.supervisor,
ENV_TIME: self.sys_timezone, ENV_TIME: self.sys_timezone,
ENV_TOKEN: self.sys_homeassistant.hassio_token, ENV_TOKEN: self.sys_homeassistant.supervisor_token,
ENV_TOKEN_OLD: self.sys_homeassistant.hassio_token, ENV_TOKEN_OLD: self.sys_homeassistant.supervisor_token,
}, },
) )

View File

@ -53,6 +53,11 @@ class DockerNetwork:
"""Return audio of the network.""" """Return audio of the network."""
return DOCKER_NETWORK_MASK[4] return DOCKER_NETWORK_MASK[4]
@property
def cli(self) -> IPv4Address:
"""Return cli of the network."""
return DOCKER_NETWORK_MASK[5]
def _get_network(self) -> docker.models.networks.Network: def _get_network(self) -> docker.models.networks.Network:
"""Get supervisor network.""" """Get supervisor network."""
try: try:

View File

@ -54,6 +54,17 @@ class HassOSNotSupportedError(HassioNotSupportedError):
"""Function not supported by HassOS.""" """Function not supported by HassOS."""
# HaCli
class CliError(HassioError):
"""HA cli exception."""
class CliUpdateError(HassOSError):
"""Error on update of a HA cli."""
# DNS # DNS

View File

@ -1,6 +1,5 @@
"""HassOS support on supervisor.""" """HassOS support on supervisor."""
import asyncio import asyncio
from contextlib import suppress
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Awaitable, Optional from typing import Awaitable, Optional
@ -10,12 +9,10 @@ from cpe import CPE
from .const import URL_HASSOS_OTA from .const import URL_HASSOS_OTA
from .coresys import CoreSysAttributes, CoreSys from .coresys import CoreSysAttributes, CoreSys
from .docker.hassos_cli import DockerHassOSCli
from .exceptions import ( from .exceptions import (
DBusError, DBusError,
HassOSNotSupportedError, HassOSNotSupportedError,
HassOSUpdateError, HassOSUpdateError,
DockerAPIError,
) )
from .dbus.rauc import RaucState from .dbus.rauc import RaucState
@ -28,7 +25,6 @@ class HassOS(CoreSysAttributes):
def __init__(self, coresys: CoreSys): def __init__(self, coresys: CoreSys):
"""Initialize HassOS handler.""" """Initialize HassOS handler."""
self.coresys: CoreSys = coresys self.coresys: CoreSys = coresys
self.instance: DockerHassOSCli = DockerHassOSCli(coresys)
self._available: bool = False self._available: bool = False
self._version: Optional[str] = None self._version: Optional[str] = None
self._board: Optional[str] = None self._board: Optional[str] = None
@ -44,29 +40,14 @@ class HassOS(CoreSysAttributes):
return self._version return self._version
@property @property
def version_cli(self) -> Optional[str]: def latest_version(self) -> str:
"""Return version of HassOS cli."""
return self.instance.version
@property
def version_latest(self) -> str:
"""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) -> str:
"""Return version of HassOS."""
return self.sys_updater.version_cli
@property @property
def need_update(self) -> bool: def need_update(self) -> bool:
"""Return true if a HassOS update is available.""" """Return true if a HassOS update is available."""
return self.version != self.version_latest return self.version != self.latest_version
@property
def need_cli_update(self) -> bool:
"""Return true if a HassOS cli update is available."""
return self.version_cli != self.version_cli_latest
@property @property
def board(self) -> Optional[str]: def board(self) -> Optional[str]:
@ -134,8 +115,6 @@ class HassOS(CoreSysAttributes):
_LOGGER.info( _LOGGER.info(
"Detect HassOS %s / BootSlot %s", self.version, self.sys_dbus.rauc.boot_slot "Detect HassOS %s / BootSlot %s", self.version, self.sys_dbus.rauc.boot_slot
) )
with suppress(DockerAPIError):
await self.instance.attach(tag="latest")
def config_sync(self) -> Awaitable[None]: def config_sync(self) -> Awaitable[None]:
"""Trigger a host config reload from usb. """Trigger a host config reload from usb.
@ -149,7 +128,7 @@ class HassOS(CoreSysAttributes):
async def update(self, version: Optional[str] = None) -> None: async def update(self, version: Optional[str] = None) -> None:
"""Update HassOS system.""" """Update HassOS system."""
version = version or self.version_latest version = version or self.latest_version
# Check installed version # Check installed version
self._check_host() self._check_host()
@ -183,35 +162,6 @@ class HassOS(CoreSysAttributes):
_LOGGER.error("HassOS update fails with: %s", self.sys_dbus.rauc.last_error) _LOGGER.error("HassOS update fails with: %s", self.sys_dbus.rauc.last_error)
raise HassOSUpdateError() raise HassOSUpdateError()
async def update_cli(self, version: Optional[str] = None) -> 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)
return
try:
await self.instance.update(version, latest=True)
except DockerAPIError:
_LOGGER.error("HassOS CLI update fails")
raise HassOSUpdateError() from None
else:
# Cleanup
with suppress(DockerAPIError):
await self.instance.cleanup()
async def repair_cli(self) -> None:
"""Repair CLI container."""
if await self.instance.exists():
return
_LOGGER.info("Repair HassOS CLI %s", self.version_cli)
try:
await self.instance.install(self.version_cli, latest=True)
except DockerAPIError:
_LOGGER.error("Repairing of HassOS CLI fails")
async def mark_healthy(self): async def mark_healthy(self):
"""Set booted partition as good for rauc.""" """Set booted partition as good for rauc."""
try: try:

View File

@ -221,7 +221,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
return self._data[ATTR_UUID] return self._data[ATTR_UUID]
@property @property
def hassio_token(self) -> str: def supervisor_token(self) -> str:
"""Return an access token for the Supervisor API.""" """Return an access token for the Supervisor API."""
return self._data.get(ATTR_ACCESS_TOKEN) return self._data.get(ATTR_ACCESS_TOKEN)
@ -465,13 +465,16 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
"python3 -m homeassistant -c /config --script check_config" "python3 -m homeassistant -c /config --script check_config"
) )
# if not valid # If not valid
if result.exit_code is None: if result.exit_code is None:
_LOGGER.error("Fatal error on config check!") _LOGGER.error("Fatal error on config check!")
raise HomeAssistantError() raise HomeAssistantError()
# parse output # Convert output
log = convert_to_ascii(result.output) log = convert_to_ascii(result.output)
_LOGGER.debug("Result config check: %s", log.strip())
# Parse output
if result.exit_code != 0 or RE_YAML_ERROR.search(log): if result.exit_code != 0 or RE_YAML_ERROR.search(log):
_LOGGER.error("Invalid Home Assistant config found!") _LOGGER.error("Invalid Home Assistant config found!")
return ConfigResult(False, log) return ConfigResult(False, log)

View File

@ -28,7 +28,7 @@ class HwMonitor(CoreSysAttributes):
self.monitor = pyudev.Monitor.from_netlink(self.context) self.monitor = pyudev.Monitor.from_netlink(self.context)
self.observer = pyudev.MonitorObserver(self.monitor, self._udev_events) self.observer = pyudev.MonitorObserver(self.monitor, self._udev_events)
except OSError: except OSError:
_LOGGER.fatal("Not privileged to run udev. Update your installation!") _LOGGER.fatal("Not privileged to run udev monitor!")
else: else:
self.observer.start() self.observer.start()
_LOGGER.info("Started Supervisor hardware monitor") _LOGGER.info("Started Supervisor hardware monitor")

View File

@ -25,7 +25,8 @@ RUN_WATCHDOG_HOMEASSISTANT_DOCKER = 15
RUN_WATCHDOG_HOMEASSISTANT_API = 300 RUN_WATCHDOG_HOMEASSISTANT_API = 300
RUN_WATCHDOG_DNS_DOCKER = 20 RUN_WATCHDOG_DNS_DOCKER = 20
RUN_WATCHDOG_AUDIO_DOCKER = 20 RUN_WATCHDOG_AUDIO_DOCKER = 30
RUN_WATCHDOG_CLI_DOCKER = 40
class Tasks(CoreSysAttributes): class Tasks(CoreSysAttributes):
@ -102,6 +103,11 @@ class Tasks(CoreSysAttributes):
self._watchdog_audio_docker, RUN_WATCHDOG_AUDIO_DOCKER self._watchdog_audio_docker, RUN_WATCHDOG_AUDIO_DOCKER
) )
) )
self.jobs.add(
self.sys_scheduler.register_task(
self._watchdog_cli_docker, RUN_WATCHDOG_CLI_DOCKER
)
)
_LOGGER.info("All core tasks are scheduled") _LOGGER.info("All core tasks are scheduled")
@ -202,12 +208,12 @@ class Tasks(CoreSysAttributes):
self._cache[HASS_WATCHDOG_API] = 0 self._cache[HASS_WATCHDOG_API] = 0
async def _update_cli(self): async def _update_cli(self):
"""Check and run update of CLI.""" """Check and run update of cli."""
if not self.sys_hassos.need_cli_update: if not self.sys_cli.need_update:
return return
_LOGGER.info("Found new CLI version") _LOGGER.info("Found new cli version")
await self.sys_hassos.update_cli() await self.sys_cli.update()
async def _update_dns(self): async def _update_dns(self):
"""Check and run update of CoreDNS plugin.""" """Check and run update of CoreDNS plugin."""
@ -232,9 +238,11 @@ class Tasks(CoreSysAttributes):
return return
_LOGGER.warning("Watchdog found a problem with CoreDNS plugin!") _LOGGER.warning("Watchdog found a problem with CoreDNS plugin!")
# Reset of fails
if await self.sys_dns.is_fails(): if await self.sys_dns.is_fails():
_LOGGER.warning("CoreDNS plugin is in fails state / Reset config") _LOGGER.error("CoreDNS plugin is in fails state / Reset config")
await self.sys_dns.reset() await self.sys_dns.reset()
await self.sys_dns.loop_detection()
try: try:
await self.sys_dns.start() await self.sys_dns.start()
@ -252,3 +260,15 @@ class Tasks(CoreSysAttributes):
await self.sys_audio.start() await self.sys_audio.start()
except CoreDNSError: except CoreDNSError:
_LOGGER.error("Watchdog PulseAudio reanimation fails!") _LOGGER.error("Watchdog PulseAudio reanimation fails!")
async def _watchdog_cli_docker(self):
"""Check running state of Docker and start if they is close."""
# if cli plugin is active
if await self.sys_cli.is_running() or self.sys_cli.in_progress:
return
_LOGGER.warning("Watchdog found a problem with cli plugin!")
try:
await self.sys_cli.start()
except CoreDNSError:
_LOGGER.error("Watchdog cli reanimation fails!")

View File

@ -208,7 +208,7 @@ class DBus:
raise exception() raise exception()
# General # General
_LOGGER.error("DBus return error: %s", error) _LOGGER.error("DBus return error: %s", error.strip())
raise DBusFatalError() raise DBusFatalError()
def attach_signals(self, filters=None): def attach_signals(self, filters=None):

View File

@ -182,3 +182,12 @@ SCHEMA_DNS_CONFIG = vol.Schema(
SCHEMA_AUDIO_CONFIG = vol.Schema( SCHEMA_AUDIO_CONFIG = vol.Schema(
{vol.Optional(ATTR_VERSION): vol.Maybe(vol.Coerce(str))}, extra=vol.REMOVE_EXTRA, {vol.Optional(ATTR_VERSION): vol.Maybe(vol.Coerce(str))}, extra=vol.REMOVE_EXTRA,
) )
SCHEMA_CLI_CONFIG = vol.Schema(
{
vol.Optional(ATTR_VERSION): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_ACCESS_TOKEN): token,
},
extra=vol.REMOVE_EXTRA,
)

View File

@ -1,19 +0,0 @@
"""Test adguard discovery."""
import voluptuous as vol
import pytest
from supervisor.discovery.validate import valid_discovery_config
def test_good_config():
"""Test good deconz config."""
valid_discovery_config("home_panel", {"host": "test", "port": 3812})
def test_bad_config():
"""Test good adguard config."""
with pytest.raises(vol.Invalid):
valid_discovery_config("home_panel", {"host": "test"})