Convert homeassistant into a dict (snapshot) (#90)

* Convert homeassistant into a dict

* fix lint

* fix bugs

* cleanup code

* fix cleanup
This commit is contained in:
Pascal Vizeli 2017-07-06 01:29:09 +02:00 committed by GitHub
parent e2a29b7290
commit 0ac96c207e
8 changed files with 85 additions and 49 deletions

7
API.md
View File

@ -180,10 +180,13 @@ Return QR-Code
{ {
"slug": "SNAPSHOT ID", "slug": "SNAPSHOT ID",
"type": "full|partial", "type": "full|partial",
"name": "custom snapshot name", "name": "custom snapshot name / description",
"date": "ISO", "date": "ISO",
"size": "SIZE_IN_MB", "size": "SIZE_IN_MB",
"homeassistant": "INSTALLED_HASS_VERSION", "homeassistant": {
"version": "INSTALLED_HASS_VERSION",
"devices": []
},
"addons": [ "addons": [
{ {
"slug": "ADDON_SLUG", "slug": "ADDON_SLUG",

View File

@ -434,14 +434,13 @@ class Addon(object):
self._restore_data(data[ATTR_USER], data[ATTR_SYSTEM]) self._restore_data(data[ATTR_USER], data[ATTR_SYSTEM])
# check version / restore image # 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") image_file = Path(temp, "image.tar")
if image_file.is_file(): if image_file.is_file():
if not await self.addon_docker.import_image(image_file): await self.addon_docker.import_image(image_file, version)
return False
else: else:
if not await self.addon_docker.install(data[ATTR_VERSION]): if await self.addon_docker.install(version):
return False
await self.addon_docker.cleanup() await self.addon_docker.cleanup()
else: else:
await self.addon_docker.stop() await self.addon_docker.stop()

View File

@ -8,7 +8,8 @@ from .util import api_process, api_validate
from ..snapshots.validate import ALL_FOLDERS from ..snapshots.validate import ALL_FOLDERS
from ..const import ( from ..const import (
ATTR_NAME, ATTR_SLUG, ATTR_DATE, ATTR_ADDONS, ATTR_REPOSITORIES, 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__) _LOGGER = logging.getLogger(__name__)
@ -69,7 +70,10 @@ class APISnapshots(object):
ATTR_NAME: snapshot.name, ATTR_NAME: snapshot.name,
ATTR_DATE: snapshot.date, ATTR_DATE: snapshot.date,
ATTR_SIZE: snapshot.size, 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_ADDONS: self._addons_list(snapshot),
ATTR_REPOSITORIES: snapshot.repositories, ATTR_REPOSITORIES: snapshot.repositories,
ATTR_FOLDERS: snapshot.folders, ATTR_FOLDERS: snapshot.folders,

View File

@ -231,6 +231,9 @@ class DockerBase(object):
_LOGGER.warning("Can't remove image %s -> %s", self.image, err) _LOGGER.warning("Can't remove image %s -> %s", self.image, err)
return False return False
# clean metadata
self.version = None
self.arch = None
return True return True
async def update(self, tag): async def update(self, tag):

View File

@ -5,6 +5,7 @@ from pathlib import Path
import shutil import shutil
import docker import docker
import requests
from . import DockerBase from . import DockerBase
from .util import dockerfile_template from .util import dockerfile_template
@ -196,7 +197,7 @@ class DockerAddon(DockerBase):
Need run inside executor. Need run inside executor.
""" """
try: 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: except docker.errors.DockerException as err:
_LOGGER.error("Can't fetch image %s -> %s", self.image, err) _LOGGER.error("Can't fetch image %s -> %s", self.image, err)
return False return False
@ -205,13 +206,14 @@ class DockerAddon(DockerBase):
with tar_file.open("wb") as write_tar: with tar_file.open("wb") as write_tar:
for chunk in image.stream(): for chunk in image.stream():
write_tar.write(chunk) 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) _LOGGER.error("Can't write tar file %s -> %s", tar_file, err)
return False return False
_LOGGER.info("Export image %s to %s", self.image, tar_file)
return True return True
async def import_image(self, path): async def import_image(self, path, tag):
"""Import a tar file as image.""" """Import a tar file as image."""
if self._lock.locked(): if self._lock.locked():
_LOGGER.error("Can't excute import while a task is in progress") _LOGGER.error("Can't excute import while a task is in progress")
@ -219,9 +221,9 @@ class DockerAddon(DockerBase):
async with self._lock: async with self._lock:
return await self.loop.run_in_executor( 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. """Import a tar file as image.
Need run inside executor. Need run inside executor.
@ -231,10 +233,12 @@ class DockerAddon(DockerBase):
self.dock.api.load_image(read_tar) self.dock.api.load_image(read_tar)
image = self.dock.images.get(self.image) image = self.dock.images.get(self.image)
image.tag(self.image, tag=tag)
except (docker.errors.DockerException, OSError) as err: except (docker.errors.DockerException, OSError) as err:
_LOGGER.error("Can't import image %s -> %s", self.image, err) _LOGGER.error("Can't import image %s -> %s", self.image, err)
return False return False
_LOGGER.info("Import image %s and tag %s", tar_file, tag)
self.process_metadata(image.attrs, force=True) self.process_metadata(image.attrs, force=True)
self._cleanup() self._cleanup()
return True return True

View File

@ -41,9 +41,15 @@ class SnapshotsManager(object):
slug = create_slug(name, date_str) slug = create_slug(name, date_str)
tar_file = Path(self.config.path_backup, "{}.tar".format(slug)) tar_file = Path(self.config.path_backup, "{}.tar".format(slug))
# init object
snapshot = Snapshot(self.config, self.loop, tar_file) snapshot = Snapshot(self.config, self.loop, tar_file)
snapshot.create(slug, name, date_str, sys_type) 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 return snapshot
async def reload(self): async def reload(self):
@ -87,9 +93,6 @@ class SnapshotsManager(object):
await self._lock.acquire() await self._lock.acquire()
async with snapshot: async with snapshot:
snapshot.homeassistant = self.homeassistant.version
snapshot.repositories = self.config.addons_repositories
# snapshot addons # snapshot addons
tasks = [] tasks = []
for addon in self.addons.list_addons: for addon in self.addons.list_addons:
@ -110,7 +113,7 @@ class SnapshotsManager(object):
self.snapshots[snapshot.slug] = snapshot self.snapshots[snapshot.slug] = snapshot
return True 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) _LOGGER.info("Full-Snapshot %s error -> %s", snapshot.slug, err)
return False return False
@ -134,9 +137,6 @@ class SnapshotsManager(object):
await self._lock.acquire() await self._lock.acquire()
async with snapshot: async with snapshot:
snapshot.homeassistant = self.homeassistant.version
snapshot.repositories = self.config.addons_repositories
# snapshot addons # snapshot addons
tasks = [] tasks = []
for slug in addons: for slug in addons:
@ -158,7 +158,7 @@ class SnapshotsManager(object):
self.snapshots[snapshot.slug] = snapshot self.snapshots[snapshot.slug] = snapshot
return True 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) _LOGGER.info("Partial-Snapshot %s error -> %s", snapshot.slug, err)
return False return False
@ -198,8 +198,10 @@ class SnapshotsManager(object):
await snapshot.restore_folders() await snapshot.restore_folders()
# start homeassistant restore # start homeassistant restore
self.config.homeassistant_devices = \
snapshot.homeassistant_devices
task_hass = self.loop.create_task( task_hass = self.loop.create_task(
self.homeassistant.update(snapshot.homeassistant)) self.homeassistant.update(snapshot.homeassistant_version))
# restore repositories # restore repositories
await self.addons.load_repositories(snapshot.repositories) await self.addons.load_repositories(snapshot.repositories)
@ -244,7 +246,7 @@ class SnapshotsManager(object):
_LOGGER.info("Full-Restore %s done", snapshot.slug) _LOGGER.info("Full-Restore %s done", snapshot.slug)
return True return True
except (OSError, tarfile.TarError) as err: except (OSError, ValueError, tarfile.TarError) as err:
_LOGGER.info("Full-Restore %s error -> %s", slug, err) _LOGGER.info("Full-Restore %s error -> %s", slug, err)
return False return False
@ -279,8 +281,10 @@ class SnapshotsManager(object):
await snapshot.restore_folders(folders) await snapshot.restore_folders(folders)
if homeassistant: if homeassistant:
self.config.homeassistant_devices = \
snapshot.homeassistant_devices
tasks.append(self.homeassistant.update( tasks.append(self.homeassistant.update(
snapshot.homeassistant)) snapshot.homeassistant_version))
for slug in addons: for slug in addons:
addon = self.addons.get(slug) addon = self.addons.get(slug)
@ -300,7 +304,7 @@ class SnapshotsManager(object):
_LOGGER.info("Partial-Restore %s done", snapshot.slug) _LOGGER.info("Partial-Restore %s done", snapshot.slug)
return True return True
except (OSError, tarfile.TarError) as err: except (OSError, ValueError, tarfile.TarError) as err:
_LOGGER.info("Partial-Restore %s error -> %s", slug, err) _LOGGER.info("Partial-Restore %s error -> %s", slug, err)
return False return False

View File

@ -13,7 +13,7 @@ from .validate import SCHEMA_SNAPSHOT, ALL_FOLDERS
from .util import remove_folder from .util import remove_folder
from ..const import ( from ..const import (
ATTR_SLUG, ATTR_NAME, ATTR_DATE, ATTR_ADDONS, ATTR_REPOSITORIES, 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 from ..tools import write_json_file
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -43,27 +43,27 @@ class Snapshot(object):
@property @property
def name(self): def name(self):
"""Return snapshot name.""" """Return snapshot name."""
return self._data.get(ATTR_NAME) return self._data[ATTR_NAME]
@property @property
def date(self): def date(self):
"""Return snapshot date.""" """Return snapshot date."""
return self._data.get(ATTR_DATE) return self._data[ATTR_DATE]
@property @property
def addons(self): def addons(self):
"""Return snapshot date.""" """Return snapshot date."""
return self._data.get(ATTR_ADDONS, []) return self._data[ATTR_ADDONS]
@property @property
def folders(self): def folders(self):
"""Return list of saved folders.""" """Return list of saved folders."""
return self._data.get(ATTR_FOLDERS, []) return self._data[ATTR_FOLDERS]
@property @property
def repositories(self): def repositories(self):
"""Return snapshot date.""" """Return snapshot date."""
return self._data.get(ATTR_REPOSITORIES, []) return self._data[ATTR_REPOSITORIES]
@repositories.setter @repositories.setter
def repositories(self, value): def repositories(self, value):
@ -71,14 +71,24 @@ class Snapshot(object):
self._data[ATTR_REPOSITORIES] = value self._data[ATTR_REPOSITORIES] = value
@property @property
def homeassistant(self): def homeassistant_version(self):
"""Return snapshot homeassistant version.""" """Return snapshot homeassistant version."""
return self._data.get(ATTR_HOMEASSISTANT) return self._data[ATTR_HOMEASSISTANT].get(ATTR_VERSION)
@homeassistant.setter @homeassistant_version.setter
def homeassistant(self, value): def homeassistant_version(self, value):
"""Set snapshot homeassistant version.""" """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 @property
def size(self): def size(self):
@ -96,6 +106,7 @@ class Snapshot(object):
self._data[ATTR_TYPE] = sys_type self._data[ATTR_TYPE] = sys_type
# init other constructs # init other constructs
self._data[ATTR_HOMEASSISTANT] = {}
self._data[ATTR_ADDONS] = [] self._data[ATTR_ADDONS] = []
self._data[ATTR_REPOSITORIES] = [] self._data[ATTR_REPOSITORIES] = []
self._data[ATTR_FOLDERS] = [] self._data[ATTR_FOLDERS] = []
@ -159,6 +170,14 @@ class Snapshot(object):
if self.tar_file.is_file() or exception_type is not None: if self.tar_file.is_file() or exception_type is not None:
return self._tmp.cleanup() 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 # new snapshot, build it
def _create_snapshot(): def _create_snapshot():
"""Create a new snapshot.""" """Create a new snapshot."""
@ -166,10 +185,7 @@ class Snapshot(object):
tar.add(self._tmp.name, arcname=".") tar.add(self._tmp.name, arcname=".")
if write_json_file(Path(self._tmp.name, "snapshot.json"), self._data): if write_json_file(Path(self._tmp.name, "snapshot.json"), self._data):
try:
await self.loop.run_in_executor(None, _create_snapshot) await self.loop.run_in_executor(None, _create_snapshot)
except tarfile.TarError as err:
_LOGGER.error("Can't create tar %s", err)
else: else:
_LOGGER.error("Can't write snapshot.json") _LOGGER.error("Can't write snapshot.json")

View File

@ -4,9 +4,9 @@ import voluptuous as vol
from ..const import ( from ..const import (
ATTR_REPOSITORIES, ATTR_ADDONS, ATTR_NAME, ATTR_SLUG, ATTR_DATE, ATTR_REPOSITORIES, ATTR_ADDONS, ATTR_NAME, ATTR_SLUG, ATTR_DATE,
ATTR_VERSION, ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_TYPE, FOLDER_SHARE, ATTR_VERSION, ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_TYPE, ATTR_DEVICES,
FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL, SNAPSHOT_FULL, FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL,
SNAPSHOT_PARTIAL) SNAPSHOT_FULL, SNAPSHOT_PARTIAL)
ALL_FOLDERS = [FOLDER_HOMEASSISTANT, FOLDER_SHARE, FOLDER_ADDONS, FOLDER_SSL] 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_TYPE): vol.In([SNAPSHOT_FULL, SNAPSHOT_PARTIAL]),
vol.Required(ATTR_NAME): vol.Coerce(str), vol.Required(ATTR_NAME): vol.Coerce(str),
vol.Required(ATTR_DATE): vol.Coerce(str), vol.Required(ATTR_DATE): vol.Coerce(str),
vol.Required(ATTR_HOMEASSISTANT): vol.Coerce(str), vol.Required(ATTR_HOMEASSISTANT): vol.Schema({
vol.Required(ATTR_FOLDERS): [vol.In(ALL_FOLDERS)], vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Required(ATTR_ADDONS): [vol.Schema({ 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_SLUG): vol.Coerce(str),
vol.Required(ATTR_NAME): vol.Coerce(str), vol.Required(ATTR_NAME): vol.Coerce(str),
vol.Required(ATTR_VERSION): 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) }, extra=vol.ALLOW_EXTRA)