Cli rebrand (#1601)

* Rebrand CLI

* forward

* Fix startup command

* add cli api

* Add token handling

* Fix security check

* fix repair

* fix lint

* Update for new cli

* Add watchdog

* rename

* use s6
This commit is contained in:
Pascal Vizeli 2020-03-27 17:37:48 +01:00 committed by GitHub
parent 24f7801ddc
commit 96c0fbaf10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 536 additions and 403 deletions

180
.vscode/tasks.json vendored
View File

@ -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": []
}
]
}
"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": []
}
]
}

182
API.md
View File

@ -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
{

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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),
]
)

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_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())

View File

@ -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!")

View File

@ -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

View File

@ -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

View File

@ -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)

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

@ -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")
@ -189,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"

View File

@ -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")

View File

@ -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."""

View File

@ -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

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,
"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,
},
)

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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")

View File

@ -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."""
@ -254,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!")

View File

@ -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):

View File

@ -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,
)