Add support for encrypted snapshot files (#354)

* Add support for encrypted files

* Update tar.py

* Update tar.py

* Update tar.py

* Update addon.py

* Update API.md

* Update API.md

* Update tar.py

* cleanup snapshot

* Update API.md

* Update const.py

* Update const.py

* Update validate.py

* Update homeassistant.py

* Update homeassistant.py

* Update validate.py

* Update validate.py

* Update snapshot.py

* Update utils.py

* Update snapshot.py

* Update utils.py

* Update snapshot.py

* Update validate.py

* Update snapshot.py

* Update validate.py

* Update const.py

* fix lint

* Update snapshot.py

* Update __init__.py

* Update snapshot.py

* Update __init__.py

* Update __init__.py

* Finish snapshot object

* Fix struct

* cleanup snapshot flow

* fix some points

* Add API upload

* fix lint

* Update voluptuous

* fix docker

* Update snapshots.py

* fix versions

* fix schema

* fix schema

* fix api

* fix path

* Handle import better

* fix routing

* fix bugs

* fix bug

* cleanup gz

* fix some bugs

* fix stage

* Fix

* fix

* protect None password

* fix API

* handle exception better

* fix

* fix remove of addons

* fix bug

* clenaup code

* fix none tasks

* Encrypt Home-Assistant

* fix decrypt

* fix binary
This commit is contained in:
Pascal Vizeli 2018-02-17 15:52:33 +01:00 committed by GitHub
parent e815223047
commit 587047f9d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 512 additions and 293 deletions

28
API.md
View File

@ -113,7 +113,8 @@ Output is the raw docker log.
"slug": "SLUG",
"date": "ISO",
"name": "Custom name",
"type": "full|partial"
"type": "full|partial",
"protected": "bool"
}
]
}
@ -121,11 +122,14 @@ Output is the raw docker log.
- POST `/snapshots/reload`
- POST `/snapshots/new/upload`
- POST `/snapshots/new/full`
```json
{
"name": "Optional"
"name": "Optional",
"password": "Optional"
}
```
@ -135,7 +139,8 @@ Output is the raw docker log.
{
"name": "Optional",
"addons": ["ADDON_SLUG"],
"folders": ["FOLDER_NAME"]
"folders": ["FOLDER_NAME"],
"password": "Optional"
}
```
@ -150,12 +155,14 @@ Output is the raw docker log.
"name": "custom snapshot name / description",
"date": "ISO",
"size": "SIZE_IN_MB",
"protected": "bool",
"homeassistant": "version",
"addons": [
{
"slug": "ADDON_SLUG",
"name": "NAME",
"version": "INSTALLED_VERSION"
"version": "INSTALLED_VERSION",
"size": "SIZE_IN_MB"
}
],
"repositories": ["URL"],
@ -164,14 +171,25 @@ Output is the raw docker log.
```
- POST `/snapshots/{slug}/remove`
- GET `/snapshots/{slug}/download`
- POST `/snapshots/{slug}/restore/full`
```json
{
"password": "Optional"
}
```
- POST `/snapshots/{slug}/restore/partial`
```json
{
"homeassistant": "bool",
"addons": ["ADDON_SLUG"],
"folders": ["FOLDER_NAME"]
"folders": ["FOLDER_NAME"],
"password": "Optional"
}
```

View File

@ -15,8 +15,9 @@ RUN apk add --no-cache \
python3-dev \
g++ \
&& pip3 install --no-cache-dir \
uvloop \
cchardet \
uvloop==0.9.1 \
cchardet==2.1.1 \
pycryptodome==3.4.11 \
&& apk del .build-dependencies
# Install HassIO

View File

@ -28,6 +28,12 @@ class AddonManager(CoreSysAttributes):
"""Return a list of all addons."""
return list(self.addons_obj.values())
@property
def list_installed(self):
"""Return a list of installed addons."""
return [addon for addon in self.addons_obj.values()
if addon.is_installed]
@property
def list_repositories(self):
"""Return list of addon repositories."""

View File

@ -693,16 +693,15 @@ class Addon(CoreSysAttributes):
return False
# write into tarfile
def _create_tar():
def _write_tarfile():
"""Write tar inside loop."""
with tarfile.open(tar_file, "w:gz",
compresslevel=1) as snapshot:
with tar_file as snapshot:
snapshot.add(temp, arcname=".")
snapshot.add(self.path_data, arcname="data")
try:
_LOGGER.info("Build snapshot for addon %s", self._id)
await self._loop.run_in_executor(None, _create_tar)
await self._loop.run_in_executor(None, _write_tarfile)
except (tarfile.TarError, OSError) as err:
_LOGGER.error("Can't write tarfile %s: %s", tar_file, err)
return False
@ -714,13 +713,13 @@ class Addon(CoreSysAttributes):
"""Restore a state of a addon."""
with TemporaryDirectory(dir=str(self._config.path_tmp)) as temp:
# extract snapshot
def _extract_tar():
def _extract_tarfile():
"""Extract tar snapshot."""
with tarfile.open(tar_file, "r:gz") as snapshot:
with tar_file as snapshot:
snapshot.extractall(path=Path(temp))
try:
await self._loop.run_in_executor(None, _extract_tar)
await self._loop.run_in_executor(None, _extract_tarfile)
except tarfile.TarError as err:
_LOGGER.error("Can't read tarfile %s: %s", tar_file, err)
return False

View File

@ -160,6 +160,8 @@ class RestAPI(CoreSysAttributes):
'/snapshots/new/full', api_snapshots.snapshot_full)
self.webapp.router.add_post(
'/snapshots/new/partial', api_snapshots.snapshot_partial)
self.webapp.router.add_post(
'/snapshots/new/upload', api_snapshots.upload)
self.webapp.router.add_get(
'/snapshots/{snapshot}/info', api_snapshots.info)
@ -170,6 +172,9 @@ class RestAPI(CoreSysAttributes):
self.webapp.router.add_post(
'/snapshots/{snapshot}/restore/partial',
api_snapshots.restore_partial)
self.webapp.router.add_get(
'/snapshots/{snapshot}/download',
api_snapshots.download)
def _register_services(self):
api_services = APIServices()

View File

