From dadb72aca8449de83df2996bd8450852644022f9 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 28 Apr 2017 02:09:21 +0200 Subject: [PATCH 01/28] Pump version 0.19 --- hassio/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/const.py b/hassio/const.py index cc509649d..ad2f812fe 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -1,5 +1,5 @@ """Const file for HassIO.""" -HASSIO_VERSION = '0.18' +HASSIO_VERSION = '0.19' URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/' 'hassio/master/version.json') From 4d8dbdb4862e69eaa61194d8d74808fb636c8233 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 28 Apr 2017 10:43:17 +0200 Subject: [PATCH 02/28] Update README.md --- README.md | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 1890ec8f9..9b0b007da 100644 --- a/README.md +++ b/README.md @@ -23,34 +23,7 @@ http: ssl_key: /ssl/privkey.pem ``` -# Hardware Image -The image is based on ResinOS and Yocto Linux. It comes with the HassIO supervisor pre-installed. This includes support to update the supervisor over the air. After flashing your host OS will not require any more maintenance! The image does not include Home Assistant, instead it will downloaded when the image boots up for the first time. - -Download can be found here: https://drive.google.com/drive/folders/0B2o1Uz6l1wVNbFJnb2gwNXJja28?usp=sharing - -After extracting the archive, flash it to a drive using [Etcher](https://etcher.io/). - -## History -- **0.1**: First techpreview with dumy supervisor (ResinOS 2.0.0-RC5) -- **0.2**: Fix some bugs and update it to HassIO 0.2 -- **0.3**: Update HostControl and feature for HassIO 0.3 (ResinOS 2.0.0 / need reflash) -- **0.4**: Update HostControl and bring resinos OTA (resinhub) back (ResinOS 2.0.0-rev3) - -## Configuring the image -You can configure the WiFi network that the image should connect to after flashing using [`resin-device-toolbox`](https://resinos.io/docs/raspberrypi3/gettingstarted/#install-resin-device-toolbox). - -## Developer access to ResinOS host -Create an `authorized_keys` file in the boot partition of your SD card with your public key. After a boot it, you can acces your device as root over ssh on port 22222. - -## Troubleshooting - -Read logoutput from supervisor: -```bash -journalctl -f -u resin-supervisor.service -docker logs homeassistant -``` - ## Install on a own System -We have a installer to install HassIO on own linux device without our hardware image: -https://github.com/home-assistant/hassio-build/tree/master/install +- Generic Linux installation: https://github.com/home-assistant/hassio-build/tree/master/install +- Hardware Images: https://github.com/home-assistant/hassio-build/blob/master/meta-hassio/ From 3a791bace6b6e224a8b4359ef3f6fd5bc4125e08 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 28 Apr 2017 22:54:09 +0200 Subject: [PATCH 03/28] Add schema --- README.md | 2 ++ misc/hassio.png | Bin 0 -> 42612 bytes misc/hassio.xml | 1 + 3 files changed, 3 insertions(+) create mode 100644 misc/hassio.png create mode 100644 misc/hassio.xml diff --git a/README.md b/README.md index 9b0b007da..a75f5d98e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ First private cloud solution for home automation. It is a docker image (supervisor) they manage HomeAssistant docker and give a interface to control itself over UI. It have a own eco system with addons to extend the functionality in a easy way. +[[https://raw.githubusercontent.com/home-assistant/hassio/dev/misc/hassio.png]] + [HassIO-Addons](https://github.com/home-assistant/hassio-addons) | [HassIO-Build](https://github.com/home-assistant/hassio-build) **HassIO is at the moment on development and not ready to use productive!** diff --git a/misc/hassio.png b/misc/hassio.png new file mode 100644 index 0000000000000000000000000000000000000000..22c01312534277c4d02c483e171091ece343e59a GIT binary patch literal 42612 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 \ No newline at end of file From cabe5b143ff550ff536a011012ca26d45fd13f58 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 28 Apr 2017 22:56:32 +0200 Subject: [PATCH 04/28] update reamde --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a75f5d98e..4d59b7e5f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ First private cloud solution for home automation. It is a docker image (supervisor) they manage HomeAssistant docker and give a interface to control itself over UI. It have a own eco system with addons to extend the functionality in a easy way. -[[https://raw.githubusercontent.com/home-assistant/hassio/dev/misc/hassio.png]] +![](misc/hassio.png?raw=true) [HassIO-Addons](https://github.com/home-assistant/hassio-addons) | [HassIO-Build](https://github.com/home-assistant/hassio-build) From c0aab8497f1f933f613043d8ea52340ada9b4449 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 28 Apr 2017 23:06:33 +0200 Subject: [PATCH 05/28] Use now dev for beta_upstream --- hassio/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/const.py b/hassio/const.py index ad2f812fe..02c917ff3 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -4,7 +4,7 @@ HASSIO_VERSION = '0.19' URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/' 'hassio/master/version.json') URL_HASSIO_VERSION_BETA = ('https://raw.githubusercontent.com/home-assistant/' - 'hassio/master/version_beta.json') + 'hassio/dev/version.json') URL_HASSIO_ADDONS = 'https://github.com/home-assistant/hassio-addons' From 7a4ed4029cd2a36ba5e060c0d3be31f45415deb2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 28 Apr 2017 23:40:11 +0200 Subject: [PATCH 06/28] Is not needed anymore --- version_beta.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 version_beta.json diff --git a/version_beta.json b/version_beta.json deleted file mode 100644 index afaadc0b6..000000000 --- a/version_beta.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "hassio": "0.18", - "homeassistant": "0.43.2", - "resinos": "0.6", - "resinhup": "0.1", - "generic": "0.3" -} From 6623ec9bbc4cf41965cb24a828a62a806f34cc3e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 30 Apr 2017 01:47:20 +0200 Subject: [PATCH 07/28] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 4d59b7e5f..dba550240 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ It is a docker image (supervisor) they manage HomeAssistant docker and give a in ## Feature in progress - Backup/Restore -- MQTT addon - DHCP-Server addon # HomeAssistant From 85ffba90b2973bfacfaa89cd77961d8d4a2457af Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 30 Apr 2017 01:57:03 +0200 Subject: [PATCH 08/28] Fix severals things with json (#22) --- hassio/tools.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hassio/tools.py b/hassio/tools.py index 3ee864de9..a012d3361 100644 --- a/hassio/tools.py +++ b/hassio/tools.py @@ -77,9 +77,10 @@ def get_local_ip(loop): def write_json_file(jsonfile, data): """Write a json file.""" try: + json_str = json.dumps(data, indent=2) with open(jsonfile, 'w') as conf_file: - conf_file.write(json.dumps(data)) - except OSError: + conf_file.write(json_str) + except (OSError, json.JSONDecodeError): return False return True From a61311e928c14916e772bc588b21171989d49e8b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 30 Apr 2017 09:46:19 +0200 Subject: [PATCH 09/28] Don't store homeassistant image from env to config (#23) --- hassio/config.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/hassio/config.py b/hassio/config.py index f40d5afd1..b176ea6a5 100644 --- a/hassio/config.py +++ b/hassio/config.py @@ -13,7 +13,6 @@ from .tools import ( _LOGGER = logging.getLogger(__name__) HOMEASSISTANT_CONFIG = "{}/homeassistant" -HOMEASSISTANT_IMAGE = 'homeassistant_image' HOMEASSISTANT_LAST = 'homeassistant_last' HASSIO_SSL = "{}/ssl" @@ -32,14 +31,8 @@ UPSTREAM_BETA = 'upstream_beta' API_ENDPOINT = 'api_endpoint' -def hass_image(): - """Return HomeAssistant docker Image.""" - return os.environ.get('HOMEASSISTANT_REPOSITORY') - - # pylint: disable=no-value-for-parameter SCHEMA_CONFIG = vol.Schema({ - vol.Optional(HOMEASSISTANT_IMAGE, default=hass_image): vol.Coerce(str), vol.Optional(UPSTREAM_BETA, default=False): vol.Boolean(), vol.Optional(API_ENDPOINT): vol.Coerce(str), vol.Optional(HOMEASSISTANT_LAST): vol.Coerce(str), @@ -142,7 +135,7 @@ class CoreConfig(Config): @property def homeassistant_image(self): """Return docker homeassistant repository.""" - return self._data.get(HOMEASSISTANT_IMAGE) + return os.environ['HOMEASSISTANT_REPOSITORY'] @property def last_homeassistant(self): From 2e168d089cbd9120e64b64fbf06fcaaa14e5696d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 30 Apr 2017 19:03:26 +0200 Subject: [PATCH 10/28] Fix validate required arguments (#25) --- hassio/addons/validate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index a8527c808..760aa6f18 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -72,6 +72,10 @@ def validate_options(raw_schema): def _single_validate(typ, value): """Validate a single element.""" try: + # if required argument + if value is None: + raise vol.Invalid("A required argument is not set!") + if typ == V_STR: return str(value) elif typ == V_INT: @@ -86,7 +90,7 @@ def _single_validate(typ, value): return vol.Url()(value) raise vol.Invalid("Fatal error for {}.".format(value)) - except TypeError: + except ValueError: raise vol.Invalid( "Type {} error for {}.".format(typ, value)) from None From c76408e4e8d339649c151c295ef6e9c3f06601b7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 30 Apr 2017 22:15:03 +0200 Subject: [PATCH 11/28] Move list of all available addons to own api call (#24) * Move list of all available addons to own api call * fix lint * fix lint * fix style --- API.md | 21 ++++++++++++++++++++- hassio/api/__init__.py | 2 ++ hassio/api/supervisor.py | 7 ++++++- hassio/api/util.py | 2 +- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/API.md b/API.md index 1077ee721..96eaf1ab8 100644 --- a/API.md +++ b/API.md @@ -26,6 +26,8 @@ On success - GET `/supervisor/info` +The addons from `addons` are only installed one. + ```json { "version": "INSTALL_VERSION", @@ -36,7 +38,7 @@ On success "name": "xy bla", "slug": "xy", "version": "LAST_VERSION", - "installed": "none|INSTALL_VERSION", + "installed": "INSTALL_VERSION", "dedicated": "bool", "description": "description" } @@ -47,6 +49,23 @@ On success } ``` +- GET `/supervisor/addons` + +Get all available addons + +```json +[ + { + "name": "xy bla", + "slug": "xy", + "version": "LAST_VERSION", + "installed": "none|INSTALL_VERSION", + "dedicated": "bool", + "description": "description" + } +] +``` + - POST `/supervisor/update` Optional: ```json diff --git a/hassio/api/__init__.py b/hassio/api/__init__.py index c3460aa60..2ee224ab9 100644 --- a/hassio/api/__init__.py +++ b/hassio/api/__init__.py @@ -48,6 +48,8 @@ class RestAPI(object): self.webapp.router.add_get('/supervisor/ping', api_supervisor.ping) self.webapp.router.add_get('/supervisor/info', api_supervisor.info) + self.webapp.router.add_get( + '/supervisor/addons', api_supervisor.available_addons) self.webapp.router.add_post( '/supervisor/update', api_supervisor.update) self.webapp.router.add_post( diff --git a/hassio/api/supervisor.py b/hassio/api/supervisor.py index 3c09cc99b..c80a92653 100644 --- a/hassio/api/supervisor.py +++ b/hassio/api/supervisor.py @@ -46,9 +46,14 @@ class APISupervisor(object): ATTR_LAST_VERSION: self.config.last_hassio, ATTR_BETA_CHANNEL: self.config.upstream_beta, ATTR_ADDONS: self.addons.list_api, - ATTR_ADDONS_REPOSITORIES: list(self.config.addons_repositories), + ATTR_ADDONS_REPOSITORIES: self.config.addons_repositories, } + @api_process + async def available_addons(self, request): + """Return information for all available addons.""" + return self.addons.list_api + @api_process async def options(self, request): """Set supervisor options.""" diff --git a/hassio/api/util.py b/hassio/api/util.py index 5204e9079..cb5161673 100644 --- a/hassio/api/util.py +++ b/hassio/api/util.py @@ -30,7 +30,7 @@ def api_process(method): except RuntimeError as err: return api_return_error(message=str(err)) - if isinstance(answer, dict): + if isinstance(answer, (dict, list)): return api_return_ok(data=answer) elif answer: return api_return_ok() From ff640c598dbeeae71834e95f398862e8d5e83fab Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 00:02:55 +0200 Subject: [PATCH 12/28] Support for repository store. (#26) * Support for repository store. * Fix api * part 1 of restruct and migrate pathlib * Migrate p2 * fix lint / cleanups * fix lint p2 * fix lint p3 --- API.md | 37 +++++--- hassio/addons/__init__.py | 6 +- hassio/addons/data.py | 169 +++++++++++++++++++++++++---------- hassio/addons/git.py | 15 ++-- hassio/addons/util.py | 21 ++--- hassio/addons/validate.py | 10 ++- hassio/api/supervisor.py | 18 ++-- hassio/api/util.py | 2 +- hassio/bootstrap.py | 35 +++++--- hassio/config.py | 73 ++++++++------- hassio/const.py | 18 ++-- hassio/dock/addon.py | 10 +-- hassio/dock/homeassistant.py | 4 +- hassio/tools.py | 4 +- 14 files changed, 269 insertions(+), 153 deletions(-) diff --git a/API.md b/API.md index 96eaf1ab8..90b3244c9 100644 --- a/API.md +++ b/API.md @@ -37,9 +37,9 @@ The addons from `addons` are only installed one. { "name": "xy bla", "slug": "xy", - "version": "LAST_VERSION", - "installed": "INSTALL_VERSION", - "dedicated": "bool", + "version": "INSTALL_VERSION", + "last_version": "VERSION_FOR_UPDATE", + "detached": "bool", "description": "description" } ], @@ -54,16 +54,27 @@ The addons from `addons` are only installed one. Get all available addons ```json -[ - { - "name": "xy bla", - "slug": "xy", - "version": "LAST_VERSION", - "installed": "none|INSTALL_VERSION", - "dedicated": "bool", - "description": "description" - } -] +{ + "addons": [ + { + "name": "xy bla", + "slug": "xy", + "repository": "12345678|null", + "version": "LAST_VERSION", + "installed": "none|INSTALL_VERSION", + "detached": "bool", + "description": "description" + } + ], + "repositories": [ + { + "slug": "12345678", + "name": "Repitory Name", + "url": "WEBSITE", + "maintainer": "BLA BLU " + } + ] +} ``` - POST `/supervisor/update` diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index 5536df866..dd315ec42 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -51,7 +51,7 @@ class AddonManager(AddonsData): self.config, self.loop, self.dock, self, addon) await self.dockers[addon].attach() - async def add_custom_repository(self, url): + async def add_git_repository(self, url): """Add a new custom repository.""" if url in self.config.addons_repositories: _LOGGER.warning("Repository already exists %s", url) @@ -67,7 +67,7 @@ class AddonManager(AddonsData): self.repositories.append(repo) return True - def drop_custom_repository(self, url): + def drop_git_repository(self, url): """Remove a custom repository.""" for repo in self.repositories: if repo.url == url: @@ -91,7 +91,7 @@ class AddonManager(AddonsData): self.merge_update_config() # remove stalled addons - for addon in self.list_removed: + for addon in self.list_detached: _LOGGER.warning("Dedicated addon '%s' found!", addon) async def auto_boot(self, start_type): diff --git a/hassio/addons/data.py b/hassio/addons/data.py index ae4286aeb..a3354eaa8 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -1,26 +1,30 @@ """Init file for HassIO addons.""" import copy import logging -import glob +from pathlib import Path, PurePath import voluptuous as vol from voluptuous.humanize import humanize_error from .util import extract_hash_from_path -from .validate import validate_options, SCHEMA_ADDON_CONFIG +from .validate import ( + validate_options, SCHEMA_ADDON_CONFIG, SCHEMA_REPOSITORY_CONFIG) from ..const import ( FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_STARTUP, ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, BOOT_AUTO, - DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA, ATTR_IMAGE, ATTR_DEDICATED, - MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP) + DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA, ATTR_IMAGE, ATTR_DETACHED, + MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, ATTR_REPOSITORY, ATTR_URL, + ATTR_MAINTAINER, ATTR_LAST_VERSION) from ..config import Config from ..tools import read_json_file, write_json_file _LOGGER = logging.getLogger(__name__) -ADDONS_REPO_PATTERN = "{}/**/config.json" -SYSTEM = "system" -USER = "user" +SYSTEM = 'system' +USER = 'user' + +REPOSITORY_CORE = 'core' +REPOSITORY_LOCAL = 'local' class AddonsData(Config): @@ -32,7 +36,8 @@ class AddonsData(Config): self.config = config self._system_data = self._data.get(SYSTEM, {}) self._user_data = self._data.get(USER, {}) - self._current_data = {} + self._addons_cache = {} + self._repositories_data = {} self.arch = None def save(self): @@ -45,29 +50,62 @@ class AddonsData(Config): def read_data_from_repositories(self): """Read data from addons repository.""" - self._current_data = {} + self._addons_cache = {} + self._repositories_data = {} - self._read_addons_folder(self.config.path_addons_repo) - self._read_addons_folder(self.config.path_addons_custom, custom=True) + # read core repository + self._read_addons_folder( + self.config.path_addons_core, REPOSITORY_CORE) - def _read_addons_folder(self, folder, custom=False): + # read local repository + self._read_addons_folder( + self.config.path_addons_local, REPOSITORY_LOCAL) + + # read custom git repositories + for repository_dir in self.config.path_addons_git.glob("/*/"): + self._read_git_repository(repository_dir) + + def _read_git_repository(self, path): + """Process a custom repository folder.""" + slug = extract_hash_from_path(path) + repository_info = {ATTR_SLUG: slug} + + # exists repository json + repository_file = Path(path, "repository.json") + try: + repository_info.update(SCHEMA_REPOSITORY_CONFIG( + read_json_file(repository_file) + )) + + except OSError: + _LOGGER.warning("Can't read repository information from %s", + repository_file) + return + + except vol.Invalid: + _LOGGER.warning("Repository parse error %s", repository_file) + return + + # process data + self._repositories_data[slug] = repository_info + self._read_addons_folder(path, slug) + + def _read_addons_folder(self, path, repository): """Read data from addons folder.""" - pattern = ADDONS_REPO_PATTERN.format(folder) - - for addon in glob.iglob(pattern, recursive=True): + for addon in path.glob("**/*.config.json"): try: addon_config = read_json_file(addon) + # validate addon_config = SCHEMA_ADDON_CONFIG(addon_config) - if custom: - addon_slug = "{}_{}".format( - extract_hash_from_path(folder, addon), - addon_config[ATTR_SLUG], - ) - else: - addon_slug = addon_config[ATTR_SLUG] - self._current_data[addon_slug] = addon_config + # Generate slug + addon_slug = "{}_{}".format( + repository, addon_config[ATTR_SLUG]) + + # store + addon_config[ATTR_REPOSITORY] = repository + self._addons_cache[addon_slug] = addon_config except OSError: _LOGGER.warning("Can't read %s", addon) @@ -84,14 +122,14 @@ class AddonsData(Config): have_change = False for addon, data in self._system_data.items(): - # dedicated - if addon not in self._current_data: + # detached + if addon not in self._addons_cache: continue - current = self._current_data[addon] - if data[ATTR_VERSION] == current[ATTR_VERSION]: - if data != current: - self._system_data[addon] = copy.deepcopy(current) + cache = self._addons_cache[addon] + if data[ATTR_VERSION] == cache[ATTR_VERSION]: + if data != cache: + self._system_data[addon] = copy.deepcopy(cache) have_change = True if have_change: @@ -103,11 +141,11 @@ class AddonsData(Config): return set(self._system_data.keys()) @property - def list_api(self): + def list_all_api(self): """Return a list of available addons for api.""" data = [] - all_addons = {**self._system_data, **self._current_data} - dedicated = self.list_removed + all_addons = {**self._system_data, **self._addons_cache} + detached = self.list_detached for addon, values in all_addons.items(): i_version = self._user_data.get(addon, {}).get(ATTR_VERSION) @@ -118,7 +156,30 @@ class AddonsData(Config): ATTR_DESCRIPTON: values[ATTR_DESCRIPTON], ATTR_VERSION: values[ATTR_VERSION], ATTR_INSTALLED: i_version, - ATTR_DEDICATED: addon in dedicated, + ATTR_DETACHED: addon in detached, + ATTR_REPOSITORY: values[ATTR_REPOSITORY], + }) + + return data + + @property + def list_installed_api(self): + """Return a list of available addons for api.""" + data = [] + all_addons = {**self._system_data, **self._addons_cache} + detached = self.list_detached + + for addon, values in all_addons.items(): + i_version = self._user_data.get(addon, {}).get(ATTR_VERSION) + + data.append({ + ATTR_NAME: values[ATTR_NAME], + ATTR_SLUG: addon, + ATTR_DESCRIPTON: values[ATTR_DESCRIPTON], + ATTR_VERSION: values[ATTR_VERSION], + ATTR_LAST_VERSION: values[ATTR_VERSION], + ATTR_INSTALLED: i_version, + ATTR_DETACHED: addon in detached }) return data @@ -140,18 +201,33 @@ class AddonsData(Config): return addon_list @property - def list_removed(self): + def list_detached(self): """Return local addons they not support from repo.""" addon_list = set() for addon in self._system_data.keys(): - if addon not in self._current_data: + if addon not in self._addons_cache: addon_list.add(addon) return addon_list + @property + def list_repositories_api(self): + """Return list of addon repositories.""" + repositories = [] + + for slug, data in self._repositories_data.items(): + repositories.append({ + ATTR_SLUG: slug, + ATTR_NAME: data[ATTR_NAME], + ATTR_URL: data.get(ATTR_URL), + ATTR_MAINTAINER: data.get(ATTR_MAINTAINER), + }) + + return repositories + def exists_addon(self, addon): """Return True if a addon exists.""" - return addon in self._current_data or addon in self._system_data + return addon in self._addons_cache or addon in self._system_data def is_installed(self, addon): """Return True if a addon is installed.""" @@ -163,7 +239,7 @@ class AddonsData(Config): def set_addon_install(self, addon, version): """Set addon as installed.""" - self._system_data[addon] = copy.deepcopy(self._current_data[addon]) + self._system_data[addon] = copy.deepcopy(self._addons_cache[addon]) self._user_data[addon] = { ATTR_OPTIONS: {}, ATTR_VERSION: version, @@ -178,7 +254,7 @@ class AddonsData(Config): def set_addon_update(self, addon, version): """Update version of addon.""" - self._system_data[addon] = copy.deepcopy(self._current_data[addon]) + self._system_data[addon] = copy.deepcopy(self._addons_cache[addon]) self._user_data[addon][ATTR_VERSION] = version self.save() @@ -216,9 +292,9 @@ class AddonsData(Config): def get_last_version(self, addon): """Return version of addon.""" - if addon not in self._current_data: + if addon not in self._addons_cache: return self.version_installed(addon) - return self._current_data[addon][ATTR_VERSION] + return self._addons_cache[addon][ATTR_VERSION] def get_ports(self, addon): """Return ports of addon.""" @@ -226,10 +302,11 @@ class AddonsData(Config): def get_image(self, addon): """Return image name of addon.""" - addon_data = self._system_data.get(addon, self._current_data[addon]) + addon_data = self._system_data.get(addon, self._addons_cache[addon]) if ATTR_IMAGE not in addon_data: - return "{}/{}-addon-{}".format(DOCKER_REPO, self.arch, addon) + return "{}/{}-addon-{}".format( + DOCKER_REPO, self.arch, addon_data[ATTR_SLUG]) return addon_data[ATTR_IMAGE].format(arch=self.arch) @@ -251,15 +328,15 @@ class AddonsData(Config): def path_data(self, addon): """Return addon data path inside supervisor.""" - return "{}/{}".format(self.config.path_addons_data, addon) + return Path(self.config.path_addons_data, addon) - def path_data_docker(self, addon): + def path_extern_data(self, addon): """Return addon data path external for docker.""" - return "{}/{}".format(self.config.path_addons_data_docker, addon) + return PurePath(self.config.path_extern_addons_data, addon) def path_addon_options(self, addon): """Return path to addons options.""" - return "{}/options.json".format(self.path_data(addon)) + return Path(self.path_data, addon, "options.json") def write_addon_options(self, addon): """Return True if addon options is written to data.""" diff --git a/hassio/addons/git.py b/hassio/addons/git.py index 69d4df698..37b519fd5 100644 --- a/hassio/addons/git.py +++ b/hassio/addons/git.py @@ -1,7 +1,7 @@ """Init file for HassIO addons git.""" import asyncio import logging -import os +from pathlib import Path import shutil import git @@ -26,14 +26,14 @@ class AddonsRepo(object): async def load(self): """Init git addon repo.""" - if not os.path.isdir(self.path): + if not self.path.is_dir(): return await self.clone() async with self._lock: try: _LOGGER.info("Load addon %s repository", self.path) self.repo = await self.loop.run_in_executor( - None, git.Repo, self.path) + None, git.Repo, str(self.path)) except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err: _LOGGER.error("Can't load %s repo: %s.", self.path, err) @@ -47,7 +47,7 @@ class AddonsRepo(object): try: _LOGGER.info("Clone addon %s repository", self.url) self.repo = await self.loop.run_in_executor( - None, git.Repo.clone_from, self.url, self.path) + None, git.Repo.clone_from, self.url, str(self.path)) except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err: _LOGGER.error("Can't clone %s repo: %s.", self.url, err) @@ -88,18 +88,17 @@ class AddonsRepoCustom(AddonsRepo): def __init__(self, config, loop, url): """Initialize git hassio addon repository.""" - path = os.path.join( - config.path_addons_custom, get_hash_from_repository(url)) + path = Path(config.path_addons_git, get_hash_from_repository(url)) super().__init__(config, loop, path, url) def remove(self): """Remove a custom addon.""" - if os.path.isdir(self.path): + if self.path.is_dir(): _LOGGER.info("Remove custom addon repository %s", self.url) def log_err(funct, path, _): """Log error.""" _LOGGER.warning("Can't remove %s", path) - shutil.rmtree(self.path, onerror=log_err) + shutil.rmtree(str(self.path), onerror=log_err) diff --git a/hassio/addons/util.py b/hassio/addons/util.py index e33afdecf..152c28866 100644 --- a/hassio/addons/util.py +++ b/hassio/addons/util.py @@ -1,28 +1,21 @@ """Util addons functions.""" import hashlib -import pathlib import re RE_SLUGIFY = re.compile(r'[^a-z0-9_]+') RE_SHA1 = re.compile(r"[a-f0-9]{8}") -def get_hash_from_repository(repo): +def get_hash_from_repository(name): """Generate a hash from repository.""" - key = repo.lower().encode() + key = name.lower().encode() return hashlib.sha1(key).hexdigest()[:8] -def extract_hash_from_path(base_path, options_path): +def extract_hash_from_path(path): """Extract repo id from path.""" - base_dir = pathlib.PurePosixPath(base_path).parts[-1] + repo_dir = path.parts[-1] - dirlist = iter(pathlib.PurePosixPath(options_path).parts) - for obj in dirlist: - if obj != base_dir: - continue - - repo_dir = next(dirlist) - if not RE_SHA1.match(repo_dir): - return get_hash_from_repository(repo_dir) - return repo_dir + if not RE_SHA1.match(repo_dir): + return get_hash_from_repository(repo_dir) + return repo_dir diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 760aa6f18..b01820e7d 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -5,7 +5,7 @@ from ..const import ( ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_STARTUP, ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, STARTUP_ONCE, STARTUP_AFTER, STARTUP_BEFORE, BOOT_AUTO, BOOT_MANUAL, ATTR_SCHEMA, ATTR_IMAGE, MAP_SSL, - MAP_CONFIG, MAP_ADDONS, MAP_BACKUP) + MAP_CONFIG, MAP_ADDONS, MAP_BACKUP, ATTR_URL, ATTR_MAINTAINER) V_STR = 'str' V_INT = 'int' @@ -40,6 +40,14 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ }, extra=vol.ALLOW_EXTRA) +# pylint: disable=no-value-for-parameter +SCHEMA_REPOSITORY_CONFIG = vol.Schema({ + vol.Required(ATTR_NAME): vol.Coerce(str), + vol.Optional(ATTR_URL): vol.Url(), + vol.Optional(ATTR_MAINTAINER): vol.Coerce(str), +}, extra=vol.ALLOW_EXTRA) + + def validate_options(raw_schema): """Validate schema.""" def validate(struct): diff --git a/hassio/api/supervisor.py b/hassio/api/supervisor.py index c80a92653..8fc841af3 100644 --- a/hassio/api/supervisor.py +++ b/hassio/api/supervisor.py @@ -7,7 +7,7 @@ import voluptuous as vol from .util import api_process, api_process_raw, api_validate from ..const import ( ATTR_ADDONS, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_BETA_CHANNEL, - HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES) + HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES, ATTR_REPOSITORIES) _LOGGER = logging.getLogger(__name__) @@ -45,14 +45,17 @@ class APISupervisor(object): ATTR_VERSION: HASSIO_VERSION, ATTR_LAST_VERSION: self.config.last_hassio, ATTR_BETA_CHANNEL: self.config.upstream_beta, - ATTR_ADDONS: self.addons.list_api, + ATTR_ADDONS: self.addons.list_installed_api, ATTR_ADDONS_REPOSITORIES: self.config.addons_repositories, } @api_process async def available_addons(self, request): """Return information for all available addons.""" - return self.addons.list_api + return { + ATTR_ADDONS: self.addons.list_all_api, + ATTR_REPOSITORIES: self.addons.list_repositories_api, + } @api_process async def options(self, request): @@ -67,12 +70,15 @@ class APISupervisor(object): old = set(self.config.addons_repositories) # add new repositories - for url in set(new - old): - await self.addons.add_custom_repository(url) + tasks = [self.addons.add_git_repository(url) for url in + set(new - old)] + if tasks: + await asyncio.shield( + asyncio.wait(tasks, loop=self.loop), loop=self.loop) # remove old repositories for url in set(old - new): - self.addons.drop_custom_repository(url) + self.addons.drop_git_repository(url) return True diff --git a/hassio/api/util.py b/hassio/api/util.py index cb5161673..5204e9079 100644 --- a/hassio/api/util.py +++ b/hassio/api/util.py @@ -30,7 +30,7 @@ def api_process(method): except RuntimeError as err: return api_return_error(message=str(err)) - if isinstance(answer, (dict, list)): + if isinstance(answer, dict): return api_return_ok(data=answer) elif answer: return api_return_ok() diff --git a/hassio/bootstrap.py b/hassio/bootstrap.py index 261ec41c9..b55b63f59 100644 --- a/hassio/bootstrap.py +++ b/hassio/bootstrap.py @@ -1,7 +1,6 @@ """Bootstrap HassIO.""" import logging import os -import stat import signal from colorlog import ColoredFormatter @@ -17,26 +16,37 @@ def initialize_system_data(websession): config = CoreConfig(websession) # homeassistant config folder - if not os.path.isdir(config.path_config): + if not config.path_config.is_dir(): _LOGGER.info( "Create Home-Assistant config folder %s", config.path_config) - os.mkdir(config.path_config) + config.path_config.mkdir() # homeassistant ssl folder - if not os.path.isdir(config.path_ssl): + if not config.path_ssl.is_dir(): _LOGGER.info("Create Home-Assistant ssl folder %s", config.path_ssl) - os.mkdir(config.path_ssl) + config.path_ssl.mkdir() # homeassistant addon data folder - if not os.path.isdir(config.path_addons_data): + if not config.path_addons_data.is_dir(): _LOGGER.info("Create Home-Assistant addon data folder %s", config.path_addons_data) - os.mkdir(config.path_addons_data) + config.path_addons_data.mkdir(parents=True) - if not os.path.isdir(config.path_addons_custom): - _LOGGER.info("Create Home-Assistant addon custom folder %s", - config.path_addons_custom) - os.mkdir(config.path_addons_custom) + if not config.path_addons_local.is_dir(): + _LOGGER.info("Create Home-Assistant addon local repository folder %s", + config.path_addons_local) + config.path_addons_local.mkdir(parents=True) + + if not config.path_addons_git.is_dir(): + _LOGGER.info("Create Home-Assistant addon git repositories folder %s", + config.path_addons_git) + config.path_addons_git.mkdir(parents=True) + + # homeassistant backup folder + if not config.path_backup.is_dir(): + _LOGGER.info("Create Home-Assistant backup folder %s", + config.path_backup) + config.path_backup.mkdir() return config @@ -76,8 +86,7 @@ def check_environment(): _LOGGER.fatal("Can't find %s in env!", key) return False - mode = os.stat(SOCKET_DOCKER)[stat.ST_MODE] - if not stat.S_ISSOCK(mode): + if not SOCKET_DOCKER.is_socket(): _LOGGER.fatal("Can't find docker socket!") return False diff --git a/hassio/config.py b/hassio/config.py index b176ea6a5..2e7b8f8a3 100644 --- a/hassio/config.py +++ b/hassio/config.py @@ -2,6 +2,7 @@ import logging import json import os +from pathlib import Path, PurePath import voluptuous as vol from voluptuous.humanize import humanize_error @@ -12,19 +13,20 @@ from .tools import ( _LOGGER = logging.getLogger(__name__) -HOMEASSISTANT_CONFIG = "{}/homeassistant" +HOMEASSISTANT_CONFIG = PurePath("homeassistant") HOMEASSISTANT_LAST = 'homeassistant_last' -HASSIO_SSL = "{}/ssl" +HASSIO_SSL = PurePath("ssl") HASSIO_LAST = 'hassio_last' HASSIO_CLEANUP = 'hassio_cleanup' -ADDONS_REPO = "{}/addons" -ADDONS_DATA = "{}/addons_data" -ADDONS_CUSTOM = "{}/addons_custom" +ADDONS_CORE = PurePath("addons/core") +ADDONS_LOCAL = PurePath("addons/local") +ADDONS_GIT = PurePath("addons/git") +ADDONS_DATA = PurePath("addons/data") ADDONS_CUSTOM_LIST = 'addons_custom_list' -BACKUP_DATA = "{}/backup" +BACKUP_DATA = PurePath("backup") UPSTREAM_BETA = 'upstream_beta' @@ -47,21 +49,21 @@ class Config(object): def __init__(self, config_file): """Initialize config object.""" - self._filename = config_file + self._file = config_file self._data = {} # init or load data - if os.path.isfile(self._filename): + if self._file.is_file(): try: - self._data = read_json_file(self._filename) + self._data = read_json_file(self._file) except (OSError, json.JSONDecodeError): - _LOGGER.warning("Can't read %s", self._filename) + _LOGGER.warning("Can't read %s", self._file) self._data = {} def save(self): """Store data to config file.""" - if not write_json_file(self._filename, self._data): - _LOGGER.error("Can't store config in %s", self._filename) + if not write_json_file(self._file, self._data): + _LOGGER.error("Can't store config in %s", self._file) return False return True @@ -148,64 +150,69 @@ class CoreConfig(Config): return self._data.get(HASSIO_LAST) @property - def path_hassio_docker(self): + def path_extern_hassio(self): """Return hassio data path extern for docker.""" - return os.environ['SUPERVISOR_SHARE'] + return PurePath(os.environ['SUPERVISOR_SHARE']) @property - def path_config_docker(self): + def path_extern_config(self): """Return config path extern for docker.""" - return HOMEASSISTANT_CONFIG.format(self.path_hassio_docker) + return str(PurePath(self.path_extern_hassio, HOMEASSISTANT_CONFIG)) @property def path_config(self): """Return config path inside supervisor.""" - return HOMEASSISTANT_CONFIG.format(HASSIO_SHARE) + return Path(HASSIO_SHARE, HOMEASSISTANT_CONFIG) @property - def path_ssl_docker(self): + def path_extern_ssl(self): """Return SSL path extern for docker.""" - return HASSIO_SSL.format(self.path_hassio_docker) + return str(PurePath(self.path_extern_hassio, HASSIO_SSL)) @property def path_ssl(self): """Return SSL path inside supervisor.""" - return HASSIO_SSL.format(HASSIO_SHARE) + return Path(HASSIO_SHARE, HASSIO_SSL) @property - def path_addons_repo(self): - """Return git repo path for addons.""" - return ADDONS_REPO.format(HASSIO_SHARE) + def path_addons_core(self): + """Return git path for core addons.""" + return Path(HASSIO_SHARE, ADDONS_CORE) @property - def path_addons_custom(self): + def path_addons_git(self): + """Return path for git addons.""" + return Path(HASSIO_SHARE, ADDONS_GIT) + + @property + def path_addons_local(self): """Return path for customs addons.""" - return ADDONS_CUSTOM.format(HASSIO_SHARE) + return Path(HASSIO_SHARE, ADDONS_LOCAL) @property - def path_addons_custom_docker(self): + def path_extern_addons_local(self): """Return path for customs addons.""" - return ADDONS_CUSTOM.format(self.path_hassio_docker) + return str(PurePath(self.path_extern_hassio, ADDONS_LOCAL)) @property def path_addons_data(self): """Return root addon data folder.""" - return ADDONS_DATA.format(HASSIO_SHARE) + return Path(HASSIO_SHARE, ADDONS_DATA) @property - def path_addons_data_docker(self): + def path_extern_addons_data(self): """Return root addon data folder extern for docker.""" - return ADDONS_DATA.format(self.path_hassio_docker) + return str(PurePath(self.path_extern_hassio, ADDONS_DATA)) @property def path_backup(self): """Return root backup data folder.""" - return BACKUP_DATA.format(HASSIO_SHARE) + return Path(HASSIO_SHARE, BACKUP_DATA) @property - def path_backup_docker(self): + def path_extern_backup(self): """Return root backup data folder extern for docker.""" - return BACKUP_DATA.format(self.path_hassio_docker) + return str(PurePath(self.path_extern_hassio, BACKUP_DATA)) @property def addons_repositories(self): diff --git a/hassio/const.py b/hassio/const.py index 02c917ff3..f48178328 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -1,4 +1,6 @@ """Const file for HassIO.""" +from pathlib import Path + HASSIO_VERSION = '0.19' URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/' @@ -10,7 +12,7 @@ URL_HASSIO_ADDONS = 'https://github.com/home-assistant/hassio-addons' DOCKER_REPO = "homeassistant" -HASSIO_SHARE = "/data" +HASSIO_SHARE = Path("/data") RUN_UPDATE_INFO_TASKS = 28800 RUN_UPDATE_SUPERVISOR_TASKS = 29100 @@ -18,11 +20,11 @@ RUN_RELOAD_ADDONS_TASKS = 28800 RESTART_EXIT_CODE = 100 -FILE_HASSIO_ADDONS = "{}/addons.json".format(HASSIO_SHARE) -FILE_HASSIO_CONFIG = "{}/config.json".format(HASSIO_SHARE) +FILE_HASSIO_ADDONS = Path(HASSIO_SHARE, "addons.json") +FILE_HASSIO_CONFIG = Path(HASSIO_SHARE, "config.json") -SOCKET_DOCKER = "/var/run/docker.sock" -SOCKET_HC = "/var/run/hassio-hc.sock" +SOCKET_DOCKER = Path("/var/run/docker.sock") +SOCKET_HC = Path("/var/run/hassio-hc.sock") JSON_RESULT = 'result' JSON_DATA = 'data' @@ -48,11 +50,15 @@ ATTR_PORTS = 'ports' ATTR_MAP = 'map' ATTR_OPTIONS = 'options' ATTR_INSTALLED = 'installed' -ATTR_DEDICATED = 'dedicated' +ATTR_DETACHED = 'detached' ATTR_STATE = 'state' ATTR_SCHEMA = 'schema' ATTR_IMAGE = 'image' ATTR_ADDONS_REPOSITORIES = 'addons_repositories' +ATTR_REPOSITORY = 'repository' +ATTR_REPOSITORIES = 'repositories' +ATTR_URL = 'url' +ATTR_MAINTAINER = 'maintainer' STARTUP_BEFORE = 'before' STARTUP_AFTER = 'after' diff --git a/hassio/dock/addon.py b/hassio/dock/addon.py index afdb6092a..1c3399bc6 100644 --- a/hassio/dock/addon.py +++ b/hassio/dock/addon.py @@ -30,31 +30,31 @@ class DockerAddon(DockerBase): def volumes(self): """Generate volumes for mappings.""" volumes = { - self.addons_data.path_data_docker(self.addon): { + self.addons_data.path_extern_data(self.addon): { 'bind': '/data', 'mode': 'rw' }} if self.addons_data.map_config(self.addon): volumes.update({ - self.config.path_config_docker: { + self.config.path_extern_config: { 'bind': '/config', 'mode': 'rw' }}) if self.addons_data.map_ssl(self.addon): volumes.update({ - self.config.path_ssl_docker: { + self.config.path_extern_ssl: { 'bind': '/ssl', 'mode': 'rw' }}) if self.addons_data.map_addons(self.addon): volumes.update({ - self.config.path_addons_custom_docker: { + self.config.path_extern_addons_local: { 'bind': '/addons', 'mode': 'rw' }}) if self.addons_data.map_backup(self.addon): volumes.update({ - self.config.path_backup_docker: { + self.config.path_extern_backup: { 'bind': '/backup', 'mode': 'rw' }}) diff --git a/hassio/dock/homeassistant.py b/hassio/dock/homeassistant.py index 635e43af4..0f314bb2d 100644 --- a/hassio/dock/homeassistant.py +++ b/hassio/dock/homeassistant.py @@ -45,9 +45,9 @@ class DockerHomeAssistant(DockerBase): 'HASSIO': self.config.api_endpoint, }, volumes={ - self.config.path_config_docker: + self.config.path_extern_config: {'bind': '/config', 'mode': 'rw'}, - self.config.path_ssl_docker: + self.config.path_extern_ssl: {'bind': '/ssl', 'mode': 'rw'}, }) diff --git a/hassio/tools.py b/hassio/tools.py index a012d3361..f44e2a9ad 100644 --- a/hassio/tools.py +++ b/hassio/tools.py @@ -78,7 +78,7 @@ def write_json_file(jsonfile, data): """Write a json file.""" try: json_str = json.dumps(data, indent=2) - with open(jsonfile, 'w') as conf_file: + with jsonfile.open('w') as conf_file: conf_file.write(json_str) except (OSError, json.JSONDecodeError): return False @@ -88,5 +88,5 @@ def write_json_file(jsonfile, data): def read_json_file(jsonfile): """Read a json file and return a dict.""" - with open(jsonfile, 'r') as cfile: + with jsonfile.open('r') as cfile: return json.loads(cfile.read()) From 768b4d2b1a9e6a7e7acb1eac4099efb7051a6e21 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 00:06:20 +0200 Subject: [PATCH 13/28] Start test for 0.19 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index afaadc0b6..882bc7a7c 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "hassio": "0.18", + "hassio": "0.19", "homeassistant": "0.43.2", "resinos": "0.6", "resinhup": "0.1", From a98a0d1a1a224fcceb50354da65e179a8419eb3f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 00:15:50 +0200 Subject: [PATCH 14/28] Fix HC for new path lib --- hassio/host_control.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/hassio/host_control.py b/hassio/host_control.py index 8900f0eec..30ab35ca0 100644 --- a/hassio/host_control.py +++ b/hassio/host_control.py @@ -2,8 +2,6 @@ import asyncio import json import logging -import os -import stat import async_timeout @@ -37,8 +35,7 @@ class HostControl(object): self.hostname = UNKNOWN self.os_info = UNKNOWN - mode = os.stat(SOCKET_HC)[stat.ST_MODE] - if stat.S_ISSOCK(mode): + if SOCKET_HC.is_socket(): self.active = True async def _send_command(self, command): From 4c135dd617044c83cf19af5598312bcb0f7336db Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 00:16:39 +0200 Subject: [PATCH 15/28] Convert dock to string --- hassio/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/core.py b/hassio/core.py index 2e8cf4184..b4fae16ae 100644 --- a/hassio/core.py +++ b/hassio/core.py @@ -32,7 +32,7 @@ class HassIO(object): self.scheduler = Scheduler(self.loop) self.api = RestAPI(self.config, self.loop) self.dock = docker.DockerClient( - base_url="unix:/{}".format(SOCKET_DOCKER), version='auto') + base_url="unix:/{}".format(str(SOCKET_DOCKER)), version='auto') # init basic docker container self.supervisor = DockerSupervisor( From d5e349266bc130c0e71156d3e4b0982ad430d30f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 00:17:19 +0200 Subject: [PATCH 16/28] Convert socket to string --- hassio/host_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/host_control.py b/hassio/host_control.py index 30ab35ca0..11a3db13c 100644 --- a/hassio/host_control.py +++ b/hassio/host_control.py @@ -47,7 +47,7 @@ class HostControl(object): return reader, writer = await asyncio.open_unix_connection( - SOCKET_HC, loop=self.loop) + str(SOCKET_HC), loop=self.loop) try: # send From b8fc002fbc8a83486567c232d62678c3b4bb39b8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 00:24:29 +0200 Subject: [PATCH 17/28] Update new path --- hassio/addons/git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/addons/git.py b/hassio/addons/git.py index 37b519fd5..e991e4241 100644 --- a/hassio/addons/git.py +++ b/hassio/addons/git.py @@ -80,7 +80,7 @@ class AddonsRepoHassIO(AddonsRepo): def __init__(self, config, loop): """Initialize git hassio addon repository.""" super().__init__( - config, loop, config.path_addons_repo, URL_HASSIO_ADDONS) + config, loop, config.path_addons_core, URL_HASSIO_ADDONS) class AddonsRepoCustom(AddonsRepo): From 58fdabb8ff91febf63da4e1e8df33db359e2cb4a Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 00:32:56 +0200 Subject: [PATCH 18/28] Fix relative glob --- hassio/addons/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index a3354eaa8..877593f63 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -62,7 +62,7 @@ class AddonsData(Config): self.config.path_addons_local, REPOSITORY_LOCAL) # read custom git repositories - for repository_dir in self.config.path_addons_git.glob("/*/"): + for repository_dir in self.config.path_addons_git.glob("*/"): self._read_git_repository(repository_dir) def _read_git_repository(self, path): From 5840290df7088bf79f20509c3a74eb4eb4834edd Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 00:57:03 +0200 Subject: [PATCH 19/28] Fix bug --- hassio/addons/data.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 877593f63..23a56278b 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -302,7 +302,8 @@ class AddonsData(Config): def get_image(self, addon): """Return image name of addon.""" - addon_data = self._system_data.get(addon, self._addons_cache[addon]) + addon_data = self._system_data.get( + addon, self._addons_cache.get(addon)) if ATTR_IMAGE not in addon_data: return "{}/{}-addon-{}".format( From 9e326f63244f3244e1fef33d39a4690a484cfb56 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 01:09:39 +0200 Subject: [PATCH 20/28] Remove old file handling --- hassio/addons/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index dd315ec42..c52fb3335 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -113,10 +113,10 @@ class AddonManager(AddonsData): _LOGGER.error("Addon %s is already installed", addon) return False - if not os.path.isdir(self.path_data(addon)): + if not self.path_data(addon).is_dir(): _LOGGER.info("Create Home-Assistant addon data folder %s", self.path_data(addon)) - os.mkdir(self.path_data(addon)) + self.path_data(addon).mkdir() addon_docker = DockerAddon( self.config, self.loop, self.dock, self, addon) @@ -142,10 +142,10 @@ class AddonManager(AddonsData): if not await self.dockers[addon].remove(): return False - if os.path.isdir(self.path_data(addon)): + if self.path_data(addon).is_dir(): _LOGGER.info("Remove Home-Assistant addon data folder %s", self.path_data(addon)) - shutil.rmtree(self.path_data(addon)) + shutil.rmtree(str(self.path_data(addon))) self.dockers.pop(addon) self.set_addon_uninstall(addon) From 088cc3ef15a2229a93222c854592665b357cc196 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 01:24:42 +0200 Subject: [PATCH 21/28] Only loop dir in custom --- hassio/addons/data.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 23a56278b..c73c1d817 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -62,8 +62,9 @@ class AddonsData(Config): self.config.path_addons_local, REPOSITORY_LOCAL) # read custom git repositories - for repository_dir in self.config.path_addons_git.glob("*/"): - self._read_git_repository(repository_dir) + for repository_element in self.config.path_addons_git.iterdir(): + if repository_element.is_dir(): + self._read_git_repository(repository_element) def _read_git_repository(self, path): """Process a custom repository folder.""" From 57c21b4eb5a277099140d9513897b537ac49f7cb Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 01:27:53 +0200 Subject: [PATCH 22/28] Fix glob --- hassio/addons/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index c73c1d817..86a7a2e6a 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -93,7 +93,7 @@ class AddonsData(Config): def _read_addons_folder(self, path, repository): """Read data from addons folder.""" - for addon in path.glob("**/*.config.json"): + for addon in path.glob("**/config.json"): try: addon_config = read_json_file(addon) From 1e4ed9c9d1a399e2d8373c8dbb8d2ebce16c557e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 01:34:54 +0200 Subject: [PATCH 23/28] Fix str by docker extern --- hassio/addons/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 86a7a2e6a..499f947d2 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -334,7 +334,7 @@ class AddonsData(Config): def path_extern_data(self, addon): """Return addon data path external for docker.""" - return PurePath(self.config.path_extern_addons_data, addon) + return str(PurePath(self.config.path_extern_addons_data, addon)) def path_addon_options(self, addon): """Return path to addons options.""" From d2f8e356224a60df201b8937b27ab961a9e0442e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 01:36:15 +0200 Subject: [PATCH 24/28] Fix path --- hassio/addons/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 499f947d2..81509d7d0 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -338,7 +338,7 @@ class AddonsData(Config): def path_addon_options(self, addon): """Return path to addons options.""" - return Path(self.path_data, addon, "options.json") + return Path(self.path_data(addon), addon, "options.json") def write_addon_options(self, addon): """Return True if addon options is written to data.""" From 76a999f650a01cac3079113fed85bc2938e4297b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 01:55:48 +0200 Subject: [PATCH 25/28] Update __init__.py --- hassio/dock/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hassio/dock/__init__.py b/hassio/dock/__init__.py index 2339d3917..386627834 100644 --- a/hassio/dock/__init__.py +++ b/hassio/dock/__init__.py @@ -203,6 +203,8 @@ class DockerBase(object): image="{}:latest".format(self.image), force=True) self.dock.images.remove( image="{}:{}".format(self.image, self.version), force=True) + except docker.errors.ImageNotFound: + return True except docker.errors.DockerException as err: _LOGGER.warning("Can't remove image %s -> %s", self.image, err) return False From c333f94cfa3363ecbac01030794345c8e444d29b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 02:01:12 +0200 Subject: [PATCH 26/28] Fix options path --- hassio/addons/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hassio/addons/data.py b/hassio/addons/data.py index 81509d7d0..5248778a6 100644 --- a/hassio/addons/data.py +++ b/hassio/addons/data.py @@ -338,7 +338,7 @@ class AddonsData(Config): def path_addon_options(self, addon): """Return path to addons options.""" - return Path(self.path_data(addon), addon, "options.json") + return Path(self.path_data(addon), "options.json") def write_addon_options(self, addon): """Return True if addon options is written to data.""" From 545d45ecf037ab6f3b2b010b3f7f729b47cd3abc Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 02:24:19 +0200 Subject: [PATCH 27/28] Add more exceptions --- hassio/addons/git.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/hassio/addons/git.py b/hassio/addons/git.py index e991e4241..f9f8ab6bd 100644 --- a/hassio/addons/git.py +++ b/hassio/addons/git.py @@ -35,7 +35,8 @@ class AddonsRepo(object): self.repo = await self.loop.run_in_executor( None, git.Repo, str(self.path)) - except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err: + except (git.InvalidGitRepositoryError, git.NoSuchPathError, + git.GitCommandError) as err: _LOGGER.error("Can't load %s repo: %s.", self.path, err) return False @@ -49,7 +50,8 @@ class AddonsRepo(object): self.repo = await self.loop.run_in_executor( None, git.Repo.clone_from, self.url, str(self.path)) - except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err: + except (git.InvalidGitRepositoryError, git.NoSuchPathError, + git.GitCommandError) as err: _LOGGER.error("Can't clone %s repo: %s.", self.url, err) return False @@ -67,7 +69,8 @@ class AddonsRepo(object): await self.loop.run_in_executor( None, self.repo.remotes.origin.pull) - except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err: + except (git.InvalidGitRepositoryError, git.NoSuchPathError, + git.exc.GitCommandError) as err: _LOGGER.error("Can't pull %s repo: %s.", self.url, err) return False From f178dde58962260af9d5983bfc81770e73489a60 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 2 May 2017 02:25:22 +0200 Subject: [PATCH 28/28] fix lint --- hassio/addons/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/hassio/addons/__init__.py b/hassio/addons/__init__.py index c52fb3335..dafef9cce 100644 --- a/hassio/addons/__init__.py +++ b/hassio/addons/__init__.py @@ -1,7 +1,6 @@ """Init file for HassIO addons.""" import asyncio import logging -import os import shutil from .data import AddonsData