Compare commits

...

45 Commits
0.15 ... 0.18

Author SHA1 Message Date
Pascal Vizeli
390d4fa6c7 Merge pull request #21 from home-assistant/dev
Release 0.18
2017-04-28 02:05:44 +02:00
Pascal Vizeli
9559c39351 fix update merge 2017-04-28 01:56:07 +02:00
Pascal Vizeli
9d95b70534 fix list remove 2017-04-28 01:40:06 +02:00
Pascal Vizeli
3bad896978 fix handling 2017-04-28 01:26:37 +02:00
Pascal Vizeli
9f406df129 Fix regex 2017-04-28 01:12:27 +02:00
Pascal Vizeli
a9b4174590 Add support for arch in custom image name 2017-04-28 01:07:54 +02:00
Pascal Vizeli
9109e3803b Use in every case a hash for addon name 2017-04-28 01:01:57 +02:00
Pascal Vizeli
422dd78489 Fix list handling 2017-04-28 00:45:53 +02:00
Pascal Vizeli
9e1d6c9d2b Change log output / fix bug with store repository 2017-04-28 00:26:58 +02:00
Pascal Vizeli
c8e3f2b48a Merge pull request #20 from pvizeli/multible_git_repos
Allow custom repository  / improve config validate
2017-04-27 23:54:29 +02:00
Pascal Vizeli
a287f52e47 fix spell 2017-04-27 23:43:18 +02:00
Pascal Vizeli
76952db3eb fix old code 2017-04-27 23:37:35 +02:00
Pascal Vizeli
871721f04b Fix lint p2 2017-04-27 23:31:34 +02:00
Pascal Vizeli
3c0ebdf643 fix lint 2017-04-27 23:28:58 +02:00
Pascal Vizeli
0e258a4ae0 short hash 2017-04-27 23:18:05 +02:00
Pascal Vizeli
645a8e2372 Add id to addons slug 2017-04-27 23:09:52 +02:00
Pascal Vizeli
c6cc8adbb7 Change handling with repo list 2017-04-27 21:58:21 +02:00
pvizeli
dd38c73b85 Fix lint p4 2017-04-27 21:08:28 +02:00
pvizeli
c916314704 Fix lint p3 2017-04-27 21:08:28 +02:00
pvizeli
e0dcce5895 Fix lint p2 2017-04-27 21:08:28 +02:00
pvizeli
906616e224 Update description 2017-04-27 21:08:28 +02:00
pvizeli
db20ea95d9 Fix lint 2017-04-27 21:08:28 +02:00
pvizeli
d142ea5d23 Allow custome repository / improve config validate 2017-04-27 21:08:28 +02:00
Pascal Vizeli
5d52404dab Update new docker hup 2017-04-27 17:11:33 +02:00
Pascal Vizeli
43f4b36cfe Change status code on api error 2017-04-27 16:25:49 +02:00
Pascal Vizeli
0393db19e6 Pump version to 0.18 2017-04-27 09:33:58 +02:00
Pascal Vizeli
b197578df4 HassIO 0.17 2017-04-27 08:55:42 +02:00
Pascal Vizeli
ed428c0df4 HassIO 0.17 2017-04-27 08:55:15 +02:00
Pascal Vizeli
d38707821c Merge pull request #19 from home-assistant/dev
Release 0.17
2017-04-27 08:44:15 +02:00
Pascal Vizeli
cfb392054e Merge pull request #18 from pvizeli/update_v3_api
* Update POST/GET api for Hass