@ -9,7 +9,7 @@ from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_IMAGE, ATTR_CUSTOM, ATTR_BOOT,
ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG, ATTR_CPU_PERCENT,
ATTR_MEMORY_USAGE, ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX,
ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_STARTUP_TIME, CONTENT_TYPE_BINARY)
ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_WAIT_BOOT, CONTENT_TYPE_BINARY)
from ..coresys import CoreSysAttributes
from ..validate import NETWORK_PORT, DOCKER_IMAGE
@ -27,7 +27,7 @@ SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
vol.Optional(ATTR_SSL): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
vol.Optional(ATTR_STARTUP_TIME):
vol.Optional(ATTR_WAIT_BOOT):
vol.All(vol.Coerce(int), vol.Range(min=60)),
})
@ -51,7 +51,7 @@ class APIHomeAssistant(CoreSysAttributes):
ATTR_PORT: self._homeassistant.api_port,
ATTR_SSL: self._homeassistant.api_ssl,
ATTR_WATCHDOG: self._homeassistant.watchdog,
ATTR_STARTUP_TIME: self._homeassistant.startup_time,
ATTR_WAIT_BOOT: self._homeassistant.wait_boot,
}
@api_process
@ -78,8 +78,8 @@ class APIHomeAssistant(CoreSysAttributes):
if ATTR_WATCHDOG in body:
self._homeassistant.watchdog = body[ATTR_WATCHDOG]
if ATTR_STARTUP_TIME in body:
self._homeassistant.startup_time = body[ATTR_STARTUP_TIME]
if ATTR_WAIT_BOOT in body:
self._homeassistant.wait_boot = body[ATTR_WAIT_BOOT]
self._homeassistant.save_data()
return True

View File

