mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-08-12 02:29:21 +00:00
Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
76cead72e8 | ||
![]() |
a0f17ffd1d | ||
![]() |
86d92bdfa2 | ||
![]() |
25a0bc6549 | ||
![]() |
96971e7054 | ||
![]() |
2729877fbf | ||
![]() |
3b8b44fcb7 | ||
![]() |
22e3d50203 | ||
![]() |
2090b336e4 | ||
![]() |
a028f11a4a | ||
![]() |
4edd02ca8e | ||
![]() |
85dad8c077 | ||
![]() |
fff2ac3c47 | ||
![]() |
f99c6335c0 | ||
![]() |
98f0ff2a01 | ||
![]() |
43c5fcd159 | ||
![]() |
784b57622c | ||
![]() |
d8d17998fc | ||
![]() |
bf4984e66b | ||
![]() |
297f4af047 | ||
![]() |
f8574a01f6 | ||
![]() |
610441f454 | ||
![]() |
fb9780c1f2 | ||
![]() |
7804c5fd6c | ||
![]() |
c711632b3e | ||
![]() |
43e245fb84 | ||
![]() |
5a4d7c5b21 | ||
![]() |
67e2ad99c9 | ||
![]() |
ea2edadac2 | ||
![]() |
1e78c60a65 | ||
![]() |
5f3147d6f4 | ||
![]() |
03c3c9b6a1 | ||
![]() |
f056d175b7 | ||
![]() |
9fb1aa626d | ||
![]() |
30243c39e6 | ||
![]() |
d285fd4ad4 | ||
![]() |
7a0b9cc1ac | ||
![]() |
cc63008a86 | ||
![]() |
f9c7371140 |
118
API.md
118
API.md
@@ -1,44 +1,8 @@
|
||||
# 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.
|
||||
Interface for HomeAssistant to control things from supervisor.
|
||||
|
||||
On error:
|
||||
```json
|
||||
@@ -65,14 +29,15 @@ On success
|
||||
```json
|
||||
{
|
||||
"version": "INSTALL_VERSION",
|
||||
"current": "CURRENT_VERSION",
|
||||
"beta": "true|false",
|
||||
"last_version": "CURRENT_VERSION",
|
||||
"beta_channel": "true|false",
|
||||
"addons": [
|
||||
{
|
||||
"name": "xy bla",
|
||||
"slug": "xy",
|
||||
"version": "CURRENT_VERSION",
|
||||
"version": "LAST_VERSION",
|
||||
"installed": "none|INSTALL_VERSION",
|
||||
"dedicated": "bool",
|
||||
"description": "description"
|
||||
}
|
||||
]
|
||||
@@ -90,10 +55,18 @@ Optional:
|
||||
- `/supervisor/option`
|
||||
```json
|
||||
{
|
||||
"beta": "true|false"
|
||||
"beta_channel": "true|false"
|
||||
}
|
||||
```
|
||||
|
||||
- `/supervisor/reload`
|
||||
|
||||
Reload addons/version.
|
||||
|
||||
- `/supervisor/logs`
|
||||
|
||||
Output the raw docker log
|
||||
|
||||
### Host
|
||||
|
||||
- `/host/shutdown`
|
||||
@@ -101,13 +74,13 @@ Optional:
|
||||
- `/host/reboot`
|
||||
|
||||
- `/host/info`
|
||||
See HostControll info command.
|
||||
See HostControl info command.
|
||||
```json
|
||||
{
|
||||
"os": "",
|
||||
"type": "",
|
||||
"version": "",
|
||||
"current": "",
|
||||
"level": "",
|
||||
"last_version": "",
|
||||
"features": ["shutdown", "reboot", "update", "network_info", "network_control"],
|
||||
"hostname": "",
|
||||
}
|
||||
```
|
||||
@@ -143,7 +116,7 @@ Optional:
|
||||
```json
|
||||
{
|
||||
"version": "INSTALL_VERSION",
|
||||
"current": "CURRENT_VERSION"
|
||||
"last_version": "LAST_VERSION"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -155,13 +128,17 @@ Optional:
|
||||
}
|
||||
```
|
||||
|
||||
- `/homeassistant/logs`
|
||||
|
||||
Output the raw docker log
|
||||
|
||||
### REST API addons
|
||||
|
||||
- `/addons/{addon}/info`
|
||||
```json
|
||||
{
|
||||
"version": "VERSION",
|
||||
"current": "CURRENT_VERSION",
|
||||
"last_version": "LAST_VERSION",
|
||||
"state": "started|stopped",
|
||||
"boot": "auto|manual",
|
||||
"options": {},
|
||||
@@ -170,7 +147,10 @@ Optional:
|
||||
|
||||
- `/addons/{addon}/options`
|
||||
```json
|
||||
{ }
|
||||
{
|
||||
"boot": "auto|manual",
|
||||
"options": {},
|
||||
}
|
||||
```
|
||||
|
||||
- `/addons/{addon}/start`
|
||||
@@ -194,3 +174,45 @@ Optional:
|
||||
"version": "VERSION"
|
||||
}
|
||||
```
|
||||
|
||||
- `/addons/{addon}/logs`
|
||||
|
||||
Output the raw docker log
|
||||
|
||||
## Host Control
|
||||
|
||||
Communicate over unix socket with a host daemon.
|
||||
|
||||
- commands
|
||||
```
|
||||
# info
|
||||
-> {'type', 'version', 'last_version', 'features', '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
|
||||
```
|
||||
|
||||
features:
|
||||
- shutdown
|
||||
- reboot
|
||||
- update
|
||||
- network_info
|
||||
- network_control
|
||||
|
||||
Answer:
|
||||
```
|
||||
{}|OK|ERROR|WRONG
|
||||
```
|
||||
|
||||
- {}: json
|
||||
- OK: call was successfully
|
||||
- ERROR: error on call
|
||||
- WRONG: not supported
|
||||
|
31
README.md
31
README.md
@@ -1,10 +1,27 @@
|
||||
# HassIO
|
||||
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.
|
||||
It is a docker image (supervisor) they manage HomeAssistant docker and give a interface to control itself over UI. It have a own eco system with addons to extend the functionality in a easy way.
|
||||
|
||||
[HassIO-Addons](https://github.com/pvizeli/hassio-addons)
|
||||
[HassIO-Build](https://github.com/pvizeli/hassio-build)
|
||||
[HassIO-Addons](https://github.com/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.
|
||||
@@ -16,7 +33,8 @@ After extracting the archive, flash it to a drive using [Etcher](https://etcher.
|
||||
## History
|
||||
- **0.1**: First techpreview with dumy supervisor (ResinOS 2.0.0-RC5)
|
||||
- **0.2**: Fix some bugs and update it to HassIO 0.2
|
||||
- **0.3**: Update HostControll and feature for HassIO 0.3 (ResinOS 2.0.0 / need reflash)
|
||||
- **0.3**: Update HostControl and feature for HassIO 0.3 (ResinOS 2.0.0 / need reflash)
|
||||
- **0.4**: Update HostControl and bring resinos OTA (resinhub) back (ResinOS 2.0.0-rev3)
|
||||
|
||||
## Configuring the image
|
||||
You can configure the WiFi network that the image should connect to after flashing using [`resin-device-toolbox`](https://resinos.io/docs/raspberrypi3/gettingstarted/#install-resin-device-toolbox).
|
||||
@@ -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
|
||||
|
@@ -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()
|
||||
|
@@ -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.dockers[addon].remove()))
|
||||
|
||||
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."""
|
||||
@@ -82,12 +78,12 @@ class AddonManager(AddonsData):
|
||||
addon_docker = DockerAddon(
|
||||
self.config, self.loop, self.dock, self, addon)
|
||||
|
||||
version = version or self.get_version(addon)
|
||||
version = version or self.get_last_version(addon)
|
||||
if not await addon_docker.install(version):
|
||||
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)
|
||||
version = version or self.get_last_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()
|
||||
|
@@ -5,43 +5,20 @@ 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, STARTUP_ONCE, STARTUP_AFTER, STARTUP_BEFORE, BOOT_AUTO,
|
||||
BOOT_MANUAL, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA, ATTR_IMAGE)
|
||||
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"
|
||||
|
||||
V_STR = 'str'
|
||||
V_INT = 'int'
|
||||
V_FLOAT = 'float'
|
||||
V_BOOL = 'bool'
|
||||
|
||||
|
||||
# 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.Required(ATTR_MAP_CONFIG): vol.Boolean(),
|
||||
vol.Required(ATTR_MAP_SSL): vol.Boolean(),
|
||||
vol.Required(ATTR_OPTIONS): dict,
|
||||
vol.Required(ATTR_SCHEMA): {
|
||||
vol.Coerce(str): vol.In([V_STR, V_INT, V_FLOAT, V_BOOL])
|
||||
},
|
||||
vol.Optional(ATTR_IMAGE): vol.Match(r"\w*/\w*"),
|
||||
})
|
||||
SYSTEM = "system"
|
||||
USER = "user"
|
||||
|
||||
|
||||
class AddonsData(Config):
|
||||
@@ -51,11 +28,23 @@ class AddonsData(Config):
|
||||
"""Initialize data holder."""
|
||||
super().__init__(FILE_HASSIO_ADDONS)
|
||||
self.config = config
|
||||
self._addons_data = {}
|
||||
self._system_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._system_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)
|
||||
|
||||
@@ -68,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)
|
||||
@@ -80,24 +69,25 @@ class AddonsData(Config):
|
||||
@property
|
||||
def list_installed(self):
|
||||
"""Return a list of installed addons."""
|
||||
return set(self._data.keys())
|
||||
return set(self._system_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."""
|
||||
def list_api(self):
|
||||
"""Return a list of available addons for api."""
|
||||
data = []
|
||||
for addon, values in self._addons_data.items():
|
||||
all_addons = {**self._system_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
|
||||
@@ -105,12 +95,12 @@ 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._system_data.keys():
|
||||
if self.get_boot(addon) != BOOT_AUTO:
|
||||
continue
|
||||
|
||||
try:
|
||||
if self._addons_data[addon][ATTR_STARTUP] == start_type:
|
||||
if self._system_data[addon][ATTR_STARTUP] == start_type:
|
||||
addon_list.add(addon)
|
||||
except KeyError:
|
||||
_LOGGER.warning("Orphaned addon detect %s", addon)
|
||||
@@ -122,106 +112,111 @@ 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._system_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._system_data
|
||||
|
||||
def is_installed(self, addon):
|
||||
"""Return True if a addon is installed."""
|
||||
return addon in self._data
|
||||
return addon in self._system_data
|
||||
|
||||
def version_installed(self, addon):
|
||||
"""Return installed version."""
|
||||
return self._data[addon][ATTR_VERSION]
|
||||
return self._user_data[addon][ATTR_VERSION]
|
||||
|
||||
def set_install_addon(self, addon, version):
|
||||
def set_addon_install(self, addon, version):
|
||||
"""Set addon as installed."""
|
||||
self._data[addon] = {
|
||||
self._system_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._system_data.pop(addon, None)
|
||||
self._user_data.pop(addon, None)
|
||||
self.save()
|
||||
|
||||
def set_addon_update(self, addon, version):
|
||||
"""Update version of addon."""
|
||||
self._system_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._system_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]
|
||||
return self._system_data[addon][ATTR_BOOT]
|
||||
|
||||
def get_name(self, addon):
|
||||
"""Return name of addon."""
|
||||
return self._addons_data[addon][ATTR_NAME]
|
||||
return self._system_data[addon][ATTR_NAME]
|
||||
|
||||
def get_description(self, addon):
|
||||
"""Return description of addon."""
|
||||
return self._addons_data[addon][ATTR_DESCRIPTON]
|
||||
return self._system_data[addon][ATTR_DESCRIPTON]
|
||||
|
||||
def get_version(self, addon):
|
||||
def get_last_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."""
|
||||
return self._addons_data[addon].get(ATTR_PORTS)
|
||||
return self._system_data[addon].get(ATTR_PORTS)
|
||||
|
||||
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._system_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."""
|
||||
return self._addons_data[addon][ATTR_MAP_CONFIG]
|
||||
return self._system_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]
|
||||
return self._system_data[addon][ATTR_MAP_SSL]
|
||||
|
||||
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."""
|
||||
@@ -229,35 +224,21 @@ class AddonsData(Config):
|
||||
|
||||
def write_addon_options(self, addon):
|
||||
"""Return True if addon options is written to data."""
|
||||
return write_json_file(
|
||||
self.path_addon_options(addon), self.get_options(addon))
|
||||
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]
|
||||
raw_schema = self._system_data[addon][ATTR_SCHEMA]
|
||||
|
||||
def validate(struct):
|
||||
"""Validate schema."""
|
||||
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 typ == V_STR:
|
||||
options[key] = str(value)
|
||||
elif typ == V_INT:
|
||||
options[key] = int(value)
|
||||
elif typ == V_FLOAT:
|
||||
options[key] = float(value)
|
||||
elif typ == V_BOOL:
|
||||
options[key] = vol.Boolean()(value)
|
||||
except TypeError:
|
||||
raise vol.Invalid(
|
||||
"Type error for {}.".format(key)) from None
|
||||
|
||||
return options
|
||||
|
||||
schema = vol.Schema(vol.All(dict(), validate))
|
||||
schema = vol.Schema(vol.All(dict, validate_options(raw_schema)))
|
||||
return schema
|
||||
|
112
hassio/addons/validate.py
Normal file
112
hassio/addons/validate.py
Normal 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
|
@@ -25,32 +25,34 @@ class RestAPI(object):
|
||||
self._handler = None
|
||||
self.server = None
|
||||
|
||||
def register_host(self, host_controll):
|
||||
"""Register hostcontroll function."""
|
||||
api_host = APIHost(self.config, self.loop, host_controll)
|
||||
def register_host(self, host_control):
|
||||
"""Register hostcontrol function."""
|
||||
api_host = APIHost(self.config, self.loop, host_control)
|
||||
|
||||
self.webapp.router.add_get('/host/info', api_host.info)
|
||||
self.webapp.router.add_get('/host/reboot', api_host.reboot)
|
||||
self.webapp.router.add_get('/host/shutdown', api_host.shutdown)
|
||||
self.webapp.router.add_get('/host/update', api_host.update)
|
||||
|
||||
def register_network(self, host_controll):
|
||||
def register_network(self, host_control):
|
||||
"""Register network function."""
|
||||
api_net = APINetwork(self.config, self.loop, host_controll)
|
||||
api_net = APINetwork(self.config, self.loop, host_control)
|
||||
|
||||
self.webapp.router.add_get('/network/info', api_net.info)
|
||||
self.webapp.router.add_get('/network/options', api_net.options)
|
||||
|
||||
def register_supervisor(self, supervisor, addons):
|
||||
def register_supervisor(self, supervisor, addons, host_control):
|
||||
"""Register supervisor function."""
|
||||
api_supervisor = APISupervisor(
|
||||
self.config, self.loop, supervisor, addons)
|
||||
self.config, self.loop, supervisor, addons, host_control)
|
||||
|
||||
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."""
|
||||
|
@@ -3,11 +3,12 @@ import asyncio
|
||||
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)
|
||||
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS,
|
||||
STATE_STOPPED, STATE_STARTED, BOOT_AUTO, BOOT_MANUAL)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -15,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."""
|
||||
@@ -42,23 +47,31 @@ class APIAddons(object):
|
||||
"""Return addon information."""
|
||||
addon = self._extract_addon(request)
|
||||
|
||||
info = {
|
||||
return {
|
||||
ATTR_VERSION: self.addons.version_installed(addon),
|
||||
ATTR_CURRENT: self.addons.get_version(addon),
|
||||
ATTR_LAST_VERSION: self.addons.get_last_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)
|
||||
schema = self.addons.get_schema(addon)
|
||||
options_schema = self.addons.get_schema(addon)
|
||||
|
||||
addon_schema = SCHEMA_OPTIONS.extend({
|
||||
vol.Optional(ATTR_OPTIONS): options_schema,
|
||||
})
|
||||
|
||||
body = await api_validate(addon_schema, request)
|
||||
|
||||
if ATTR_OPTIONS in body:
|
||||
self.addons.set_options(addon, body[ATTR_OPTIONS])
|
||||
if ATTR_BOOT in body:
|
||||
self.addons.set_boot(addon, body[ATTR_BOOT])
|
||||
|
||||
options = await api_validate(schema, request)
|
||||
self.addons.set_options(addon, options)
|
||||
return True
|
||||
|
||||
@api_process
|
||||
@@ -67,7 +80,7 @@ class APIAddons(object):
|
||||
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))
|
||||
ATTR_VERSION, self.addons.get_last_version(addon))
|
||||
|
||||
return await asyncio.shield(
|
||||
self.addons.install(addon, version), loop=self.loop)
|
||||
@@ -88,6 +101,14 @@ class APIAddons(object):
|
||||
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)
|
||||
|
||||
@@ -108,10 +129,16 @@ class APIAddons(object):
|
||||
body = await api_validate(SCHEMA_VERSION, request)
|
||||
addon = self._extract_addon(request)
|
||||
version = body.get(
|
||||
ATTR_VERSION, self.addons.get_version(addon))
|
||||
ATTR_VERSION, self.addons.get_last_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)
|
||||
|
@@ -4,8 +4,8 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from .util import api_process, api_validate
|
||||
from ..const import ATTR_VERSION, ATTR_CURRENT
|
||||
from .util import api_process, api_process_raw, api_validate
|
||||
from ..const import ATTR_VERSION, ATTR_LAST_VERSION
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,18 +17,18 @@ 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_CURRENT: self.config.current_homeassistant,
|
||||
ATTR_VERSION: self.homeassistant.version,
|
||||
ATTR_LAST_VERSION: self.config.last_homeassistant,
|
||||
}
|
||||
|
||||
return info
|
||||
@@ -37,13 +37,21 @@ class APIHomeAssistant(object):
|
||||
async def update(self, request):
|
||||
"""Update host OS."""
|
||||
body = await api_validate(SCHEMA_VERSION, request)
|
||||
version = body.get(ATTR_VERSION, self.config.current_homeassistant)
|
||||
version = body.get(ATTR_VERSION, self.config.last_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()
|
||||
|
@@ -3,13 +3,12 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from .util import api_process_hostcontroll, api_process, api_validate
|
||||
from ..const import ATTR_VERSION
|
||||
from .util import api_process_hostcontrol, api_process, api_validate
|
||||
from ..const import (
|
||||
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_TYPE, ATTR_HOSTNAME, ATTR_FEATURES)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UNKNOWN = 'unknown'
|
||||
|
||||
SCHEMA_VERSION = vol.Schema({
|
||||
vol.Optional(ATTR_VERSION): vol.Coerce(str),
|
||||
})
|
||||
@@ -18,44 +17,40 @@ SCHEMA_VERSION = vol.Schema({
|
||||
class APIHost(object):
|
||||
"""Handle rest api for host functions."""
|
||||
|
||||
def __init__(self, config, loop, host_controll):
|
||||
def __init__(self, config, loop, host_control):
|
||||
"""Initialize host rest api part."""
|
||||
self.config = config
|
||||
self.loop = loop
|
||||
self.host_controll = host_controll
|
||||
self.host_control = host_control
|
||||
|
||||
@api_process
|
||||
async def info(self, request):
|
||||
"""Return host information."""
|
||||
if not self.host_controll.active:
|
||||
info = {
|
||||
'os': UNKNOWN,
|
||||
'version': UNKNOWN,
|
||||
'current': UNKNOWN,
|
||||
'level': 0,
|
||||
'hostname': UNKNOWN,
|
||||
}
|
||||
return info
|
||||
return {
|
||||
ATTR_TYPE: self.host_control.type,
|
||||
ATTR_VERSION: self.host_control.version,
|
||||
ATTR_LAST_VERSION: self.host_control.last,
|
||||
ATTR_FEATURES: self.host_control.features,
|
||||
ATTR_HOSTNAME: self.host_control.hostname,
|
||||
}
|
||||
|
||||
return await self.host_controll.info()
|
||||
|
||||
@api_process_hostcontroll
|
||||
@api_process_hostcontrol
|
||||
def reboot(self, request):
|
||||
"""Reboot host."""
|
||||
return self.host_controll.reboot()
|
||||
return self.host_control.reboot()
|
||||
|
||||
@api_process_hostcontroll
|
||||
@api_process_hostcontrol
|
||||
def shutdown(self, request):
|
||||
"""Poweroff host."""
|
||||
return self.host_controll.shutdown()
|
||||
return self.host_control.shutdown()
|
||||
|
||||
@api_process_hostcontroll
|
||||
@api_process_hostcontrol
|
||||
async def update(self, request):
|
||||
"""Update host OS."""
|
||||
body = await api_validate(SCHEMA_VERSION, request)
|
||||
version = body.get(ATTR_VERSION)
|
||||
|
||||
if version == self.host_controll.version:
|
||||
if version == self.host_control.version:
|
||||
raise RuntimeError("Version is already in use")
|
||||
|
||||
return await self.host_controll.host_update(version=version)
|
||||
return await self.host_control.update(version=version)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""Init file for HassIO network rest api."""
|
||||
import logging
|
||||
|
||||
from .util import api_process_hostcontroll
|
||||
from .util import api_process_hostcontrol
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -9,18 +9,18 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class APINetwork(object):
|
||||
"""Handle rest api for network functions."""
|
||||
|
||||
def __init__(self, config, loop, host_controll):
|
||||
def __init__(self, config, loop, host_control):
|
||||
"""Initialize network rest api part."""
|
||||
self.config = config
|
||||
self.loop = loop
|
||||
self.host_controll = host_controll
|
||||
self.host_control = host_control
|
||||
|
||||
@api_process_hostcontroll
|
||||
@api_process_hostcontrol
|
||||
def info(self, request):
|
||||
"""Show network settings."""
|
||||
pass
|
||||
|
||||
@api_process_hostcontroll
|
||||
@api_process_hostcontrol
|
||||
def options(self, request):
|
||||
"""Edit network settings."""
|
||||
pass
|
||||
|
@@ -4,15 +4,16 @@ 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)
|
||||
ATTR_ADDONS, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_BETA_CHANNEL,
|
||||
HASSIO_VERSION)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCHEMA_OPTIONS = vol.Schema({
|
||||
# pylint: disable=no-value-for-parameter
|
||||
vol.Optional(ATTR_BETA): vol.Boolean(),
|
||||
vol.Optional(ATTR_BETA_CHANNEL): vol.Boolean(),
|
||||
})
|
||||
|
||||
SCHEMA_VERSION = vol.Schema({
|
||||
@@ -23,12 +24,13 @@ SCHEMA_VERSION = vol.Schema({
|
||||
class APISupervisor(object):
|
||||
"""Handle rest api for supervisor functions."""
|
||||
|
||||
def __init__(self, config, loop, supervisor, addons):
|
||||
def __init__(self, config, loop, supervisor, addons, host_control):
|
||||
"""Initialize supervisor rest api part."""
|
||||
self.config = config
|
||||
self.loop = loop
|
||||
self.supervisor = supervisor
|
||||
self.addons = addons
|
||||
self.host_control = host_control
|
||||
|
||||
@api_process
|
||||
async def ping(self, request):
|
||||
@@ -38,21 +40,20 @@ class APISupervisor(object):
|
||||
@api_process
|
||||
async def info(self, request):
|
||||
"""Return host information."""
|
||||
info = {
|
||||
return {
|
||||
ATTR_VERSION: HASSIO_VERSION,
|
||||
ATTR_CURRENT: self.config.current_hassio,
|
||||
ATTR_BETA: self.config.upstream_beta,
|
||||
ATTR_ADDONS: self.addons.list,
|
||||
ATTR_LAST_VERSION: self.config.last_hassio,
|
||||
ATTR_BETA_CHANNEL: self.config.upstream_beta,
|
||||
ATTR_ADDONS: self.addons.list_api,
|
||||
}
|
||||
return info
|
||||
|
||||
@api_process
|
||||
async def options(self, request):
|
||||
"""Set supervisor options."""
|
||||
body = await api_validate(SCHEMA_OPTIONS, request)
|
||||
|
||||
if ATTR_BETA in body:
|
||||
self.config.upstream_beta = body[ATTR_BETA]
|
||||
if ATTR_BETA_CHANNEL in body:
|
||||
self.config.upstream_beta = body[ATTR_BETA_CHANNEL]
|
||||
|
||||
return self.config.save()
|
||||
|
||||
@@ -60,9 +61,34 @@ class APISupervisor(object):
|
||||
async def update(self, request):
|
||||
"""Update supervisor OS."""
|
||||
body = await api_validate(SCHEMA_VERSION, request)
|
||||
version = body.get(ATTR_VERSION, self.config.current_hassio)
|
||||
version = body.get(ATTR_VERSION, self.config.last_hassio)
|
||||
|
||||
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(),
|
||||
self.host_control.load()
|
||||
]
|
||||
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()
|
||||
|
@@ -39,11 +39,11 @@ def api_process(method):
|
||||
return wrap_api
|
||||
|
||||
|
||||
def api_process_hostcontroll(method):
|
||||
"""Wrap HostControll calls to rest api."""
|
||||
async def wrap_hostcontroll(api, *args, **kwargs):
|
||||
def api_process_hostcontrol(method):
|
||||
"""Wrap HostControl calls to rest api."""
|
||||
async def wrap_hostcontrol(api, *args, **kwargs):
|
||||
"""Return host information."""
|
||||
if not api.host_controll.active:
|
||||
if not api.host_control.active:
|
||||
raise HTTPServiceUnavailable()
|
||||
|
||||
try:
|
||||
@@ -59,7 +59,21 @@ def api_process_hostcontroll(method):
|
||||
return api_return_ok()
|
||||
return api_return_error()
|
||||
|
||||
return wrap_hostcontroll
|
||||
return wrap_hostcontrol
|
||||
|
||||
|
||||
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):
|
||||
|
@@ -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")
|
||||
|
@@ -8,12 +8,12 @@ from .tools import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HOMEASSISTANT_CONFIG = "{}/homeassistant_config"
|
||||
HOMEASSISTANT_CONFIG = "{}/homeassistant"
|
||||
HOMEASSISTANT_IMAGE = 'homeassistant_image'
|
||||
HOMEASSISTANT_CURRENT = 'homeassistant_current'
|
||||
HOMEASSISTANT_LAST = 'homeassistant_last'
|
||||
|
||||
HASSIO_SSL = "{}/ssl"
|
||||
HASSIO_CURRENT = 'hassio_current'
|
||||
HASSIO_LAST = 'hassio_last'
|
||||
HASSIO_CLEANUP = 'hassio_cleanup'
|
||||
|
||||
ADDONS_REPO = "{}/addons"
|
||||
@@ -22,6 +22,8 @@ ADDONS_CUSTOM = "{}/addons_custom"
|
||||
|
||||
UPSTREAM_BETA = 'upstream_beta'
|
||||
|
||||
API_ENDPOINT = 'api_endpoint'
|
||||
|
||||
|
||||
class Config(object):
|
||||
"""Hold all config data."""
|
||||
@@ -65,19 +67,29 @@ class CoreConfig(Config):
|
||||
|
||||
async def fetch_update_infos(self):
|
||||
"""Read current versions from web."""
|
||||
current = await fetch_current_versions(
|
||||
last = await fetch_current_versions(
|
||||
self.websession, beta=self.upstream_beta)
|
||||
|
||||
if current:
|
||||
if last:
|
||||
self._data.update({
|
||||
HOMEASSISTANT_CURRENT: current.get('homeassistant_tag'),
|
||||
HASSIO_CURRENT: current.get('hassio_tag'),
|
||||
HOMEASSISTANT_LAST: last.get('homeassistant'),
|
||||
HASSIO_LAST: last.get('hassio'),
|
||||
})
|
||||
self.save()
|
||||
return True
|
||||
|
||||
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."""
|
||||
@@ -108,19 +120,24 @@ class CoreConfig(Config):
|
||||
return self._data.get(HOMEASSISTANT_IMAGE)
|
||||
|
||||
@property
|
||||
def current_homeassistant(self):
|
||||
def last_homeassistant(self):
|
||||
"""Actual version of homeassistant."""
|
||||
return self._data.get(HOMEASSISTANT_CURRENT)
|
||||
return self._data.get(HOMEASSISTANT_LAST)
|
||||
|
||||
@property
|
||||
def current_hassio(self):
|
||||
def last_hassio(self):
|
||||
"""Actual version of hassio."""
|
||||
return self._data.get(HASSIO_CURRENT)
|
||||
return self._data.get(HASSIO_LAST)
|
||||
|
||||
@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):
|
||||
@@ -130,7 +147,7 @@ class CoreConfig(Config):
|
||||
@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):
|
||||
@@ -155,4 +172,4 @@ class CoreConfig(Config):
|
||||
@property
|
||||
def path_addons_data_docker(self):
|
||||
"""Return root addon data folder extern for docker."""
|
||||
return ADDONS_DATA.format(os.environ['SUPERVISOR_SHARE'])
|
||||
return ADDONS_DATA.format(self.path_hassio_docker)
|
||||
|
@@ -1,5 +1,5 @@
|
||||
"""Const file for HassIO."""
|
||||
HASSIO_VERSION = '0.7'
|
||||
HASSIO_VERSION = '0.15'
|
||||
|
||||
URL_HASSIO_VERSION = \
|
||||
'https://raw.githubusercontent.com/pvizeli/hassio/master/version.json'
|
||||
@@ -13,6 +13,7 @@ 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
|
||||
@@ -30,10 +31,13 @@ JSON_MESSAGE = 'message'
|
||||
RESULT_ERROR = 'error'
|
||||
RESULT_OK = 'ok'
|
||||
|
||||
ATTR_HOSTNAME = 'hostname'
|
||||
ATTR_TYPE = 'type'
|
||||
ATTR_FEATURES = 'features'
|
||||
ATTR_ADDONS = 'addons'
|
||||
ATTR_VERSION = 'version'
|
||||
ATTR_CURRENT = 'current'
|
||||
ATTR_BETA = 'beta'
|
||||
ATTR_LAST_VERSION = 'last_version'
|
||||
ATTR_BETA_CHANNEL = 'beta_channel'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_SLUG = 'slug'
|
||||
ATTR_DESCRIPTON = 'description'
|
||||
@@ -44,6 +48,7 @@ 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'
|
||||
|
@@ -8,14 +8,14 @@ import docker
|
||||
from . import bootstrap
|
||||
from .addons import AddonManager
|
||||
from .api import RestAPI
|
||||
from .host_controll import HostControll
|
||||
from .host_control import HostControl
|
||||
from .const import (
|
||||
SOCKET_DOCKER, RUN_UPDATE_INFO_TASKS, RUN_RELOAD_ADDONS_TASKS,
|
||||
STARTUP_AFTER, STARTUP_BEFORE)
|
||||
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
|
||||
from .tools import get_arch_from_image, get_local_ip
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -40,8 +40,8 @@ class HassIO(object):
|
||||
self.homeassistant = DockerHomeAssistant(
|
||||
self.config, self.loop, self.dock)
|
||||
|
||||
# init HostControll
|
||||
self.host_controll = HostControll(self.loop)
|
||||
# init HostControl
|
||||
self.host_control = HostControl(self.loop)
|
||||
|
||||
# init addon system
|
||||
self.addons = AddonManager(self.config, self.loop, self.dock)
|
||||
@@ -52,27 +52,29 @@ class HassIO(object):
|
||||
await self.supervisor.attach()
|
||||
await self.supervisor.cleanup()
|
||||
|
||||
# hostcontroll
|
||||
host_info = await self.host_controll.info()
|
||||
if host_info:
|
||||
self.host_controll.version = host_info.get('version')
|
||||
_LOGGER.info(
|
||||
"Connected to HostControll. OS: %s Version: %s Hostname: %s "
|
||||
"Feature-lvl: %d", host_info.get('os'),
|
||||
host_info.get('version'), host_info.get('hostname'),
|
||||
host_info.get('level', 0))
|
||||
# set api endpoint
|
||||
self.config.api_endpoint = await get_local_ip(self.loop)
|
||||
|
||||
# hostcontrol
|
||||
await self.host_control.load()
|
||||
_LOGGER.info(
|
||||
"Connected to HostControl. Type: %s Version: %s Hostname: %s "
|
||||
"Features: %s", self.host_control.type,
|
||||
self.host_control.version, self.host_control.hostname,
|
||||
self.host_control.features)
|
||||
|
||||
# rest api views
|
||||
self.api.register_host(self.host_controll)
|
||||
self.api.register_network(self.host_controll)
|
||||
self.api.register_supervisor(self.supervisor, self.addons)
|
||||
self.api.register_host(self.host_control)
|
||||
self.api.register_network(self.host_control)
|
||||
self.api.register_supervisor(
|
||||
self.supervisor, self.addons, self.host_control)
|
||||
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():
|
||||
@@ -85,12 +87,17 @@ 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(
|
||||
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():
|
||||
@@ -108,6 +115,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)
|
||||
|
||||
@@ -118,10 +129,10 @@ class HassIO(object):
|
||||
"""Install a homeassistant docker container."""
|
||||
while True:
|
||||
# read homeassistant tag and install it
|
||||
if not self.config.current_homeassistant:
|
||||
if not self.config.last_homeassistant:
|
||||
await self.config.fetch_update_infos()
|
||||
|
||||
tag = self.config.current_homeassistant
|
||||
tag = self.config.last_homeassistant
|
||||
if tag and await self.homeassistant.install(tag):
|
||||
break
|
||||
_LOGGER.warning("Error on setup HomeAssistant. Retry in 60.")
|
||||
@@ -129,3 +140,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.last_hassio == self.supervisor.version:
|
||||
return
|
||||
|
||||
_LOGGER.info(
|
||||
"Found new HassIO version %s.", self.config.last_hassio)
|
||||
await self.supervisor.update(self.config.last_hassio)
|
||||
|
@@ -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,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):
|
||||
@@ -222,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",
|
||||
@@ -230,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)
|
||||
|
@@ -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.
|
||||
@@ -60,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
|
||||
|
@@ -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
|
||||
|
@@ -37,6 +37,9 @@ class DockerSupervisor(DockerBase):
|
||||
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."""
|
||||
|
@@ -1,4 +1,4 @@
|
||||
"""Host controll for HassIO."""
|
||||
"""Host control for HassIO."""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
@@ -7,25 +7,34 @@ import stat
|
||||
|
||||
import async_timeout
|
||||
|
||||
from .const import SOCKET_HC
|
||||
from .const import (
|
||||
SOCKET_HC, ATTR_LAST_VERSION, ATTR_VERSION, ATTR_TYPE, ATTR_FEATURES,
|
||||
ATTR_HOSTNAME)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TIMEOUT = 15
|
||||
UNKNOWN = 'unknown'
|
||||
|
||||
LEVEL_POWER = 1
|
||||
LEVEL_UPDATE_HOST = 2
|
||||
LEVEL_NETWORK = 4
|
||||
FEATURES_SHUTDOWN = 'shutdown'
|
||||
FEATURES_REBOOT = 'reboot'
|
||||
FEATURES_UPDATE = 'update'
|
||||
FEATURES_NETWORK_INFO = 'network_info'
|
||||
FEATURES_NETWORK_CONTROL = 'network_control'
|
||||
|
||||
|
||||
class HostControll(object):
|
||||
"""Client for host controll."""
|
||||
class HostControl(object):
|
||||
"""Client for host control."""
|
||||
|
||||
def __init__(self, loop):
|
||||
"""Initialize HostControll socket client."""
|
||||
"""Initialize HostControl socket client."""
|
||||
self.loop = loop
|
||||
self.active = False
|
||||
self.version = None
|
||||
self.version = UNKNOWN
|
||||
self.last = UNKNOWN
|
||||
self.type = UNKNOWN
|
||||
self.features = []
|
||||
self.hostname = UNKNOWN
|
||||
|
||||
mode = os.stat(SOCKET_HC)[stat.ST_MODE]
|
||||
if stat.S_ISSOCK(mode):
|
||||
@@ -44,14 +53,14 @@ class HostControll(object):
|
||||
|
||||
try:
|
||||
# send
|
||||
_LOGGER.info("Send '%s' to HostControll.", command)
|
||||
_LOGGER.info("Send '%s' to HostControl.", command)
|
||||
|
||||
with async_timeout.timeout(TIMEOUT, loop=self.loop):
|
||||
writer.write("{}\n".format(command).encode())
|
||||
data = await reader.readline()
|
||||
|
||||
response = data.decode()
|
||||
_LOGGER.debug("Receive from HostControll: %s.", response)
|
||||
_LOGGER.debug("Receive from HostControl: %s.", response)
|
||||
|
||||
if response == "OK":
|
||||
return True
|
||||
@@ -63,20 +72,28 @@ class HostControll(object):
|
||||
try:
|
||||
return json.loads(response)
|
||||
except json.JSONDecodeError:
|
||||
_LOGGER.warning("Json parse error from HostControll.")
|
||||
_LOGGER.warning("Json parse error from HostControl.")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error("Timeout from HostControll!")
|
||||
_LOGGER.error("Timeout from HostControl!")
|
||||
|
||||
finally:
|
||||
writer.close()
|
||||
|
||||
def info(self):
|
||||
"""Return Info from host.
|
||||
async def load(self):
|
||||
"""Load Info from host.
|
||||
|
||||
Return a coroutine.
|
||||
"""
|
||||
return self._send_command("info")
|
||||
info = await self._send_command("info")
|
||||
if not info:
|
||||
return
|
||||
|
||||
self.version = info.get(ATTR_VERSION, UNKNOWN)
|
||||
self.last = info.get(ATTR_LAST_VERSION, UNKNOWN)
|
||||
self.type = info.get(ATTR_TYPE, UNKNOWN)
|
||||
self.features = info.get(ATTR_FEATURES, [])
|
||||
self.hostname = info.get(ATTR_HOSTNAME, UNKNOWN)
|
||||
|
||||
def reboot(self):
|
||||
"""Reboot the host system.
|
||||
@@ -92,11 +109,11 @@ class HostControll(object):
|
||||
"""
|
||||
return self._send_command("shutdown")
|
||||
|
||||
def host_update(self, version=None):
|
||||
def update(self, version=None):
|
||||
"""Update the host system.
|
||||
|
||||
Return a coroutine.
|
||||
"""
|
||||
if version:
|
||||
return self._send_command("host-update {}".format(version))
|
||||
return self._send_command("host-update")
|
||||
return self._send_command("update {}".format(version))
|
||||
return self._send_command("update")
|
@@ -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]:
|
||||
|
@@ -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):
|
||||
|
14
version.json
14
version.json
@@ -1,6 +1,12 @@
|
||||
{
|
||||
"hassio_tag": "0.7",
|
||||
"homeassistant_tag": "0.42.3",
|
||||
"resinos_version": "0.3",
|
||||
"resinhup_version": "0.1"
|
||||
"hassio_tag": "0.14",
|
||||
"homeassistant_tag": "0.43.1",
|
||||
"resinos_version": "0.4",
|
||||
"resinhup_version": "0.1",
|
||||
"generic_hc_version": "0.1",
|
||||
"hassio": "0.14",
|
||||
"homeassistant": "0.43.1",
|
||||
"resinos": "0.4",
|
||||
"resinhup": "0.1",
|
||||
"generic": "0.1"
|
||||
}
|
||||
|
@@ -1,6 +1,12 @@
|
||||
{
|
||||
"hassio_tag": "0.7",
|
||||
"homeassistant_tag": "0.42.3",
|
||||
"resinos_version": "0.3",
|
||||
"resinhup_version": "0.1"
|
||||
"hassio_tag": "0.15",
|
||||
"homeassistant_tag": "0.43.1",
|
||||
"resinos_version": "0.4",
|
||||
"resinhup_version": "0.1",
|
||||
"generic_hc_version": "0.1",
|
||||
"hassio": "0.15",
|
||||
"homeassistant": "0.43.1",
|
||||
"resinos": "0.4",
|
||||
"resinhup": "0.1",
|
||||
"generic": "0.1"
|
||||
}
|
||||
|
Reference in New Issue
Block a user