* fix lint
2017-04-27 08:42:40 +02:00
pvizeli
0ea65efeb3 fix lint 2017-04-27 08:41:33 +02:00
pvizeli
c4ce7d1a74 Update POST/GET api for Hass 2017-04-27 08:31:42 +02:00
Pascal Vizeli
7ac95b98bc Update readme and fix links 2017-04-26 23:16:00 +02:00
Pascal Vizeli
f8413d8d63 Update version of HassIO 2017-04-26 23:08:30 +02:00
Pascal Vizeli
709b80b864 Pump version 0.17 2017-04-26 22:42:54 +02:00
Pascal Vizeli
b5b68c5c42 Release 0.16
Release 0.16
2017-04-26 22:38:16 +02:00
Pascal Vizeli
d58e847978 Merge remote-tracking branch 'origin/master' into dev 2017-04-26 22:19:08 +02:00
Pascal Vizeli
aad9ae6997 Add OS attribute for hostcontrol (#16)
* Add OS attribute for hostcontrol

* fix lint
2017-04-26 22:14:58 +02:00
Pascal Vizeli
139cf4fae4 Update path (#15) 2017-04-26 22:08:49 +02:00
Pascal Vizeli
e01b2da223 Cleanup 2017-04-26 21:49:29 +02:00
Pascal Vizeli
cbbe2d2d3c Cleanup 2017-04-26 21:48:27 +02:00
Pascal Vizeli
7ca11a96b9 Pump version 2017-04-26 21:47:04 +02:00
Pascal Vizeli
3443d6d715 Update API.md 2017-04-26 17:13:16 +02:00
Pascal Vizeli
99730734a0 update generic HostControll v0.2 2017-04-26 12:59:19 +02:00
Pascal Vizeli
20fcd28dbe Update version.json 2017-04-26 11:27:29 +02:00
17 changed files with 391 additions and 142 deletions

57
API.md
View File

@@ -22,14 +22,14 @@ On success
### HassIO
- `/supervisor/ping`
- GET `/supervisor/ping`
- `/supervisor/info`
- GET `/supervisor/info`
```json
{
"version": "INSTALL_VERSION",
"last_version": "CURRENT_VERSION",
"last_version": "LAST_VERSION",
"beta_channel": "true|false",
"addons": [
{
@@ -40,11 +40,14 @@ On success
"dedicated": "bool",
"description": "description"
}
],
"addons_repositories": [
"REPO_URL"
]
}
```
- `/supervisor/update`
- POST `/supervisor/update`
Optional:
```json
{
@@ -52,28 +55,31 @@ Optional:
}
```
- `/supervisor/option`
- POST `/supervisor/options`
```json
{
"beta_channel": "true|false"
"beta_channel": "true|false",
"addons_repositories": [
"REPO_URL"
]
}
```
- `/supervisor/reload`
- POST `/supervisor/reload`
Reload addons/version.
- `/supervisor/logs`
- GET `/supervisor/logs`
Output the raw docker log
### Host
- `/host/shutdown`
- POST `/host/shutdown`
- `/host/reboot`
- POST `/host/reboot`
- `/host/info`
- GET `/host/info`
See HostControl info command.
```json
{
@@ -82,10 +88,11 @@ See HostControl info command.
"last_version": "",
"features": ["shutdown", "reboot", "update", "network_info", "network_control"],
"hostname": "",
"os": ""
}
```
- `/host/update`
- POST `/host/update`
Optional:
```json
{
@@ -95,9 +102,9 @@ Optional:
### Network
- `/network/info`
- GET `/network/info`
- `/network/options`
- POST `/network/options`
```json
{
"hostname": "",
@@ -111,7 +118,7 @@ Optional:
### HomeAssistant
- `/homeassistant/info`
- GET `/homeassistant/info`
```json
{
@@ -120,7 +127,7 @@ Optional:
}
```
- `/homeassistant/update`
- POST `/homeassistant/update`
Optional:
```json
{
@@ -128,13 +135,13 @@ Optional:
}
```
- `/homeassistant/logs`
- GET `/homeassistant/logs`
Output the raw docker log
### REST API addons
- `/addons/{addon}/info`
- GET `/addons/{addon}/info`
```json
{
"version": "VERSION",
@@ -145,7 +152,7 @@ Output the raw docker log
}
```
- `/addons/{addon}/options`
- POST `/addons/{addon}/options`
```json
{
"boot": "auto|manual",
@@ -153,11 +160,11 @@ Output the raw docker log
}
```
- `/addons/{addon}/start`
- POST `/addons/{addon}/start`
- `/addons/{addon}/stop`
- POST `/addons/{addon}/stop`
- `/addons/{addon}/install`
- POST `/addons/{addon}/install`
Optional:
```json
{
@@ -165,9 +172,9 @@ Optional:
}
```
- `/addons/{addon}/uninstall`
- POST `/addons/{addon}/uninstall`
- `/addons/{addon}/update`
- POST `/addons/{addon}/update`
Optional:
```json
{
@@ -175,7 +182,7 @@ Optional:
}
```
- `/addons/{addon}/logs`
- GET `/addons/{addon}/logs`
Output the raw docker log

View File

@@ -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.
[HassIO-Addons](https://github.com/pvizeli/hassio-addons) | [HassIO-Build](https://github.com/pvizeli/hassio-build)
[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!**
@@ -53,4 +53,4 @@ 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/pvizeli/hassio-build/tree/master/install
https://github.com/home-assistant/hassio-build/tree/master/install

View File

@@ -5,7 +5,7 @@ import os
import shutil
from .data import AddonsData
from .git import AddonsRepo
from .git import AddonsRepoHassIO, AddonsRepoCustom
from ..const import STATE_STOPPED, STATE_STARTED
from ..dock.addon import DockerAddon
@@ -21,16 +21,29 @@ class AddonManager(AddonsData):
self.loop = loop
self.dock = dock
self.repo = AddonsRepo(config, loop)
self.repositories = []
self.dockers = {}
async def prepare(self, arch):
"""Startup addon management."""
self.arch = arch
# init hassio repository
self.repositories.append(AddonsRepoHassIO(self.config, self.loop))
# init custom repositories
for url in self.config.addons_repositories:
self.repositories.append(
AddonsRepoCustom(self.config, self.loop, url))
# load addon repository
if await self.repo.load():
self.read_addons_repo()
tasks = [addon.load() for addon in self.repositories]
if tasks:
await asyncio.wait(tasks, loop=self.loop)
# read data from repositories
self.read_data_from_repositories()
self.merge_update_config()
# load installed addons
for addon in self.list_installed:
@@ -38,11 +51,44 @@ class AddonManager(AddonsData):
self.config, self.loop, self.dock, self, addon)
await self.dockers[addon].attach()
async def add_custom_repository(self, url):
"""Add a new custom repository."""
if url in self.config.addons_repositories:
_LOGGER.warning("Repository already exists %s", url)
return False
repo = AddonsRepoCustom(self.config, self.loop, url)
if not await repo.load():
_LOGGER.error("Can't load from repository %s", url)
return False
self.config.addons_repositories = url
self.repositories.append(repo)
return True
def drop_custom_repository(self, url):
"""Remove a custom repository."""
for repo in self.repositories:
if repo.url == url:
self.repositories.remove(repo)
self.config.drop_addon_repository(url)
repo.remove()
return True
return False
async def reload(self):
"""Update addons from repo and reload list."""
if not await self.repo.pull():
tasks = [addon.pull() for addon in self.repositories]
if not tasks:
return
self.read_addons_repo()
await asyncio.wait(tasks, loop=self.loop)
# read data from repositories
self.read_data_from_repositories()
self.merge_update_config()
# remove stalled addons
for addon in self.list_removed:
@@ -51,10 +97,7 @@ class AddonManager(AddonsData):
async def auto_boot(self, start_type):
"""Boot addons with mode auto."""
boot_list = self.list_startup(start_type)
tasks = []
for addon in boot_list:
tasks.append(self.loop.create_task(self.start(addon)))
tasks = [self.start(addon) for addon in boot_list]
_LOGGER.info("Startup %s run %d addons", start_type, len(tasks))
if tasks:

View File

@@ -1,22 +1,24 @@
"""Init file for HassIO addons."""
import copy
import logging
import glob
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 ..const import (
FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON,
ATTR_STARTUP, ATTR_BOOT, ATTR_MAP_SSL, ATTR_MAP_CONFIG, ATTR_OPTIONS,
ATTR_PORTS, BOOT_AUTO, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA,
ATTR_IMAGE, ATTR_DEDICATED)
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)
from ..config import Config
from ..tools import read_json_file, write_json_file
_LOGGER = logging.getLogger(__name__)
ADDONS_REPO_PATTERN = "{}/*/config.json"
ADDONS_REPO_PATTERN = "{}/**/config.json"
SYSTEM = "system"
USER = "user"
@@ -41,31 +43,60 @@ class AddonsData(Config):
}
super().save()
def read_addons_repo(self):
def read_data_from_repositories(self):
"""Read data from addons repository."""
self._current_data = {}
self._read_addons_folder(self.config.path_addons_repo)
self._read_addons_folder(self.config.path_addons_custom)
self._read_addons_folder(self.config.path_addons_custom, custom=True)
def _read_addons_folder(self, folder):
def _read_addons_folder(self, folder, custom=False):
"""Read data from addons folder."""
pattern = ADDONS_REPO_PATTERN.format(folder)
for addon in glob.iglob(pattern):
for addon in glob.iglob(pattern, recursive=True):
try:
addon_config = read_json_file(addon)
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
self._current_data[addon_config[ATTR_SLUG]] = addon_config
if custom:
addon_slug = "{}_{}".format(
extract_hash_from_path(folder, addon),
addon_config[ATTR_SLUG],
)
else:
addon_slug = addon_config[ATTR_SLUG]
except (OSError, KeyError):
self._current_data[addon_slug] = addon_config
except OSError:
_LOGGER.warning("Can't read %s", addon)
except vol.Invalid as ex:
_LOGGER.warning("Can't read %s -> %s", addon,
humanize_error(addon_config, ex))
def merge_update_config(self):
"""Update local config if they have update.
It need to be the same version as the local version is.
"""
have_change = False
for addon, data in self._system_data.items():
# dedicated
if addon not in self._current_data:
continue
current = self._current_data[addon]
if data[ATTR_VERSION] == current[ATTR_VERSION]:
if data != current:
self._system_data[addon] = copy.deepcopy(current)
have_change = True
if have_change:
self.save()
@property
def list_installed(self):
"""Return a list of installed addons."""
@@ -83,7 +114,7 @@ class AddonsData(Config):
data.append({
ATTR_NAME: values[ATTR_NAME],
ATTR_SLUG: values[ATTR_SLUG],
ATTR_SLUG: addon,
ATTR_DESCRIPTON: values[ATTR_DESCRIPTON],
ATTR_VERSION: values[ATTR_VERSION],
ATTR_INSTALLED: i_version,
@@ -132,7 +163,7 @@ class AddonsData(Config):
def set_addon_install(self, addon, version):
"""Set addon as installed."""
self._system_data[addon] = self._current_data[addon]
self._system_data[addon] = copy.deepcopy(self._current_data[addon])
self._user_data[addon] = {
ATTR_OPTIONS: {},
ATTR_VERSION: version,
@@ -147,13 +178,13 @@ class AddonsData(Config):
def set_addon_update(self, addon, version):
"""Update version of addon."""
self._system_data[addon] = self._current_data[addon]
self._system_data[addon] = copy.deepcopy(self._current_data[addon])
self._user_data[addon][ATTR_VERSION] = version
self.save()
def set_options(self, addon, options):
"""Store user addon options."""
self._user_data[addon][ATTR_OPTIONS] = options
self._user_data[addon][ATTR_OPTIONS] = copy.deepcopy(options)
self.save()
def set_boot(self, addon, boot):
@@ -200,15 +231,23 @@ class AddonsData(Config):
if ATTR_IMAGE not in addon_data:
return "{}/{}-addon-{}".format(DOCKER_REPO, self.arch, addon)
return addon_data[ATTR_IMAGE]
return addon_data[ATTR_IMAGE].format(arch=self.arch)
def need_config(self, addon):
def map_config(self, addon):
"""Return True if config map is needed."""
return self._system_data[addon][ATTR_MAP_CONFIG]
return MAP_CONFIG in self._system_data[addon][ATTR_MAP]
def need_ssl(self, addon):
def map_ssl(self, addon):
"""Return True if ssl map is needed."""
return self._system_data[addon][ATTR_MAP_SSL]
return MAP_SSL in self._system_data[addon][ATTR_MAP]
def map_addons(self, addon):
"""Return True if addons map is needed."""
return MAP_ADDONS in self._system_data[addon][ATTR_MAP]
def map_backup(self, addon):
"""Return True if backup map is needed."""
return MAP_BACKUP in self._system_data[addon][ATTR_MAP]
def path_data(self, addon):
"""Return addon data path inside supervisor."""

View File

@@ -2,9 +2,11 @@
import asyncio
import logging
import os
import shutil
import git
from .util import get_hash_from_repository
from ..const import URL_HASSIO_ADDONS
_LOGGER = logging.getLogger(__name__)
@@ -13,26 +15,28 @@ _LOGGER = logging.getLogger(__name__)
class AddonsRepo(object):
"""Manage addons git repo."""
def __init__(self, config, loop):
"""Initialize docker base wrapper."""
def __init__(self, config, loop, path, url):
"""Initialize git base wrapper."""
self.config = config
self.loop = loop
self.repo = None
self.path = path
self.url = url
self._lock = asyncio.Lock(loop=loop)
async def load(self):
"""Init git addon repo."""
if not os.path.isdir(self.config.path_addons_repo):
if not os.path.isdir(self.path):
return await self.clone()
async with self._lock:
try:
_LOGGER.info("Load addons repository")
_LOGGER.info("Load addon %s repository", self.path)
self.repo = await self.loop.run_in_executor(
None, git.Repo, self.config.path_addons_repo)
None, git.Repo, self.path)
except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err:
_LOGGER.error("Can't load addons repo: %s.", err)
_LOGGER.error("Can't load %s repo: %s.", self.path, err)
return False
return True
@@ -41,13 +45,12 @@ class AddonsRepo(object):
"""Clone git addon repo."""
async with self._lock:
try:
_LOGGER.info("Clone addons repository")
_LOGGER.info("Clone addon %s repository", self.url)
self.repo = await self.loop.run_in_executor(
None, git.Repo.clone_from, URL_HASSIO_ADDONS,
self.config.path_addons_repo)
None, git.Repo.clone_from, self.url, self.path)
except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err:
_LOGGER.error("Can't clone addons repo: %s.", err)
_LOGGER.error("Can't clone %s repo: %s.", self.url, err)
return False
return True
@@ -60,12 +63,43 @@ class AddonsRepo(object):
async with self._lock:
try:
_LOGGER.info("Pull addons repository")
_LOGGER.info("Pull addon %s repository", self.url)
await self.loop.run_in_executor(
None, self.repo.remotes.origin.pull)
except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err:
_LOGGER.error("Can't pull addons repo: %s.", err)
_LOGGER.error("Can't pull %s repo: %s.", self.url, err)
return False
return True
class AddonsRepoHassIO(AddonsRepo):
"""HassIO addons repository."""
def __init__(self, config, loop):
"""Initialize git hassio addon repository."""
super().__init__(
config, loop, config.path_addons_repo, URL_HASSIO_ADDONS)
class AddonsRepoCustom(AddonsRepo):
"""Custom addons repository."""
def __init__(self, config, loop, url):
"""Initialize git hassio addon repository."""
path = os.path.join(
config.path_addons_custom, get_hash_from_repository(url))
super().__init__(config, loop, path, url)
def remove(self):
"""Remove a custom addon."""
if os.path.isdir(self.path):
_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)

28
hassio/addons/util.py Normal file
View File

@@ -0,0 +1,28 @@
"""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):
"""Generate a hash from repository."""
key = repo.lower().encode()
return hashlib.sha1(key).hexdigest()[:8]
def extract_hash_from_path(base_path, options_path):
"""Extract repo id from path."""
base_dir = pathlib.PurePosixPath(base_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

View File

@@ -3,9 +3,9 @@ import voluptuous as vol
from ..const import (
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_STARTUP,
ATTR_BOOT, ATTR_MAP_SSL, ATTR_MAP_CONFIG, ATTR_OPTIONS,
ATTR_PORTS, STARTUP_ONCE, STARTUP_AFTER, STARTUP_BEFORE, BOOT_AUTO,
BOOT_MANUAL, ATTR_SCHEMA, ATTR_IMAGE)
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)
V_STR = 'str'
V_INT = 'int'
@@ -27,8 +27,9 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Required(ATTR_BOOT):
vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_PORTS): dict,
vol.Optional(ATTR_MAP_CONFIG, default=False): vol.Boolean(),
vol.Optional(ATTR_MAP_SSL, default=False): vol.Boolean(),
vol.Optional(ATTR_MAP, default=[]): [
vol.In([MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP])
],
vol.Required(ATTR_OPTIONS): dict,
vol.Required(ATTR_SCHEMA): {
vol.Coerce(str): vol.Any(ADDON_ELEMENT, [
@@ -36,7 +37,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
])
},
vol.Optional(ATTR_IMAGE): vol.Match(r"\w*/\w*"),
})
}, extra=vol.ALLOW_EXTRA)
def validate_options(raw_schema):

View File

@@ -30,16 +30,16 @@ class RestAPI(object):
api_host = APIHost(self.config, self.loop, host_control)
self.webapp.router.add_get('/host/info', api_host.info)
self.webapp.router.add_get('/host/reboot', api_host.reboot)
self.webapp.router.add_get('/host/shutdown', api_host.shutdown)
self.webapp.router.add_get('/host/update', api_host.update)
self.webapp.router.add_post('/host/reboot', api_host.reboot)
self.webapp.router.add_post('/host/shutdown', api_host.shutdown)
self.webapp.router.add_post('/host/update', api_host.update)
def register_network(self, host_control):
"""Register network function."""
api_net = APINetwork(self.config, self.loop, host_control)
self.webapp.router.add_get('/network/info', api_net.info)
self.webapp.router.add_get('/network/options', api_net.options)
self.webapp.router.add_post('/network/options', api_net.options)
def register_supervisor(self, supervisor, addons, host_control):
"""Register supervisor function."""
@@ -48,9 +48,11 @@ 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/update', api_supervisor.update)
self.webapp.router.add_get('/supervisor/reload', api_supervisor.reload)
self.webapp.router.add_get(
self.webapp.router.add_post(
'/supervisor/update', api_supervisor.update)
self.webapp.router.add_post(
'/supervisor/reload', api_supervisor.reload)
self.webapp.router.add_post(
'/supervisor/options', api_supervisor.options)
self.webapp.router.add_get('/supervisor/logs', api_supervisor.logs)
@@ -59,7 +61,7 @@ class RestAPI(object):
api_hass = APIHomeAssistant(self.config, self.loop, dock_homeassistant)
self.webapp.router.add_get('/homeassistant/info', api_hass.info)
self.webapp.router.add_get('/homeassistant/update', api_hass.update)
self.webapp.router.add_post('/homeassistant/update', api_hass.update)
self.webapp.router.add_get('/homeassistant/logs', api_hass.logs)
def register_addons(self, addons):
@@ -67,14 +69,15 @@ class RestAPI(object):
api_addons = APIAddons(self.config, self.loop, addons)
self.webapp.router.add_get('/addons/{addon}/info', api_addons.info)
self.webapp.router.add_get(
self.webapp.router.add_post(
'/addons/{addon}/install', api_addons.install)
self.webapp.router.add_get(
self.webapp.router.add_post(
'/addons/{addon}/uninstall', api_addons.uninstall)
self.webapp.router.add_get('/addons/{addon}/start', api_addons.start)
self.webapp.router.add_get('/addons/{addon}/stop', api_addons.stop)
self.webapp.router.add_get('/addons/{addon}/update', api_addons.update)
self.webapp.router.add_get(
self.webapp.router.add_post('/addons/{addon}/start', api_addons.start)
self.webapp.router.add_post('/addons/{addon}/stop', api_addons.stop)
self.webapp.router.add_post(
'/addons/{addon}/update', api_addons.update)
self.webapp.router.add_post(
'/addons/{addon}/options', api_addons.options)
self.webapp.router.add_get('/addons/{addon}/logs', api_addons.logs)

View File

@@ -5,7 +5,8 @@ import voluptuous as vol
from .util import api_process_hostcontrol, api_process, api_validate
from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_TYPE, ATTR_HOSTNAME, ATTR_FEATURES)
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_TYPE, ATTR_HOSTNAME, ATTR_FEATURES,
ATTR_OS)
_LOGGER = logging.getLogger(__name__)
@@ -32,6 +33,7 @@ class APIHost(object):
ATTR_LAST_VERSION: self.host_control.last,
ATTR_FEATURES: self.host_control.features,
ATTR_HOSTNAME: self.host_control.hostname,
ATTR_OS: self.host_control.os_info,
}
@api_process_hostcontrol

View File

@@ -7,13 +7,14 @@ 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)
HASSIO_VERSION, ATTR_ADDONS_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()],
})
SCHEMA_VERSION = vol.Schema({
@@ -45,6 +46,7 @@ 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),
}
@api_process
@@ -55,7 +57,19 @@ class APISupervisor(object):
if ATTR_BETA_CHANNEL in body:
self.config.upstream_beta = body[ATTR_BETA_CHANNEL]
return self.config.save()
if ATTR_ADDONS_REPOSITORIES in body:
new = set(body[ATTR_ADDONS_REPOSITORIES])
old = set(self.config.addons_repositories)
# add new repositories
for url in set(new - old):
await self.addons.add_custom_repository(url)
# remove old repositories
for url in set(old - new):
self.addons.drop_custom_repository(url)
return True
@api_process
async def update(self, request):

View File

@@ -81,7 +81,7 @@ def api_return_error(message=None):
return web.json_response({
JSON_RESULT: RESULT_ERROR,
JSON_MESSAGE: message,
})
}, status=400)
def api_return_ok(data=None):

View File

@@ -1,7 +1,11 @@
"""Bootstrap HassIO."""
import logging
import json
import os
import voluptuous as vol
from voluptuous.humanize import humanize_error
from .const import FILE_HASSIO_CONFIG, HASSIO_SHARE
from .tools import (
fetch_current_versions, write_json_file, read_json_file)
@@ -19,12 +23,32 @@ HASSIO_CLEANUP = 'hassio_cleanup'
ADDONS_REPO = "{}/addons"
ADDONS_DATA = "{}/addons_data"
ADDONS_CUSTOM = "{}/addons_custom"
ADDONS_CUSTOM_LIST = 'addons_custom_list'
BACKUP_DATA = "{}/backup"
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),
vol.Optional(HASSIO_LAST): vol.Coerce(str),
vol.Optional(HASSIO_CLEANUP): vol.Coerce(str),
vol.Optional(ADDONS_CUSTOM_LIST, default=[]): [vol.Url()],
}, extra=vol.REMOVE_EXTRA)
class Config(object):
"""Hold all config data."""
@@ -37,13 +61,14 @@ class Config(object):
if os.path.isfile(self._filename):
try:
self._data = read_json_file(self._filename)
except OSError:
except (OSError, json.JSONDecodeError):
_LOGGER.warning("Can't read %s", self._filename)
self._data = {}
def save(self):
"""Store data to config file."""
if not write_json_file(self._filename, self._data):
_LOGGER.exception("Can't store config in %s", self._filename)
_LOGGER.error("Can't store config in %s", self._filename)
return False
return True
@@ -57,13 +82,13 @@ class CoreConfig(Config):
super().__init__(FILE_HASSIO_CONFIG)
# init data
if not self._data:
self._data.update({
HOMEASSISTANT_IMAGE: os.environ['HOMEASSISTANT_REPOSITORY'],
UPSTREAM_BETA: False,
})
# validate data
try:
self._data = SCHEMA_CONFIG(self._data)
self.save()
except vol.Invalid as ex:
_LOGGER.warning(
"Invalid config %s", humanize_error(self._data, ex))
async def fetch_update_infos(self):
"""Read current versions from web."""
@@ -93,7 +118,7 @@ class CoreConfig(Config):
@property
def upstream_beta(self):
"""Return True if we run in beta upstream."""
return self._data.get(UPSTREAM_BETA, False)
return self._data[UPSTREAM_BETA]
@upstream_beta.setter
def upstream_beta(self, value):
@@ -164,6 +189,11 @@ class CoreConfig(Config):
"""Return path for customs addons."""
return ADDONS_CUSTOM.format(HASSIO_SHARE)
@property
def path_addons_custom_docker(self):
"""Return path for customs addons."""
return ADDONS_CUSTOM.format(self.path_hassio_docker)
@property
def path_addons_data(self):
"""Return root addon data folder."""
@@ -173,3 +203,35 @@ class CoreConfig(Config):
def path_addons_data_docker(self):
"""Return root addon data folder extern for docker."""
return ADDONS_DATA.format(self.path_hassio_docker)
@property
def path_backup(self):
"""Return root backup data folder."""
return BACKUP_DATA.format(HASSIO_SHARE)
@property
def path_backup_docker(self):
"""Return root backup data folder extern for docker."""
return BACKUP_DATA.format(self.path_hassio_docker)
@property
def addons_repositories(self):
"""Return list of addons custom repositories."""
return self._data[ADDONS_CUSTOM_LIST]
@addons_repositories.setter
def addons_repositories(self, repo):
"""Add a custom repository to list."""
if repo in self._data[ADDONS_CUSTOM_LIST]:
return
self._data[ADDONS_CUSTOM_LIST].append(repo)
self.save()
def drop_addon_repository(self, repo):
"""Remove a custom repository from list."""
if repo not in self._data[ADDONS_CUSTOM_LIST]:
return
self._data[ADDONS_CUSTOM_LIST].remove(repo)
self.save()

View File

@@ -1,14 +1,14 @@
"""Const file for HassIO."""
HASSIO_VERSION = '0.15'
HASSIO_VERSION = '0.18'
URL_HASSIO_VERSION = \
'https://raw.githubusercontent.com/pvizeli/hassio/master/version.json'
URL_HASSIO_VERSION_BETA = \
'https://raw.githubusercontent.com/pvizeli/hassio/master/version_beta.json'
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')
URL_HASSIO_ADDONS = 'https://github.com/pvizeli/hassio-addons'
URL_HASSIO_ADDONS = 'https://github.com/home-assistant/hassio-addons'
DOCKER_REPO = "pvizeli"
DOCKER_REPO = "homeassistant"
HASSIO_SHARE = "/data"
@@ -32,6 +32,7 @@ RESULT_ERROR = 'error'
RESULT_OK = 'ok'
ATTR_HOSTNAME = 'hostname'
ATTR_OS = 'os'
ATTR_TYPE = 'type'
ATTR_FEATURES = 'features'
ATTR_ADDONS = 'addons'
@@ -44,14 +45,14 @@ ATTR_DESCRIPTON = 'description'
ATTR_STARTUP = 'startup'
ATTR_BOOT = 'boot'
ATTR_PORTS = 'ports'
ATTR_MAP_CONFIG = 'map_config'
ATTR_MAP_SSL = 'map_ssl'
ATTR_MAP = 'map'
ATTR_OPTIONS = 'options'
ATTR_INSTALLED = 'installed'
ATTR_DEDICATED = 'dedicated'
ATTR_STATE = 'state'
ATTR_SCHEMA = 'schema'
ATTR_IMAGE = 'image'
ATTR_ADDONS_REPOSITORIES = 'addons_repositories'
STARTUP_BEFORE = 'before'
STARTUP_AFTER = 'after'
@@ -62,3 +63,8 @@ BOOT_MANUAL = 'manual'
STATE_STARTED = 'started'
STATE_STOPPED = 'stopped'
MAP_CONFIG = 'config'
MAP_SSL = 'ssl'
MAP_ADDONS = 'addons'
MAP_BACKUP = 'backup'

View File

@@ -26,6 +26,40 @@ class DockerAddon(DockerBase):
"""Return name of docker container."""
return "addon_{}".format(self.addon)
@property
def volumes(self):
"""Generate volumes for mappings."""
volumes = {
self.addons_data.path_data_docker(self.addon): {
'bind': '/data', 'mode': 'rw'
}}
if self.addons_data.map_config(self.addon):
volumes.update({
self.config.path_config_docker: {
'bind': '/config', 'mode': 'rw'
}})
if self.addons_data.map_ssl(self.addon):
volumes.update({
self.config.path_ssl_docker: {
'bind': '/ssl', 'mode': 'rw'
}})
if self.addons_data.map_addons(self.addon):
volumes.update({
self.config.path_addons_custom_docker: {
'bind': '/addons', 'mode': 'rw'
}})
if self.addons_data.map_backup(self.addon):
volumes.update({
self.config.path_backup_docker: {
'bind': '/backup', 'mode': 'rw'
}})
return volumes
def _run(self):
"""Run docker image.
@@ -37,22 +71,6 @@ class DockerAddon(DockerBase):
# cleanup old container
self._stop()
# volumes
volumes = {
self.addons_data.path_data_docker(self.addon): {
'bind': '/data', 'mode': 'rw'
}}
if self.addons_data.need_config(self.addon):
volumes.update({
self.config.path_config_docker: {
'bind': '/config', 'mode': 'rw'
}})
if self.addons_data.need_ssl(self.addon):
volumes.update({
self.config.path_ssl_docker: {
'bind': '/ssl', 'mode': 'rw'
}})
try:
self.container = self.dock.containers.run(
self.image,
@@ -60,7 +78,7 @@ class DockerAddon(DockerBase):
detach=True,
network_mode='bridge',
ports=self.addons_data.get_ports(self.addon),
volumes=volumes,
volumes=self.volumes,
)
self.version = get_version_from_env(

View File

@@ -9,7 +9,7 @@ import async_timeout
from .const import (
SOCKET_HC, ATTR_LAST_VERSION, ATTR_VERSION, ATTR_TYPE, ATTR_FEATURES,
ATTR_HOSTNAME)
ATTR_HOSTNAME, ATTR_OS)
_LOGGER = logging.getLogger(__name__)
@@ -35,6 +35,7 @@ class HostControl(object):
self.type = UNKNOWN
self.features = []
self.hostname = UNKNOWN
self.os_info = UNKNOWN
mode = os.stat(SOCKET_HC)[stat.ST_MODE]
if stat.S_ISSOCK(mode):
@@ -94,6 +95,7 @@ class HostControl(object):
self.type = info.get(ATTR_TYPE, UNKNOWN)
self.features = info.get(ATTR_FEATURES, [])
self.hostname = info.get(ATTR_HOSTNAME, UNKNOWN)
self.os_info = info.get(ATTR_OS, UNKNOWN)
def reboot(self):
"""Reboot the host system.

View File

@@ -1,12 +1,7 @@
{
"hassio_tag": "0.14",
"homeassistant_tag": "0.43.1",
"resinos_version": "0.4",
"resinhup_version": "0.1",
"generic_hc_version": "0.1",
"hassio": "0.14",
"hassio": "0.17",
"homeassistant": "0.43.1",
"resinos": "0.4",
"resinhup": "0.1",
"generic": "0.1"
"generic": "0.3"
}

View File

@@ -1,12 +1,7 @@
{
"hassio_tag": "0.15",
"homeassistant_tag": "0.43.1",
"resinos_version": "0.4",
"resinhup_version": "0.1",
"generic_hc_version": "0.1",
"hassio": "0.15",
"hassio": "0.17",
"homeassistant": "0.43.1",
"resinos": "0.4",
"resinhup": "0.1",
"generic": "0.1"
"generic": "0.3"
}