diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ea1a90de5..c4ca20cbd 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,92 +1,90 @@ { - "version": "2.0.0", - "tasks": [ - { - "label": "Run Testenv", - "type": "shell", - "command": "./scripts/test_env.sh", - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Run Testenv CLI", - "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", - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Update UI", - "type": "shell", - "command": "./scripts/update-frontend.sh", - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Pytest", - "type": "shell", - "command": "pytest --timeout=10 tests", - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Flake8", - "type": "shell", - "command": "flake8 hassio tests", - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Pylint", - "type": "shell", - "command": "pylint hassio", - "dependsOn": [ - "Install all Requirements" - ], - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - } - ] -} \ No newline at end of file + "version": "2.0.0", + "tasks": [ + { + "label": "Run Testenv", + "type": "shell", + "command": "./scripts/test_env.sh", + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Run Testenv CLI", + "type": "shell", + "command": "docker exec -ti hassio_cli /usr/bin/cli.sh", + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Update UI", + "type": "shell", + "command": "./scripts/update-frontend.sh", + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Pytest", + "type": "shell", + "command": "pytest --timeout=10 tests", + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Flake8", + "type": "shell", + "command": "flake8 hassio tests", + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Pylint", + "type": "shell", + "command": "pylint hassio", + "dependsOn": ["Install all Requirements"], + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + } + ] +} diff --git a/API.md b/API.md index 63a954591..f60930681 100644 --- a/API.md +++ b/API.md @@ -291,9 +291,7 @@ return: ```json { "version": "2.3", - "version_cli": "7", "version_latest": "2.4", - "version_cli_latest": "8", "board": "ova|rpi", "boot": "rauc boot slot" } @@ -307,14 +305,6 @@ return: } ``` -- POST `/os/update/cli` - -```json -{ - "version": "optional" -} -``` - - POST `/os/config/sync` Load host configs from a USB stick. @@ -857,90 +847,29 @@ return: } ``` -### Audio +### DNS -- GET `/audio/info` +- GET `/dns/info` ```json { "host": "ip-address", "version": "1", "latest_version": "2", - "audio": { - "card": [ - { - "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": "" - } - ] - } + "servers": ["dns://8.8.8.8"], + "locals": ["dns://xy"] } ``` -- POST `/audio/update` +- POST `/dns/options` + +```json +{ + "servers": ["dns://8.8.8.8"] +} +``` + +- POST `/dns/update` ```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 { - "index": "...", - "volume": 0.5 + "cpu_percent": 0.0, + "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 { - "index": "...", - "volume": 0.5 + "version": "1", + "version_latest": "2" } ``` -- POST `/audio/volume/{output|input}/application` +- POST `/cli/update` ```json { - "index": "...", - "volume": 0.5 + "version": "VERSION" } ``` -- POST `/audio/mute/input` - -```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` +- GET `/cli/stats` ```json { diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 0d07aea1e..ba0bab474 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -8,53 +8,19 @@ trigger: pr: none variables: - name: versionWheels - value: "1.6-3.7-alpine3.11" - - group: wheels + value: '1.6.1-3.7-alpine3.11' +resources: + repositories: + - repository: azure + type: github + name: 'home-assistant/ci-azure' + endpoint: 'home-assistant' + jobs: - - job: "Wheels" - timeoutInMinutes: 360 - pool: - vmImage: "ubuntu-latest" - strategy: - maxParallel: 5 - 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" +- template: templates/azp-job-wheels.yaml@azure + parameters: + builderVersion: '$(versionWheels)' + builderApk: 'build-base;libffi-dev;openssl-dev' + builderPip: 'Cython' + wheelsRequirement: 'requirements.txt' diff --git a/requirements.txt b/requirements.txt index 149bad24f..c15237824 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ aiohttp==3.6.1 async_timeout==3.0.1 attrs==19.3.0 -cchardet==2.1.5 +cchardet==2.1.6 colorlog==4.1.0 cpe==1.2.1 cryptography==2.8 diff --git a/requirements_tests.txt b/requirements_tests.txt index 6057c1721..a19f1345e 100644 --- a/requirements_tests.txt +++ b/requirements_tests.txt @@ -1,6 +1,6 @@ flake8==3.7.9 pylint==2.4.4 -pytest==5.3.5 +pytest==5.4.1 pytest-timeout==1.3.4 pytest-aiohttp==0.3.0 black==19.10b0 diff --git a/rootfs/etc/services.d/supervisor/run b/rootfs/etc/services.d/supervisor/run index 4450c0086..ddb3e92da 100644 --- a/rootfs/etc/services.d/supervisor/run +++ b/rootfs/etc/services.d/supervisor/run @@ -2,4 +2,6 @@ # ============================================================================== # Start Service service # ============================================================================== +export LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" + exec python3 -m supervisor \ No newline at end of file diff --git a/scripts/test_env.sh b/scripts/test_env.sh index ab0d675eb..fface59b2 100755 --- a/scripts/test_env.sh +++ b/scripts/test_env.sh @@ -81,11 +81,6 @@ function cleanup_docker() { } -function install_cli() { - docker pull homeassistant/amd64-hassio-cli:dev -} - - function setup_test_env() { mkdir -p /workspaces/test_supervisor @@ -131,7 +126,6 @@ start_docker trap "stop_docker" ERR build_supervisor -install_cli cleanup_lastboot cleanup_docker init_dbus diff --git a/supervisor/addons/__init__.py b/supervisor/addons/__init__.py index be2139830..865f66b8e 100644 --- a/supervisor/addons/__init__.py +++ b/supervisor/addons/__init__.py @@ -59,7 +59,7 @@ class AddonManager(CoreSysAttributes): def from_token(self, token: str) -> Optional[Addon]: """Return an add-on from Supervisor token.""" for addon in self.installed: - if token == addon.hassio_token: + if token == addon.supervisor_token: return addon return None diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index 80a4efbb7..38c6b4ad4 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -164,7 +164,7 @@ class Addon(AddonModel): return self.persist[ATTR_UUID] @property - def hassio_token(self) -> Optional[str]: + def supervisor_token(self) -> Optional[str]: """Return access token for Supervisor API.""" return self.persist.get(ATTR_ACCESS_TOKEN) diff --git a/supervisor/addons/model.py b/supervisor/addons/model.py index c9bfe202d..4d4960a68 100644 --- a/supervisor/addons/model.py +++ b/supervisor/addons/model.py @@ -137,7 +137,7 @@ class AddonModel(CoreSysAttributes): return None @property - def hassio_token(self) -> Optional[str]: + def supervisor_token(self) -> Optional[str]: """Return access token for Supervisor API.""" return None diff --git a/supervisor/addons/validate.py b/supervisor/addons/validate.py index 0809ee7a2..a0527984f 100644 --- a/supervisor/addons/validate.py +++ b/supervisor/addons/validate.py @@ -261,7 +261,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema( ), vol.Optional(ATTR_IMAGE): vol.Match(RE_DOCKER_IMAGE), 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, diff --git a/supervisor/api/__init__.py b/supervisor/api/__init__.py index 055af90b3..846f2ff2f 100644 --- a/supervisor/api/__init__.py +++ b/supervisor/api/__init__.py @@ -7,11 +7,13 @@ from aiohttp import web from ..coresys import CoreSys, CoreSysAttributes from .addons import APIAddons +from .audio import APIAudio from .auth import APIAuth +from .cli import APICli from .discovery import APIDiscovery from .dns import APICoreDNS from .hardware import APIHardware -from .hassos import APIHassOS +from .os import APIOS from .homeassistant import APIHomeAssistant from .host import APIHost from .info import APIInfo @@ -21,7 +23,6 @@ from .security import SecurityMiddleware from .services import APIServices from .snapshots import APISnapshots from .supervisor import APISupervisor -from .audio import APIAudio _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -49,7 +50,8 @@ class RestAPI(CoreSysAttributes): """Register REST API Calls.""" self._register_supervisor() self._register_host() - self._register_hassos() + self._register_os() + self._register_cli() self._register_hardware() self._register_homeassistant() self._register_proxy() @@ -84,22 +86,29 @@ class RestAPI(CoreSysAttributes): ] ) - def _register_hassos(self) -> None: - """Register HassOS functions.""" - api_hassos = APIHassOS() - api_hassos.coresys = self.coresys + def _register_os(self) -> None: + """Register OS functions.""" + api_os = APIOS() + api_os.coresys = self.coresys self.webapp.add_routes( [ - web.get("/os/info", api_hassos.info), - web.post("/os/update", api_hassos.update), - web.post("/os/update/cli", api_hassos.update_cli), - 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), - web.post("/hassos/update/cli", api_hassos.update_cli), - web.post("/hassos/config/sync", api_hassos.config_sync), + web.get("/os/info", api_os.info), + web.post("/os/update", api_os.update), + web.post("/os/config/sync", api_os.config_sync), + ] + ) + + def _register_cli(self) -> None: + """Register HA cli functions.""" + 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), ] ) diff --git a/supervisor/api/audio.py b/supervisor/api/audio.py index fa269a81c..f133db535 100644 --- a/supervisor/api/audio.py +++ b/supervisor/api/audio.py @@ -166,5 +166,5 @@ class APIAudio(CoreSysAttributes): body = await api_validate(SCHEMA_PROFILE, request) 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]) ) diff --git a/supervisor/api/cli.py b/supervisor/api/cli.py new file mode 100644 index 000000000..34cf13545 --- /dev/null +++ b/supervisor/api/cli.py @@ -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)) diff --git a/supervisor/api/hassos.py b/supervisor/api/os.py similarity index 56% rename from supervisor/api/hassos.py rename to supervisor/api/os.py index 95128f525..e89474d55 100644 --- a/supervisor/api/hassos.py +++ b/supervisor/api/os.py @@ -10,8 +10,6 @@ from ..const import ( ATTR_BOARD, ATTR_BOOT, ATTR_VERSION, - ATTR_VERSION_CLI, - ATTR_VERSION_CLI_LATEST, ATTR_VERSION_LATEST, ) 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)}) -class APIHassOS(CoreSysAttributes): - """Handle RESTful API for HassOS functions.""" +class APIOS(CoreSysAttributes): + """Handle RESTful API for OS functions.""" @api_process async def info(self, request: web.Request) -> Dict[str, Any]: - """Return HassOS information.""" + """Return OS information.""" return { ATTR_VERSION: self.sys_hassos.version, - ATTR_VERSION_CLI: self.sys_hassos.version_cli, - ATTR_VERSION_LATEST: self.sys_hassos.version_latest, - ATTR_VERSION_CLI_LATEST: self.sys_hassos.version_cli_latest, + ATTR_VERSION_LATEST: self.sys_hassos.latest_version, ATTR_BOARD: self.sys_hassos.board, ATTR_BOOT: self.sys_dbus.rauc.boot_slot, } @api_process async def update(self, request: web.Request) -> None: - """Update HassOS.""" + """Update OS.""" 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)) - @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 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()) diff --git a/supervisor/api/proxy.py b/supervisor/api/proxy.py index 9e8204301..f7f6d74f8 100644 --- a/supervisor/api/proxy.py +++ b/supervisor/api/proxy.py @@ -26,11 +26,11 @@ class APIProxy(CoreSysAttributes): """Check the Supervisor token.""" if AUTHORIZATION in request.headers: bearer = request.headers[AUTHORIZATION] - hassio_token = bearer.split(" ")[-1] + supervisor_token = bearer.split(" ")[-1] 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: _LOGGER.warning("Unknown Home Assistant API access!") elif not addon.access_homeassistant_api: @@ -177,8 +177,10 @@ class APIProxy(CoreSysAttributes): # Check API access response = await server.receive_json() - hassio_token = response.get("api_password") or response.get("access_token") - addon = self.sys_addons.from_token(hassio_token) + supervisor_token = response.get("api_password") or response.get( + "access_token" + ) + addon = self.sys_addons.from_token(supervisor_token) if not addon or not addon.access_homeassistant_api: _LOGGER.warning("Unauthorized WebSocket access!") diff --git a/supervisor/api/security.py b/supervisor/api/security.py index e338600be..663c4b062 100644 --- a/supervisor/api/security.py +++ b/supervisor/api/security.py @@ -75,6 +75,7 @@ ADDONS_ROLE_ACCESS = { r"^(?:" r"|/audio/.*" r"|/dns/.*" + r"|/cli/.*" r"|/core/.+" r"|/homeassistant/.+" r"|/host/.+" @@ -123,12 +124,13 @@ class SecurityMiddleware(CoreSysAttributes): raise HTTPUnauthorized() # 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) request_from = self.sys_homeassistant # 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) request_from = self.sys_host diff --git a/supervisor/audio.py b/supervisor/audio.py index e7d84236d..3a4adfe61 100644 --- a/supervisor/audio.py +++ b/supervisor/audio.py @@ -49,7 +49,7 @@ class Audio(JsonConfig, CoreSysAttributes): @version.setter def version(self, value: str) -> None: - """Return current version of Audio.""" + """Set current version of Audio.""" self._data[ATTR_VERSION] = value @property diff --git a/supervisor/bootstrap.py b/supervisor/bootstrap.py index 1c467a246..26ae31352 100644 --- a/supervisor/bootstrap.py +++ b/supervisor/bootstrap.py @@ -14,6 +14,7 @@ from .auth import Auth from .audio import Audio from .const import SOCKET_DOCKER, UpdateChannels from .core import Core +from .cli import HaCli from .coresys import CoreSys from .dbus import DBusManager from .discovery import Discovery @@ -67,6 +68,7 @@ async def initialize_coresys(): coresys.dbus = DBusManager(coresys) coresys.hassos = HassOS(coresys) coresys.secrets = SecretsManager(coresys) + coresys.cli = HaCli(coresys) # bootstrap config initialize_system_data(coresys) diff --git a/supervisor/cli.py b/supervisor/cli.py new file mode 100644 index 000000000..6b2d9bc49 --- /dev/null +++ b/supervisor/cli.py @@ -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") diff --git a/supervisor/const.py b/supervisor/const.py index efb0d6810..2f79ed49f 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -3,7 +3,7 @@ from enum import Enum from ipaddress import ip_network from pathlib import Path -SUPERVISOR_VERSION = "209" +SUPERVISOR_VERSION = "210" 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_DNS = Path(SUPERVISOR_DATA, "dns.json") FILE_HASSIO_AUDIO = Path(SUPERVISOR_DATA, "audio.json") +FILE_HASSIO_CLI = Path(SUPERVISOR_DATA, "cli.json") 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_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" LABEL_VERSION = "io.hass.version" @@ -190,9 +190,6 @@ ATTR_DEVICETREE = "devicetree" ATTR_CPE = "cpe" ATTR_BOARD = "board" 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_ACCESS_TOKEN = "access_token" ATTR_DOCKER_API = "docker_api" diff --git a/supervisor/core.py b/supervisor/core.py index bc7f62371..77d0ee5a1 100644 --- a/supervisor/core.py +++ b/supervisor/core.py @@ -41,7 +41,9 @@ class Core(CoreSysAttributes): await self.sys_host.load() # 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 await self.sys_homeassistant.load() @@ -203,13 +205,11 @@ class Core(CoreSysAttributes): # Restore core functionality await self.sys_dns.repair() + await self.sys_audio.repair() + await self.sys_cli.repair() await self.sys_addons.repair() await self.sys_homeassistant.repair() - # Fix HassOS specific - if self.sys_hassos.available: - await self.sys_hassos.repair_cli() - # Tag version for latest await self.sys_supervisor.repair() _LOGGER.info("Finished repairing of Supervisor Environment") diff --git a/supervisor/coresys.py b/supervisor/coresys.py index e4aae72b1..6015df87c 100644 --- a/supervisor/coresys.py +++ b/supervisor/coresys.py @@ -18,6 +18,7 @@ if TYPE_CHECKING: from .audio import Audio from .auth import Auth from .core import Core + from .cli import HaCli from .dbus import DBusManager from .discovery import Discovery from .dns import CoreDNS @@ -62,6 +63,7 @@ class CoreSys: self._audio: Optional[Audio] = None self._auth: Optional[Auth] = None self._dns: Optional[CoreDNS] = None + self._cli: Optional[HaCli] = None self._homeassistant: Optional[HomeAssistant] = None self._supervisor: Optional[Supervisor] = None self._addons: Optional[AddonManager] = None @@ -143,6 +145,18 @@ class CoreSys: raise RuntimeError("Core already set!") 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 def arch(self) -> CpuArch: """Return CpuArch object.""" @@ -449,6 +463,11 @@ class CoreSysAttributes: """Return core object.""" return self.coresys.core + @property + def sys_cli(self) -> HaCli: + """Return HaCli object.""" + return self.coresys.cli + @property def sys_arch(self) -> CpuArch: """Return CpuArch object.""" diff --git a/supervisor/data/coredns.tmpl b/supervisor/data/coredns.tmpl index 7ab08f4aa..50b6dc540 100644 --- a/supervisor/data/coredns.tmpl +++ b/supervisor/data/coredns.tmpl @@ -1,15 +1,31 @@ .:53 { log errors + loop hosts /config/hosts { fallthrough } template ANY AAAA local.hass.io hassio { rcode NOERROR } - forward . $servers { + forward . {{ locals | join(" ") }} dns://127.0.0.1:5353 { except local.hass.io 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 } + cache 30 } diff --git a/supervisor/discovery/services/home_panel.py b/supervisor/discovery/services/home_panel.py deleted file mode 100644 index 147c2bdb4..000000000 --- a/supervisor/discovery/services/home_panel.py +++ /dev/null @@ -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} -) diff --git a/supervisor/dns.py b/supervisor/dns.py index 306daacc6..98b20cf57 100644 --- a/supervisor/dns.py +++ b/supervisor/dns.py @@ -4,13 +4,13 @@ from contextlib import suppress from ipaddress import IPv4Address import logging from pathlib import Path -from string import Template from typing import Awaitable, List, Optional import attr +import jinja2 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 .docker.dns import DockerDNS from .docker.stats import DockerStats @@ -42,8 +42,10 @@ class CoreDNS(JsonConfig, CoreSysAttributes): self.coresys: CoreSys = coresys self.instance: DockerDNS = DockerDNS(coresys) self.forwarder: DNSForward = DNSForward() + self.coredns_template: Optional[jinja2.Template] = None self._hosts: List[HostEntry] = [] + self._loop: bool = False @property def corefile(self) -> Path: @@ -116,6 +118,12 @@ class CoreDNS(JsonConfig, CoreSysAttributes): # Start DNS forwarder 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 with suppress(CoreDNSError): if await self.instance.is_running(): @@ -202,30 +210,43 @@ class CoreDNS(JsonConfig, CoreSysAttributes): self.hosts.unlink() self._init_hosts() + # Reset loop protection + self._loop = False + 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: """Write CoreDNS config.""" dns_servers: List[str] = [] - - # Load Template - 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 + local_dns: List[str] = [] + servers: List[str] = [] # 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"] - servers: List[str] = self.servers + local_dns + DNS_SERVERS + if not self._loop: + 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( - "config-dns = %s, local-dns = %s , backup-dns = %s", + "config-dns = %s, local-dns = %s , backup-dns = CloudFlare DoT", self.servers, local_dns, - DNS_SERVERS, ) + # Make sure, they are valid for server in servers: try: dns_url(server) @@ -235,7 +256,7 @@ class CoreDNS(JsonConfig, CoreSysAttributes): _LOGGER.warning("Ignore invalid DNS Server: %s", server) # Generate config file - data = corefile_template.safe_substitute(servers=" ".join(dns_servers)) + data = self.coredns_template.render(locals=dns_servers) try: self.corefile.write_text(data) @@ -339,7 +360,6 @@ class CoreDNS(JsonConfig, CoreSysAttributes): def is_fails(self) -> Awaitable[bool]: """Return True if a Docker container is fails state. - Return a coroutine. """ return self.instance.is_fails() diff --git a/supervisor/docker/addon.py b/supervisor/docker/addon.py index a8e27d650..05dd420a3 100644 --- a/supervisor/docker/addon.py +++ b/supervisor/docker/addon.py @@ -117,8 +117,8 @@ class DockerAddon(DockerInterface): return { **addon_env, ENV_TIME: self.sys_timezone, - ENV_TOKEN: self.addon.hassio_token, - ENV_TOKEN_OLD: self.addon.hassio_token, + ENV_TOKEN: self.addon.supervisor_token, + ENV_TOKEN_OLD: self.addon.supervisor_token, } @property diff --git a/supervisor/docker/cli.py b/supervisor/docker/cli.py new file mode 100644 index 000000000..08ebca7d3 --- /dev/null +++ b/supervisor/docker/cli.py @@ -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, + ) diff --git a/supervisor/docker/hassos_cli.py b/supervisor/docker/hassos_cli.py deleted file mode 100644 index 64b30c858..000000000 --- a/supervisor/docker/hassos_cli.py +++ /dev/null @@ -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 - ) diff --git a/supervisor/docker/homeassistant.py b/supervisor/docker/homeassistant.py index 3e0eb2c89..95cc3e419 100644 --- a/supervisor/docker/homeassistant.py +++ b/supervisor/docker/homeassistant.py @@ -112,8 +112,8 @@ class DockerHomeAssistant(DockerInterface): "HASSIO": self.sys_docker.network.supervisor, "SUPERVISOR": self.sys_docker.network.supervisor, ENV_TIME: self.sys_timezone, - ENV_TOKEN: self.sys_homeassistant.hassio_token, - ENV_TOKEN_OLD: self.sys_homeassistant.hassio_token, + ENV_TOKEN: self.sys_homeassistant.supervisor_token, + ENV_TOKEN_OLD: self.sys_homeassistant.supervisor_token, }, ) diff --git a/supervisor/docker/network.py b/supervisor/docker/network.py index 1a0fbcf4e..de78aa273 100644 --- a/supervisor/docker/network.py +++ b/supervisor/docker/network.py @@ -53,6 +53,11 @@ class DockerNetwork: """Return audio of the network.""" 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: """Get supervisor network.""" try: diff --git a/supervisor/exceptions.py b/supervisor/exceptions.py index 30b35c11e..c5484daff 100644 --- a/supervisor/exceptions.py +++ b/supervisor/exceptions.py @@ -54,6 +54,17 @@ class HassOSNotSupportedError(HassioNotSupportedError): """Function not supported by HassOS.""" +# HaCli + + +class CliError(HassioError): + """HA cli exception.""" + + +class CliUpdateError(HassOSError): + """Error on update of a HA cli.""" + + # DNS diff --git a/supervisor/hassos.py b/supervisor/hassos.py index a7f745251..8645a648b 100644 --- a/supervisor/hassos.py +++ b/supervisor/hassos.py @@ -1,6 +1,5 @@ """HassOS support on supervisor.""" import asyncio -from contextlib import suppress import logging from pathlib import Path from typing import Awaitable, Optional @@ -10,12 +9,10 @@ from cpe import CPE from .const import URL_HASSOS_OTA from .coresys import CoreSysAttributes, CoreSys -from .docker.hassos_cli import DockerHassOSCli from .exceptions import ( DBusError, HassOSNotSupportedError, HassOSUpdateError, - DockerAPIError, ) from .dbus.rauc import RaucState @@ -28,7 +25,6 @@ class HassOS(CoreSysAttributes): def __init__(self, coresys: CoreSys): """Initialize HassOS handler.""" self.coresys: CoreSys = coresys - self.instance: DockerHassOSCli = DockerHassOSCli(coresys) self._available: bool = False self._version: Optional[str] = None self._board: Optional[str] = None @@ -44,29 +40,14 @@ class HassOS(CoreSysAttributes): return self._version @property - def version_cli(self) -> Optional[str]: - """Return version of HassOS cli.""" - return self.instance.version - - @property - def version_latest(self) -> str: + def latest_version(self) -> str: """Return version of 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 def need_update(self) -> bool: """Return true if a HassOS update is available.""" - return self.version != self.version_latest - - @property - def need_cli_update(self) -> bool: - """Return true if a HassOS cli update is available.""" - return self.version_cli != self.version_cli_latest + return self.version != self.latest_version @property def board(self) -> Optional[str]: @@ -134,8 +115,6 @@ class HassOS(CoreSysAttributes): _LOGGER.info( "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]: """Trigger a host config reload from usb. @@ -149,7 +128,7 @@ class HassOS(CoreSysAttributes): async def update(self, version: Optional[str] = None) -> None: """Update HassOS system.""" - version = version or self.version_latest + version = version or self.latest_version # Check installed version self._check_host() @@ -183,35 +162,6 @@ class HassOS(CoreSysAttributes): _LOGGER.error("HassOS update fails with: %s", self.sys_dbus.rauc.last_error) 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): """Set booted partition as good for rauc.""" try: diff --git a/supervisor/homeassistant.py b/supervisor/homeassistant.py index fae77c2b1..481c7355e 100644 --- a/supervisor/homeassistant.py +++ b/supervisor/homeassistant.py @@ -221,7 +221,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): return self._data[ATTR_UUID] @property - def hassio_token(self) -> str: + def supervisor_token(self) -> str: """Return an access token for the Supervisor API.""" return self._data.get(ATTR_ACCESS_TOKEN) @@ -465,13 +465,16 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): "python3 -m homeassistant -c /config --script check_config" ) - # if not valid + # If not valid if result.exit_code is None: _LOGGER.error("Fatal error on config check!") raise HomeAssistantError() - # parse output + # Convert 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): _LOGGER.error("Invalid Home Assistant config found!") return ConfigResult(False, log) diff --git a/supervisor/hwmon.py b/supervisor/hwmon.py index 6ce1d90bb..13b7baf3c 100644 --- a/supervisor/hwmon.py +++ b/supervisor/hwmon.py @@ -28,7 +28,7 @@ class HwMonitor(CoreSysAttributes): self.monitor = pyudev.Monitor.from_netlink(self.context) self.observer = pyudev.MonitorObserver(self.monitor, self._udev_events) except OSError: - _LOGGER.fatal("Not privileged to run udev. Update your installation!") + _LOGGER.fatal("Not privileged to run udev monitor!") else: self.observer.start() _LOGGER.info("Started Supervisor hardware monitor") diff --git a/supervisor/tasks.py b/supervisor/tasks.py index bab7a8211..a24f9b3f5 100644 --- a/supervisor/tasks.py +++ b/supervisor/tasks.py @@ -25,7 +25,8 @@ RUN_WATCHDOG_HOMEASSISTANT_DOCKER = 15 RUN_WATCHDOG_HOMEASSISTANT_API = 300 RUN_WATCHDOG_DNS_DOCKER = 20 -RUN_WATCHDOG_AUDIO_DOCKER = 20 +RUN_WATCHDOG_AUDIO_DOCKER = 30 +RUN_WATCHDOG_CLI_DOCKER = 40 class Tasks(CoreSysAttributes): @@ -102,6 +103,11 @@ class Tasks(CoreSysAttributes): 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") @@ -202,12 +208,12 @@ class Tasks(CoreSysAttributes): self._cache[HASS_WATCHDOG_API] = 0 async def _update_cli(self): - """Check and run update of CLI.""" - if not self.sys_hassos.need_cli_update: + """Check and run update of cli.""" + if not self.sys_cli.need_update: return - _LOGGER.info("Found new CLI version") - await self.sys_hassos.update_cli() + _LOGGER.info("Found new cli version") + await self.sys_cli.update() async def _update_dns(self): """Check and run update of CoreDNS plugin.""" @@ -232,9 +238,11 @@ class Tasks(CoreSysAttributes): return _LOGGER.warning("Watchdog found a problem with CoreDNS plugin!") + # Reset of 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.loop_detection() try: await self.sys_dns.start() @@ -252,3 +260,15 @@ class Tasks(CoreSysAttributes): await self.sys_audio.start() except CoreDNSError: _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!") diff --git a/supervisor/utils/gdbus.py b/supervisor/utils/gdbus.py index 617fa3c83..62bf0ad74 100644 --- a/supervisor/utils/gdbus.py +++ b/supervisor/utils/gdbus.py @@ -208,7 +208,7 @@ class DBus: raise exception() # General - _LOGGER.error("DBus return error: %s", error) + _LOGGER.error("DBus return error: %s", error.strip()) raise DBusFatalError() def attach_signals(self, filters=None): diff --git a/supervisor/validate.py b/supervisor/validate.py index 7fb1df5b5..d4ca5835c 100644 --- a/supervisor/validate.py +++ b/supervisor/validate.py @@ -182,3 +182,12 @@ SCHEMA_DNS_CONFIG = vol.Schema( SCHEMA_AUDIO_CONFIG = vol.Schema( {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, +) diff --git a/tests/discovery/test_home_panel.py b/tests/discovery/test_home_panel.py deleted file mode 100644 index cb768751c..000000000 --- a/tests/discovery/test_home_panel.py +++ /dev/null @@ -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"})