From 0ac96c207e6d5ad1dc487320dc0dc2e305fa6b24 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 6 Jul 2017 01:29:09 +0200 Subject: [PATCH] Convert homeassistant into a dict (snapshot) (#90) * Convert homeassistant into a dict * fix lint * fix bugs * cleanup code * fix cleanup --- API.md | 7 ++++-- hassio/addons/addon.py | 11 ++++----- hassio/api/snapshots.py | 8 +++++-- hassio/dock/__init__.py | 3 +++ hassio/dock/addon.py | 14 +++++++---- hassio/snapshots/__init__.py | 28 ++++++++++++---------- hassio/snapshots/snapshot.py | 46 ++++++++++++++++++++++++------------ hassio/snapshots/validate.py | 17 +++++++------ 8 files changed, 85 insertions(+), 49 deletions(-) diff --git a/API.md b/API.md index ca66274ea..899082608 100644 --- a/API.md +++ b/API.md @@ -180,10 +180,13 @@ Return QR-Code { "slug": "SNAPSHOT ID", "type": "full|partial", - "name": "custom snapshot name", + "name": "custom snapshot name / description", "date": "ISO", "size": "SIZE_IN_MB", - "homeassistant": "INSTALLED_HASS_VERSION", + "homeassistant": { + "version": "INSTALLED_HASS_VERSION", + "devices": [] + }, "addons": [ { "slug": "ADDON_SLUG", diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index b5d10395a..633ac674c 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -434,15 +434,14 @@ class Addon(object): self._restore_data(data[ATTR_USER], data[ATTR_SYSTEM]) # check version / restore image - if data[ATTR_VERSION] != self.addon_docker.version: + version = data[ATTR_VERSION] + if version != self.addon_docker.version: image_file = Path(temp, "image.tar") if image_file.is_file(): - if not await self.addon_docker.import_image(image_file): - return False + await self.addon_docker.import_image(image_file, version) else: - if not await self.addon_docker.install(data[ATTR_VERSION]): - return False - await self.addon_docker.cleanup() + if await self.addon_docker.install(version): + await self.addon_docker.cleanup() else: await self.addon_docker.stop() diff --git a/hassio/api/snapshots.py b/hassio/api/snapshots.py index 4ba7652da..0fdab2fec 100644 --- a/hassio/api/snapshots.py +++ b/hassio/api/snapshots.py @@ -8,7 +8,8 @@ from .util import api_process, api_validate from ..snapshots.validate import ALL_FOLDERS from ..const import ( ATTR_NAME, ATTR_SLUG, ATTR_DATE, ATTR_ADDONS, ATTR_REPOSITORIES, - ATTR_HOMEASSISTANT, ATTR_VERSION, ATTR_SIZE, ATTR_FOLDERS, ATTR_TYPE) + ATTR_HOMEASSISTANT, ATTR_VERSION, ATTR_SIZE, ATTR_FOLDERS, ATTR_TYPE, + ATTR_DEVICES) _LOGGER = logging.getLogger(__name__) @@ -69,7 +70,10 @@ class APISnapshots(object): ATTR_NAME: snapshot.name, ATTR_DATE: snapshot.date, ATTR_SIZE: snapshot.size, - ATTR_HOMEASSISTANT: snapshot.homeassistant, + ATTR_HOMEASSISTANT: { + ATTR_VERSION: snapshot.homeassistant_version, + ATTR_DEVICES: snapshot.homeassistant_devices, + }, ATTR_ADDONS: self._addons_list(snapshot), ATTR_REPOSITORIES: snapshot.repositories, ATTR_FOLDERS: snapshot.folders, diff --git a/hassio/dock/__init__.py b/hassio/dock/__init__.py index e84dd4a5a..6cfd9d678 100644 --- a/hassio/dock/__init__.py +++ b/hassio/dock/__init__.py @@ -231,6 +231,9 @@ class DockerBase(object): _LOGGER.warning("Can't remove image %s -> %s", self.image, err) return False + # clean metadata + self.version = None + self.arch = None return True async def update(self, tag): diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index 77c878251..3db9f87b1 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -5,6 +5,7 @@ from pathlib import Path import shutil import docker +import requests from . import DockerBase from .util import dockerfile_template @@ -196,7 +197,7 @@ class DockerAddon(DockerBase): Need run inside executor. """ try: - image = self.dock.api.get_image("{}:latest".format(self.image)) + image = self.dock.api.get_image(self.image) except docker.errors.DockerException as err: _LOGGER.error("Can't fetch image %s -> %s", self.image, err) return False @@ -205,13 +206,14 @@ class DockerAddon(DockerBase): with tar_file.open("wb") as write_tar: for chunk in image.stream(): write_tar.write(chunk) - except OSError() as err: + except (OSError, requests.exceptions.ReadTimeout) as err: _LOGGER.error("Can't write tar file %s -> %s", tar_file, err) return False + _LOGGER.info("Export image %s to %s", self.image, tar_file) return True - async def import_image(self, path): + async def import_image(self, path, tag): """Import a tar file as image.""" if self._lock.locked(): _LOGGER.error("Can't excute import while a task is in progress") @@ -219,9 +221,9 @@ class DockerAddon(DockerBase): async with self._lock: return await self.loop.run_in_executor( - None, self._import_image, path) + None, self._import_image, path, tag) - def _import_image(self, tar_file): + def _import_image(self, tar_file, tag): """Import a tar file as image. Need run inside executor. @@ -231,10 +233,12 @@ class DockerAddon(DockerBase): self.dock.api.load_image(read_tar) image = self.dock.images.get(self.image) + image.tag(self.image, tag=tag) except (docker.errors.DockerException, OSError) as err: _LOGGER.error("Can't import image %s -> %s", self.image, err) return False + _LOGGER.info("Import image %s and tag %s", tar_file, tag) self.process_metadata(image.attrs, force=True) self._cleanup() return True diff --git a/hassio/snapshots/__init__.py b/hassio/snapshots/__init__.py index e757ac487..29f3b277a 100644 --- a/hassio/snapshots/__init__.py +++ b/hassio/snapshots/__init__.py @@ -41,9 +41,15 @@ class SnapshotsManager(object): slug = create_slug(name, date_str) tar_file = Path(self.config.path_backup, "{}.tar".format(slug)) + # init object snapshot = Snapshot(self.config, self.loop, tar_file) snapshot.create(slug, name, date_str, sys_type) + # set general data + snapshot.homeassistant_version = self.homeassistant.version + snapshot.homeassistant_devices = self.config.homeassistant_devices + snapshot.repositories = self.config.addons_repositories + return snapshot async def reload(self): @@ -87,9 +93,6 @@ class SnapshotsManager(object): await self._lock.acquire() async with snapshot: - snapshot.homeassistant = self.homeassistant.version - snapshot.repositories = self.config.addons_repositories - # snapshot addons tasks = [] for addon in self.addons.list_addons: @@ -110,7 +113,7 @@ class SnapshotsManager(object): self.snapshots[snapshot.slug] = snapshot return True - except (OSError, tarfile.TarError) as err: + except (OSError, ValueError, tarfile.TarError) as err: _LOGGER.info("Full-Snapshot %s error -> %s", snapshot.slug, err) return False @@ -134,9 +137,6 @@ class SnapshotsManager(object): await self._lock.acquire() async with snapshot: - snapshot.homeassistant = self.homeassistant.version - snapshot.repositories = self.config.addons_repositories - # snapshot addons tasks = [] for slug in addons: @@ -158,7 +158,7 @@ class SnapshotsManager(object): self.snapshots[snapshot.slug] = snapshot return True - except (OSError, tarfile.TarError) as err: + except (OSError, ValueError, tarfile.TarError) as err: _LOGGER.info("Partial-Snapshot %s error -> %s", snapshot.slug, err) return False @@ -198,8 +198,10 @@ class SnapshotsManager(object): await snapshot.restore_folders() # start homeassistant restore + self.config.homeassistant_devices = \ + snapshot.homeassistant_devices task_hass = self.loop.create_task( - self.homeassistant.update(snapshot.homeassistant)) + self.homeassistant.update(snapshot.homeassistant_version)) # restore repositories await self.addons.load_repositories(snapshot.repositories) @@ -244,7 +246,7 @@ class SnapshotsManager(object): _LOGGER.info("Full-Restore %s done", snapshot.slug) return True - except (OSError, tarfile.TarError) as err: + except (OSError, ValueError, tarfile.TarError) as err: _LOGGER.info("Full-Restore %s error -> %s", slug, err) return False @@ -279,8 +281,10 @@ class SnapshotsManager(object): await snapshot.restore_folders(folders) if homeassistant: + self.config.homeassistant_devices = \ + snapshot.homeassistant_devices tasks.append(self.homeassistant.update( - snapshot.homeassistant)) + snapshot.homeassistant_version)) for slug in addons: addon = self.addons.get(slug) @@ -300,7 +304,7 @@ class SnapshotsManager(object): _LOGGER.info("Partial-Restore %s done", snapshot.slug) return True - except (OSError, tarfile.TarError) as err: + except (OSError, ValueError, tarfile.TarError) as err: _LOGGER.info("Partial-Restore %s error -> %s", slug, err) return False diff --git a/hassio/snapshots/snapshot.py b/hassio/snapshots/snapshot.py index 729f722e8..1882a5f07 100644 --- a/hassio/snapshots/snapshot.py +++ b/hassio/snapshots/snapshot.py @@ -13,7 +13,7 @@ from .validate import SCHEMA_SNAPSHOT, ALL_FOLDERS from .util import remove_folder from ..const import ( ATTR_SLUG, ATTR_NAME, ATTR_DATE, ATTR_ADDONS, ATTR_REPOSITORIES, - ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_VERSION, ATTR_TYPE) + ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_VERSION, ATTR_TYPE, ATTR_DEVICES) from ..tools import write_json_file _LOGGER = logging.getLogger(__name__) @@ -43,27 +43,27 @@ class Snapshot(object): @property def name(self): """Return snapshot name.""" - return self._data.get(ATTR_NAME) + return self._data[ATTR_NAME] @property def date(self): """Return snapshot date.""" - return self._data.get(ATTR_DATE) + return self._data[ATTR_DATE] @property def addons(self): """Return snapshot date.""" - return self._data.get(ATTR_ADDONS, []) + return self._data[ATTR_ADDONS] @property def folders(self): """Return list of saved folders.""" - return self._data.get(ATTR_FOLDERS, []) + return self._data[ATTR_FOLDERS] @property def repositories(self): """Return snapshot date.""" - return self._data.get(ATTR_REPOSITORIES, []) + return self._data[ATTR_REPOSITORIES] @repositories.setter def repositories(self, value): @@ -71,14 +71,24 @@ class Snapshot(object): self._data[ATTR_REPOSITORIES] = value @property - def homeassistant(self): + def homeassistant_version(self): """Return snapshot homeassistant version.""" - return self._data.get(ATTR_HOMEASSISTANT) + return self._data[ATTR_HOMEASSISTANT].get(ATTR_VERSION) - @homeassistant.setter - def homeassistant(self, value): + @homeassistant_version.setter + def homeassistant_version(self, value): """Set snapshot homeassistant version.""" - self._data[ATTR_HOMEASSISTANT] = value + self._data[ATTR_HOMEASSISTANT][ATTR_VERSION] = value + + @property + def homeassistant_devices(self): + """Return snapshot homeassistant devices.""" + return self._data[ATTR_HOMEASSISTANT].get(ATTR_DEVICES) + + @homeassistant_devices.setter + def homeassistant_devices(self, value): + """Set snapshot homeassistant devices.""" + self._data[ATTR_HOMEASSISTANT][ATTR_DEVICES] = value @property def size(self): @@ -96,6 +106,7 @@ class Snapshot(object): self._data[ATTR_TYPE] = sys_type # init other constructs + self._data[ATTR_HOMEASSISTANT] = {} self._data[ATTR_ADDONS] = [] self._data[ATTR_REPOSITORIES] = [] self._data[ATTR_FOLDERS] = [] @@ -159,6 +170,14 @@ class Snapshot(object): if self.tar_file.is_file() or exception_type is not None: return self._tmp.cleanup() + # validate data + try: + self._data = SCHEMA_SNAPSHOT(self._data) + except vol.Invalid as err: + _LOGGER.error("Invalid data for %s -> %s", self.tar_file, + humanize_error(self._data, err)) + raise ValueError("Invalid config") from None + # new snapshot, build it def _create_snapshot(): """Create a new snapshot.""" @@ -166,10 +185,7 @@ class Snapshot(object): tar.add(self._tmp.name, arcname=".") if write_json_file(Path(self._tmp.name, "snapshot.json"), self._data): - try: - await self.loop.run_in_executor(None, _create_snapshot) - except tarfile.TarError as err: - _LOGGER.error("Can't create tar %s", err) + await self.loop.run_in_executor(None, _create_snapshot) else: _LOGGER.error("Can't write snapshot.json") diff --git a/hassio/snapshots/validate.py b/hassio/snapshots/validate.py index aabea42e3..0efe2ee28 100644 --- a/hassio/snapshots/validate.py +++ b/hassio/snapshots/validate.py @@ -4,9 +4,9 @@ import voluptuous as vol from ..const import ( ATTR_REPOSITORIES, ATTR_ADDONS, ATTR_NAME, ATTR_SLUG, ATTR_DATE, - ATTR_VERSION, ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_TYPE, FOLDER_SHARE, - FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL, SNAPSHOT_FULL, - SNAPSHOT_PARTIAL) + ATTR_VERSION, ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_TYPE, ATTR_DEVICES, + FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL, + SNAPSHOT_FULL, SNAPSHOT_PARTIAL) ALL_FOLDERS = [FOLDER_HOMEASSISTANT, FOLDER_SHARE, FOLDER_ADDONS, FOLDER_SSL] @@ -16,12 +16,15 @@ SCHEMA_SNAPSHOT = vol.Schema({ vol.Required(ATTR_TYPE): vol.In([SNAPSHOT_FULL, SNAPSHOT_PARTIAL]), vol.Required(ATTR_NAME): vol.Coerce(str), vol.Required(ATTR_DATE): vol.Coerce(str), - vol.Required(ATTR_HOMEASSISTANT): vol.Coerce(str), - vol.Required(ATTR_FOLDERS): [vol.In(ALL_FOLDERS)], - vol.Required(ATTR_ADDONS): [vol.Schema({ + vol.Required(ATTR_HOMEASSISTANT): vol.Schema({ + vol.Required(ATTR_VERSION): vol.Coerce(str), + vol.Optional(ATTR_DEVICES, default=[]): [vol.Match(r"^[^/]*$")], + }), + vol.Optional(ATTR_FOLDERS, default=[]): [vol.In(ALL_FOLDERS)], + vol.Optional(ATTR_ADDONS, default=[]): [vol.Schema({ vol.Required(ATTR_SLUG): vol.Coerce(str), vol.Required(ATTR_NAME): vol.Coerce(str), vol.Required(ATTR_VERSION): vol.Coerce(str), })], - vol.Required(ATTR_REPOSITORIES): [vol.Url()], + vol.Optional(ATTR_REPOSITORIES, default=[]): [vol.Url()], }, extra=vol.ALLOW_EXTRA)