Compare commits

...

31 Commits
0.8 ... 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
22 changed files with 397 additions and 178 deletions

94
API.md
View File

@@ -1,41 +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]
# 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
## HassIO REST API
Interface for HomeAssistant to controll things from supervisor.
@@ -73,6 +37,7 @@ On success
"slug": "xy",
"version": "CURRENT_VERSION",
"installed": "none|INSTALL_VERSION",
"dedicated": "bool",
"description": "description"
}
]
@@ -94,6 +59,14 @@ Optional:
}
```
- `/supervisor/reload`
Reload addons/version.
- `/supervisor/logs`
Output the raw docker log
### Host
- `/host/shutdown`
@@ -155,6 +128,10 @@ Optional:
}
```
- `/homeassistant/logs`
Output the raw docker log
### REST API addons
- `/addons/{addon}/info`
@@ -170,7 +147,10 @@ Optional:
- `/addons/{addon}/options`
```json
{ }
{
"boot": "auto|manual",
"options": {},
}
```
- `/addons/{addon}/start`
@@ -194,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,8 +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)
**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.
@@ -17,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).
@@ -31,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,6 @@
"""Main file for HassIO."""
import asyncio
import logging
import signal
import sys
import hassio.bootstrap as bootstrap
@@ -25,12 +24,7 @@ if __name__ == "__main__":
_LOGGER.info("Start Hassio task")
loop.call_soon_threadsafe(loop.create_task, hassio.start())
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(bootstrap.reg_signal, loop, hassio)
loop.run_forever()
loop.close()

View File

