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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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