From 5c8aa71c31be492c926e5ad28045eed61a645141 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 16 Jan 2018 12:45:14 +0100 Subject: [PATCH 1/5] Pump version to 0.83 --- hassio/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/const.py b/hassio/const.py index ebf46b48e..a3dfb4ff9 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -2,7 +2,7 @@ from pathlib import Path from ipaddress import ip_network -HASSIO_VERSION = '0.82' +HASSIO_VERSION = '0.83' URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/' 'hassio/{}/version.json') From 94b44ec7fe40ff40f08f0751d6d53ea8a36810c0 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 17 Jan 2018 12:27:28 +0100 Subject: [PATCH 2/5] Update HomeAssistant to version 0.61.1 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 69f1a888e..5376f5e85 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "hassio": "0.82", - "homeassistant": "0.61", + "homeassistant": "0.61.1", "resinos": "1.1", "resinhup": "0.3", "generic": "0.3", From 70a721a47d65026aa649a295953cd68acfbebad3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 18 Jan 2018 10:21:16 +0100 Subject: [PATCH 3/5] Reset default config with None (#320) * Update addons.py * Update addon.py * Update API.md --- API.md | 2 +- hassio/addons/addon.py | 5 ++++- hassio/api/addons.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/API.md b/API.md index e6720bd71..62222d8bd 100644 --- a/API.md +++ b/API.md @@ -417,7 +417,7 @@ Get all available addons. } ``` -For reset custom network/audio settings, set it `null`. +Reset custom network/audio/options, set it `null`. - POST `/addons/{addon}/start` diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index 7944de1b6..c66a5e4c3 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -120,7 +120,10 @@ class Addon(CoreSysAttributes): @options.setter def options(self, value): """Store user addon options.""" - self._data.user[self._id][ATTR_OPTIONS] = deepcopy(value) + if value is None: + self._data.user[self._id][ATTR_OPTIONS] = {} + else: + self._data.user[self._id][ATTR_OPTIONS] = deepcopy(value) self._data.save() @property diff --git a/hassio/api/addons.py b/hassio/api/addons.py index 8f593f79f..ddbfd8894 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -140,7 +140,7 @@ class APIAddons(CoreSysAttributes): addon = self._extract_addon(request) addon_schema = SCHEMA_OPTIONS.extend({ - vol.Optional(ATTR_OPTIONS): addon.schema, + vol.Optional(ATTR_OPTIONS): vol.Any(None, addon.schema), }) body = await api_validate(addon_schema, request) From 6f2cf2ef8514396b56346c09cccaebfe3c3127d5 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 18 Jan 2018 23:33:05 +0100 Subject: [PATCH 4/5] Robust json file handling with default reset on runtime (#321) * Update json.py * Update validate.py * Update validate.py * Update snapshots.py * Update validate.py * Update homeassistant.py * Update validate.py * Update snapshot.py * Update snapshot.py * Update snapshot.py * Update json.py * Update json.py * Update json.py * Update validate.py * Update snapshots.py * Update validate.py * Update validate.py * improve config updates * fix lint * update build * fix schema * fix validate * fix lint * fix some styles * fix * fix snapshot * fix errors * Update API --- API.md | 3 ++- hassio/addons/addon.py | 31 +++++++++++++++------------ hassio/addons/build.py | 13 ++++++++---- hassio/addons/data.py | 2 +- hassio/addons/validate.py | 10 ++++----- hassio/api/addons.py | 1 + hassio/api/homeassistant.py | 6 +++--- hassio/api/host.py | 1 + hassio/api/snapshots.py | 13 ++++++++---- hassio/api/supervisor.py | 8 +++---- hassio/config.py | 7 ------ hassio/docker/addon.py | 4 ++-- hassio/homeassistant.py | 1 - hassio/snapshots/__init__.py | 41 ++++++++++++++++++++---------------- hassio/snapshots/snapshot.py | 21 +++++++++--------- hassio/snapshots/validate.py | 27 +++++++++++++++++------- hassio/updater.py | 2 +- hassio/utils/json.py | 40 +++++++++++++++++++---------------- hassio/validate.py | 8 +++++-- 19 files changed, 136 insertions(+), 103 deletions(-) diff --git a/API.md b/API.md index 62222d8bd..94a0c6f0d 100644 --- a/API.md +++ b/API.md @@ -111,7 +111,8 @@ Output is the raw docker log. { "slug": "SLUG", "date": "ISO", - "name": "Custom name" + "name": "Custom name", + "type": "full|partial" } ] } diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index c66a5e4c3..128daa062 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -87,25 +87,25 @@ class Addon(CoreSysAttributes): ATTR_OPTIONS: {}, ATTR_VERSION: version, } - self._data.save() + self._data.save_data() def _set_uninstall(self): """Set addon as uninstalled.""" self._data.system.pop(self._id, None) self._data.user.pop(self._id, None) - self._data.save() + self._data.save_data() def _set_update(self, version): """Update version of addon.""" self._data.system[self._id] = deepcopy(self._data.cache[self._id]) self._data.user[self._id][ATTR_VERSION] = version - self._data.save() + self._data.save_data() def _restore_data(self, user, system): """Restore data to addon.""" self._data.user[self._id] = deepcopy(user) self._data.system[self._id] = deepcopy(system) - self._data.save() + self._data.save_data() @property def options(self): @@ -124,7 +124,6 @@ class Addon(CoreSysAttributes): self._data.user[self._id][ATTR_OPTIONS] = {} else: self._data.user[self._id][ATTR_OPTIONS] = deepcopy(value) - self._data.save() @property def boot(self): @@ -137,7 +136,6 @@ class Addon(CoreSysAttributes): def boot(self, value): """Store user boot options.""" self._data.user[self._id][ATTR_BOOT] = value - self._data.save() @property def auto_update(self): @@ -150,7 +148,6 @@ class Addon(CoreSysAttributes): def auto_update(self, value): """Set auto update.""" self._data.user[self._id][ATTR_AUTO_UPDATE] = value - self._data.save() @property def name(self): @@ -228,8 +225,6 @@ class Addon(CoreSysAttributes): self._data.user[self._id][ATTR_NETWORK] = new_ports - self._data.save() - @property def webui(self): """Return URL to webui or None.""" @@ -350,7 +345,6 @@ class Addon(CoreSysAttributes): self._data.user[self._id].pop(ATTR_AUDIO_OUTPUT, None) else: self._data.user[self._id][ATTR_AUDIO_OUTPUT] = value - self._data.save() @property def audio_input(self): @@ -370,7 +364,6 @@ class Addon(CoreSysAttributes): self._data.user[self._id].pop(ATTR_AUDIO_INPUT, None) else: self._data.user[self._id][ATTR_AUDIO_INPUT] = value - self._data.save() @property def url(self): @@ -451,6 +444,10 @@ class Addon(CoreSysAttributes): """Return path to addon changelog.""" return Path(self.path_location, 'CHANGELOG.md') + def save_data(self): + """Save data of addon.""" + self._addons.data.save_data() + def write_options(self): """Return True if addon options is written to data.""" schema = self.schema @@ -458,10 +455,14 @@ class Addon(CoreSysAttributes): try: schema(options) - return write_json_file(self.path_options, options) + write_json_file(self.path_options, options) except vol.Invalid as ex: _LOGGER.error("Addon %s have wrong options: %s", self._id, humanize_error(options, ex)) + except (OSError, json.JSONDecodeError) as err: + _LOGGER.error("Addon %s can't write options: %s", self._id, err) + else: + return True return False @@ -657,8 +658,10 @@ class Addon(CoreSysAttributes): } # store local configs/state - if not write_json_file(Path(temp, "addon.json"), data): - _LOGGER.error("Can't write addon.json for %s", self._id) + try: + write_json_file(Path(temp, "addon.json"), data) + except (OSError, json.JSONDecodeError) as err: + _LOGGER.error("Can't save meta for %s: %s", self._id, err) return False # write into tarfile diff --git a/hassio/addons/build.py b/hassio/addons/build.py index d5570fc7d..a3dcfb5b3 100644 --- a/hassio/addons/build.py +++ b/hassio/addons/build.py @@ -10,18 +10,23 @@ from ..utils.json import JsonConfig class AddonBuild(JsonConfig, CoreSysAttributes): """Handle build options for addons.""" - def __init__(self, coresys, addon): + def __init__(self, coresys, slug): """Initialize addon builder.""" self.coresys = coresys - self.addon = addon + self._id = slug super().__init__( - Path(addon.path_location, 'build.json'), SCHEMA_BUILD_CONFIG) + Path(self.addon.path_location, 'build.json'), SCHEMA_BUILD_CONFIG) - def save(self): + def save_data(self): """Ignore save function.""" pass + @property + def addon(self): + """Return addon of build data.""" + return self._addons.get(self._id) + @property def base_image(self): """Base images for this addon.""" diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 9fb0aff70..1fe362fd9 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -159,4 +159,4 @@ class Data(JsonConfig, CoreSysAttributes): have_change = True if have_change: - self.save() + self.save_data() diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index becf7588f..e60ce5211 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -101,7 +101,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ vol.Optional(ATTR_AUTO_UART, default=False): vol.Boolean(), vol.Optional(ATTR_TMPFS): vol.Match(r"^size=(\d)*[kmg](,uid=\d{1,4})?(,rw)?$"), - vol.Optional(ATTR_MAP, default=[]): [vol.Match(RE_VOLUME)], + vol.Optional(ATTR_MAP, default=list): [vol.Match(RE_VOLUME)], vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)}, vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)], vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(), @@ -141,7 +141,7 @@ SCHEMA_BUILD_CONFIG = vol.Schema({ vol.In(ARCH_ALL): vol.Match(r"(?:^[\w{}]+/)?[\-\w{}]+:[\.\-\w{}]+$"), }), vol.Optional(ATTR_SQUASH, default=False): vol.Boolean(), - vol.Optional(ATTR_ARGS, default={}): vol.Schema({ + vol.Optional(ATTR_ARGS, default=dict): vol.Schema({ vol.Coerce(str): vol.Coerce(str) }), }, extra=vol.REMOVE_EXTRA) @@ -152,7 +152,7 @@ SCHEMA_ADDON_USER = vol.Schema({ vol.Required(ATTR_VERSION): vol.Coerce(str), vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): vol.Match(r"^[0-9a-f]{32}$"), - vol.Optional(ATTR_OPTIONS, default={}): dict, + vol.Optional(ATTR_OPTIONS, default=dict): dict, vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(), vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]), @@ -169,10 +169,10 @@ SCHEMA_ADDON_SYSTEM = SCHEMA_ADDON_CONFIG.extend({ SCHEMA_ADDON_FILE = vol.Schema({ - vol.Optional(ATTR_USER, default={}): { + vol.Optional(ATTR_USER, default=dict): { vol.Coerce(str): SCHEMA_ADDON_USER, }, - vol.Optional(ATTR_SYSTEM, default={}): { + vol.Optional(ATTR_SYSTEM, default=dict): { vol.Coerce(str): SCHEMA_ADDON_SYSTEM, } }) diff --git a/hassio/api/addons.py b/hassio/api/addons.py index ddbfd8894..9880ad9ec 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -158,6 +158,7 @@ class APIAddons(CoreSysAttributes): if ATTR_AUDIO_OUTPUT in body: addon.audio_output = body[ATTR_AUDIO_OUTPUT] + addon.save_data() return True @api_process diff --git a/hassio/api/homeassistant.py b/hassio/api/homeassistant.py index 2fb8216bf..33b68977d 100644 --- a/hassio/api/homeassistant.py +++ b/hassio/api/homeassistant.py @@ -11,7 +11,7 @@ from ..const import ( ATTR_MEMORY_USAGE, ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_BLK_READ, ATTR_BLK_WRITE, CONTENT_TYPE_BINARY) from ..coresys import CoreSysAttributes -from ..validate import NETWORK_PORT +from ..validate import NETWORK_PORT, DOCKER_IMAGE _LOGGER = logging.getLogger(__name__) @@ -22,7 +22,7 @@ SCHEMA_OPTIONS = vol.Schema({ vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Any(None, vol.Coerce(str)), vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): - vol.Any(None, vol.Coerce(str)), + vol.Any(None, DOCKER_IMAGE), vol.Optional(ATTR_PORT): NETWORK_PORT, vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)), vol.Optional(ATTR_SSL): vol.Boolean(), @@ -75,7 +75,7 @@ class APIHomeAssistant(CoreSysAttributes): if ATTR_WATCHDOG in body: self._homeassistant.watchdog = body[ATTR_WATCHDOG] - self._homeassistant.save() + self._homeassistant.save_data() return True @api_process diff --git a/hassio/api/host.py b/hassio/api/host.py index e9452bbc3..7bc69826e 100644 --- a/hassio/api/host.py +++ b/hassio/api/host.py @@ -49,6 +49,7 @@ class APIHost(CoreSysAttributes): if ATTR_AUDIO_INPUT in body: self._config.audio_input = body[ATTR_AUDIO_INPUT] + self._config.save_data() return True @api_process_hostcontrol diff --git a/hassio/api/snapshots.py b/hassio/api/snapshots.py index 804bd0439..59f692328 100644 --- a/hassio/api/snapshots.py +++ b/hassio/api/snapshots.py @@ -18,8 +18,10 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=no-value-for-parameter SCHEMA_RESTORE_PARTIAL = vol.Schema({ vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(), - vol.Optional(ATTR_ADDONS): [vol.Coerce(str)], - vol.Optional(ATTR_FOLDERS): [vol.In(ALL_FOLDERS)], + vol.Optional(ATTR_ADDONS): + vol.All([vol.Coerce(str)], vol.Unique()), + vol.Optional(ATTR_FOLDERS): + vol.All([vol.In(ALL_FOLDERS)], vol.Unique()), }) SCHEMA_SNAPSHOT_FULL = vol.Schema({ @@ -27,8 +29,10 @@ SCHEMA_SNAPSHOT_FULL = vol.Schema({ }) SCHEMA_SNAPSHOT_PARTIAL = SCHEMA_SNAPSHOT_FULL.extend({ - vol.Optional(ATTR_ADDONS): [vol.Coerce(str)], - vol.Optional(ATTR_FOLDERS): [vol.In(ALL_FOLDERS)], + vol.Optional(ATTR_ADDONS): + vol.All([vol.Coerce(str)], vol.Unique()), + vol.Optional(ATTR_FOLDERS): + vol.All([vol.In(ALL_FOLDERS)], vol.Unique()), }) @@ -51,6 +55,7 @@ class APISnapshots(CoreSysAttributes): ATTR_SLUG: snapshot.slug, ATTR_NAME: snapshot.name, ATTR_DATE: snapshot.date, + ATTR_TYPE: snapshot.sys_type, }) return { diff --git a/hassio/api/supervisor.py b/hassio/api/supervisor.py index efa0e25ef..0ed39868e 100644 --- a/hassio/api/supervisor.py +++ b/hassio/api/supervisor.py @@ -13,14 +13,14 @@ from ..const import ( ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_BLK_READ, ATTR_BLK_WRITE, CONTENT_TYPE_BINARY) from ..coresys import CoreSysAttributes -from ..validate import validate_timezone, WAIT_BOOT +from ..validate import validate_timezone, WAIT_BOOT, REPOSITORIES _LOGGER = logging.getLogger(__name__) SCHEMA_OPTIONS = vol.Schema({ # pylint: disable=no-value-for-parameter vol.Optional(ATTR_BETA_CHANNEL): vol.Boolean(), - vol.Optional(ATTR_ADDONS_REPOSITORIES): [vol.Url()], + vol.Optional(ATTR_ADDONS_REPOSITORIES): REPOSITORIES, vol.Optional(ATTR_TIMEZONE): validate_timezone, vol.Optional(ATTR_WAIT_BOOT): WAIT_BOOT, }) @@ -84,8 +84,8 @@ class APISupervisor(CoreSysAttributes): new = set(body[ATTR_ADDONS_REPOSITORIES]) await asyncio.shield(self._addons.load_repositories(new)) - self._updater.save() - self._config.save() + self._updater.save_data() + self._config.save_data() return True @api_process diff --git a/hassio/config.py b/hassio/config.py index 6e50f0bc2..baf31fb27 100644 --- a/hassio/config.py +++ b/hassio/config.py @@ -45,7 +45,6 @@ class CoreConfig(JsonConfig): def timezone(self, value): """Set system timezone.""" self._data[ATTR_TIMEZONE] = value - self.save() @property def wait_boot(self): @@ -56,7 +55,6 @@ class CoreConfig(JsonConfig): def wait_boot(self, value): """Set wait boot time.""" self._data[ATTR_WAIT_BOOT] = value - self.save() @property def last_boot(self): @@ -72,7 +70,6 @@ class CoreConfig(JsonConfig): def last_boot(self, value): """Set last boot datetime.""" self._data[ATTR_LAST_BOOT] = value.isoformat() - self.save() @property def path_hassio(self): @@ -170,7 +167,6 @@ class CoreConfig(JsonConfig): return self._data[ATTR_ADDONS_CUSTOM_LIST].append(repo) - self.save() def drop_addon_repository(self, repo): """Remove a custom repository from list.""" @@ -178,7 +174,6 @@ class CoreConfig(JsonConfig): return self._data[ATTR_ADDONS_CUSTOM_LIST].remove(repo) - self.save() @property def audio_output(self): @@ -189,7 +184,6 @@ class CoreConfig(JsonConfig): def audio_output(self, value): """Set ALSA audio output card,dev.""" self._data[ATTR_AUDIO_OUTPUT] = value - self.save() @property def audio_input(self): @@ -200,4 +194,3 @@ class CoreConfig(JsonConfig): def audio_input(self, value): """Set ALSA audio input card,dev.""" self._data[ATTR_AUDIO_INPUT] = value - self.save() diff --git a/hassio/docker/addon.py b/hassio/docker/addon.py index e69512139..26df96c8f 100644 --- a/hassio/docker/addon.py +++ b/hassio/docker/addon.py @@ -26,7 +26,7 @@ class DockerAddon(DockerInterface): @property def addon(self): - """Return name of docker image.""" + """Return addon of docker image.""" return self._addons.get(self._id) @property @@ -269,7 +269,7 @@ class DockerAddon(DockerInterface): Need run inside executor. """ - build_env = AddonBuild(self.coresys, self.addon) + build_env = AddonBuild(self.coresys, self._id) _LOGGER.info("Start build %s:%s", self.image, tag) try: diff --git a/hassio/homeassistant.py b/hassio/homeassistant.py index c36949e87..ffc7eccff 100644 --- a/hassio/homeassistant.py +++ b/hassio/homeassistant.py @@ -53,7 +53,6 @@ class HomeAssistant(JsonConfig, CoreSysAttributes): def api_port(self, value): """Set network port for home-assistant instance.""" self._data[ATTR_PORT] = value - self.save() @property def api_password(self): diff --git a/hassio/snapshots/__init__.py b/hassio/snapshots/__init__.py index 748dda6f3..28e206ec8 100644 --- a/hassio/snapshots/__init__.py +++ b/hassio/snapshots/__init__.py @@ -112,14 +112,15 @@ class SnapshotsManager(CoreSysAttributes): _LOGGER.info("Full-Snapshot %s store folders", snapshot.slug) await snapshot.store_folders() - _LOGGER.info("Full-Snapshot %s done", snapshot.slug) - self.snapshots_obj[snapshot.slug] = snapshot - return True - except (OSError, ValueError, tarfile.TarError) as err: _LOGGER.info("Full-Snapshot %s error: %s", snapshot.slug, err) return False + else: + _LOGGER.info("Full-Snapshot %s done", snapshot.slug) + self.snapshots_obj[snapshot.slug] = snapshot + return True + finally: self._scheduler.suspend = False self.lock.release() @@ -157,14 +158,15 @@ class SnapshotsManager(CoreSysAttributes): snapshot.slug, folders) await snapshot.store_folders(folders) - _LOGGER.info("Partial-Snapshot %s done", snapshot.slug) - self.snapshots_obj[snapshot.slug] = snapshot - return True - except (OSError, ValueError, tarfile.TarError) as err: _LOGGER.info("Partial-Snapshot %s error: %s", snapshot.slug, err) return False + else: + _LOGGER.info("Partial-Snapshot %s done", snapshot.slug) + self.snapshots_obj[snapshot.slug] = snapshot + return True + finally: self._scheduler.suspend = False self.lock.release() @@ -229,7 +231,7 @@ class SnapshotsManager(CoreSysAttributes): if addon: tasks.append(addon.uninstall()) else: - _LOGGER.warning("Can't remove addon %s", slug) + _LOGGER.warning("Can't remove addon %s", snapshot.slug) for slug in restore_addons: addon = self._addons.get(slug) @@ -249,13 +251,14 @@ class SnapshotsManager(CoreSysAttributes): await task_hass await self._homeassistant.run() + except (OSError, ValueError, tarfile.TarError) as err: + _LOGGER.info("Full-Restore %s error: %s", snapshot.slug, err) + return False + + else: _LOGGER.info("Full-Restore %s done", snapshot.slug) return True - except (OSError, ValueError, tarfile.TarError) as err: - _LOGGER.info("Full-Restore %s error: %s", slug, err) - return False - finally: self._scheduler.suspend = False self.lock.release() @@ -298,7 +301,8 @@ class SnapshotsManager(CoreSysAttributes): if addon: tasks.append(snapshot.export_addon(addon)) else: - _LOGGER.warning("Can't restore addon %s", slug) + _LOGGER.warning("Can't restore addon %s", + snapshot.slug) if tasks: _LOGGER.info("Partial-Restore %s run %d tasks", @@ -308,13 +312,14 @@ class SnapshotsManager(CoreSysAttributes): # make sure homeassistant run agen await self._homeassistant.run() + except (OSError, ValueError, tarfile.TarError) as err: + _LOGGER.info("Partial-Restore %s error: %s", snapshot.slug, err) + return False + + else: _LOGGER.info("Partial-Restore %s done", snapshot.slug) return True - except (OSError, ValueError, tarfile.TarError) as err: - _LOGGER.info("Partial-Restore %s error: %s", slug, err) - return False - finally: self._scheduler.suspend = False self.lock.release() diff --git a/hassio/snapshots/snapshot.py b/hassio/snapshots/snapshot.py index b32c8475e..caaa2bbf6 100644 --- a/hassio/snapshots/snapshot.py +++ b/hassio/snapshots/snapshot.py @@ -244,12 +244,13 @@ class Snapshot(CoreSysAttributes): with tarfile.open(self.tar_file, "w:") as tar: tar.add(self._tmp.name, arcname=".") - if write_json_file(Path(self._tmp.name, "snapshot.json"), self._data): + try: + write_json_file(Path(self._tmp.name, "snapshot.json"), self._data) await self._loop.run_in_executor(None, _create_snapshot) - else: - _LOGGER.error("Can't write snapshot.json") - - self._tmp.cleanup() + except (OSError, json.JSONDecodeError) as err: + _LOGGER.error("Can't write snapshot: %s", err) + finally: + self._tmp.cleanup() async def import_addon(self, addon): """Add a addon into snapshot.""" @@ -280,7 +281,7 @@ class Snapshot(CoreSysAttributes): async def store_folders(self, folder_list=None): """Backup hassio data into snapshot.""" - folder_list = folder_list or ALL_FOLDERS + folder_list = set(folder_list or ALL_FOLDERS) def _folder_save(name): """Intenal function to snapshot a folder.""" @@ -293,8 +294,8 @@ class Snapshot(CoreSysAttributes): with tarfile.open(snapshot_tar, "w:gz", compresslevel=1) as tar_file: tar_file.add(origin_dir, arcname=".") - _LOGGER.info("Snapshot folder %s done", name) + _LOGGER.info("Snapshot folder %s done", name) self._data[ATTR_FOLDERS].append(name) except (tarfile.TarError, OSError) as err: _LOGGER.warning("Can't snapshot folder %s: %s", name, err) @@ -307,7 +308,7 @@ class Snapshot(CoreSysAttributes): async def restore_folders(self, folder_list=None): """Backup hassio data into snapshot.""" - folder_list = folder_list or ALL_FOLDERS + folder_list = set(folder_list or self.folders) def _folder_restore(name): """Intenal function to restore a folder.""" @@ -323,7 +324,7 @@ class Snapshot(CoreSysAttributes): _LOGGER.info("Restore folder %s", name) with tarfile.open(snapshot_tar, "r:gz") as tar_file: tar_file.extractall(path=origin_dir) - _LOGGER.info("Restore folder %s done", name) + _LOGGER.info("Restore folder %s done", name) except (tarfile.TarError, OSError) as err: _LOGGER.warning("Can't restore folder %s: %s", name, err) @@ -365,7 +366,7 @@ class Snapshot(CoreSysAttributes): self._homeassistant.api_password = self.homeassistant_password # save - self._homeassistant.save() + self._homeassistant.save_data() def store_repositories(self): """Store repository list into snapshot.""" diff --git a/hassio/snapshots/validate.py b/hassio/snapshots/validate.py index 5007179a8..d27cbde22 100644 --- a/hassio/snapshots/validate.py +++ b/hassio/snapshots/validate.py @@ -9,31 +9,42 @@ from ..const import ( ATTR_LAST_VERSION, FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL, SNAPSHOT_FULL, SNAPSHOT_PARTIAL) -from ..validate import NETWORK_PORT +from ..validate import NETWORK_PORT, REPOSITORIES, DOCKER_IMAGE ALL_FOLDERS = [FOLDER_HOMEASSISTANT, FOLDER_SHARE, FOLDER_ADDONS, FOLDER_SSL] + +def unique_addons(addons_list): + """Validate that a add-on is unique.""" + single = set([addon[ATTR_SLUG] for addon in addons_list]) + + if len(single) != len(addons_list): + raise vol.Invalid("Invalid addon list on snapshot!") + return addons_list + + # pylint: disable=no-value-for-parameter SCHEMA_SNAPSHOT = vol.Schema({ vol.Required(ATTR_SLUG): vol.Coerce(str), 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.Optional(ATTR_HOMEASSISTANT, default={}): vol.Schema({ + vol.Optional(ATTR_HOMEASSISTANT, default=dict): vol.Schema({ vol.Required(ATTR_VERSION): vol.Coerce(str), - vol.Optional(ATTR_IMAGE): vol.Coerce(str), - vol.Optional(ATTR_LAST_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(), vol.Optional(ATTR_SSL, default=False): vol.Boolean(), 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(), }, extra=vol.REMOVE_EXTRA), - vol.Optional(ATTR_FOLDERS, default=[]): [vol.In(ALL_FOLDERS)], - vol.Optional(ATTR_ADDONS, default=[]): [vol.Schema({ + vol.Optional(ATTR_FOLDERS, default=list): + vol.All([vol.In(ALL_FOLDERS)], vol.Unique()), + vol.Optional(ATTR_ADDONS, default=list): vol.All([vol.Schema({ vol.Required(ATTR_SLUG): vol.Coerce(str), vol.Required(ATTR_NAME): vol.Coerce(str), vol.Required(ATTR_VERSION): vol.Coerce(str), - }, extra=vol.REMOVE_EXTRA)], - vol.Optional(ATTR_REPOSITORIES, default=[]): [vol.Url()], + }, extra=vol.REMOVE_EXTRA)], unique_addons), + vol.Optional(ATTR_REPOSITORIES, default=list): REPOSITORIES, }, extra=vol.ALLOW_EXTRA) diff --git a/hassio/updater.py b/hassio/updater.py index 8a1c05d5f..168e2585e 100644 --- a/hassio/updater.py +++ b/hassio/updater.py @@ -89,4 +89,4 @@ class Updater(JsonConfig, CoreSysAttributes): # update versions self._data[ATTR_HOMEASSISTANT] = data.get('homeassistant') self._data[ATTR_HASSIO] = data.get('hassio') - self.save() + self.save_data() diff --git a/hassio/utils/json.py b/hassio/utils/json.py index 8f1e98115..b4cfded06 100644 --- a/hassio/utils/json.py +++ b/hassio/utils/json.py @@ -10,14 +10,9 @@ _LOGGER = logging.getLogger(__name__) def write_json_file(jsonfile, data): """Write a json file.""" - try: - json_str = json.dumps(data, indent=2) - with jsonfile.open('w') as conf_file: - conf_file.write(json_str) - except (OSError, json.JSONDecodeError): - return False - - return True + json_str = json.dumps(data, indent=2) + with jsonfile.open('w') as conf_file: + conf_file.write(json_str) def read_json_file(jsonfile): @@ -35,7 +30,10 @@ class JsonConfig(object): self._schema = schema self._data = {} - # init or load data + self.read_data() + + def read_data(self): + """Read json file & validate.""" if self._file.is_file(): try: self._data = read_json_file(self._file) @@ -43,27 +41,33 @@ class JsonConfig(object): _LOGGER.warning("Can't read %s", self._file) self._data = {} - # validate + # Validate try: self._data = self._schema(self._data) except vol.Invalid as ex: _LOGGER.error("Can't parse %s: %s", self._file, humanize_error(self._data, ex)) - # reset data to default + + # Reset data to default + _LOGGER.warning("Reset %s to default", self._file) self._data = self._schema({}) - def save(self): + def save_data(self): """Store data to config file.""" - # validate + # Validate try: self._data = self._schema(self._data) except vol.Invalid as ex: _LOGGER.error("Can't parse data: %s", humanize_error(self._data, ex)) - return False + + # Load last valid data + _LOGGER.warning("Reset %s to last version", self._file) + self.save_data() + return # write - if not write_json_file(self._file, self._data): - _LOGGER.error("Can't store config in %s", self._file) - return False - return True + try: + write_json_file(self._file, self._data) + except (OSError, json.JSONDecodeError) as err: + _LOGGER.error("Can't store config in %s: %s", self._file, err) diff --git a/hassio/validate.py b/hassio/validate.py index 71b843d81..2c81f1a40 100644 --- a/hassio/validate.py +++ b/hassio/validate.py @@ -14,6 +14,10 @@ from .const import ( NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)) ALSA_CHANNEL = vol.Match(r"\d+,\d+") WAIT_BOOT = vol.All(vol.Coerce(int), vol.Range(min=1, max=60)) +DOCKER_IMAGE = vol.Match(r"^[\w{}]+/[\-\w{}]+$") + +# pylint: disable=no-value-for-parameter +REPOSITORIES = vol.All([vol.Url()], vol.Unique()) def validate_timezone(timezone): @@ -62,7 +66,7 @@ SCHEMA_HASS_CONFIG = vol.Schema({ vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): vol.Match(r"^[0-9a-f]{32}$"), vol.Optional(ATTR_BOOT, default=True): vol.Boolean(), - vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Coerce(str), + vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE, vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str), vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT, vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)), @@ -85,7 +89,7 @@ SCHEMA_HASSIO_CONFIG = vol.Schema({ vol.Optional(ATTR_LAST_BOOT): vol.Coerce(str), vol.Optional(ATTR_ADDONS_CUSTOM_LIST, default=[ "https://github.com/hassio-addons/repository", - ]): [vol.Url()], + ]): REPOSITORIES, vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL, vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL, vol.Optional(ATTR_WAIT_BOOT, default=5): WAIT_BOOT, From 0f2c333484640e7bb8f8f6b780880e26d6871327 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 18 Jan 2018 23:36:46 +0100 Subject: [PATCH 5/5] Update Hass.io to version 0.83 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 5376f5e85..9fe3c7c88 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "hassio": "0.82", + "hassio": "0.83", "homeassistant": "0.61.1", "resinos": "1.1", "resinhup": "0.3",