@@ -36,21 +36,17 @@ class AddonManager(AddonsData):
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 relaod(self):
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
tasks = []
for addon in self.list_removed:
_LOGGER.info("Old addon %s found")
tasks.append(self.loop.create_task(self.uninstall(addon)))
if tasks:
await asyncio.wait(tasks, loop=self.loop)
_LOGGER.warning("Dedicated addon '%s' found!", addon)
async def auto_boot(self, start_type):
"""Boot addons with mode auto."""
@@ -87,7 +83,7 @@ class AddonManager(AddonsData):
return False
self.dockers[addon] = addon_docker
self.set_install_addon(addon, version)
self.set_addon_install(addon, version)
return True
async def uninstall(self, addon):
@@ -109,7 +105,7 @@ class AddonManager(AddonsData):
shutil.rmtree(self.path_data(addon))
self.dockers.pop(addon)
self.set_uninstall_addon(addon)
self.set_addon_uninstall(addon)
return True
async def state(self, addon):
@@ -144,16 +140,25 @@ class AddonManager(AddonsData):
async def update(self, addon, version=None):
"""Update addon."""
if not self.is_installed(addon):
_LOGGER.error("Addon %s is not installed", addon)
return False
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_version(addon, 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()

View File

@@ -10,13 +10,15 @@ 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_MAP_HASSIO)
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):
@@ -26,12 +28,22 @@ class AddonsData(Config):
"""Initialize data holder."""
super().__init__(FILE_HASSIO_ADDONS)
self.config = config
self._addons_data = {}
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._addons_data = {}
self._current_data = {}
self._read_addons_folder(self.config.path_addons_repo)
self._read_addons_folder(self.config.path_addons_custom)
@@ -45,7 +57,7 @@ class AddonsData(Config):
addon_config = read_json_file(addon)
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
self._addons_data[addon_config[ATTR_SLUG]] = addon_config
self._current_data[addon_config[ATTR_SLUG]] = addon_config
except (OSError, KeyError):
_LOGGER.warning("Can't read %s", addon)
@@ -57,24 +69,25 @@ class AddonsData(Config):
@property
def list_installed(self):
"""Return a list of installed addons."""
return set(self._data.keys())
@property
def list_all(self):
"""Return a list of available addons."""
return set(self._addons_data.keys())
@property
def list(self):
"""Return a list of available addons."""
data = []
for addon, values in self._addons_data.items():
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: self._data.get(addon, {}).get(ATTR_VERSION),
ATTR_INSTALLED: i_version,
ATTR_DEDICATED: addon in dedicated,
})
return data
@@ -82,7 +95,7 @@ class AddonsData(Config):
def list_startup(self, start_type):
"""Get list of installed addon with need start by type."""
addon_list = set()
for addon in self._data.keys():
for addon in self._addons_data.keys():
if self.get_boot(addon) != BOOT_AUTO:
continue
@@ -99,58 +112,69 @@ class AddonsData(Config):
def list_removed(self):
"""Return local addons they not support from repo."""
addon_list = set()
for addon in self._data.keys():
if addon not in self._addons_data:
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._addons_data
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._data
return addon in self._addons_data
def version_installed(self, addon):
"""Return installed version."""
return self._data[addon][ATTR_VERSION]
if ATTR_VERSION not in self._user_data[addon]:
return self._addons_data[addon][ATTR_VERSION]
def set_install_addon(self, addon, version):
return self._user_data[addon][ATTR_VERSION]
def set_addon_install(self, addon, version):
"""Set addon as installed."""
self._data[addon] = {
self._addons_data[addon] = self._current_data[addon]
self._user_data[addon] = {
ATTR_OPTIONS: {},
ATTR_VERSION: version,
ATTR_OPTIONS: {}
}
self.save()
def set_uninstall_addon(self, addon):
def set_addon_uninstall(self, addon):
"""Set addon as uninstalled."""
self._data.pop(addon, None)
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._data[addon][ATTR_OPTIONS] = options
self._user_data[addon][ATTR_OPTIONS] = options
self.save()
def set_version(self, addon, version):
"""Update version of addon."""
self._data[addon][ATTR_VERSION] = version
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."""
opt = self._addons_data[addon][ATTR_OPTIONS]
if addon in self._data:
opt.update(self._data[addon][ATTR_OPTIONS])
return opt
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._data[addon]:
return self._data[addon][ATTR_BOOT]
if ATTR_BOOT in self._user_data[addon]:
return self._user_data[addon][ATTR_BOOT]
return self._addons_data[addon][ATTR_BOOT]
@@ -164,11 +188,9 @@ class AddonsData(Config):
def get_version(self, addon):
"""Return version of addon."""
return self._addons_data[addon][ATTR_VERSION]
def get_slug(self, addon):
"""Return slug of addon."""
return self._addons_data[addon][ATTR_SLUG]
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."""
@@ -176,11 +198,12 @@ class AddonsData(Config):
def get_image(self, addon):
"""Return image name of addon."""
if ATTR_IMAGE not in self._addons_data[addon]:
return "{}/{}-addon-{}".format(
DOCKER_REPO, self.arch, self.get_slug(addon))
addon_data = self._addons_data.get(addon, self._current_data[addon])
return self._addons_data[addon][ATTR_IMAGE]
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."""
@@ -190,19 +213,13 @@ class AddonsData(Config):
"""Return True if ssl map is needed."""
return self._addons_data[addon][ATTR_MAP_SSL]
def need_hassio(self, addon):
"""Return True if hassio map is needed."""
return self._addons_data[addon][ATTR_MAP_HASSIO]
def path_data(self, addon):
"""Return addon data path inside supervisor."""
return "{}/{}".format(
self.config.path_addons_data, self._addons_data[addon][ATTR_SLUG])
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,
self._addons_data[addon][ATTR_SLUG])
return "{}/{}".format(self.config.path_addons_data_docker, addon)
def path_addon_options(self, addon):
"""Return path to addons options."""

View File

@@ -5,7 +5,7 @@ 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_MAP_HASSIO)
BOOT_MANUAL, ATTR_SCHEMA, ATTR_IMAGE)
V_STR = 'str'
V_INT = 'int'
@@ -29,7 +29,6 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
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_HASSIO, default=False): vol.Boolean(),
vol.Required(ATTR_OPTIONS): dict,
vol.Required(ATTR_SCHEMA): {
vol.Coerce(str): vol.Any(ADDON_ELEMENT, [

View File

@@ -49,8 +49,10 @@ 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(
'/supervisor/options', api_supervisor.options)
self.webapp.router.add_get('/supervisor/logs', api_supervisor.logs)
def register_homeassistant(self, dock_homeassistant):
"""Register homeassistant function."""
@@ -58,6 +60,7 @@ 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."""
@@ -73,6 +76,7 @@ class RestAPI(object):
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."""

View File

@@ -5,10 +5,10 @@ import logging
import voluptuous as vol
from voluptuous.humanize import humanize_error
from .util import api_process, api_validate
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)
STATE_STOPPED, STATE_STARTED, BOOT_AUTO, BOOT_MANUAL)
_LOGGER = logging.getLogger(__name__)
@@ -16,6 +16,10 @@ 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."""
@@ -56,10 +60,19 @@ class APIAddons(object):
async def options(self, request):
"""Store user options for addon."""
addon = self._extract_addon(request)
schema = self.addons.get_schema(addon)
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])
options = await api_validate(schema, request)
self.addons.set_options(addon, options)
return True
@api_process
@@ -124,3 +137,9 @@ class APIAddons(object):
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,11 +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:
if self.homeassistant.in_progress:
raise RuntimeError("Other task is in progress")
if version == self.dock_hass.version:
if version == self.homeassistant.version:
raise RuntimeError("Version is already in use")
return await asyncio.shield(
self.dock_hass.update(version), loop=self.loop)
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

