From 35b7c2269c3d9a5335c68b6b9d003d8066412975 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 20 Jul 2018 23:45:36 +0200 Subject: [PATCH] Support control of hassos-cli (#555) * Support control of hassos-cli * Update const.py * Update validate.py * Update supervisor.py * Create hassos_cli.py * Update hassos_cli.py * Update hassos_cli.py * Update hassos.py * Update tasks.py * Update hassos.py * Update API.md * Update API.md * Update const.py * Update hassos.py * Update __init__.py * Fix lint * fix * Fix logging * change order * Fix download --- API.md | 9 ++++++++ hassio/api/__init__.py | 1 + hassio/api/hassos.py | 14 +++++++++++- hassio/const.py | 3 +++ hassio/core.py | 6 +++--- hassio/docker/hassos_cli.py | 37 +++++++++++++++++++++++++++++++ hassio/docker/supervisor.py | 2 +- hassio/hassos.py | 43 ++++++++++++++++++++++++++++++++++++- hassio/tasks.py | 18 +++++++++++++++- hassio/updater.py | 8 ++++++- hassio/validate.py | 3 ++- 11 files changed, 135 insertions(+), 9 deletions(-) create mode 100644 hassio/docker/hassos_cli.py diff --git a/API.md b/API.md index 47ad52f69..f3a6c0935 100644 --- a/API.md +++ b/API.md @@ -273,7 +273,9 @@ return: ```json { "version": "2.3", + "version_cli": "7", "version_latest": "2.4", + "version_cli_latest": "8", "board": "ova|rpi" } ``` @@ -285,6 +287,13 @@ return: } ``` +- POST `/hassos/update/cli` +```json +{ + "version": "optional" +} +``` + - POST `/hassos/config/sync` Load host configs from a USB stick. diff --git a/hassio/api/__init__.py b/hassio/api/__init__.py index f5866c930..c5cf8eee6 100644 --- a/hassio/api/__init__.py +++ b/hassio/api/__init__.py @@ -76,6 +76,7 @@ class RestAPI(CoreSysAttributes): self.webapp.add_routes([ 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), ]) diff --git a/hassio/api/hassos.py b/hassio/api/hassos.py index c5faeefaf..ac8166027 100644 --- a/hassio/api/hassos.py +++ b/hassio/api/hassos.py @@ -5,7 +5,9 @@ import logging import voluptuous as vol from .utils import api_process, api_validate -from ..const import ATTR_VERSION, ATTR_BOARD, ATTR_VERSION_LATEST +from ..const import ( + ATTR_VERSION, ATTR_BOARD, ATTR_VERSION_LATEST, ATTR_VERSION_CLI, + ATTR_VERSION_CLI_LATEST) from ..coresys import CoreSysAttributes _LOGGER = logging.getLogger(__name__) @@ -23,7 +25,9 @@ class APIHassOS(CoreSysAttributes): """Return hassos 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_BOARD: self.sys_hassos.board, } @@ -35,6 +39,14 @@ class APIHassOS(CoreSysAttributes): await asyncio.shield(self.sys_hassos.update(version)) + @api_process + async def update_cli(self, request): + """Update HassOS CLI.""" + body = await api_validate(SCHEMA_VERSION, request) + version = body.get(ATTR_VERSION, self.sys_hassos.version_cli_latest) + + await asyncio.shield(self.sys_hassos.update_cli(version)) + @api_process def config_sync(self, request): """Trigger config reload on HassOS.""" diff --git a/hassio/const.py b/hassio/const.py index 1b664dd26..a0e27a4f2 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -174,6 +174,9 @@ 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' SERVICE_MQTT = 'mqtt' diff --git a/hassio/core.py b/hassio/core.py index 4181d8f27..d7b846257 100644 --- a/hassio/core.py +++ b/hassio/core.py @@ -26,6 +26,9 @@ class HassIO(CoreSysAttributes): self.sys_config.timezone = \ await fetch_timezone(self.sys_websession) + # Load Supervisor + await self.sys_supervisor.load() + # Load DBus await self.sys_dbus.load() @@ -35,9 +38,6 @@ class HassIO(CoreSysAttributes): # Load HassOS await self.sys_hassos.load() - # Load Supervisor - await self.sys_supervisor.load() - # Load Home Assistant await self.sys_homeassistant.load() diff --git a/hassio/docker/hassos_cli.py b/hassio/docker/hassos_cli.py new file mode 100644 index 000000000..e76e9d8a8 --- /dev/null +++ b/hassio/docker/hassos_cli.py @@ -0,0 +1,37 @@ +"""HassOS Cli docker object.""" +import logging + +import docker + +from .interface import DockerInterface +from ..coresys import CoreSysAttributes + +_LOGGER = logging.getLogger(__name__) + + +class DockerHassOSCli(DockerInterface, CoreSysAttributes): + """Docker hassio wrapper for HassOS Cli.""" + + @property + def image(self): + """Return name of HassOS cli image.""" + return f"homeassistant/{self.sys_arch}-hassio-cli" + + def _stop(self): + """Don't need stop.""" + return True + + def _attach(self): + """Attach to running docker container. + Need run inside executor. + """ + try: + image = self.sys_docker.images.get(self.image) + + except docker.errors.DockerException: + _LOGGER.warning("Can't find a HassOS cli %s", self.image) + + else: + self._meta = image.attrs + _LOGGER.info("Found HassOS cli %s with version %s", + self.image, self.version) diff --git a/hassio/docker/supervisor.py b/hassio/docker/supervisor.py index 1a93015e7..66b6d4d94 100644 --- a/hassio/docker/supervisor.py +++ b/hassio/docker/supervisor.py @@ -11,7 +11,7 @@ _LOGGER = logging.getLogger(__name__) class DockerSupervisor(DockerInterface, CoreSysAttributes): - """Docker hassio wrapper for HomeAssistant.""" + """Docker hassio wrapper for Supervisor.""" @property def name(self): diff --git a/hassio/hassos.py b/hassio/hassos.py index e646289df..f48698d35 100644 --- a/hassio/hassos.py +++ b/hassio/hassos.py @@ -7,6 +7,7 @@ from cpe import CPE from .coresys import CoreSysAttributes from .const import URL_HASSOS_OTA +from .docker.hassos_cli import DockerHassOSCli from .exceptions import HassOSNotSupportedError, HassOSUpdateError, DBusError _LOGGER = logging.getLogger(__name__) @@ -18,6 +19,7 @@ class HassOS(CoreSysAttributes): def __init__(self, coresys): """Initialize HassOS handler.""" self.coresys = coresys + self.instance = DockerHassOSCli(coresys) self._available = False self._version = None self._board = None @@ -32,11 +34,31 @@ class HassOS(CoreSysAttributes): """Return version of HassOS.""" return self._version + @property + def version_cli(self): + """Return version of HassOS cli.""" + return self.instance.version + @property def version_latest(self): """Return version of HassOS.""" return self.sys_updater.version_hassos + @property + def version_cli_latest(self): + """Return version of HassOS.""" + return self.sys_updater.version_hassos_cli + + @property + def need_update(self): + """Return true if a HassOS update is available.""" + return self.version != self.version_latest + + @property + def need_cli_update(self): + """Return true if a HassOS cli update is available.""" + return self.version_cli != self.version_cli_latest + @property def board(self): """Return board name.""" @@ -56,6 +78,10 @@ class HassOS(CoreSysAttributes): try: _LOGGER.info("Fetch OTA update from %s", url) async with self.sys_websession.get(url) as request: + if request.status != 200: + raise HassOSUpdateError() + + # Download RAUCB file with raucb.open('wb') as ota_file: while True: chunk = await request.content.read(1048576) @@ -86,7 +112,7 @@ class HassOS(CoreSysAttributes): cpe = CPE(self.sys_host.info.cpe) assert cpe.get_product()[0] == 'hassos' except (AssertionError, NotImplementedError): - _LOGGER.debug("Ignore HassOS") + _LOGGER.debug("Found no HassOS") return # Store meta data @@ -95,6 +121,7 @@ class HassOS(CoreSysAttributes): self._board = cpe.get_target_hardware()[0] _LOGGER.info("Detect HassOS %s on host system", self.version) + await self.instance.attach() def config_sync(self): """Trigger a host config reload from usb. @@ -142,3 +169,17 @@ class HassOS(CoreSysAttributes): _LOGGER.error( "HassOS update fails with: %s", rauc_status.get('LastError')) raise HassOSUpdateError() + + async def update_cli(self, version=None): + """Update local HassOS cli.""" + version = version or self.version_cli_latest + + if version == self.version_cli: + _LOGGER.warning("Version %s is already installed for CLI", version) + raise HassOSUpdateError() + + if await self.instance.update(version): + return + + _LOGGER.error("HassOS CLI update fails.") + raise HassOSUpdateError() diff --git a/hassio/tasks.py b/hassio/tasks.py index ff1a50092..5a0abdd8e 100644 --- a/hassio/tasks.py +++ b/hassio/tasks.py @@ -10,6 +10,7 @@ HASS_WATCHDOG_API = 'HASS_WATCHDOG_API' RUN_UPDATE_SUPERVISOR = 29100 RUN_UPDATE_ADDONS = 57600 +RUN_UPDATE_HASSOSCLI = 29100 RUN_RELOAD_ADDONS = 21600 RUN_RELOAD_SNAPSHOTS = 72000 @@ -35,6 +36,8 @@ class Tasks(CoreSysAttributes): self._update_addons, RUN_UPDATE_ADDONS)) self.jobs.add(self.sys_scheduler.register_task( self._update_supervisor, RUN_UPDATE_SUPERVISOR)) + self.jobs.add(self.sys_scheduler.register_task( + self._update_hassos_cli, RUN_UPDATE_HASSOSCLI)) self.jobs.add(self.sys_scheduler.register_task( self.sys_addons.reload, RUN_RELOAD_ADDONS)) @@ -79,7 +82,7 @@ class Tasks(CoreSysAttributes): if not self.sys_supervisor.need_update: return - # don't perform an update on beta/dev channel + # don't perform an update on dev channel if self.sys_dev: _LOGGER.warning("Ignore Hass.io update on dev channel!") return @@ -135,3 +138,16 @@ class Tasks(CoreSysAttributes): await self.sys_homeassistant.restart() finally: self._cache[HASS_WATCHDOG_API] = 0 + + async def _update_hassos_cli(self): + """Check and run update of HassOS CLI.""" + if not self.sys_hassos.need_cli_update: + return + + # don't perform an update on dev channel + if self.sys_dev: + _LOGGER.warning("Ignore HassOS CLI update on dev channel!") + return + + _LOGGER.info("Found new HassOS CLI version") + await self.sys_hassos.update_cli() diff --git a/hassio/updater.py b/hassio/updater.py index 78cb3bed6..fb8efe876 100644 --- a/hassio/updater.py +++ b/hassio/updater.py @@ -8,7 +8,7 @@ import aiohttp from .const import ( URL_HASSIO_VERSION, FILE_HASSIO_UPDATER, ATTR_HOMEASSISTANT, ATTR_HASSIO, - ATTR_CHANNEL, ATTR_HASSOS) + ATTR_CHANNEL, ATTR_HASSOS, ATTR_HASSOS_CLI) from .coresys import CoreSysAttributes from .utils import AsyncThrottle from .utils.json import JsonConfig @@ -51,6 +51,11 @@ class Updater(JsonConfig, CoreSysAttributes): """Return last version of hassos.""" return self._data.get(ATTR_HASSOS) + @property + def version_hassos_cli(self): + """Return last version of hassos cli.""" + return self._data.get(ATTR_HASSOS_CLI) + @property def channel(self): """Return upstream channel of hassio instance.""" @@ -99,6 +104,7 @@ class Updater(JsonConfig, CoreSysAttributes): # update hassos version if self.sys_hassos.available and board: self._data[ATTR_HASSOS] = data['hassos'][board] + self._data[ATTR_HASSOS_CLI] = data['hassos-cli'] except KeyError as err: _LOGGER.warning("Can't process version data: %s", err) diff --git a/hassio/validate.py b/hassio/validate.py index fc2040eda..86a8b1e85 100644 --- a/hassio/validate.py +++ b/hassio/validate.py @@ -9,7 +9,7 @@ from .const import ( ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_CHANNEL, ATTR_TIMEZONE, ATTR_HASSOS, ATTR_ADDONS_CUSTOM_LIST, ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO, ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, - ATTR_WAIT_BOOT, ATTR_UUID, ATTR_REFRESH_TOKEN, + ATTR_WAIT_BOOT, ATTR_UUID, ATTR_REFRESH_TOKEN, ATTR_HASSOS_CLI, CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV) @@ -102,6 +102,7 @@ SCHEMA_UPDATER_CONFIG = vol.Schema({ vol.Optional(ATTR_HOMEASSISTANT): vol.Coerce(str), vol.Optional(ATTR_HASSIO): vol.Coerce(str), vol.Optional(ATTR_HASSOS): vol.Coerce(str), + vol.Optional(ATTR_HASSOS_CLI): vol.Coerce(str), }, extra=vol.REMOVE_EXTRA)