Compare commits

...

83 Commits
0.5 ... 0.14

Author SHA1 Message Date
Pascal Vizeli
3b8b44fcb7 Store update version for addons (#13) 2017-04-26 00:07:09 +02:00
Pascal Vizeli
22e3d50203 Update homeassistant image 2017-04-25 21:40:01 +02:00
Pascal Vizeli
2090b336e4 Update hassio 0.13 2017-04-25 18:38:56 +02:00
Pascal Vizeli
a028f11a4a Change handling of addon config (#12)
Add an optional extended description…
2017-04-25 18:36:40 +02:00
Pascal Vizeli
4edd02ca8e Update HassIO 0.13 2017-04-25 17:59:30 +02:00
Pascal Vizeli
85dad8c077 Update README.md 2017-04-25 14:21:30 +02:00
Pascal Vizeli
fff2ac3c47 Update HassIO to v0.12 2017-04-24 12:11:21 +02:00
Pascal Vizeli
f99c6335c0 Extend API to view logs from docker (#11)
* Extend API to view logs from docker

* Pump version

* Fix lint

* Change to raw api output

* Fix aiohttp response

* Fix aiohttp response p2

* Fix body convert

* Add attach to docker addon
2017-04-24 12:00:44 +02:00
Pascal Vizeli
98f0ff2a01 Update to hassio 0.12 2017-04-24 10:34:34 +02:00
Pascal Vizeli
43c5fcd159 Update README.md 2017-04-24 00:56:14 +02:00
Pascal Vizeli
784b57622c fix name 2017-04-23 23:27:15 +02:00
Pascal Vizeli
d8d17998fc add generic update function 2017-04-23 21:59:02 +02:00
Pascal Vizeli
bf4984e66b Update HassIO 0.11 2017-04-23 16:35:11 +02:00
Pascal Vizeli
297f4af047 Cleanup old stuff / make more secure (#10)
* Cleanup old stuff / make more secure

* pump version
2017-04-23 15:45:03 +02:00
Pascal Vizeli
f8574a01f6 Update README.md 2017-04-23 10:59:47 +02:00
Pascal Vizeli
610441f454 add ssl config 2017-04-23 00:48:40 +02:00
Pascal Vizeli
fb9780c1f2 update 0.43 2017-04-22 19:32:09 +02:00
Pascal Vizeli
7804c5fd6c Update 0.43 2017-04-22 19:31:35 +02:00
Pascal Vizeli
c711632b3e Update version_beta.json 2017-04-21 17:09:55 +02:00
Pascal Vizeli
43e245fb84 Update version.json 2017-04-21 17:09:37 +02:00
Pascal Vizeli
5a4d7c5b21 Update README.md 2017-04-21 17:09:20 +02:00
Pascal Vizeli
67e2ad99c9 Update version.json 2017-04-21 14:13:43 +02:00
Pascal Vizeli
ea2edadac2 Pvizeli patch 1 (#9)
* Update __init__.py

* Update core.py
2017-04-21 12:38:13 +02:00
Pascal Vizeli
1e78c60a65 Fix bug 2017-04-21 12:10:03 +02:00
Pascal Vizeli
5f3147d6f4 HassIO v0.10 2017-04-21 11:33:55 +02:00
Pascal Vizeli
03c3c9b6a1 Add reload api call to supervisor (#8)
* Add reload api call to supervisor

* Pump version
2017-04-21 11:30:22 +02:00
Pascal Vizeli
f056d175b7 Update version_beta.json 2017-04-21 10:44:41 +02:00
Pascal Vizeli
9fb1aa626d Update HomeAssistant to 0.42.4 2017-04-21 10:44:21 +02:00
Pascal Vizeli
30243c39e6 Update version.json 2017-04-20 17:05:19 +02:00
Pascal Vizeli
d285fd4ad4 Fix handling with docker container (#7)
* Fix handling with docker container

* Fix lint

* update version

* fix lint v2

* fix signal handling

* fix log output
2017-04-20 14:59:03 +02:00
Pascal Vizeli
7a0b9cc1ac Update version_beta.json 2017-04-20 09:50:58 +02:00
Pascal Vizeli
cc63008a86 Add selfupdate task (#6)
Add an optional extended description…
2017-04-19 17:51:48 +02:00
Pascal Vizeli
f9c7371140 Extend addons options to allow lists (#5)
Extend addons options to allow lists
2017-04-19 17:07:24 +02:00
Pascal Vizeli
71590f90ae Merge pull request #4 from pvizeli/selfupdate
Allow supervisor to update itself.
2017-04-18 17:48:55 +02:00
pvizeli
e1028d6eca update version 2017-04-18 17:47:28 +02:00
pvizeli
f231d54daa Use supervisor version 2017-04-18 17:46:21 +02:00
pvizeli
094c5968f4 Fix cleanup name 2017-04-18 17:18:32 +02:00
pvizeli
6c217d506c update version 2017-04-18 16:28:20 +02:00
pvizeli
0d867af79f Fix lint 2017-04-18 16:24:33 +02:00
pvizeli
c9876988da Add cleanup 2017-04-18 16:20:19 +02:00
pvizeli
454d82d985 Cleanups 2017-04-18 12:35:32 +02:00
pvizeli
14ee26ea29 fix lint 2017-04-18 12:34:15 +02:00
pvizeli
86a7f11f64 Allow supervisor to update itself. 2017-04-18 12:29:43 +02:00
Pascal Vizeli
78d1e1d9e7 Update README.md 2017-04-18 00:31:23 +02:00
Pascal Vizeli
a3f67809a6 Merge pull request #3 from pvizeli/addons
Addon support
2017-04-18 00:28:59 +02:00
Pascal Vizeli
e0be15cb45 update version 2017-04-18 00:26:50 +02:00
Pascal Vizeli
f1ce5faf17 update version in config 2017-04-17 23:49:52 +02:00
Pascal Vizeli
322480bba1 Fix bug with check installed 2017-04-17 23:31:49 +02:00
Pascal Vizeli
d2db89a665 Fix cleanup 2017-04-17 22:36:13 +02:00
Pascal Vizeli
fc17893158 Fix cleanup 2017-04-17 22:34:35 +02:00
Pascal Vizeli
e2bf267713 fix addon name 2017-04-17 22:04:24 +02:00
Pascal Vizeli
e25d30af52 fix lint 2017-04-17 18:54:09 +02:00
Pascal Vizeli
2bd1636097 change handling with arch detection 2017-04-17 18:50:05 +02:00
Pascal Vizeli
c019d1f3c5 Fix api function names 2017-04-17 18:12:58 +02:00
Pascal Vizeli
5b23347563 fix addon name 2017-04-17 17:36:29 +02:00
Pascal Vizeli
daab4a86b2 fix validate schema 2017-04-17 00:31:34 +02:00
Pascal Vizeli
5831177fd8 fix git repo open 2017-04-17 00:03:57 +02:00
Pascal Vizeli
f9500f6d90 fix fetch time bug 2017-04-16 23:32:23 +02:00
Pascal Vizeli
29ac861b87 fix bug 2017-04-16 23:00:12 +02:00
Pascal Vizeli
b05f2db023 fix lint 2017-04-16 22:49:28 +02:00
Pascal Vizeli
8af1dfc882 fix lint 2017-04-16 22:47:33 +02:00
Pascal Vizeli
c76e851029 fix lint 2017-04-16 22:42:57 +02:00
Pascal Vizeli
b5ec1e0cfd add support for custom image 2017-04-16 11:39:54 +02:00
Pascal Vizeli
fe72e768ec fix lint 2017-04-14 01:16:43 +02:00
Pascal Vizeli
360f546ab0 add log output 2017-04-13 23:45:48 +02:00
Pascal Vizeli
eb0ee31b5a update version 2017-04-13 23:09:46 +02:00
pvizeli
62df079be7 Fix lint 2017-04-13 23:09:46 +02:00
pvizeli
40e5e6eb9d some cleanups / auto remove old addons. 2017-04-13 23:09:46 +02:00
pvizeli
dbc080c24d Add automode 2017-04-13 23:09:45 +02:00
Pascal Vizeli
f340a19e40 fix lint 2017-04-13 23:09:45 +02:00
pvizeli
20856126c8 Finish api, add options validate 2017-04-13 23:09:45 +02:00
Pascal Vizeli
3ef76a4ada Add first part of api 2017-04-13 23:09:45 +02:00
pvizeli
14500d3ac4 Cleanups 2017-04-13 23:09:45 +02:00
Pascal Vizeli
318ca828cc Add install/uninstall to manager 2017-04-13 23:09:45 +02:00
Pascal Vizeli
5c70d68262 Addons config support 2017-04-13 23:09:45 +02:00
pvizeli
082770256b save, for next working too 2017-04-13 23:09:45 +02:00
pvizeli
ae003e5b76 Add first version of docker for addons 2017-04-13 23:09:45 +02:00
pvizeli
530f17d502 Add support for addon config. 2017-04-13 23:09:45 +02:00
Pascal Vizeli
f127de8059 Add support for git repo 2017-04-13 23:09:45 +02:00
Pascal Vizeli
9afb136648 Bugfix 2017-04-13 23:03:34 +02:00
Pascal Vizeli
07239fec08 Update version_beta.json 2017-04-13 22:47:08 +02:00
Pascal Vizeli
23661dc2fd Update version_beta.json 2017-04-13 22:17:25 +02:00
Pascal Vizeli
de34c058a1 Update version.json 2017-04-13 22:16:56 +02:00
27 changed files with 1427 additions and 167 deletions

106
API.md
View File

@@ -1,43 +1,5 @@
# HassIO Server
## Host Controll
Communicate over unix socket with a host daemon.
- commands
```
# info
-> {'os', 'version', 'current', 'level', 'hostname'}
# reboot
# shutdown
# host-update [v]
# supervisor-update [v]
# network info
# network hostname xy
# network wlan ssd xy
# network wlan password xy
# network int ip xy
# network int netmask xy
# network int route xy
```
level:
- 1: power functions
- 2: supervisor update
- 4: host update
- 8: network functions
Answer:
```
{}|OK|ERROR|WRONG
```
- {}: json
- OK: call was successfully
- ERROR: error on call
- WRONG: not supported
## HassIO REST API
Interface for HomeAssistant to controll things from supervisor.
@@ -69,7 +31,16 @@ On success
"version": "INSTALL_VERSION",
"current": "CURRENT_VERSION",
"beta": "true|false",
"addons": {}
"addons": [
{
"name": "xy bla",
"slug": "xy",
"version": "CURRENT_VERSION",
"installed": "none|INSTALL_VERSION",
"dedicated": "bool",
"description": "description"
}
]
}
```
@@ -88,6 +59,14 @@ Optional:
}
```
- `/supervisor/reload`
Reload addons/version.
- `/supervisor/logs`
Output the raw docker log
### Host
- `/host/shutdown`
@@ -149,6 +128,10 @@ Optional:
}
```
- `/homeassistant/logs`
Output the raw docker log
### REST API addons
- `/addons/{addon}/info`
@@ -164,7 +147,10 @@ Optional:
- `/addons/{addon}/options`
```json
{ }
{
"boot": "auto|manual",
"options": {},
}
```
- `/addons/{addon}/start`
@@ -188,3 +174,43 @@ Optional:
"version": "VERSION"
}
```
- `/addons/{addon}/logs`
Output the raw docker log
## Host Controll
Communicate over unix socket with a host daemon.
- commands
```
# info
-> {'os', 'version', 'current', 'level', 'hostname'}
# reboot
# shutdown
# host-update [v]
# network info
# network hostname xy
# network wlan ssd xy
# network wlan password xy
# network int ip xy
# network int netmask xy
# network int route xy
```
level:
- 1: power functions
- 2: host update
- 4: network functions
Answer:
```
{}|OK|ERROR|WRONG
```
- {}: json
- OK: call was successfully
- ERROR: error on call
- WRONG: not supported

View File

@@ -3,15 +3,25 @@ First private cloud solution for home automation.
It is a docker image (supervisor) they manage HomeAssistant docker and give a interface to controll 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/pvizeli/hassio-addons) | [HassIO-Build](https://github.com/pvizeli/hassio-build)
## History
- **0.1**: Initial supervisor with setup HomeAssistant docker
- **0.2**: Support for basic HostControll
- **0.3**: Refactor code and add basic rest api
- **0.4**: Move network api code / ssl folder
- **0.5**: Make api compatible to hass component v1
**HassIO is at the moment on development and not ready to use productive!**
## Feature in progress
- Backup/Restore
- MQTT addon
- DHCP-Server addon
# HomeAssistant
## SSL
All addons they can create SSL certs do that in same schema. So you can put follow lines to your `configuration.yaml`.
```yaml
http:
ssl_certificate: /ssl/fullchain.pem
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.
@@ -24,6 +34,7 @@ After extracting the archive, flash it to a drive using [Etcher](https://etcher.
- **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 HostControll and feature for HassIO 0.3 (ResinOS 2.0.0 / need reflash)
- **0.4**: Update HostControll 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).
@@ -38,3 +49,8 @@ Read logoutput from supervisor:
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/pvizeli/hassio-build/tree/master/install

View File

@@ -1,7 +1,7 @@
"""Main file for HassIO."""
import asyncio
import logging
import signal
import sys
import hassio.bootstrap as bootstrap
import hassio.core as core
@@ -23,14 +23,11 @@ if __name__ == "__main__":
loop.run_until_complete(hassio.setup())
_LOGGER.info("Start Hassio task")
loop.call_soon_threadsafe(asyncio.ensure_future, hassio.start(), loop)
try:
loop.add_signal_handler(
signal.SIGTERM, lambda: loop.create_task(hassio.stop()))
except ValueError:
_LOGGER.warning("Could not bind to SIGTERM")
loop.call_soon_threadsafe(loop.create_task, hassio.start())
loop.call_soon_threadsafe(bootstrap.reg_signal, loop, hassio)
loop.run_forever()
loop.close()
_LOGGER.info("Close Hassio")
sys.exit(hassio.exit_code)

164
hassio/addons/__init__.py Normal file
View File

@@ -0,0 +1,164 @@
"""Init file for HassIO addons."""
import asyncio
import logging
import os
import shutil
from .data import AddonsData
from .git import AddonsRepo
from ..const import STATE_STOPPED, STATE_STARTED
from ..dock.addon import DockerAddon
_LOGGER = logging.getLogger(__name__)
class AddonManager(AddonsData):
"""Manage addons inside HassIO."""
def __init__(self, config, loop, dock):
"""Initialize docker base wrapper."""
super().__init__(config)
self.loop = loop
self.dock = dock
self.repo = AddonsRepo(config, loop)
self.dockers = {}
async def prepare(self, arch):
"""Startup addon management."""
self.arch = arch
# load addon repository
if await self.repo.load():
self.read_addons_repo()
# load installed addons
for addon in self.list_installed:
self.dockers[addon] = DockerAddon(
self.config, self.loop, self.dock, self, addon)
await self.dockers[addon].attach()
async def reload(self):
"""Update addons from repo and reload list."""
if not await self.repo.pull():
return
self.read_addons_repo()
# remove stalled addons
for addon in self.list_removed:
_LOGGER.warning("Dedicated addon '%s' found!", addon)
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)))
_LOGGER.info("Startup %s run %d addons", start_type, len(tasks))
if tasks:
await asyncio.wait(tasks, loop=self.loop)
async def install(self, addon, version=None):
"""Install a addon."""
if not self.exists_addon(addon):
_LOGGER.error("Addon %s not exists for install", addon)
return False
if self.is_installed(addon):
_LOGGER.error("Addon %s is already installed", addon)
return False
if not os.path.isdir(self.path_data(addon)):
_LOGGER.info("Create Home-Assistant addon data folder %s",
self.path_data(addon))
os.mkdir(self.path_data(addon))
addon_docker = DockerAddon(
self.config, self.loop, self.dock, self, addon)
version = version or self.get_version(addon)
if not await addon_docker.install(version):
return False
self.dockers[addon] = addon_docker
self.set_addon_install(addon, version)
return True
async def uninstall(self, addon):
"""Remove a addon."""
if not self.is_installed(addon):
_LOGGER.error("Addon %s is already uninstalled", addon)
return False
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return False
if not await self.dockers[addon].remove():
return False
if os.path.isdir(self.path_data(addon)):
_LOGGER.info("Remove Home-Assistant addon data folder %s",
self.path_data(addon))
shutil.rmtree(self.path_data(addon))
self.dockers.pop(addon)
self.set_addon_uninstall(addon)
return True
async def state(self, addon):
"""Return running state of addon."""
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return
if await self.dockers[addon].is_running():
return STATE_STARTED
return STATE_STOPPED
async def start(self, addon):
"""Set options and start addon."""
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return False
if not self.write_addon_options(addon):
_LOGGER.error("Can't write options for addon %s", addon)
return False
return await self.dockers[addon].run()
async def stop(self, addon):
"""Stop addon."""
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return False
return await self.dockers[addon].stop()
async def update(self, addon, version=None):
"""Update addon."""
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return False
version = version or self.get_version(addon)
is_running = self.dockers[addon].is_running()
# update
if await self.dockers[addon].update(version):
self.set_addon_update(addon, version)
if is_running:
await self.start(addon)
return True
return False
async def logs(self, addon):
"""Return addons log output."""
if addon not in self.dockers:
_LOGGER.error("No docker found for addon %s", addon)
return False
return await self.dockers[addon].logs()

247
hassio/addons/data.py Normal file
View File

@@ -0,0 +1,247 @@
"""Init file for HassIO addons."""
import logging
import glob
import voluptuous as vol
from voluptuous.humanize import humanize_error
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)
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"
class AddonsData(Config):
"""Hold data for addons inside HassIO."""
def __init__(self, config):
"""Initialize data holder."""
super().__init__(FILE_HASSIO_ADDONS)
self.config = config
self._addons_data = self._data.get(SYSTEM, {})
self._user_data = self._data.get(USER, {})
self._current_data = {}
self.arch = None
def save(self):
"""Store data to config file."""
self._data = {
USER: self._user_data,
SYSTEM: self._addons_data,
}
super().save()
def read_addons_repo(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)
def _read_addons_folder(self, folder):
"""Read data from addons folder."""
pattern = ADDONS_REPO_PATTERN.format(folder)
for addon in glob.iglob(pattern):
try:
addon_config = read_json_file(addon)
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
self._current_data[addon_config[ATTR_SLUG]] = addon_config
except (OSError, KeyError):
_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))
@property
def list_installed(self):
"""Return a list of installed addons."""
return set(self._addons_data.keys())
@property
def list(self):
"""Return a list of available addons."""
data = []
all_addons = {**self._addons_data, **self._current_data}
dedicated = self.list_removed
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: values[ATTR_SLUG],
ATTR_DESCRIPTON: values[ATTR_DESCRIPTON],
ATTR_VERSION: values[ATTR_VERSION],
ATTR_INSTALLED: i_version,
ATTR_DEDICATED: addon in dedicated,
})
return data
def list_startup(self, start_type):
"""Get list of installed addon with need start by type."""
addon_list = set()
for addon in self._addons_data.keys():
if self.get_boot(addon) != BOOT_AUTO:
continue
try:
if self._addons_data[addon][ATTR_STARTUP] == start_type:
addon_list.add(addon)
except KeyError:
_LOGGER.warning("Orphaned addon detect %s", addon)
continue
return addon_list
@property
def list_removed(self):
"""Return local addons they not support from repo."""
addon_list = set()
for addon in self._addons_data.keys():
if addon not in self._current_data:
addon_list.add(addon)
return addon_list
def exists_addon(self, addon):
"""Return True if a addon exists."""
return addon in self._current_data or addon in self._addons_data
def is_installed(self, addon):
"""Return True if a addon is installed."""
return addon in self._addons_data
def version_installed(self, addon):
"""Return installed version."""
if ATTR_VERSION not in self._user_data[addon]:
return self._addons_data[addon][ATTR_VERSION]
return self._user_data[addon][ATTR_VERSION]
def set_addon_install(self, addon, version):
"""Set addon as installed."""
self._addons_data[addon] = self._current_data[addon]
self._user_data[addon] = {
ATTR_OPTIONS: {},
ATTR_VERSION: version,
}
self.save()
def set_addon_uninstall(self, addon):
"""Set addon as uninstalled."""
self._addons_data.pop(addon, None)
self._user_data.pop(addon, None)
self.save()
def set_addon_update(self, addon, version):
"""Update version of addon."""
self._addons_data[addon] = 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.save()
def set_boot(self, addon, boot):
"""Store user boot options."""
self._user_data[addon][ATTR_BOOT] = boot
self.save()
def get_options(self, addon):
"""Return options with local changes."""
return {
**self._addons_data[addon][ATTR_OPTIONS],
**self._user_data[addon][ATTR_OPTIONS],
}
def get_boot(self, addon):
"""Return boot config with prio local settings."""
if ATTR_BOOT in self._user_data[addon]:
return self._user_data[addon][ATTR_BOOT]
return self._addons_data[addon][ATTR_BOOT]
def get_name(self, addon):
"""Return name of addon."""
return self._addons_data[addon][ATTR_NAME]
def get_description(self, addon):
"""Return description of addon."""
return self._addons_data[addon][ATTR_DESCRIPTON]
def get_version(self, addon):
"""Return version of addon."""
if addon not in self._current_data:
return self.version_installed(addon)
return self._current_data[addon][ATTR_VERSION]
def get_ports(self, addon):
"""Return ports of addon."""
return self._addons_data[addon].get(ATTR_PORTS)
def get_image(self, addon):
"""Return image name of addon."""
addon_data = self._addons_data.get(addon, self._current_data[addon])
if ATTR_IMAGE not in addon_data:
return "{}/{}-addon-{}".format(DOCKER_REPO, self.arch, addon)
return addon_data[ATTR_IMAGE]
def need_config(self, addon):
"""Return True if config map is needed."""
return self._addons_data[addon][ATTR_MAP_CONFIG]
def need_ssl(self, addon):
"""Return True if ssl map is needed."""
return self._addons_data[addon][ATTR_MAP_SSL]
def path_data(self, addon):
"""Return addon data path inside supervisor."""
return "{}/{}".format(self.config.path_addons_data, addon)
def path_data_docker(self, addon):
"""Return addon data path external for docker."""
return "{}/{}".format(self.config.path_addons_data_docker, addon)
def path_addon_options(self, addon):
"""Return path to addons options."""
return "{}/options.json".format(self.path_data(addon))
def write_addon_options(self, addon):
"""Return True if addon options is written to data."""
schema = self.get_schema(addon)
options = self.get_options(addon)
try:
schema(options)
return write_json_file(self.path_addon_options(addon), options)
except vol.Invalid as ex:
_LOGGER.error("Addon %s have wrong options -> %s", addon,
humanize_error(options, ex))
return False
def get_schema(self, addon):
"""Create a schema for addon options."""
raw_schema = self._addons_data[addon][ATTR_SCHEMA]
schema = vol.Schema(vol.All(dict, validate_options(raw_schema)))
return schema

71
hassio/addons/git.py Normal file
View File

@@ -0,0 +1,71 @@
"""Init file for HassIO addons git."""
import asyncio
import logging
import os
import git
from ..const import URL_HASSIO_ADDONS
_LOGGER = logging.getLogger(__name__)
class AddonsRepo(object):
"""Manage addons git repo."""
def __init__(self, config, loop):
"""Initialize docker base wrapper."""
self.config = config
self.loop = loop
self.repo = None
self._lock = asyncio.Lock(loop=loop)
async def load(self):
"""Init git addon repo."""
if not os.path.isdir(self.config.path_addons_repo):
return await self.clone()
async with self._lock:
try:
_LOGGER.info("Load addons repository")
self.repo = await self.loop.run_in_executor(
None, git.Repo, self.config.path_addons_repo)
except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err:
_LOGGER.error("Can't load addons repo: %s.", err)
return False
return True
async def clone(self):
"""Clone git addon repo."""
async with self._lock:
try:
_LOGGER.info("Clone addons repository")
self.repo = await self.loop.run_in_executor(
None, git.Repo.clone_from, URL_HASSIO_ADDONS,
self.config.path_addons_repo)
except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err:
_LOGGER.error("Can't clone addons repo: %s.", err)
return False
return True
async def pull(self):
"""Pull git addon repo."""
if self._lock.locked():
_LOGGER.warning("It is already a task in progress.")
return False
async with self._lock:
try:
_LOGGER.info("Pull addons repository")
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)
return False
return True

112
hassio/addons/validate.py Normal file
View File

@@ -0,0 +1,112 @@
"""Validate addons options schema."""
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)
V_STR = 'str'
V_INT = 'int'
V_FLOAT = 'float'
V_BOOL = 'bool'
V_EMAIL = 'email'
V_URL = 'url'
ADDON_ELEMENT = vol.In([V_STR, V_INT, V_FLOAT, V_BOOL, V_EMAIL, V_URL])
# pylint: disable=no-value-for-parameter
SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Required(ATTR_NAME): vol.Coerce(str),
vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Required(ATTR_SLUG): vol.Coerce(str),
vol.Required(ATTR_DESCRIPTON): vol.Coerce(str),
vol.Required(ATTR_STARTUP):
vol.In([STARTUP_BEFORE, STARTUP_AFTER, STARTUP_ONCE]),
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.Required(ATTR_OPTIONS): dict,
vol.Required(ATTR_SCHEMA): {
vol.Coerce(str): vol.Any(ADDON_ELEMENT, [
vol.Any(ADDON_ELEMENT, {vol.Coerce(str): ADDON_ELEMENT})
])
},
vol.Optional(ATTR_IMAGE): vol.Match(r"\w*/\w*"),
})
def validate_options(raw_schema):
"""Validate schema."""
def validate(struct):
"""Create schema validator for addons options."""
options = {}
# read options
for key, value in struct.items():
if key not in raw_schema:
raise vol.Invalid("Unknown options {}.".format(key))
typ = raw_schema[key]
try:
if isinstance(typ, list):
# nested value
options[key] = _nested_validate(typ[0], value)
else:
# normal value
options[key] = _single_validate(typ, value)
except (IndexError, KeyError):
raise vol.Invalid(
"Type error for {}.".format(key)) from None
return options
return validate
# pylint: disable=no-value-for-parameter
def _single_validate(typ, value):
"""Validate a single element."""
try:
if typ == V_STR:
return str(value)
elif typ == V_INT:
return int(value)
elif typ == V_FLOAT:
return float(value)
elif typ == V_BOOL:
return vol.Boolean()(value)
elif typ == V_EMAIL:
return vol.Email()(value)
elif typ == V_URL:
return vol.Url()(value)
raise vol.Invalid("Fatal error for {}.".format(value))
except TypeError:
raise vol.Invalid(
"Type {} error for {}.".format(typ, value)) from None
def _nested_validate(typ, data_list):
"""Validate nested items."""
options = []
for element in data_list:
# dict list
if isinstance(typ, dict):
c_options = {}
for c_key, c_value in element.items():
if c_key not in typ:
raise vol.Invalid(
"Unknown nested options {}.".format(c_key))
c_options[c_key] = _single_validate(typ[c_key], c_value)
options.append(c_options)
# normal list
else:
options.append(_single_validate(typ, element))
return options

View File

@@ -3,10 +3,11 @@ import logging
from aiohttp import web
from .addons import APIAddons
from .homeassistant import APIHomeAssistant
from .host import APIHost
from .network import APINetwork
from .supervisor import APISupervisor
from .homeassistant import APIHomeAssistant
_LOGGER = logging.getLogger(__name__)
@@ -40,15 +41,18 @@ class RestAPI(object):
self.webapp.router.add_get('/network/info', api_net.info)
self.webapp.router.add_get('/network/options', api_net.options)
def register_supervisor(self, host_controll):
def register_supervisor(self, supervisor, addons):
"""Register supervisor function."""
api_supervisor = APISupervisor(self.config, self.loop, host_controll)
api_supervisor = APISupervisor(
self.config, self.loop, supervisor, addons)
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(
'/supervisor/options', api_supervisor.options)
self.webapp.router.add_get('/supervisor/logs', api_supervisor.logs)
def register_homeassistant(self, dock_homeassistant):
"""Register homeassistant function."""
@@ -56,6 +60,23 @@ class RestAPI(object):
self.webapp.router.add_get('/homeassistant/info', api_hass.info)
self.webapp.router.add_get('/homeassistant/update', api_hass.update)
self.webapp.router.add_get('/homeassistant/logs', api_hass.logs)
def register_addons(self, addons):
"""Register homeassistant function."""
api_addons = APIAddons(self.config, self.loop, addons)
self.webapp.router.add_get('/addons/{addon}/info', api_addons.info)
self.webapp.router.add_get(
'/addons/{addon}/install', api_addons.install)
self.webapp.router.add_get(
'/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(
'/addons/{addon}/options', api_addons.options)
self.webapp.router.add_get('/addons/{addon}/logs', api_addons.logs)
async def start(self):
"""Run rest api webserver."""

145
hassio/api/addons.py Normal file
View File

@@ -0,0 +1,145 @@
"""Init file for HassIO homeassistant rest api."""
import asyncio
import logging
import voluptuous as vol
from voluptuous.humanize import humanize_error
from .util import api_process, api_process_raw, api_validate
from ..const import (
ATTR_VERSION, ATTR_CURRENT, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS,
STATE_STOPPED, STATE_STARTED, BOOT_AUTO, BOOT_MANUAL)
_LOGGER = logging.getLogger(__name__)
SCHEMA_VERSION = vol.Schema({
vol.Optional(ATTR_VERSION): vol.Coerce(str),
})
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL])
})
class APIAddons(object):
"""Handle rest api for addons functions."""
def __init__(self, config, loop, addons):
"""Initialize homeassistant rest api part."""
self.config = config
self.loop = loop
self.addons = addons
def _extract_addon(self, request, check_installed=True):
"""Return addon and if not exists trow a exception."""
addon = request.match_info.get('addon')
# check data
if not self.addons.exists_addon(addon):
raise RuntimeError("Addon not exists")
if check_installed and not self.addons.is_installed(addon):
raise RuntimeError("Addon is not installed")
return addon
@api_process
async def info(self, request):
"""Return addon information."""
addon = self._extract_addon(request)
info = {
ATTR_VERSION: self.addons.version_installed(addon),
ATTR_CURRENT: self.addons.get_version(addon),
ATTR_STATE: await self.addons.state(addon),
ATTR_BOOT: self.addons.get_boot(addon),
ATTR_OPTIONS: self.addons.get_options(addon),
}
return info
@api_process
async def options(self, request):
"""Store user options for addon."""
addon = self._extract_addon(request)
options_schema = self.addons.get_schema(addon)
addon_schema = SCHEMA_OPTIONS.extend({
vol.Optional(ATTR_OPTIONS): options_schema,
})
addon_config = await api_validate(addon_schema, request)
if ATTR_OPTIONS in addon_config:
self.addons.set_options(addon, addon_config[ATTR_OPTIONS])
if ATTR_BOOT in addon_config:
self.addons.set_options(addon, addon_config[ATTR_BOOT])
return True
@api_process
async def install(self, request):
"""Install addon."""
body = await api_validate(SCHEMA_VERSION, request)
addon = self._extract_addon(request, check_installed=False)
version = body.get(
ATTR_VERSION, self.addons.get_version(addon))
return await asyncio.shield(
self.addons.install(addon, version), loop=self.loop)
@api_process
async def uninstall(self, request):
"""Uninstall addon."""
addon = self._extract_addon(request)
return await asyncio.shield(
self.addons.uninstall(addon), loop=self.loop)
@api_process
async def start(self, request):
"""Start addon."""
addon = self._extract_addon(request)
if await self.addons.state(addon) == STATE_STARTED:
raise RuntimeError("Addon is already running")
# validate options
try:
schema = self.addons.get_schema(addon)
options = self.addons.get_options(addon)
schema(options)
except vol.Invalid as ex:
raise RuntimeError(humanize_error(options, ex)) from None
return await asyncio.shield(
self.addons.start(addon), loop=self.loop)
@api_process
async def stop(self, request):
"""Stop addon."""
addon = self._extract_addon(request)
if await self.addons.state(addon) == STATE_STOPPED:
raise RuntimeError("Addon is already stoped")
return await asyncio.shield(
self.addons.stop(addon), loop=self.loop)
@api_process
async def update(self, request):
"""Update addon."""
body = await api_validate(SCHEMA_VERSION, request)
addon = self._extract_addon(request)
version = body.get(
ATTR_VERSION, self.addons.get_version(addon))
if version == self.addons.version_installed(addon):
raise RuntimeError("Version is already in use")
return await asyncio.shield(
self.addons.update(addon, version), loop=self.loop)
@api_process_raw
def logs(self, request):
"""Return logs from addon."""
addon = self._extract_addon(request)
return self.addons.logs(addon)

View File

@@ -4,7 +4,7 @@ import logging
import voluptuous as vol
from .util import api_process, api_validate
from .util import api_process, api_process_raw, api_validate
from ..const import ATTR_VERSION, ATTR_CURRENT
_LOGGER = logging.getLogger(__name__)
@@ -17,17 +17,17 @@ SCHEMA_VERSION = vol.Schema({
class APIHomeAssistant(object):
"""Handle rest api for homeassistant functions."""
def __init__(self, config, loop, dock_hass):
def __init__(self, config, loop, homeassistant):
"""Initialize homeassistant rest api part."""
self.config = config
self.loop = loop
self.dock_hass = dock_hass
self.homeassistant = homeassistant
@api_process
async def info(self, request):
"""Return host information."""
info = {
ATTR_VERSION: self.dock_hass.version,
ATTR_VERSION: self.homeassistant.version,
ATTR_CURRENT: self.config.current_homeassistant,
}
@@ -39,10 +39,19 @@ class APIHomeAssistant(object):
body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.config.current_homeassistant)
if self.dock_hass.in_progress:
raise RuntimeError("Other task is in progress.")
if self.homeassistant.in_progress:
raise RuntimeError("Other task is in progress")
if version == self.dock_hass.version:
raise RuntimeError("%s is already in use.", version)
if version == self.homeassistant.version:
raise RuntimeError("Version is already in use")
return await asyncio.shield(self.dock_hass.update(version))
return await asyncio.shield(
self.homeassistant.update(version), loop=self.loop)
@api_process_raw
def logs(self, request):
"""Return homeassistant docker logs.
Return a coroutine.
"""
return self.homeassistant.logs()

View File

@@ -56,6 +56,6 @@ class APIHost(object):
version = body.get(ATTR_VERSION)
if version == self.host_controll.version:
raise RuntimeError("%s is already in use.", version)
raise RuntimeError("Version is already in use")
return await self.host_controll.host_update(version=version)

View File

@@ -1,10 +1,12 @@
"""Init file for HassIO supervisor rest api."""
import asyncio
import logging
import voluptuous as vol
from .util import api_process, api_process_hostcontroll, api_validate
from ..const import ATTR_VERSION, ATTR_CURRENT, ATTR_BETA, HASSIO_VERSION
from .util import api_process, api_process_raw, api_validate
from ..const import (
ATTR_ADDONS, ATTR_VERSION, ATTR_CURRENT, ATTR_BETA, HASSIO_VERSION)
_LOGGER = logging.getLogger(__name__)
@@ -21,11 +23,12 @@ SCHEMA_VERSION = vol.Schema({
class APISupervisor(object):
"""Handle rest api for supervisor functions."""
def __init__(self, config, loop, host_controll):
def __init__(self, config, loop, supervisor, addons):
"""Initialize supervisor rest api part."""
self.config = config
self.loop = loop
self.host_controll = host_controll
self.supervisor = supervisor
self.addons = addons
@api_process
async def ping(self, request):
@@ -39,8 +42,8 @@ class APISupervisor(object):
ATTR_VERSION: HASSIO_VERSION,
ATTR_CURRENT: self.config.current_hassio,
ATTR_BETA: self.config.upstream_beta,
ATTR_ADDONS: self.addons.list,
}
return info
@api_process
@@ -53,13 +56,35 @@ class APISupervisor(object):
return self.config.save()
@api_process_hostcontroll
@api_process
async def update(self, request):
"""Update host OS."""
"""Update supervisor OS."""
body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.config.current_hassio)
if version == HASSIO_VERSION:
raise RuntimeError("%s is already in use.", version)
if version == self.supervisor.version:
raise RuntimeError("Version is already in use")
return await self.host_controll.supervisor_update(version=version)
return await asyncio.shield(
self.supervisor.update(version), loop=self.loop)
@api_process
async def reload(self, request):
"""Reload addons, config ect."""
tasks = [self.addons.reload(), self.config.fetch_update_infos()]
results, _ = await asyncio.shield(
asyncio.wait(tasks, loop=self.loop), loop=self.loop)
for result in results:
if result.exception() is not None:
raise RuntimeError("Some reload task fails!")
return True
@api_process_raw
def logs(self, request):
"""Return supervisor docker logs.
Return a coroutine.
"""
return self.supervisor.logs()

View File

@@ -62,6 +62,20 @@ def api_process_hostcontroll(method):
return wrap_hostcontroll
def api_process_raw(method):
"""Wrap function with raw output to rest api."""
async def wrap_api(api, *args, **kwargs):
"""Return api information."""
try:
message = await method(api, *args, **kwargs)
except RuntimeError as err:
message = str(err).encode()
return web.Response(body=message)
return wrap_api
def api_return_error(message=None):
"""Return a API error message."""
return web.json_response({
@@ -82,7 +96,7 @@ async def api_validate(schema, request):
"""Validate request data with schema."""
data = await request.json(loads=json_loads)
try:
schema(data)
data = schema(data)
except vol.Invalid as ex:
raise RuntimeError(humanize_error(data, ex)) from None

View File

@@ -2,6 +2,7 @@
import logging
import os
import stat
import signal
from colorlog import ColoredFormatter
@@ -26,6 +27,17 @@ def initialize_system_data(websession):
_LOGGER.info("Create Home-Assistant ssl folder %s", config.path_ssl)
os.mkdir(config.path_ssl)
# homeassistant addon data folder
if not os.path.isdir(config.path_addons_data):
_LOGGER.info("Create Home-Assistant addon data folder %s",
config.path_addons_data)
os.mkdir(config.path_addons_data)
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)
return config
@@ -70,3 +82,24 @@ def check_environment():
return False
return True
def reg_signal(loop, hassio):
"""Register SIGTERM, SIGKILL to stop system."""
try:
loop.add_signal_handler(
signal.SIGTERM, lambda: loop.create_task(hassio.stop()))
except (ValueError, RuntimeError):
_LOGGER.warning("Could not bind to SIGTERM")
try:
loop.add_signal_handler(
signal.SIGHUP, lambda: loop.create_task(hassio.stop()))
except (ValueError, RuntimeError):
_LOGGER.warning("Could not bind to SIGHUP")
try:
loop.add_signal_handler(
signal.SIGINT, lambda: loop.create_task(hassio.stop()))
except (ValueError, RuntimeError):
_LOGGER.warning("Could not bind to SIGINT")

View File

@@ -1,39 +1,62 @@
"""Bootstrap HassIO."""
import json
import logging
import os
from .const import FILE_HASSIO_CONFIG, HASSIO_SHARE
from .tools import fetch_current_versions
from .tools import (
fetch_current_versions, write_json_file, read_json_file)
_LOGGER = logging.getLogger(__name__)
HOMEASSISTANT_CONFIG = "{}/homeassistant_config"
HOMEASSISTANT_CONFIG = "{}/homeassistant"
HOMEASSISTANT_IMAGE = 'homeassistant_image'
HOMEASSISTANT_CURRENT = 'homeassistant_current'
HASSIO_SSL = "{}/ssl"
HASSIO_CURRENT = 'hassio_current'
HASSIO_CLEANUP = 'hassio_cleanup'
ADDONS_REPO = "{}/addons"
ADDONS_DATA = "{}/addons_data"
ADDONS_CUSTOM = "{}/addons_custom"
UPSTREAM_BETA = 'upstream_beta'
API_ENDPOINT = 'api_endpoint'
class CoreConfig(object):
class Config(object):
"""Hold all config data."""
def __init__(self, websession, config_file=FILE_HASSIO_CONFIG):
def __init__(self, config_file):
"""Initialize config object."""
self.websession = websession
self._filename = config_file
self._data = {}
# init or load data
if os.path.isfile(self._filename):
try:
with open(self._filename, 'r') as cfile:
self._data = json.loads(cfile.read())
self._data = read_json_file(self._filename)
except OSError:
_LOGGER.warning("Can't read %s", self._filename)
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)
return False
return True
class CoreConfig(Config):
"""Hold all core config data."""
def __init__(self, websession):
"""Initialize config object."""
self.websession = websession
super().__init__(FILE_HASSIO_CONFIG)
# init data
if not self._data:
self._data.update({
@@ -42,17 +65,6 @@ class CoreConfig(object):
})
self.save()
def save(self):
"""Store data to config file."""
try:
with open(self._filename, 'w') as conf_file:
conf_file.write(json.dumps(self._data))
except OSError:
_LOGGER.exception("Can't store config in %s", self._filename)
return False
return True
async def fetch_update_infos(self):
"""Read current versions from web."""
current = await fetch_current_versions(
@@ -68,6 +80,16 @@ class CoreConfig(object):
return False
@property
def api_endpoint(self):
"""Return IP address of api endpoint."""
return self._data[API_ENDPOINT]
@api_endpoint.setter
def api_endpoint(self, value):
"""Store IP address of api endpoint."""
self._data[API_ENDPOINT] = value
@property
def upstream_beta(self):
"""Return True if we run in beta upstream."""
@@ -78,6 +100,20 @@ class CoreConfig(object):
"""Set beta upstream mode."""
self._data[UPSTREAM_BETA] = bool(value)
@property
def hassio_cleanup(self):
"""Return Version they need to cleanup."""
return self._data.get(HASSIO_CLEANUP)
@hassio_cleanup.setter
def hassio_cleanup(self, version):
"""Set or remove cleanup flag."""
if version is None:
self._data.pop(HASSIO_CLEANUP, None)
else:
self._data[HASSIO_CLEANUP] = version
self.save()
@property
def homeassistant_image(self):
"""Return docker homeassistant repository."""
@@ -93,10 +129,15 @@ class CoreConfig(object):
"""Actual version of hassio."""
return self._data.get(HASSIO_CURRENT)
@property
def path_hassio_docker(self):
"""Return hassio data path extern for docker."""
return os.environ['SUPERVISOR_SHARE']
@property
def path_config_docker(self):
"""Return config path extern for docker."""
return HOMEASSISTANT_CONFIG.format(os.environ['SUPERVISOR_SHARE'])
return HOMEASSISTANT_CONFIG.format(self.path_hassio_docker)
@property
def path_config(self):
@@ -106,9 +147,29 @@ class CoreConfig(object):
@property
def path_ssl_docker(self):
"""Return SSL path extern for docker."""
return HASSIO_SSL.format(os.environ['SUPERVISOR_SHARE'])
return HASSIO_SSL.format(self.path_hassio_docker)
@property
def path_ssl(self):
"""Return SSL path inside supervisor."""
return HASSIO_SSL.format(HASSIO_SHARE)
@property
def path_addons_repo(self):
"""Return git repo path for addons."""
return ADDONS_REPO.format(HASSIO_SHARE)
@property
def path_addons_custom(self):
"""Return path for customs addons."""
return ADDONS_CUSTOM.format(HASSIO_SHARE)
@property
def path_addons_data(self):
"""Return root addon data folder."""
return ADDONS_DATA.format(HASSIO_SHARE)
@property
def path_addons_data_docker(self):
"""Return root addon data folder extern for docker."""
return ADDONS_DATA.format(self.path_hassio_docker)

View File

@@ -1,16 +1,22 @@
"""Const file for HassIO."""
HASSIO_VERSION = '0.5'
HASSIO_VERSION = '0.14'
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_ADDONS_REPO = 'https://github.com/pvizeli/hassio-addons'
URL_HASSIO_ADDONS = 'https://github.com/pvizeli/hassio-addons'
DOCKER_REPO = "pvizeli"
HASSIO_SHARE = "/data"
RUN_UPDATE_INFO_TASKS = 28800
RUN_UPDATE_SUPERVISOR_TASKS = 29100
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)
@@ -25,6 +31,31 @@ JSON_MESSAGE = 'message'
RESULT_ERROR = 'error'
RESULT_OK = 'ok'
ATTR_ADDONS = 'addons'
ATTR_VERSION = 'version'
ATTR_CURRENT = 'current'
ATTR_BETA = 'beta'
ATTR_NAME = 'name'
ATTR_SLUG = 'slug'
ATTR_DESCRIPTON = 'description'
ATTR_STARTUP = 'startup'
ATTR_BOOT = 'boot'
ATTR_PORTS = 'ports'
ATTR_MAP_CONFIG = 'map_config'
ATTR_MAP_SSL = 'map_ssl'
ATTR_OPTIONS = 'options'
ATTR_INSTALLED = 'installed'
ATTR_DEDICATED = 'dedicated'
ATTR_STATE = 'state'
ATTR_SCHEMA = 'schema'
ATTR_IMAGE = 'image'
STARTUP_BEFORE = 'before'
STARTUP_AFTER = 'after'
STARTUP_ONCE = 'once'
BOOT_AUTO = 'auto'
BOOT_MANUAL = 'manual'
STATE_STARTED = 'started'
STATE_STOPPED = 'stopped'

View File

@@ -6,12 +6,16 @@ import aiohttp
import docker
from . import bootstrap
from .addons import AddonManager
from .api import RestAPI
from .host_controll import HostControll
from .const import SOCKET_DOCKER, RUN_UPDATE_INFO_TASKS
from .const import (
SOCKET_DOCKER, RUN_UPDATE_INFO_TASKS, RUN_RELOAD_ADDONS_TASKS,
RUN_UPDATE_SUPERVISOR_TASKS, STARTUP_AFTER, STARTUP_BEFORE)
from .scheduler import Scheduler
from .dock.homeassistant import DockerHomeAssistant
from .dock.supervisor import DockerSupervisor
from .tools import get_arch_from_image, get_local_ip
_LOGGER = logging.getLogger(__name__)
@@ -21,6 +25,7 @@ class HassIO(object):
def __init__(self, loop):
"""Initialize hassio object."""
self.exit_code = 0
self.loop = loop
self.websession = aiohttp.ClientSession(loop=self.loop)
self.config = bootstrap.initialize_system_data(self.websession)
@@ -31,17 +36,24 @@ class HassIO(object):
# init basic docker container
self.supervisor = DockerSupervisor(
self.config, self.loop, self.dock)
self.config, self.loop, self.dock, self)
self.homeassistant = DockerHomeAssistant(
self.config, self.loop, self.dock)
# init HostControll
self.host_controll = HostControll(self.loop)
# init addon system
self.addons = AddonManager(self.config, self.loop, self.dock)
async def setup(self):
"""Setup HassIO orchestration."""
# supervisor
await self.supervisor.attach()
await self.supervisor.cleanup()
# set api endpoint
self.config.api_endpoint = await get_local_ip(self.loop)
# hostcontroll
host_info = await self.host_controll.info()
@@ -56,32 +68,62 @@ class HassIO(object):
# rest api views
self.api.register_host(self.host_controll)
self.api.register_network(self.host_controll)
self.api.register_supervisor(self.host_controll)
self.api.register_supervisor(self.supervisor, self.addons)
self.api.register_homeassistant(self.homeassistant)
self.api.register_addons(self.addons)
# schedule update info tasks
self.scheduler.register_task(
self.config.fetch_update_infos, RUN_UPDATE_INFO_TASKS,
first_run=True)
now=True)
# first start of supervisor?
if not await self.homeassistant.exists():
_LOGGER.info("No HomeAssistant docker found.")
await self._setup_homeassistant()
# Load addons
arch = get_arch_from_image(self.supervisor.image)
await self.addons.prepare(arch)
# schedule addon update task
self.scheduler.register_task(
self.addons.reload, RUN_RELOAD_ADDONS_TASKS, now=True)
# schedule self update task
self.scheduler.register_task(
self._hassio_update, RUN_UPDATE_SUPERVISOR_TASKS)
async def start(self):
"""Start HassIO orchestration."""
# start api
await self.api.start()
_LOGGER.info("Start hassio api on %s", self.config.api_endpoint)
# HomeAssistant is already running / supervisor have only reboot
if await self.homeassistant.is_running():
_LOGGER.info("HassIO reboot detected")
return
# start addon mark as before
await self.addons.auto_boot(STARTUP_BEFORE)
# run HomeAssistant
await self.homeassistant.run()
async def stop(self):
# start addon mark as after
await self.addons.auto_boot(STARTUP_AFTER)
async def stop(self, exit_code=0):
"""Stop a running orchestration."""
# don't process scheduler anymore
self.scheduler.stop()
# process stop task pararell
tasks = [self.websession.close(), self.api.stop()]
await asyncio.wait(tasks, loop=self.loop)
self.exit_code = exit_code
self.loop.stop()
async def _setup_homeassistant(self):
@@ -99,3 +141,12 @@ class HassIO(object):
# store version
_LOGGER.info("HomeAssistant docker now installed.")
async def _hassio_update(self):
"""Check and run update of supervisor hassio."""
if self.config.current_hassio == self.supervisor.version:
return
_LOGGER.info(
"Found new HassIO version %s.", self.config.current_hassio)
await self.supervisor.update(self.config.current_hassio)

View File

@@ -53,7 +53,7 @@ class DockerBase(object):
image.tag(self.image, tag='latest')
self.version = get_version_from_env(image.attrs['Config']['Env'])
_LOGGER.info("Tag image %s with version %s as latest.",
_LOGGER.info("Tag image %s with version %s as latest",
self.image, self.version)
except docker.errors.APIError as err:
_LOGGER.error("Can't install %s:%s -> %s.", self.image, tag, err)
@@ -99,8 +99,9 @@ class DockerBase(object):
self.container.attrs['Config']['Env'])
except docker.errors.DockerException:
return False
else:
self.container.reload()
self.container.reload()
return self.container.status == 'running'
async def attach(self):
@@ -122,7 +123,7 @@ class DockerBase(object):
self.image = self.container.attrs['Config']['Image']
self.version = get_version_from_env(
self.container.attrs['Config']['Env'])
_LOGGER.info("Attach to image %s with version %s.",
_LOGGER.info("Attach to image %s with version %s",
self.image, self.version)
except (docker.errors.DockerException, KeyError):
_LOGGER.fatal(
@@ -138,8 +139,6 @@ class DockerBase(object):
return False
async with self._lock:
_LOGGER.info("Run docker image %s with version %s.",
self.image, self.version)
return await self.loop.run_in_executor(None, self._run)
def _run(self):
@@ -167,6 +166,8 @@ class DockerBase(object):
if not self.container:
return
_LOGGER.info("Stop %s docker application", self.image)
self.container.reload()
if self.container.status == 'running':
with suppress(docker.errors.DockerException):
@@ -177,11 +178,39 @@ class DockerBase(object):
self.container = None
async def update(self, tag):
"""Update a docker image.
async def remove(self):
"""Remove docker container."""
if self._lock.locked():
_LOGGER.error("Can't excute remove while a task is in progress")
return False
Return a Future.
async with self._lock:
return await self.loop.run_in_executor(None, self._remove)
def _remove(self):
"""remove docker container.
Need run inside executor.
"""
if self._is_running():
self._stop()
_LOGGER.info("Remove docker %s with latest and %s",
self.image, self.version)
try:
self.dock.images.remove(
image="{}:latest".format(self.image), force=True)
self.dock.images.remove(
image="{}:{}".format(self.image, self.version), force=True)
except docker.errors.DockerException as err:
_LOGGER.warning("Can't remove image %s -> %s", self.image, err)
return False
return True
async def update(self, tag):
"""Update a docker image."""
if self._lock.locked():
_LOGGER.error("Can't excute update while a task is in progress")
return False
@@ -195,23 +224,41 @@ class DockerBase(object):
Need run inside executor.
"""
old_image = "{}:{}".format(self.image, self.version)
old_run = self._is_running()
_LOGGER.info("Update docker %s with %s:%s.",
_LOGGER.info("Update docker %s with %s:%s",
old_image, self.image, tag)
# update docker image
if self._install(tag):
_LOGGER.info("Cleanup old %s docker.", old_image)
_LOGGER.info("Cleanup old %s docker", old_image)
self._stop()
try:
self.dock.images.remove(image=old_image, force=True)
except docker.errors.DockerException as err:
_LOGGER.warning(
"Can't remove old image %s -> %s.", old_image, err)
# restore
if old_run:
self._run()
"Can't remove old image %s -> %s", old_image, err)
return True
return False
async def logs(self):
"""Return docker logs of container."""
if self._lock.locked():
_LOGGER.error("Can't excute logs while a task is in progress")
return False
async with self._lock:
return await self.loop.run_in_executor(None, self._logs)
def _logs(self):
"""Return docker logs of container.
Need run inside executor.
"""
if not self.container:
return
try:
return self.container.logs(tail=100, stdout=True, stderr=True)
except docker.errors.DockerException as err:
_LOGGER.warning("Can't grap logs from %s -> %s", self.image, err)

90
hassio/dock/addon.py Normal file
View File

@@ -0,0 +1,90 @@
"""Init file for HassIO addon docker object."""
import logging
import docker
from . import DockerBase
from ..tools import get_version_from_env
_LOGGER = logging.getLogger(__name__)
HASS_DOCKER_NAME = 'homeassistant'
class DockerAddon(DockerBase):
"""Docker hassio wrapper for HomeAssistant."""
def __init__(self, config, loop, dock, addons_data, addon):
"""Initialize docker homeassistant wrapper."""
super().__init__(
config, loop, dock, image=addons_data.get_image(addon))
self.addon = addon
self.addons_data = addons_data
@property
def docker_name(self):
"""Return name of docker container."""
return "addon_{}".format(self.addon)
def _run(self):
"""Run docker image.
Need run inside executor.
"""
if self._is_running():
return
# 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,
name=self.docker_name,
detach=True,
network_mode='bridge',
ports=self.addons_data.get_ports(self.addon),
volumes=volumes,
)
self.version = get_version_from_env(
self.container.attrs['Config']['Env'])
_LOGGER.info("Start docker addon %s with version %s",
self.image, self.version)
except docker.errors.DockerException as err:
_LOGGER.error("Can't run %s -> %s", self.image, err)
return False
return True
def _attach(self):
"""Attach to running docker container.
Need run inside executor.
"""
try:
self.container = self.dock.containers.get(self.docker_name)
self.version = get_version_from_env(
self.container.attrs['Config']['Env'])
_LOGGER.info("Attach to image %s with version %s",
self.image, self.version)
except (docker.errors.DockerException, KeyError):
pass

View File

@@ -4,7 +4,7 @@ import logging
import docker
from . import DockerBase
from ..tools import get_version_from_env, get_local_ip
from ..tools import get_version_from_env
_LOGGER = logging.getLogger(__name__)
@@ -31,8 +31,6 @@ class DockerHomeAssistant(DockerBase):
if self._is_running():
return
api_endpoint = get_local_ip(self.loop)
# cleanup old container
self._stop()
@@ -43,12 +41,8 @@ class DockerHomeAssistant(DockerBase):
detach=True,
privileged=True,
network_mode='host',
restart_policy={
"Name": "always",
"MaximumRetryCount": 10,
},
environment={
'HASSIO': api_endpoint,
'HASSIO': self.config.api_endpoint,
},
volumes={
self.config.path_config_docker:
@@ -59,8 +53,25 @@ class DockerHomeAssistant(DockerBase):
self.version = get_version_from_env(
self.container.attrs['Config']['Env'])
_LOGGER.info("Start docker addon %s with version %s",
self.image, self.version)
except docker.errors.DockerException as err:
_LOGGER.error("Can't run %s -> %s.", self.image, err)
_LOGGER.error("Can't run %s -> %s", self.image, err)
return False
return True
async def update(self, tag):
"""Update homeassistant docker image."""
if self._lock.locked():
_LOGGER.error("Can't excute update while a task is in progress")
return False
async with self._lock:
if await self.loop.run_in_executor(None, self._update, tag):
await self.loop.run_in_executor(None, self._run)
return True
return False

View File

@@ -1,17 +1,71 @@
"""Init file for HassIO docker object."""
import logging
import os
import docker
from . import DockerBase
from ..const import RESTART_EXIT_CODE
_LOGGER = logging.getLogger(__name__)
class DockerSupervisor(DockerBase):
"""Docker hassio wrapper for HomeAssistant."""
def __init__(self, config, loop, dock, hassio, image=None):
"""Initialize docker base wrapper."""
super().__init__(config, loop, dock, image=image)
self.hassio = hassio
@property
def docker_name(self):
"""Return name of docker container."""
return os.environ['SUPERVISOR_NAME']
async def update(self, tag):
"""Update a supervisor docker image."""
if self._lock.locked():
_LOGGER.error("Can't excute update while a task is in progress")
return False
_LOGGER.info("Update supervisor docker to %s:%s", self.image, tag)
old_version = self.version
async with self._lock:
if await self.loop.run_in_executor(None, self._install, tag):
self.config.hassio_cleanup = old_version
self.loop.create_task(self.hassio.stop(RESTART_EXIT_CODE))
return True
return False
async def cleanup(self):
"""Check if old supervisor version exists and cleanup."""
if not self.config.hassio_cleanup:
return
async with self._lock:
if await self.loop.run_in_executor(None, self._cleanup):
self.config.hassio_cleanup = None
def _cleanup(self):
"""Remove old image.
Need run inside executor.
"""
old_image = "{}:{}".format(self.image, self.config.hassio_cleanup)
_LOGGER.info("Old supervisor docker found %s", old_image)
try:
self.dock.images.remove(image=old_image, force=True)
except docker.errors.DockerException as err:
_LOGGER.warning("Can't remove old image %s -> %s", old_image, err)
return False
return True
async def run(self):
"""Run docker image."""
raise RuntimeError("Not support on supervisor docker container!")
@@ -24,6 +78,6 @@ class DockerSupervisor(DockerBase):
"""Stop/remove docker container."""
raise RuntimeError("Not support on supervisor docker container!")
async def update(self, tag):
"""Update docker image."""
async def remove(self):
"""Remove docker image."""
raise RuntimeError("Not support on supervisor docker container!")

View File

@@ -14,9 +14,8 @@ _LOGGER = logging.getLogger(__name__)
TIMEOUT = 15
LEVEL_POWER = 1
LEVEL_UPDATE_SUPERVISOR = 2
LEVEL_UPDATE_HOST = 4
LEVEL_NETWORK = 8
LEVEL_UPDATE_HOST = 2
LEVEL_NETWORK = 4
class HostControll(object):
@@ -101,12 +100,3 @@ class HostControll(object):
if version:
return self._send_command("host-update {}".format(version))
return self._send_command("host-update")
def supervisor_update(self, version=None):
"""Update the supervisor on host system.
Return a coroutine.
"""
if version:
return self._send_command("supervisor-update {}".format(version))
return self._send_command("supervisor-update")

View File

@@ -16,9 +16,14 @@ class Scheduler(object):
"""Initialize task schedule."""
self.loop = loop
self._data = {}
self._stop = False
def stop(self):
"""Stop to execute tasks in scheduler."""
self._stop = True
def register_task(self, coro_callback, seconds, repeat=True,
first_run=False):
now=False):
"""Schedule a coroutine.
The coroutien need to be a callback without arguments.
@@ -34,7 +39,7 @@ class Scheduler(object):
self._data[idx] = opts
# schedule task
if first_run:
if now:
self._run_task(idx)
else:
task = self.loop.call_later(seconds, self._run_task, idx)
@@ -46,6 +51,10 @@ class Scheduler(object):
"""Run a scheduled task."""
data = self._data.pop(idx)
# stop execute tasks
if self._stop:
return
self.loop.create_task(data[CALL]())
if data[REPEAT]:

View File

@@ -1,5 +1,6 @@
"""Tools file for HassIO."""
import asyncio
import json
import logging
import re
import socket
@@ -12,6 +13,7 @@ from .const import URL_HASSIO_VERSION, URL_HASSIO_VERSION_BETA
_LOGGER = logging.getLogger(__name__)
_RE_VERSION = re.compile(r"VERSION=(.*)")
_IMAGE_ARCH = re.compile(r".*/([a-z0-9]*)-hassio-supervisor")
async def fetch_current_versions(websession, beta=False):
@@ -25,9 +27,19 @@ async def fetch_current_versions(websession, beta=False):
async with websession.get(url) as request:
return await request.json(content_type=None)
except (ValueError, aiohttp.ClientError, asyncio.TimeoutError) as err:
except (aiohttp.ClientError, asyncio.TimeoutError, KeyError) as err:
_LOGGER.warning("Can't fetch versions from %s! %s", url, err)
except json.JSONDecodeError as err:
_LOGGER.warning("Can't parse versions from %s! %s", url, err)
def get_arch_from_image(image):
"""Return arch from hassio image name."""
found = _IMAGE_ARCH.match(image)
if found:
return found.group(1)
def get_version_from_env(env_list):
"""Extract Version from ENV list."""
@@ -43,16 +55,37 @@ def get_version_from_env(env_list):
def get_local_ip(loop):
"""Retrieve local IP address.
Need run inside executor.
Return a future.
"""
def local_ip():
"""Return local ip."""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Use Google Public DNS server to determine own IP
sock.connect(('8.8.8.8', 80))
return sock.getsockname()[0]
except socket.error:
return socket.gethostbyname(socket.gethostname())
finally:
sock.close()
return loop.run_in_executor(None, local_ip)
def write_json_file(jsonfile, data):
"""Write a json file."""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
with open(jsonfile, 'w') as conf_file:
conf_file.write(json.dumps(data))
except OSError:
return False
# Use Google Public DNS server to determine own IP
sock.connect(('8.8.8.8', 80))
return True
return sock.getsockname()[0]
except socket.error:
return socket.gethostbyname(socket.gethostname())
finally:
sock.close()
def read_json_file(jsonfile):
"""Read a json file and return a dict."""
with open(jsonfile, 'r') as cfile:
return json.loads(cfile.read())

View File

@@ -29,7 +29,7 @@ setup(
keywords=['docker', 'home-assistant', 'api'],
zip_safe=False,
platforms='any',
packages=['hassio', 'hassio.dock', 'hassio.api'],
packages=['hassio', 'hassio.dock', 'hassio.api', 'hassio.addons'],
include_package_data=True,
install_requires=[
'async_timeout',
@@ -37,5 +37,6 @@ setup(
'docker',
'colorlog',
'voluptuous',
'gitpython',
]
)

View File

@@ -1,6 +1,7 @@
{
"hassio_tag": "0.5",
"homeassistant_tag": "0.41",
"resinos_version": "0.3",
"resinhup_version": "0.1"
"hassio_tag": "0.13",
"homeassistant_tag": "0.43.1",
"resinos_version": "0.4",
"resinhup_version": "0.1",
"generic_hc_version": "0.1"
}

View File

@@ -1,6 +1,7 @@
{
"hassio_tag": "0.5",
"homeassistant_tag": "0.41",
"resinos_version": "0.3",
"resinhup_version": "0.1"
"hassio_tag": "0.13",
"homeassistant_tag": "0.43.1",
"resinos_version": "0.4",
"resinhup_version": "0.1",
"generic_hc_version": "0.1"
}