Skip to content

Commit 390d4fa

Browse files
authored
Merge pull request #21 from home-assistant/dev
Release 0.18
2 parents b197578 + 9559c39 commit 390d4fa

File tree

11 files changed

+329
-79
lines changed

11 files changed

+329
-79
lines changed

API.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ On success
4040
"dedicated": "bool",
4141
"description": "description"
4242
}
43+
],
44+
"addons_repositories": [
45+
"REPO_URL"
4346
]
4447
}
4548
```
@@ -55,7 +58,10 @@ Optional:
5558
- POST `/supervisor/options`
5659
```json
5760
{
58-
"beta_channel": "true|false"
61+
"beta_channel": "true|false",
62+
"addons_repositories": [
63+
"REPO_URL"
64+
]
5965
}
6066
```
6167

hassio/addons/__init__.py

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import shutil
66

77
from .data import AddonsData
8-
from .git import AddonsRepo
8+
from .git import AddonsRepoHassIO, AddonsRepoCustom
99
from ..const import STATE_STOPPED, STATE_STARTED
1010
from ..dock.addon import DockerAddon
1111

@@ -21,28 +21,74 @@ def __init__(self, config, loop, dock):
2121

2222
self.loop = loop
2323
self.dock = dock
24-
self.repo = AddonsRepo(config, loop)
24+
self.repositories = []
2525
self.dockers = {}
2626

2727
async def prepare(self, arch):
2828
"""Startup addon management."""
2929
self.arch = arch
3030

31+
# init hassio repository
32+
self.repositories.append(AddonsRepoHassIO(self.config, self.loop))
33+
34+
# init custom repositories
35+
for url in self.config.addons_repositories:
36+
self.repositories.append(
37+
AddonsRepoCustom(self.config, self.loop, url))
38+
3139
# load addon repository
32-
if await self.repo.load():
33-
self.read_addons_repo()
40+
tasks = [addon.load() for addon in self.repositories]
41+
if tasks:
42+
await asyncio.wait(tasks, loop=self.loop)
43+
44+
# read data from repositories
45+
self.read_data_from_repositories()
46+
self.merge_update_config()
3447

3548
# load installed addons
3649
for addon in self.list_installed:
3750
self.dockers[addon] = DockerAddon(
3851
self.config, self.loop, self.dock, self, addon)
3952
await self.dockers[addon].attach()
4053

54+
async def add_custom_repository(self, url):
55+
"""Add a new custom repository."""
56+
if url in self.config.addons_repositories:
57+
_LOGGER.warning("Repository already exists %s", url)
58+
return False
59+
60+
repo = AddonsRepoCustom(self.config, self.loop, url)
61+
62+
if not await repo.load():
63+
_LOGGER.error("Can't load from repository %s", url)
64+
return False
65+
66+
self.config.addons_repositories = url
67+
self.repositories.append(repo)
68+
return True
69+
70+
def drop_custom_repository(self, url):
71+
"""Remove a custom repository."""
72+
for repo in self.repositories:
73+
if repo.url == url:
74+
self.repositories.remove(repo)
75+
self.config.drop_addon_repository(url)
76+
repo.remove()
77+
return True
78+
79+
return False
80+
4181
async def reload(self):
4282
"""Update addons from repo and reload list."""
43-
if not await self.repo.pull():
83+
tasks = [addon.pull() for addon in self.repositories]
84+
if not tasks:
4485
return
45-
self.read_addons_repo()
86+
87+
await asyncio.wait(tasks, loop=self.loop)
88+
89+
# read data from repositories
90+
self.read_data_from_repositories()
91+
self.merge_update_config()
4692

4793
# remove stalled addons
4894
for addon in self.list_removed:
@@ -51,10 +97,7 @@ async def reload(self):
5197
async def auto_boot(self, start_type):
5298
"""Boot addons with mode auto."""
5399
boot_list = self.list_startup(start_type)
54-
tasks = []
55-
56-
for addon in boot_list:
57-
tasks.append(self.loop.create_task(self.start(addon)))
100+
tasks = [self.start(addon) for addon in boot_list]
58101

59102
_LOGGER.info("Startup %s run %d addons", start_type, len(tasks))
60103
if tasks:

hassio/addons/data.py

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
"""Init file for HassIO addons."""
2+
import copy
23
import logging
34
import glob
45

56
import voluptuous as vol
67
from voluptuous.humanize import humanize_error
78

9+
from .util import extract_hash_from_path
810
from .validate import validate_options, SCHEMA_ADDON_CONFIG
911
from ..const import (
1012
FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON,
11-
ATTR_STARTUP, ATTR_BOOT, ATTR_MAP_SSL, ATTR_MAP_CONFIG, ATTR_OPTIONS,
12-
ATTR_PORTS, BOOT_AUTO, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA,
13-
ATTR_IMAGE, ATTR_DEDICATED)
13+
ATTR_STARTUP, ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, BOOT_AUTO,
14+
DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA, ATTR_IMAGE, ATTR_DEDICATED,
15+
MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP)
1416
from ..config import Config
1517
from ..tools import read_json_file, write_json_file
1618

1719
_LOGGER = logging.getLogger(__name__)
1820

19-
ADDONS_REPO_PATTERN = "{}/*/config.json"
21+
ADDONS_REPO_PATTERN = "{}/**/config.json"
2022
SYSTEM = "system"
2123
USER = "user"
2224

@@ -41,31 +43,60 @@ def save(self):
4143
}
4244
super().save()
4345

44-
def read_addons_repo(self):
46+
def read_data_from_repositories(self):
4547
"""Read data from addons repository."""
4648
self._current_data = {}
4749

4850
self._read_addons_folder(self.config.path_addons_repo)
49-
self._read_addons_folder(self.config.path_addons_custom)
51+
self._read_addons_folder(self.config.path_addons_custom, custom=True)
5052

51-
def _read_addons_folder(self, folder):
53+
def _read_addons_folder(self, folder, custom=False):
5254
"""Read data from addons folder."""
5355
pattern = ADDONS_REPO_PATTERN.format(folder)
5456

55-
for addon in glob.iglob(pattern):
57+
for addon in glob.iglob(pattern, recursive=True):
5658
try:
5759
addon_config = read_json_file(addon)
5860

5961
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
60-
self._current_data[addon_config[ATTR_SLUG]] = addon_config
62+
if custom:
63+
addon_slug = "{}_{}".format(
64+
extract_hash_from_path(folder, addon),
65+
addon_config[ATTR_SLUG],
66+
)
67+
else:
68+
addon_slug = addon_config[ATTR_SLUG]
6169

62-
except (OSError, KeyError):
70+
self._current_data[addon_slug] = addon_config
71+
72+
except OSError:
6373
_LOGGER.warning("Can't read %s", addon)
6474

6575
except vol.Invalid as ex:
6676
_LOGGER.warning("Can't read %s -> %s", addon,
6777
humanize_error(addon_config, ex))
6878

79+
def merge_update_config(self):
80+
"""Update local config if they have update.
81+
82+
It need to be the same version as the local version is.
83+
"""
84+
have_change = False
85+
86+
for addon, data in self._system_data.items():
87+
# dedicated
88+
if addon not in self._current_data:
89+
continue
90+
91+
current = self._current_data[addon]
92+
if data[ATTR_VERSION] == current[ATTR_VERSION]:
93+
if data != current:
94+
self._system_data[addon] = copy.deepcopy(current)
95+
have_change = True
96+
97+
if have_change:
98+
self.save()
99+
69100
@property
70101
def list_installed(self):
71102
"""Return a list of installed addons."""
@@ -83,7 +114,7 @@ def list_api(self):
83114

84115
data.append({
85116
ATTR_NAME: values[ATTR_NAME],
86-
ATTR_SLUG: values[ATTR_SLUG],
117+
ATTR_SLUG: addon,
87118
ATTR_DESCRIPTON: values[ATTR_DESCRIPTON],
88119
ATTR_VERSION: values[ATTR_VERSION],
89120
ATTR_INSTALLED: i_version,
@@ -132,7 +163,7 @@ def version_installed(self, addon):
132163

133164
def set_addon_install(self, addon, version):
134165
"""Set addon as installed."""
135-
self._system_data[addon] = self._current_data[addon]
166+
self._system_data[addon] = copy.deepcopy(self._current_data[addon])
136167
self._user_data[addon] = {
137168
ATTR_OPTIONS: {},
138169
ATTR_VERSION: version,
@@ -147,13 +178,13 @@ def set_addon_uninstall(self, addon):
147178

148179
def set_addon_update(self, addon, version):
149180
"""Update version of addon."""
150-
self._system_data[addon] = self._current_data[addon]
181+
self._system_data[addon] = copy.deepcopy(self._current_data[addon])
151182
self._user_data[addon][ATTR_VERSION] = version
152183
self.save()
153184

154185
def set_options(self, addon, options):
155186
"""Store user addon options."""
156-
self._user_data[addon][ATTR_OPTIONS] = options
187+
self._user_data[addon][ATTR_OPTIONS] = copy.deepcopy(options)
157188
self.save()
158189

159190
def set_boot(self, addon, boot):
@@ -200,15 +231,23 @@ def get_image(self, addon):
200231
if ATTR_IMAGE not in addon_data:
201232
return "{}/{}-addon-{}".format(DOCKER_REPO, self.arch, addon)
202233

203-
return addon_data[ATTR_IMAGE]
234+
return addon_data[ATTR_IMAGE].format(arch=self.arch)
204235

205-
def need_config(self, addon):
236+
def map_config(self, addon):
206237
"""Return True if config map is needed."""
207-
return self._system_data[addon][ATTR_MAP_CONFIG]
238+
return MAP_CONFIG in self._system_data[addon][ATTR_MAP]
208239

209-
def need_ssl(self, addon):
240+
def map_ssl(self, addon):
210241
"""Return True if ssl map is needed."""
211-
return self._system_data[addon][ATTR_MAP_SSL]
242+
return MAP_SSL in self._system_data[addon][ATTR_MAP]
243+
244+
def map_addons(self, addon):
245+
"""Return True if addons map is needed."""
246+
return MAP_ADDONS in self._system_data[addon][ATTR_MAP]
247+
248+
def map_backup(self, addon):
249+
"""Return True if backup map is needed."""
250+
return MAP_BACKUP in self._system_data[addon][ATTR_MAP]
212251

213252
def path_data(self, addon):
214253
"""Return addon data path inside supervisor."""

hassio/addons/git.py

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
import asyncio
33
import logging
44
import os
5+
import shutil
56

67
import git
78

9+
from .util import get_hash_from_repository
810
from ..const import URL_HASSIO_ADDONS
911

1012
_LOGGER = logging.getLogger(__name__)
@@ -13,26 +15,28 @@
1315
class AddonsRepo(object):
1416
"""Manage addons git repo."""
1517

16-
def __init__(self, config, loop):
17-
"""Initialize docker base wrapper."""
18+
def __init__(self, config, loop, path, url):
19+
"""Initialize git base wrapper."""
1820
self.config = config
1921
self.loop = loop
2022
self.repo = None
23+
self.path = path
24+
self.url = url
2125
self._lock = asyncio.Lock(loop=loop)
2226

2327
async def load(self):
2428
"""Init git addon repo."""
25-
if not os.path.isdir(self.config.path_addons_repo):
29+
if not os.path.isdir(self.path):
2630
return await self.clone()
2731

2832
async with self._lock:
2933
try:
30-
_LOGGER.info("Load addons repository")
34+
_LOGGER.info("Load addon %s repository", self.path)
3135
self.repo = await self.loop.run_in_executor(
32-
None, git.Repo, self.config.path_addons_repo)
36+
None, git.Repo, self.path)
3337

3438
except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err:
35-
_LOGGER.error("Can't load addons repo: %s.", err)
39+
_LOGGER.error("Can't load %s repo: %s.", self.path, err)
3640
return False
3741

3842
return True
@@ -41,13 +45,12 @@ async def clone(self):
4145
"""Clone git addon repo."""
4246
async with self._lock:
4347
try:
44-
_LOGGER.info("Clone addons repository")
48+
_LOGGER.info("Clone addon %s repository", self.url)
4549
self.repo = await self.loop.run_in_executor(
46-
None, git.Repo.clone_from, URL_HASSIO_ADDONS,
47-
self.config.path_addons_repo)
50+
None, git.Repo.clone_from, self.url, self.path)
4851

4952
except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err:
50-
_LOGGER.error("Can't clone addons repo: %s.", err)
53+
_LOGGER.error("Can't clone %s repo: %s.", self.url, err)
5154
return False
5255

5356
return True
@@ -60,12 +63,43 @@ async def pull(self):
6063

6164
async with self._lock:
6265
try:
63-
_LOGGER.info("Pull addons repository")
66+
_LOGGER.info("Pull addon %s repository", self.url)
6467
await self.loop.run_in_executor(
6568
None, self.repo.remotes.origin.pull)
6669

6770
except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err:
68-
_LOGGER.error("Can't pull addons repo: %s.", err)
71+
_LOGGER.error("Can't pull %s repo: %s.", self.url, err)
6972
return False
7073

7174
return True
75+
76+
77+
class AddonsRepoHassIO(AddonsRepo):
78+
"""HassIO addons repository."""
79+
80+
def __init__(self, config, loop):
81+
"""Initialize git hassio addon repository."""
82+
super().__init__(
83+
config, loop, config.path_addons_repo, URL_HASSIO_ADDONS)
84+
85+
86+
class AddonsRepoCustom(AddonsRepo):
87+
"""Custom addons repository."""
88+
89+
def __init__(self, config, loop, url):
90+
"""Initialize git hassio addon repository."""
91+
path = os.path.join(
92+
config.path_addons_custom, get_hash_from_repository(url))
93+
94+
super().__init__(config, loop, path, url)
95+
96+
def remove(self):
97+
"""Remove a custom addon."""
98+
if os.path.isdir(self.path):
99+
_LOGGER.info("Remove custom addon repository %s", self.url)
100+
101+
def log_err(funct, path, _):
102+
"""Log error."""
103+
_LOGGER.warning("Can't remove %s", path)
104+
105+
shutil.rmtree(self.path, onerror=log_err)

0 commit comments

Comments
 (0)