@ -1,7 +1,10 @@
"""Init file for HassIO snapshot rest api."""
import asyncio
import logging
from pathlib import Path
from tempfile import TemporaryDirectory
from aiohttp import web
import voluptuous as vol
from .utils import api_process, api_validate
@ -9,7 +12,7 @@ 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_SNAPSHOTS)
ATTR_SNAPSHOTS, ATTR_PASSWORD, ATTR_PROTECTED, CONTENT_TYPE_TAR)
from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__)
@ -17,6 +20,7 @@ _LOGGER = logging.getLogger(__name__)
# pylint: disable=no-value-for-parameter
SCHEMA_RESTORE_PARTIAL = vol.Schema({
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(),
vol.Optional(ATTR_ADDONS):
vol.All([vol.Coerce(str)], vol.Unique()),
@ -24,8 +28,13 @@ SCHEMA_RESTORE_PARTIAL = vol.Schema({
vol.All([vol.In(ALL_FOLDERS)], vol.Unique()),
})
SCHEMA_RESTORE_FULL = vol.Schema({
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
})
SCHEMA_SNAPSHOT_FULL = vol.Schema({
vol.Optional(ATTR_NAME): vol.Coerce(str),
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
})
SCHEMA_SNAPSHOT_PARTIAL = SCHEMA_SNAPSHOT_FULL.extend({
@ -56,6 +65,7 @@ class APISnapshots(CoreSysAttributes):
ATTR_NAME: snapshot.name,
ATTR_DATE: snapshot.date,
ATTR_TYPE: snapshot.sys_type,
ATTR_PROTECTED: snapshot.protected,
})
return {
@ -79,6 +89,7 @@ class APISnapshots(CoreSysAttributes):
ATTR_SLUG: addon_data[ATTR_SLUG],
ATTR_NAME: addon_data[ATTR_NAME],
ATTR_VERSION: addon_data[ATTR_VERSION],
ATTR_SIZE: addon_data[ATTR_SIZE],
})
return {
@ -87,6 +98,7 @@ class APISnapshots(CoreSysAttributes):
ATTR_NAME: snapshot.name,
ATTR_DATE: snapshot.date,
ATTR_SIZE: snapshot.size,
ATTR_PROTECTED: snapshot.protected,
ATTR_HOMEASSISTANT: snapshot.homeassistant_version,
ATTR_ADDONS: data_addons,
ATTR_REPOSITORIES: snapshot.repositories,
@ -108,17 +120,21 @@ class APISnapshots(CoreSysAttributes):
self._snapshots.do_snapshot_partial(**body), loop=self._loop)
@api_process
def restore_full(self, request):
async def restore_full(self, request):
"""Full-Restore a snapshot."""
snapshot = self._extract_snapshot(request)
return asyncio.shield(
self._snapshots.do_restore_full(snapshot), loop=self._loop)
body = await api_validate(SCHEMA_RESTORE_FULL, request)
return await asyncio.shield(
self._snapshots.do_restore_full(snapshot, **body),
loop=self._loop
)
@api_process
async def restore_partial(self, request):
"""Partial-Restore a snapshot."""
snapshot = self._extract_snapshot(request)
body = await api_validate(SCHEMA_SNAPSHOT_PARTIAL, request)
body = await api_validate(SCHEMA_RESTORE_PARTIAL, request)
return await asyncio.shield(
self._snapshots.do_restore_partial(snapshot, **body),
@ -130,3 +146,33 @@ class APISnapshots(CoreSysAttributes):
"""Remove a snapshot."""
snapshot = self._extract_snapshot(request)
return self._snapshots.remove(snapshot)
async def download(self, request):
"""Download a snapshot file."""
snapshot = self._extract_snapshot(request)
_LOGGER.info("Download snapshot %s", snapshot.slug)
response = web.FileResponse(snapshot.tarfile)
response.content_type = CONTENT_TYPE_TAR
return response
@api_process
async def upload(self, request):
"""Upload a snapshot file."""
with TemporaryDirectory(dir=str(self._config.path_tmp)) as temp_dir:
tar_file = Path(temp_dir, f"snapshot.tar")
try:
with tar_file.open('wb') as snapshot:
async for data in request.content.iter_any():
snapshot.write(data)
except OSError as err:
_LOGGER.error("Can't write new snapshot file: %s", err)
return False
except asyncio.CancelledError:
return False
return await asyncio.shield(
self._snapshots.import_snapshot(tar_file), loop=self._loop)

View File

@ -43,6 +43,7 @@ CONTENT_TYPE_BINARY = 'application/octet-stream'
CONTENT_TYPE_PNG = 'image/png'
CONTENT_TYPE_JSON = 'application/json'
CONTENT_TYPE_TEXT = 'text/plain'
CONTENT_TYPE_TAR = 'application/tar'
HEADER_HA_ACCESS = 'x-ha-access'
HEADER_TOKEN = 'X-HASSIO-KEY'
@ -155,7 +156,8 @@ ATTR_CONFIG = 'config'
ATTR_DISCOVERY_ID = 'discovery_id'
ATTR_SERVICES = 'services'
ATTR_DISCOVERY = 'discovery'
ATTR_STARTUP_TIME = 'startup_time'
ATTR_PROTECTED = 'protected'
ATTR_CRYPTO = 'crypto'
SERVICE_MQTT = 'mqtt'
@ -193,3 +195,5 @@ FOLDER_SSL = 'ssl'
SNAPSHOT_FULL = 'full'
SNAPSHOT_PARTIAL = 'partial'
CRYPTO_AES128 = 'aes128'

View File

@ -13,7 +13,7 @@ from aiohttp.hdrs import CONTENT_TYPE
from .const import (
FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_UUID,
ATTR_BOOT, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG,
ATTR_STARTUP_TIME, HEADER_HA_ACCESS, CONTENT_TYPE_JSON)
ATTR_WAIT_BOOT, HEADER_HA_ACCESS, CONTENT_TYPE_JSON)
from .coresys import CoreSysAttributes
from .docker.homeassistant import DockerHomeAssistant
from .utils import convert_to_ascii
@ -97,14 +97,14 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
self._data[ATTR_WATCHDOG] = value
@property
def startup_time(self):
def wait_boot(self):
"""Return time to wait for Home-Assistant startup."""
return self._data[ATTR_STARTUP_TIME]
return self._data[ATTR_WAIT_BOOT]
@startup_time.setter
def startup_time(self, value):
@wait_boot.setter
def wait_boot(self, value):
"""Set time to wait for Home-Assistant startup."""
self._data[ATTR_STARTUP_TIME] = value
self._data[ATTR_WAIT_BOOT] = value
@property
def version(self):
@ -343,7 +343,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
except OSError:
pass
while time.monotonic() - start_time < self.startup_time:
while time.monotonic() - start_time < self.wait_boot:
if await self._loop.run_in_executor(None, check_port):
_LOGGER.info("Detect a running Home-Assistant instance")
return True

View File

@ -3,12 +3,11 @@ import asyncio
from datetime import datetime
import logging
from pathlib import Path
import tarfile
from .snapshot import Snapshot
from .utils import create_slug
from ..const import (
ATTR_SLUG, FOLDER_HOMEASSISTANT, SNAPSHOT_FULL, SNAPSHOT_PARTIAL)
FOLDER_HOMEASSISTANT, SNAPSHOT_FULL, SNAPSHOT_PARTIAL)
from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__)
@ -32,15 +31,15 @@ class SnapshotManager(CoreSysAttributes):
"""Return snapshot object."""
return self.snapshots_obj.get(slug)
def _create_snapshot(self, name, sys_type):
def _create_snapshot(self, name, sys_type, password):
"""Initialize a new snapshot object from name."""
date_str = datetime.utcnow().isoformat()
slug = create_slug(name, date_str)
tar_file = Path(self._config.path_backup, "{}.tar".format(slug))
tar_file = Path(self._config.path_backup, f"{slug}.tar")
# init object
snapshot = Snapshot(self.coresys, tar_file)
snapshot.create(slug, name, date_str, sys_type)
snapshot.new(slug, name, date_str, sys_type, password)
# set general data
snapshot.store_homeassistant()
@ -75,45 +74,64 @@ class SnapshotManager(CoreSysAttributes):
def remove(self, snapshot):
"""Remove a snapshot."""
try:
snapshot.tar_file.unlink()
snapshot.tarfile.unlink()
self.snapshots_obj.pop(snapshot.slug, None)
_LOGGER.info("Removed snapshot file %s", snapshot.slug)
except OSError as err:
_LOGGER.error("Can't remove snapshot %s: %s", snapshot.slug, err)
return False
return True
async def do_snapshot_full(self, name=""):
async def import_snapshot(self, tar_file):
"""Check snapshot tarfile and import it."""
snapshot = Snapshot(self.coresys, tar_file)
# Read meta data
if not await snapshot.load():
return False
# Allready exists?
if snapshot.slug in self.snapshots_obj:
_LOGGER.error("Snapshot %s allready exists!", snapshot.slug)
return False
# Move snapshot to backup
try:
snapshot.tarfile.rename(
Path(self._config.path_backup, f"{snapshot.slug}.tar"))
except OSError as err:
_LOGGER.error("Can't move snapshot file to storage: %s", err)
return False
await self.reload()
return True
async def do_snapshot_full(self, name="", password=None):
"""Create a full snapshot."""
if self.lock.locked():
_LOGGER.error("It is already a snapshot/restore process running")
return False
snapshot = self._create_snapshot(name, SNAPSHOT_FULL)
snapshot = self._create_snapshot(name, SNAPSHOT_FULL, password)
_LOGGER.info("Full-Snapshot %s start", snapshot.slug)
try:
self._scheduler.suspend = True
await self.lock.acquire()
async with snapshot:
# snapshot addons
tasks = []
for addon in self._addons.list_addons:
if not addon.is_installed:
continue
tasks.append(snapshot.import_addon(addon))
# Snapshot add-ons
_LOGGER.info("Snapshot %s store Add-ons", snapshot.slug)
await snapshot.store_addons()
if tasks:
_LOGGER.info("Full-Snapshot %s run %d addons",
snapshot.slug, len(tasks))
await asyncio.wait(tasks, loop=self._loop)
# snapshot folders
_LOGGER.info("Full-Snapshot %s store folders", snapshot.slug)
# Snapshot folders
_LOGGER.info("Snapshot %s store folders", snapshot.slug)
await snapshot.store_folders()
except (OSError, ValueError, tarfile.TarError) as err:
_LOGGER.info("Full-Snapshot %s error: %s", snapshot.slug, err)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Snapshot %s error", snapshot.slug)
return False
else:
@ -125,7 +143,8 @@ class SnapshotManager(CoreSysAttributes):
self._scheduler.suspend = False
self.lock.release()
async def do_snapshot_partial(self, name="", addons=None, folders=None):
async def do_snapshot_partial(self, name="", addons=None, folders=None,
password=None):
"""Create a partial snapshot."""
if self.lock.locked():
_LOGGER.error("It is already a snapshot/restore process running")
@ -133,7 +152,7 @@ class SnapshotManager(CoreSysAttributes):
addons = addons or []
folders = folders or []
snapshot = self._create_snapshot(name, SNAPSHOT_PARTIAL)
snapshot = self._create_snapshot(name, SNAPSHOT_PARTIAL, password)
_LOGGER.info("Partial-Snapshot %s start", snapshot.slug)
try:
@ -141,25 +160,24 @@ class SnapshotManager(CoreSysAttributes):
await self.lock.acquire()
async with snapshot:
# snapshot addons
tasks = []
for slug in addons:
addon = self._addons.get(slug)
if addon.is_installed:
tasks.append(snapshot.import_addon(addon))
# Snapshot add-ons
addon_list = []
for addon_slug in addons:
addon = self._addons.get(addon_slug)
if addon and addon.is_installed:
addon_list.append(addon)
continue
_LOGGER.warning("Add-on %s not found", addon_slug)
if tasks:
_LOGGER.info("Partial-Snapshot %s run %d addons",
snapshot.slug, len(tasks))
await asyncio.wait(tasks, loop=self._loop)
_LOGGER.info("Snapshot %s store Add-ons", snapshot.slug)
await snapshot.store_addons(addon_list)
# snapshot folders
_LOGGER.info("Partial-Snapshot %s store folders %s",
snapshot.slug, folders)
_LOGGER.info("Snapshot %s store folders", snapshot.slug)
await snapshot.store_folders(folders)
except (OSError, ValueError, tarfile.TarError) as err:
_LOGGER.info("Partial-Snapshot %s error: %s", snapshot.slug, err)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Snapshot %s error", snapshot.slug)
return False
else:
@ -171,15 +189,19 @@ class SnapshotManager(CoreSysAttributes):
self._scheduler.suspend = False
self.lock.release()
async def do_restore_full(self, snapshot):
async def do_restore_full(self, snapshot, password=None):
"""Restore a snapshot."""
if self.lock.locked():
_LOGGER.error("It is already a snapshot/restore process running")
return False
if snapshot.sys_type != SNAPSHOT_FULL:
_LOGGER.error(
"Full-Restore %s is only a partial snapshot!", snapshot.slug)
_LOGGER.error("Restore %s is only a partial snapshot!",
snapshot.slug)
return False
if snapshot.protected and not snapshot.set_password(password):
_LOGGER.error("Invalid password for snapshot %s", snapshot.slug)
return False
_LOGGER.info("Full-Restore %s start", snapshot.slug)
@ -188,71 +210,54 @@ class SnapshotManager(CoreSysAttributes):
await self.lock.acquire()
async with snapshot:
# stop system
tasks = []
tasks.append(self._homeassistant.stop())
# Stop Home-Assistant / Add-ons
tasks.append(self._homeassistant.stop())
for addon in self._addons.list_addons:
if addon.is_installed:
tasks.append(addon.stop())
await asyncio.wait(tasks, loop=self._loop)
if tasks:
_LOGGER.info("Restore %s stop tasks", snapshot.slug)
await asyncio.wait(tasks, loop=self._loop)
# restore folders
_LOGGER.info("Full-Restore %s restore folders", snapshot.slug)
# Restore folders
_LOGGER.info("Restore %s run folders", snapshot.slug)
await snapshot.restore_folders()
# start homeassistant restore
_LOGGER.info("Full-Restore %s restore Home-Assistant",
snapshot.slug)
# Start homeassistant restore
_LOGGER.info("Restore %s run Home-Assistant", snapshot.slug)
snapshot.restore_homeassistant()
task_hass = self._loop.create_task(
self._homeassistant.update(snapshot.homeassistant_version))
# restore repositories
_LOGGER.info("Full-Restore %s restore Repositories",
snapshot.slug)
# Restore repositories
_LOGGER.info("Restore %s run Repositories", snapshot.slug)
await snapshot.restore_repositories()
# restore addons
tasks = []
actual_addons = \
set(addon.slug for addon in self._addons.list_addons
if addon.is_installed)
restore_addons = \
set(data[ATTR_SLUG] for data in snapshot.addons)
remove_addons = actual_addons - restore_addons
_LOGGER.info("Full-Restore %s restore addons %s, remove %s",
snapshot.slug, restore_addons, remove_addons)
for slug in remove_addons:
addon = self._addons.get(slug)
if addon:
# Delete delta add-ons
tasks.clear()
for addon in self._addons.list_installed:
if addon.slug not in snapshot.addon_list:
tasks.append(addon.uninstall())
else:
_LOGGER.warning("Can't remove addon %s", snapshot.slug)
for slug in restore_addons:
addon = self._addons.get(slug)
if addon:
tasks.append(snapshot.export_addon(addon))
else:
_LOGGER.warning("Can't restore addon %s", slug)
if tasks:
_LOGGER.info("Full-Restore %s restore addons tasks %d",
snapshot.slug, len(tasks))
_LOGGER.info("Restore %s remove add-ons", snapshot.slug)
await asyncio.wait(tasks, loop=self._loop)
# Restore add-ons
_LOGGER.info("Restore %s old add-ons", snapshot.slug)
await snapshot.restore_addons()
# finish homeassistant task
_LOGGER.info("Full-Restore %s wait until homeassistant ready",
_LOGGER.info("Restore %s wait until homeassistant ready",
snapshot.slug)
await task_hass
await self._homeassistant.start()
except (OSError, ValueError, tarfile.TarError) as err:
_LOGGER.info("Full-Restore %s error: %s", snapshot.slug, err)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Restore %s error", snapshot.slug)
return False
else:
@ -264,12 +269,16 @@ class SnapshotManager(CoreSysAttributes):
self.lock.release()
async def do_restore_partial(self, snapshot, homeassistant=False,
addons=None, folders=None):
addons=None, folders=None, password=None):
"""Restore a snapshot."""
if self.lock.locked():
_LOGGER.error("It is already a snapshot/restore process running")
return False
if snapshot.protected and not snapshot.set_password(password):
_LOGGER.error("Invalid password for snapshot %s", snapshot.slug)
return False
addons = addons or []
folders = folders or []
@ -279,41 +288,46 @@ class SnapshotManager(CoreSysAttributes):
await self.lock.acquire()
async with snapshot:
tasks = []
if FOLDER_HOMEASSISTANT in folders:
await self._homeassistant.stop()
# Process folders
if folders:
_LOGGER.info("Partial-Restore %s restore folders %s",
snapshot.slug, folders)
_LOGGER.info("Restore %s run folders", snapshot.slug)
await snapshot.restore_folders(folders)
# Process Home-Assistant
task_hass = None
if homeassistant:
_LOGGER.info("Partial-Restore %s restore Home-Assistant",
_LOGGER.info("Restore %s run Home-Assistant",
snapshot.slug)
snapshot.restore_homeassistant()
tasks.append(self._homeassistant.update(
snapshot.homeassistant_version))
task_hass = self._loop.create_task(
self._homeassistant.update(
snapshot.homeassistant_version))
# Process Add-ons
addon_list = []
for slug in addons:
addon = self._addons.get(slug)
if addon:
tasks.append(snapshot.export_addon(addon))
else:
_LOGGER.warning("Can't restore addon %s",
snapshot.slug)
addon_list.append(addon)
continue
_LOGGER.warning("Can't restore addon %s", snapshot.slug)
if tasks:
_LOGGER.info("Partial-Restore %s run %d tasks",
snapshot.slug, len(tasks))
await asyncio.wait(tasks, loop=self._loop)
if addon_list:
_LOGGER.info("Restore %s old add-ons", snapshot.slug)
await snapshot.restore_addons(addon_list)
# make sure homeassistant run agen
if task_hass:
_LOGGER.info("Restore %s wait for Home-Assistant",
snapshot.slug)
await task_hass
await self._homeassistant.start()
except (OSError, ValueError, tarfile.TarError) as err:
_LOGGER.info("Partial-Restore %s error: %s", snapshot.slug, err)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Restore %s error", snapshot.slug)
return False
else:

View File

@ -1,23 +1,28 @@
"""Represent a snapshot file."""
import asyncio
from base64 import b64decode, b64encode
import json
import logging
from pathlib import Path
import tarfile
from tempfile import TemporaryDirectory
from Crypto.Cipher import AES
from Crypto.Util import Padding
import voluptuous as vol
from voluptuous.humanize import humanize_error
from .validate import SCHEMA_SNAPSHOT, ALL_FOLDERS
from .utils import remove_folder
from .utils import remove_folder, password_to_key, password_for_validating
from ..const import (
ATTR_SLUG, ATTR_NAME, ATTR_DATE, ATTR_ADDONS, ATTR_REPOSITORIES,
ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_VERSION, ATTR_TYPE, ATTR_IMAGE,
ATTR_PORT, ATTR_SSL, ATTR_PASSWORD, ATTR_WATCHDOG, ATTR_BOOT,
ATTR_LAST_VERSION, ATTR_STARTUP_TIME)
ATTR_PORT, ATTR_SSL, ATTR_PASSWORD, ATTR_WATCHDOG, ATTR_BOOT, ATTR_CRYPTO,
ATTR_LAST_VERSION, ATTR_PROTECTED, ATTR_WAIT_BOOT, ATTR_SIZE,
CRYPTO_AES128)
from ..coresys import CoreSysAttributes
from ..utils.json import write_json_file
from ..utils.tar import SecureTarFile
_LOGGER = logging.getLogger(__name__)
@ -28,9 +33,11 @@ class Snapshot(CoreSysAttributes):
def __init__(self, coresys, tar_file):
"""Initialize a snapshot."""
self.coresys = coresys
self.tar_file = tar_file
self._tarfile = tar_file
self._data = {}
self._tmp = None
self._key = None
self._aes = None
@property
def slug(self):
@ -52,11 +59,21 @@ class Snapshot(CoreSysAttributes):
"""Return snapshot date."""
return self._data[ATTR_DATE]
@property
def protected(self):
"""Return snapshot date."""
return self._data.get(ATTR_PROTECTED) is not None
@property
def addons(self):
"""Return snapshot date."""
return self._data[ATTR_ADDONS]
@property
def addon_list(self):
"""Return a list of addons slugs."""
return [addon_data[ATTR_SLUG] for addon_data in self.addons]
@property
def folders(self):
"""Return list of saved folders."""
@ -77,99 +94,29 @@ class Snapshot(CoreSysAttributes):
"""Return snapshot homeassistant version."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_VERSION)
@homeassistant_version.setter
def homeassistant_version(self, value):
"""Set snapshot homeassistant version."""
self._data[ATTR_HOMEASSISTANT][ATTR_VERSION] = value
@property
def homeassistant_last_version(self):
"""Return snapshot homeassistant last version (custom)."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_LAST_VERSION)
@homeassistant_last_version.setter
def homeassistant_last_version(self, value):
"""Set snapshot homeassistant last version (custom)."""
self._data[ATTR_HOMEASSISTANT][ATTR_LAST_VERSION] = value
@property
def homeassistant_image(self):
"""Return snapshot homeassistant custom image."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_IMAGE)
@homeassistant_image.setter
def homeassistant_image(self, value):
"""Set snapshot homeassistant custom image."""
self._data[ATTR_HOMEASSISTANT][ATTR_IMAGE] = value
@property
def homeassistant_ssl(self):
"""Return snapshot homeassistant api ssl."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_SSL)
@homeassistant_ssl.setter
def homeassistant_ssl(self, value):
"""Set snapshot homeassistant api ssl."""
self._data[ATTR_HOMEASSISTANT][ATTR_SSL] = value
@property
def homeassistant_port(self):
"""Return snapshot homeassistant api port."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_PORT)
@homeassistant_port.setter
def homeassistant_port(self, value):
"""Set snapshot homeassistant api port."""
self._data[ATTR_HOMEASSISTANT][ATTR_PORT] = value
@property
def homeassistant_password(self):
"""Return snapshot homeassistant api password."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_PASSWORD)
@homeassistant_password.setter
def homeassistant_password(self, value):
"""Set snapshot homeassistant api password."""
self._data[ATTR_HOMEASSISTANT][ATTR_PASSWORD] = value
@property
def homeassistant_watchdog(self):
"""Return snapshot homeassistant watchdog options."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_WATCHDOG)
@homeassistant_watchdog.setter
def homeassistant_watchdog(self, value):
"""Set snapshot homeassistant watchdog options."""
self._data[ATTR_HOMEASSISTANT][ATTR_WATCHDOG] = value
@property
def homeassistant_startup_time(self):
"""Return snapshot homeassistant startup time options."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_STARTUP_TIME)
@homeassistant_startup_time.setter
def homeassistant_startup_time(self, value):
"""Set snapshot homeassistant startup time options."""
self._data[ATTR_HOMEASSISTANT][ATTR_STARTUP_TIME] = value
@property
def homeassistant_boot(self):
"""Return snapshot homeassistant boot options."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_BOOT)
@homeassistant_boot.setter
def homeassistant_boot(self, value):
"""Set snapshot homeassistant boot options."""
self._data[ATTR_HOMEASSISTANT][ATTR_BOOT] = value
def homeassistant(self):
"""Return snapshot homeassistant data."""
return self._data[ATTR_HOMEASSISTANT]
@property
def size(self):
"""Return snapshot size."""
if not self.tar_file.is_file():
if not self.tarfile.is_file():
return 0
return self.tar_file.stat().st_size / 1048576 # calc mbyte
return round(self.tarfile.stat().st_size / 1048576, 2) # calc mbyte
def create(self, slug, name, date, sys_type):
@property
def is_new(self):
"""Return True if there is new."""
return not self.tarfile.exists()
@property
def tarfile(self):
"""Return path to Snapshot tarfile."""
return self._tarfile
def new(self, slug, name, date, sys_type, password=None):
"""Initialize a new snapshot."""
# init metadata
self._data[ATTR_SLUG] = slug
@ -180,15 +127,51 @@ class Snapshot(CoreSysAttributes):
# Add defaults
self._data = SCHEMA_SNAPSHOT(self._data)
# Set password
if password:
self._key = password_to_key(password)
self._aes = AES.new(self._key, AES.MODE_ECB)
self._data[ATTR_PROTECTED] = password_for_validating(password)
self._data[ATTR_CRYPTO] = CRYPTO_AES128
def set_password(self, password):
"""Set the password for a exists snapshot."""
if not password:
return False
validating = password_for_validating(password)
if validating != self._data[ATTR_PROTECTED]:
return False
self._key = password_to_key(password)
self._aes = AES.new(self._key, AES.MODE_ECB)
return True
def _encrypt_data(self, data):
"""Make data secure."""
if not self._key:
return data
return b64encode(
self._aes.encrypt(Padding.pad(data.encode(), 16))).decode()
def _decrypt_data(self, data):
"""Make data readable."""
if not self._key:
return data
return Padding.unpad(
self._aes.decrypt(b64decode(data)), 16).decode()
async def load(self):
"""Read snapshot.json from tar file."""
if not self.tar_file.is_file():
_LOGGER.error("No tarfile %s", self.tar_file)
if not self.tarfile.is_file():
_LOGGER.error("No tarfile %s", self.tarfile)
return False
def _load_file():
"""Read snapshot.json."""
with tarfile.open(self.tar_file, "r:") as snapshot:
with tarfile.open(self.tarfile, "r:") as snapshot:
json_file = snapshot.extractfile("./snapshot.json")
return json_file.read()
@ -197,21 +180,21 @@ class Snapshot(CoreSysAttributes):
raw = await self._loop.run_in_executor(None, _load_file)
except (tarfile.TarError, KeyError) as err:
_LOGGER.error(
"Can't read snapshot tarfile %s: %s", self.tar_file, err)
"Can't read snapshot tarfile %s: %s", self.tarfile, err)
return False
# parse data
try:
raw_dict = json.loads(raw)
except json.JSONDecodeError as err:
_LOGGER.error("Can't read data for %s: %s", self.tar_file, err)
_LOGGER.error("Can't read data for %s: %s", self.tarfile, err)
return False
# validate
try:
self._data = SCHEMA_SNAPSHOT(raw_dict)
except vol.Invalid as err:
_LOGGER.error("Can't validate data for %s: %s", self.tar_file,
_LOGGER.error("Can't validate data for %s: %s", self.tarfile,
humanize_error(raw_dict, err))
return False
@ -222,13 +205,13 @@ class Snapshot(CoreSysAttributes):
self._tmp = TemporaryDirectory(dir=str(self._config.path_tmp))
# create a snapshot
if not self.tar_file.is_file():
if not self.tarfile.is_file():
return self
# extract a exists snapshot
def _extract_snapshot():
"""Extract a snapshot."""
with tarfile.open(self.tar_file, "r:") as tar:
with tarfile.open(self.tarfile, "r:") as tar:
tar.extractall(path=self._tmp.name)
await self._loop.run_in_executor(None, _extract_snapshot)
@ -236,7 +219,7 @@ class Snapshot(CoreSysAttributes):
async def __aexit__(self, exception_type, exception_value, traceback):
"""Async context to close a snapshot."""
# exists snapshot or exception on build
if self.tar_file.is_file() or exception_type is not None:
if self.tarfile.is_file() or exception_type is not None:
self._tmp.cleanup()
return
@ -244,14 +227,14 @@ class Snapshot(CoreSysAttributes):
try:
self._data = SCHEMA_SNAPSHOT(self._data)
except vol.Invalid as err:
_LOGGER.error("Invalid data for %s: %s", self.tar_file,
_LOGGER.error("Invalid data for %s: %s", self.tarfile,
humanize_error(self._data, err))
raise ValueError("Invalid config") from None
# new snapshot, build it
def _create_snapshot():
"""Create a new snapshot."""
with tarfile.open(self.tar_file, "w:") as tar:
with tarfile.open(self.tarfile, "w:") as tar:
tar.add(self._tmp.name, arcname=".")
try:
@ -262,32 +245,63 @@ class Snapshot(CoreSysAttributes):
finally:
self._tmp.cleanup()
async def import_addon(self, addon):
"""Add a addon into snapshot."""
snapshot_file = Path(self._tmp.name, "{}.tar.gz".format(addon.slug))
async def store_addons(self, addon_list=None):
"""Add a list of add-ons into snapshot."""
addon_list = addon_list or self._addons.list_installed
if not await addon.snapshot(snapshot_file):
_LOGGER.error("Can't make snapshot from %s", addon.slug)
return False
async def _addon_save(addon):
"""Task to store a add-on into snapshot."""
addon_file = SecureTarFile(
Path(self._tmp.name, f"{addon.slug}.tar.gz"),
'w', key=self._key)
# store to config
self._data[ATTR_ADDONS].append({
ATTR_SLUG: addon.slug,
ATTR_NAME: addon.name,
ATTR_VERSION: addon.version_installed,
})
# Take snapshot
if not await addon.snapshot(addon_file):
_LOGGER.error("Can't make snapshot from %s", addon.slug)
return
return True
# Store to config
self._data[ATTR_ADDONS].append({
ATTR_SLUG: addon.slug,
ATTR_NAME: addon.name,
ATTR_VERSION: addon.version_installed,
ATTR_SIZE: addon_file.size,
})
async def export_addon(self, addon):
"""Restore a addon from snapshot."""
snapshot_file = Path(self._tmp.name, "{}.tar.gz".format(addon.slug))
# Run tasks
tasks = [_addon_save(addon) for addon in addon_list]
if tasks:
await asyncio.wait(tasks, loop=self._loop)
if not await addon.restore(snapshot_file):
_LOGGER.error("Can't restore snapshot for %s", addon.slug)
return False
async def restore_addons(self, addon_list=None):
"""Restore a list add-on from snapshot."""
if not addon_list:
addon_list = []
for addon_slug in self.addon_list:
addon = self._addons.get(addon_slug)
if addon:
addon_list.append(addon)
return True
async def _addon_restore(addon):
"""Task to restore a add-on into snapshot."""
addon_file = SecureTarFile(
Path(self._tmp.name, f"{addon.slug}.tar.gz"),
'r', key=self._key)
# If exists inside snapshot
if not addon_file.path.exists():
_LOGGER.error("Can't find snapshot for %s", addon.slug)
return
# Performe a restore
if not await addon.restore(addon_file):
_LOGGER.error("Can't restore snapshot for %s", addon.slug)
return
# Run tasks
tasks = [_addon_restore(addon) for addon in addon_list]
if tasks:
await asyncio.wait(tasks, loop=self._loop)
async def store_folders(self, folder_list=None):
"""Backup hassio data into snapshot."""
@ -296,13 +310,18 @@ class Snapshot(CoreSysAttributes):
def _folder_save(name):
"""Intenal function to snapshot a folder."""
slug_name = name.replace("/", "_")
snapshot_tar = Path(self._tmp.name, "{}.tar.gz".format(slug_name))
tar_name = Path(self._tmp.name, f"{slug_name}.tar.gz")
origin_dir = Path(self._config.path_hassio, name)
# Check if exsits
if not origin_dir.is_dir():
_LOGGER.warning("Can't find snapshot folder %s", name)
return
# Take snapshot
try:
_LOGGER.info("Snapshot folder %s", name)
with tarfile.open(snapshot_tar, "w:gz",
compresslevel=1) as tar_file:
with SecureTarFile(tar_name, 'w', key=self._key) as tar_file:
tar_file.add(origin_dir, arcname=".")
_LOGGER.info("Snapshot folder %s done", name)
@ -310,7 +329,7 @@ class Snapshot(CoreSysAttributes):
except (tarfile.TarError, OSError) as err:
_LOGGER.warning("Can't snapshot folder %s: %s", name, err)
# run tasks
# Run tasks
tasks = [self._loop.run_in_executor(None, _folder_save, folder)
for folder in folder_list]
if tasks:
@ -323,22 +342,28 @@ class Snapshot(CoreSysAttributes):
def _folder_restore(name):
"""Intenal function to restore a folder."""
slug_name = name.replace("/", "_")
snapshot_tar = Path(self._tmp.name, "{}.tar.gz".format(slug_name))
tar_name = Path(self._tmp.name, f"{slug_name}.tar.gz")
origin_dir = Path(self._config.path_hassio, name)
# clean old stuff
# Check if exists inside snapshot
if not tar_name.exists():
_LOGGER.warning("Can't find restore folder %s", name)
return
# Clean old stuff
if origin_dir.is_dir():
remove_folder(origin_dir)
# Performe a restore
try:
_LOGGER.info("Restore folder %s", name)
with tarfile.open(snapshot_tar, "r:gz") as tar_file:
with SecureTarFile(tar_name, 'r', key=self._key) as tar_file:
tar_file.extractall(path=origin_dir)
_LOGGER.info("Restore folder %s done", name)
except (tarfile.TarError, OSError) as err:
_LOGGER.warning("Can't restore folder %s: %s", name, err)
# run tasks
# Run tasks
tasks = [self._loop.run_in_executor(None, _folder_restore, folder)
for folder in folder_list]
if tasks:
@ -346,36 +371,40 @@ class Snapshot(CoreSysAttributes):
def store_homeassistant(self):
"""Read all data from homeassistant object."""
self.homeassistant_version = self._homeassistant.version
self.homeassistant_watchdog = self._homeassistant.watchdog
self.homeassistant_boot = self._homeassistant.boot
self.homeassistant_startup_time = self._homeassistant.startup_time
self.homeassistant[ATTR_VERSION] = self._homeassistant.version
self.homeassistant[ATTR_WATCHDOG] = self._homeassistant.watchdog
self.homeassistant[ATTR_BOOT] = self._homeassistant.boot
self.homeassistant[ATTR_WAIT_BOOT] = self._homeassistant.wait_boot
# custom image
# Custom image
if self._homeassistant.is_custom_image:
self.homeassistant_image = self._homeassistant.image
self.homeassistant_last_version = self._homeassistant.last_version
self.homeassistant[ATTR_IMAGE] = self._homeassistant.image
self.homeassistant[ATTR_LAST_VERSION] = \
self._homeassistant.last_version
# api
self.homeassistant_port = self._homeassistant.api_port
self.homeassistant_ssl = self._homeassistant.api_ssl
self.homeassistant_password = self._homeassistant.api_password
# API/Proxy
self.homeassistant[ATTR_PORT] = self._homeassistant.api_port
self.homeassistant[ATTR_SSL] = self._homeassistant.api_ssl
self.homeassistant[ATTR_PASSWORD] = \
self._encrypt_data(self._homeassistant.api_password)
def restore_homeassistant(self):
"""Write all data to homeassistant object."""
self._homeassistant.watchdog = self.homeassistant_watchdog
self._homeassistant.boot = self.homeassistant_boot
self._homeassistant.startup_time = self.homeassistant_startup_time
self._homeassistant.watchdog = self.homeassistant[ATTR_WATCHDOG]
self._homeassistant.boot = self.homeassistant[ATTR_BOOT]
self._homeassistant.wait_boot = self.homeassistant[ATTR_WAIT_BOOT]
# custom image
if self.homeassistant_image:
self._homeassistant.image = self.homeassistant_image
self._homeassistant.last_version = self.homeassistant_last_version
# Custom image
if self.homeassistant.get(ATTR_IMAGE):
self._homeassistant.image = self.homeassistant[ATTR_IMAGE]
self._homeassistant.last_version = \
self.homeassistant[ATTR_LAST_VERSION]
# api
self._homeassistant.api_port = self.homeassistant_port
self._homeassistant.api_ssl = self.homeassistant_ssl
self._homeassistant.api_password = self.homeassistant_password
# API/Proxy
self._homeassistant.api_port = self.homeassistant[ATTR_PORT]
self._homeassistant.api_ssl = self.homeassistant[ATTR_SSL]
self._homeassistant.api_password = \
self._decrypt_data(self.homeassistant[ATTR_PASSWORD])
# save
self._homeassistant.save_data()

View File

@ -3,6 +3,21 @@ import hashlib
import shutil
def password_to_key(password):
"""Generate a AES Key from password"""
password = password.encode()
for _ in range(100):
password = hashlib.sha256(password).digest()
return password[:16]
def password_for_validating(password):
"""Generate a SHA256 hash from password"""
for _ in range(100):
password = hashlib.sha256(password.encode()).hexdigest()
return password
def create_slug(name, date_str):
"""Generate a hash from repository."""
key = "{} - {}".format(date_str, name).lower().encode()

View File

@ -5,10 +5,10 @@ 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, ATTR_IMAGE,
ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG, ATTR_BOOT,
ATTR_LAST_VERSION, ATTR_STARTUP_TIME,
ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG, ATTR_BOOT, ATTR_SIZE,
ATTR_LAST_VERSION, ATTR_WAIT_BOOT, ATTR_PROTECTED, ATTR_CRYPTO,
FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL,
SNAPSHOT_FULL, SNAPSHOT_PARTIAL)
SNAPSHOT_FULL, SNAPSHOT_PARTIAL, CRYPTO_AES128)
from ..validate import NETWORK_PORT, REPOSITORIES, DOCKER_IMAGE
ALL_FOLDERS = [FOLDER_HOMEASSISTANT, FOLDER_SHARE, FOLDER_ADDONS, FOLDER_SSL]
@ -29,8 +29,11 @@ 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.Inclusive(ATTR_PROTECTED, 'encrypted'):
vol.All(vol.Coerce(str), vol.Length(64)),
vol.Inclusive(ATTR_CRYPTO, 'encrypted'): CRYPTO_AES128,
vol.Optional(ATTR_HOMEASSISTANT, default=dict): vol.Schema({
vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Optional(ATTR_VERSION): vol.Coerce(str),
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE,
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
@ -38,7 +41,7 @@ SCHEMA_SNAPSHOT = vol.Schema({
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
vol.Optional(ATTR_STARTUP_TIME, default=600):
vol.Optional(ATTR_WAIT_BOOT, default=600):
vol.All(vol.Coerce(int), vol.Range(min=60)),
}, extra=vol.REMOVE_EXTRA),
vol.Optional(ATTR_FOLDERS, default=list):
@ -47,6 +50,7 @@ SCHEMA_SNAPSHOT = vol.Schema({
vol.Required(ATTR_SLUG): vol.Coerce(str),
vol.Required(ATTR_NAME): vol.Coerce(str),
vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Optional(ATTR_SIZE, default=0): vol.Coerce(float),
}, extra=vol.REMOVE_EXTRA)], unique_addons),
vol.Optional(ATTR_REPOSITORIES, default=list): REPOSITORIES,
}, extra=vol.ALLOW_EXTRA)

78
hassio/utils/tar.py Normal file
View File

@ -0,0 +1,78 @@
"""Tarfile fileobject handler for encrypted files."""
import tarfile
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad
BLOCK_SIZE = 16
MOD_READ = 'r'
MOD_WRITE = 'w'
class SecureTarFile(object):
"""Handle encrypted files for tarfile library."""
def __init__(self, name, mode, key=None, gzip=True):
"""Initialize encryption handler."""
self._file = None
self._mode = mode
self._name = name
# Tarfile options
self._tar = None
self._tar_mode = f"{mode}|gz" if gzip else f"{mode}|"
# Encryption/Decription
self._aes = None
self._key = key
def __enter__(self):
"""Start context manager tarfile."""
if not self._key:
self._tar = tarfile.open(name=str(self._name), mode=self._tar_mode)
return self._tar
# Encrypted/Decryped Tarfile
self._file = self._name.open(f"{self._mode}b")
# Extract IV for CBC
if self._mode == MOD_READ:
cbc_iv = self._file.read(16)
else:
cbc_iv = get_random_bytes(16)
self._file.write(cbc_iv)
self._aes = AES.new(self._key, AES.MODE_CBC, iv=cbc_iv)
self._tar = tarfile.open(fileobj=self, mode=self._tar_mode)
return self._tar
def __exit__(self, exc_type, exc_value, traceback):
"""Close file."""
if self._tar:
self._tar.close()
if self._file:
self._file.close()
def write(self, data):
"""Write data."""
if len(data) % BLOCK_SIZE != 0:
data = pad(data, BLOCK_SIZE)
self._file.write(self._aes.encrypt(data))
def read(self, size=0):
"""Read data."""
return self._aes.decrypt(self._file.read(size))
@property
def path(self):
"""Return path object of tarfile."""
return self._name
@property
def size(self):
"""Return snapshot size."""
if not self._name.is_file():
return 0
return round(self._name.stat().st_size / 1048576, 2) # calc mbyte

View File

@ -8,8 +8,7 @@ from .const import (
ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_BETA_CHANNEL, ATTR_TIMEZONE,
ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO, ATTR_BOOT, ATTR_LAST_BOOT,
ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, ATTR_WAIT_BOOT, ATTR_UUID,
ATTR_STARTUP_TIME)
ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, ATTR_WAIT_BOOT, ATTR_UUID)
NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
@ -73,7 +72,7 @@ SCHEMA_HASS_CONFIG = vol.Schema({
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
vol.Optional(ATTR_STARTUP_TIME, default=600):
vol.Optional(ATTR_WAIT_BOOT, default=600):
vol.All(vol.Coerce(int), vol.Range(min=60)),
}, extra=vol.REMOVE_EXTRA)

View File

@ -44,9 +44,10 @@ setup(
'aiohttp==2.3.10',
'docker==3.0.1',
'colorlog==3.1.2',
'voluptuous==0.10.5',
'voluptuous==0.11.1',
'gitpython==2.1.8',
'pytz==2018.3',
'pyudev==0.21.0'
'pyudev==0.21.0',
'pycryptodome==3.4.11'
]
)