@@ -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_ADDONS, ATTR_VERSION, ATTR_CURRENT, ATTR_BETA, HASSIO_VERSION)
@@ -65,4 +65,26 @@ class APISupervisor(object):
if version == self.supervisor.version:
raise RuntimeError("Version is already in use")
return await asyncio.shield(self.supervisor.update(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({

View File

@@ -2,6 +2,7 @@
import logging
import os
import stat
import signal
from colorlog import ColoredFormatter
@@ -81,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

@@ -8,7 +8,7 @@ from .tools import (
_LOGGER = logging.getLogger(__name__)
HOMEASSISTANT_CONFIG = "{}/homeassistant_config"
HOMEASSISTANT_CONFIG = "{}/homeassistant"
HOMEASSISTANT_IMAGE = 'homeassistant_image'
HOMEASSISTANT_CURRENT = 'homeassistant_current'
@@ -22,6 +22,8 @@ ADDONS_CUSTOM = "{}/addons_custom"
UPSTREAM_BETA = 'upstream_beta'
API_ENDPOINT = 'api_endpoint'
class Config(object):
"""Hold all config data."""
@@ -78,6 +80,16 @@ class CoreConfig(Config):
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."""

View File

@@ -1,5 +1,5 @@
"""Const file for HassIO."""
HASSIO_VERSION = '0.8'
HASSIO_VERSION = '0.14'
URL_HASSIO_VERSION = \
'https://raw.githubusercontent.com/pvizeli/hassio/master/version.json'
@@ -43,9 +43,9 @@ ATTR_BOOT = 'boot'
ATTR_PORTS = 'ports'
ATTR_MAP_CONFIG = 'map_config'
ATTR_MAP_SSL = 'map_ssl'
ATTR_MAP_HASSIO = 'map_hassio'
ATTR_OPTIONS = 'options'
ATTR_INSTALLED = 'installed'
ATTR_DEDICATED = 'dedicated'
ATTR_STATE = 'state'
ATTR_SCHEMA = 'schema'
ATTR_IMAGE = 'image'

View File

@@ -15,7 +15,7 @@ from .const import (
from .scheduler import Scheduler
from .dock.homeassistant import DockerHomeAssistant
from .dock.supervisor import DockerSupervisor
from .tools import get_arch_from_image
from .tools import get_arch_from_image, get_local_ip
_LOGGER = logging.getLogger(__name__)
@@ -52,6 +52,9 @@ class HassIO(object):
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()
if host_info:
@@ -72,7 +75,7 @@ class HassIO(object):
# 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():
@@ -85,7 +88,7 @@ class HassIO(object):
# schedule addon update task
self.scheduler.register_task(
self.addons.relaod, RUN_RELOAD_ADDONS_TASKS, first_run=True)
self.addons.reload, RUN_RELOAD_ADDONS_TASKS, now=True)
# schedule self update task
self.scheduler.register_task(
@@ -95,6 +98,7 @@ class HassIO(object):
"""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():
@@ -112,6 +116,10 @@ class HassIO(object):
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)

View File

@@ -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):
@@ -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,7 +166,7 @@ class DockerBase(object):
if not self.container:
return
_LOGGER.info("Stop %s docker application.", self.image)
_LOGGER.info("Stop %s docker application", self.image)
self.container.reload()
if self.container.status == 'running':
@@ -224,7 +223,6 @@ class DockerBase(object):
Need run inside executor.
"""
old_run = self._is_running()
old_image = "{}:{}".format(self.image, self.version)
_LOGGER.info("Update docker %s with %s:%s",
@@ -232,16 +230,35 @@ class DockerBase(object):
# 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()
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)

View File

@@ -24,7 +24,7 @@ class DockerAddon(DockerBase):
@property
def docker_name(self):
"""Return name of docker container."""
return "addon_{}".format(self.addons_data.get_slug(self.addon))
return "addon_{}".format(self.addon)
def _run(self):
"""Run docker image.
@@ -52,11 +52,6 @@ class DockerAddon(DockerBase):
self.config.path_ssl_docker: {
'bind': '/ssl', 'mode': 'rw'
}})
if self.addons_data.need_hassio(self.addon):
volumes.update({
self.config.path_hassio_docker: {
'bind': '/hassio', 'mode': 'rw'
}})
try:
self.container = self.dock.containers.run(
@@ -65,17 +60,31 @@ class DockerAddon(DockerBase):
detach=True,
network_mode='bridge',
ports=self.addons_data.get_ports(self.addon),
restart_policy={
"Name": "on-failure",
"MaximumRetryCount": 10,
},
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)
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

@@ -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

@@ -55,19 +55,23 @@ def get_version_from_env(env_list):
def get_local_ip(loop):
"""Retrieve local IP address.
Need run inside executor.
Return a future.
"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
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))
# 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 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):

View File

@@ -1,6 +1,7 @@
{
"hassio_tag": "0.8",
"homeassistant_tag": "0.42.3",
"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.8",
"homeassistant_tag": "0.42.3",
"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"
}