mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-09-17 00:49:43 +00:00
Compare commits
51 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
81e1227a7b | ||
![]() |
75be8666a6 | ||
![]() |
6031a60084 | ||
![]() |
39d5785118 | ||
![]() |
bddcdcadb2 | ||
![]() |
3eac6a3366 | ||
![]() |
3c7b962cf9 | ||
![]() |
bd756e2a9c | ||
![]() |
e7920bee2a | ||
![]() |
ebcc21370e | ||
![]() |
34c4acf199 | ||
![]() |
47e45dfc9f | ||
![]() |
2ecea7c1b4 | ||
![]() |
5c0eccd12f | ||
![]() |
f34ab9402b | ||
![]() |
2569a82caf | ||
![]() |
4bdd256000 | ||
![]() |
6f4f6338c5 | ||
![]() |
7cb72b55a8 | ||
![]() |
1a9a08cbfb | ||
![]() |
237ee0363d | ||
![]() |
86180ddc34 | ||
![]() |
eed41d30ec | ||
![]() |
0b0fd6b910 | ||
![]() |
1f887b47ab | ||
![]() |
affd8057ca | ||
![]() |
7a8ee2c46a | ||
![]() |
35fe1f464c | ||
![]() |
0955bafebd | ||
![]() |
2e0c540c63 | ||
![]() |
6e9ef17a28 | ||
![]() |
eb3cdbfeb9 | ||
![]() |
f4cb16ad09 | ||
![]() |
956af2bd62 | ||
![]() |
b76cd5c004 | ||
![]() |
61d9301dcc | ||
![]() |
2ded05be83 | ||
![]() |
899d6766c5 | ||
![]() |
c67d57cef4 | ||
![]() |
b5cca7d341 | ||
![]() |
8919f13911 | ||
![]() |
990ae49608 | ||
![]() |
c2ba02722c | ||
![]() |
5bd1957337 | ||
![]() |
f59f0793bc | ||
![]() |
63b96700e0 | ||
![]() |
dffbcc2c7e | ||
![]() |
0dbe1ecc2a | ||
![]() |
da8526fcec | ||
![]() |
933b6f4d1e | ||
![]() |
16f2dfeebd |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "home-assistant-polymer"]
|
||||
path = home-assistant-polymer
|
||||
url = https://github.com/home-assistant/home-assistant-polymer
|
54
API.md
54
API.md
@@ -32,16 +32,20 @@ The addons from `addons` are only installed one.
|
||||
{
|
||||
"version": "INSTALL_VERSION",
|
||||
"last_version": "LAST_VERSION",
|
||||
"arch": "armhf|aarch64|i386|amd64",
|
||||
"beta_channel": "true|false",
|
||||
"addons": [
|
||||
{
|
||||
"name": "xy bla",
|
||||
"slug": "xy",
|
||||
"description": "description",
|
||||
"arch": ["armhf", "aarch64", "i386", "amd64"],
|
||||
"repository": "12345678|null",
|
||||
"version": "LAST_VERSION",
|
||||
"installed": "INSTALL_VERSION",
|
||||
"detached": "bool",
|
||||
"description": "description"
|
||||
"build": "bool",
|
||||
"url": "null|url"
|
||||
}
|
||||
],
|
||||
"addons_repositories": [
|
||||
@@ -60,11 +64,14 @@ Get all available addons
|
||||
{
|
||||
"name": "xy bla",
|
||||
"slug": "xy",
|
||||
"description": "description",
|
||||
"arch": ["armhf", "aarch64", "i386", "amd64"],
|
||||
"repository": "core|local|REP_ID",
|
||||
"version": "LAST_VERSION",
|
||||
"installed": "none|INSTALL_VERSION",
|
||||
"detached": "bool",
|
||||
"description": "description"
|
||||
"build": "bool",
|
||||
"url": "null|url"
|
||||
}
|
||||
],
|
||||
"repositories": [
|
||||
@@ -105,6 +112,40 @@ Reload addons/version.
|
||||
|
||||
Output the raw docker log
|
||||
|
||||
### Security
|
||||
|
||||
- GET `/security/info`
|
||||
```json
|
||||
{
|
||||
"initialize": "bool",
|
||||
"totp": "bool"
|
||||
}
|
||||
```
|
||||
|
||||
- POST `/security/options`
|
||||
```json
|
||||
{
|
||||
"password": "xy"
|
||||
}
|
||||
```
|
||||
|
||||
- POST `/security/totp`
|
||||
```json
|
||||
{
|
||||
"password": "xy"
|
||||
}
|
||||
```
|
||||
|
||||
Return QR-Code
|
||||
|
||||
- POST `/security/session`
|
||||
```json
|
||||
{
|
||||
"password": "xy",
|
||||
"totp": "null|123456"
|
||||
}
|
||||
```
|
||||
|
||||
### Host
|
||||
|
||||
- POST `/host/shutdown`
|
||||
@@ -171,16 +212,23 @@ Optional:
|
||||
|
||||
Output the raw docker log
|
||||
|
||||
- POST `/homeassistant/restart`
|
||||
|
||||
### REST API addons
|
||||
|
||||
- GET `/addons/{addon}/info`
|
||||
```json
|
||||
{
|
||||
"name": "xy bla",
|
||||
"description": "description",
|
||||
"url": "null|url of addon",
|
||||
"detached": "bool",
|
||||
"repository": "12345678|null",
|
||||
"version": "VERSION",
|
||||
"last_version": "LAST_VERSION",
|
||||
"state": "started|stopped",
|
||||
"boot": "auto|manual",
|
||||
"build": "bool",
|
||||
"options": {},
|
||||
}
|
||||
```
|
||||
@@ -219,6 +267,8 @@ Optional:
|
||||
|
||||
Output the raw docker log
|
||||
|
||||
- POST `/addons/{addon}/restart`
|
||||
|
||||
## Host Control
|
||||
|
||||
Communicate over unix socket with a host daemon.
|
||||
|
@@ -11,12 +11,7 @@ Hass.io is a Docker based system for managing your Home Assistant installation a
|
||||
|
||||
## Installing Hass.io
|
||||
|
||||
- Generic Linux installation: https://github.com/home-assistant/hassio-build/tree/master/install
|
||||
- Hardware Images: https://github.com/home-assistant/hassio-build/blob/master/meta-hassio/
|
||||
|
||||
## Feature in progress
|
||||
- Backup/Restore
|
||||
- DHCP-Server addon
|
||||
Looks to our [website](https://home-assistant.io/hassio).
|
||||
|
||||
# HomeAssistant
|
||||
|
||||
|
@@ -108,6 +108,10 @@ class AddonManager(AddonsData):
|
||||
_LOGGER.error("Addon %s not exists for install", addon)
|
||||
return False
|
||||
|
||||
if self.arch not in self.get_arch(addon):
|
||||
_LOGGER.error("Addon %s not supported on %s", addon, self.arch)
|
||||
return False
|
||||
|
||||
if self.is_installed(addon):
|
||||
_LOGGER.error("Addon %s is already installed", addon)
|
||||
return False
|
||||
@@ -187,7 +191,7 @@ class AddonManager(AddonsData):
|
||||
return False
|
||||
|
||||
version = version or self.get_last_version(addon)
|
||||
is_running = self.dockers[addon].is_running()
|
||||
is_running = await self.dockers[addon].is_running()
|
||||
|
||||
# update
|
||||
if await self.dockers[addon].update(version):
|
||||
@@ -197,6 +201,14 @@ class AddonManager(AddonsData):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def restart(self, addon):
|
||||
"""Restart addon."""
|
||||
if addon not in self.dockers:
|
||||
_LOGGER.error("No docker found for addon %s", addon)
|
||||
return False
|
||||
|
||||
return await self.dockers[addon].restart()
|
||||
|
||||
async def logs(self, addon):
|
||||
"""Return addons log output."""
|
||||
if addon not in self.dockers:
|
||||
|
@@ -3,18 +3,20 @@ import copy
|
||||
import logging
|
||||
import json
|
||||
from pathlib import Path, PurePath
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from .util import extract_hash_from_path
|
||||
from .validate import (
|
||||
validate_options, SCHEMA_ADDON_CONFIG, SCHEMA_REPOSITORY_CONFIG)
|
||||
validate_options, SCHEMA_ADDON_CONFIG, SCHEMA_REPOSITORY_CONFIG,
|
||||
MAP_VOLUME)
|
||||
from ..const import (
|
||||
FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON,
|
||||
ATTR_STARTUP, ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, BOOT_AUTO,
|
||||
DOCKER_REPO, ATTR_SCHEMA, ATTR_IMAGE, MAP_CONFIG, MAP_SSL, MAP_ADDONS,
|
||||
MAP_BACKUP, ATTR_REPOSITORY, ATTR_URL)
|
||||
ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY, ATTR_URL, ATTR_ARCH,
|
||||
ATTR_LOCATON, ATTR_DEVICES, ATTR_ENVIRONMENT)
|
||||
from ..config import Config
|
||||
from ..tools import read_json_file, write_json_file
|
||||
|
||||
@@ -26,6 +28,8 @@ USER = 'user'
|
||||
REPOSITORY_CORE = 'core'
|
||||
REPOSITORY_LOCAL = 'local'
|
||||
|
||||
RE_VOLUME = re.compile(MAP_VOLUME)
|
||||
|
||||
|
||||
class AddonsData(Config):
|
||||
"""Hold data for addons inside HassIO."""
|
||||
@@ -109,6 +113,7 @@ class AddonsData(Config):
|
||||
|
||||
# store
|
||||
addon_config[ATTR_REPOSITORY] = repository
|
||||
addon_config[ATTR_LOCATON] = str(addon.parent)
|
||||
self._addons_cache[addon_slug] = addon_config
|
||||
|
||||
except OSError:
|
||||
@@ -148,12 +153,13 @@ class AddonsData(Config):
|
||||
"""
|
||||
have_change = False
|
||||
|
||||
for addon, data in self._system_data.items():
|
||||
for addon in self.list_installed:
|
||||
# detached
|
||||
if addon not in self._addons_cache:
|
||||
continue
|
||||
|
||||
cache = self._addons_cache[addon]
|
||||
data = self._system_data[addon]
|
||||
if data[ATTR_VERSION] == cache[ATTR_VERSION]:
|
||||
if data != cache:
|
||||
self._system_data[addon] = copy.deepcopy(cache)
|
||||
@@ -165,20 +171,12 @@ class AddonsData(Config):
|
||||
@property
|
||||
def list_installed(self):
|
||||
"""Return a list of installed addons."""
|
||||
return set(self._system_data.keys())
|
||||
return set(self._system_data)
|
||||
|
||||
@property
|
||||
def data_all(self):
|
||||
def list_all(self):
|
||||
"""Return a dict of all addons."""
|
||||
return {
|
||||
**self._system_data,
|
||||
**self._addons_cache
|
||||
}
|
||||
|
||||
@property
|
||||
def data_installed(self):
|
||||
"""Return a dict of installed addons."""
|
||||
return self._system_data.copy()
|
||||
return set(self._system_data) | set(self._addons_cache)
|
||||
|
||||
def list_startup(self, start_type):
|
||||
"""Get list of installed addon with need start by type."""
|
||||
@@ -270,52 +268,81 @@ class AddonsData(Config):
|
||||
|
||||
def get_name(self, addon):
|
||||
"""Return name of addon."""
|
||||
if addon in self._addons_cache:
|
||||
return self._addons_cache[addon][ATTR_NAME]
|
||||
return self._system_data[addon][ATTR_NAME]
|
||||
|
||||
def get_description(self, addon):
|
||||
"""Return description of addon."""
|
||||
if addon in self._addons_cache:
|
||||
return self._addons_cache[addon][ATTR_DESCRIPTON]
|
||||
return self._system_data[addon][ATTR_DESCRIPTON]
|
||||
|
||||
def get_repository(self, addon):
|
||||
"""Return repository of addon."""
|
||||
if addon in self._addons_cache:
|
||||
return self._addons_cache[addon][ATTR_REPOSITORY]
|
||||
return self._system_data[addon][ATTR_REPOSITORY]
|
||||
|
||||
def get_last_version(self, addon):
|
||||
"""Return version of addon."""
|
||||
if addon not in self._addons_cache:
|
||||
return self.version_installed(addon)
|
||||
return self._addons_cache[addon][ATTR_VERSION]
|
||||
if addon in self._addons_cache:
|
||||
return self._addons_cache[addon][ATTR_VERSION]
|
||||
return self.version_installed(addon)
|
||||
|
||||
def get_ports(self, addon):
|
||||
"""Return ports of addon."""
|
||||
return self._system_data[addon].get(ATTR_PORTS)
|
||||
|
||||
def get_devices(self, addon):
|
||||
"""Return devices of addon."""
|
||||
return self._system_data[addon].get(ATTR_DEVICES)
|
||||
|
||||
def get_environment(self, addon):
|
||||
"""Return environment of addon."""
|
||||
return self._system_data[addon].get(ATTR_ENVIRONMENT)
|
||||
|
||||
def get_url(self, addon):
|
||||
"""Return url of addon."""
|
||||
if addon in self._addons_cache:
|
||||
return self._addons_cache[addon].get(ATTR_URL)
|
||||
return self._system_data[addon].get(ATTR_URL)
|
||||
|
||||
def get_arch(self, addon):
|
||||
"""Return list of supported arch."""
|
||||
if addon in self._addons_cache:
|
||||
return self._addons_cache[addon][ATTR_ARCH]
|
||||
return self._system_data[addon][ATTR_ARCH]
|
||||
|
||||
def get_image(self, addon):
|
||||
"""Return image name of addon."""
|
||||
addon_data = self._system_data.get(
|
||||
addon, self._addons_cache.get(addon))
|
||||
addon, self._addons_cache.get(addon)
|
||||
)
|
||||
|
||||
if ATTR_IMAGE not in addon_data:
|
||||
return "{}/{}-addon-{}".format(
|
||||
DOCKER_REPO, self.arch, addon_data[ATTR_SLUG])
|
||||
# Repository with dockerhub images
|
||||
if ATTR_IMAGE in addon_data:
|
||||
return addon_data[ATTR_IMAGE].format(arch=self.arch)
|
||||
|
||||
return addon_data[ATTR_IMAGE].format(arch=self.arch)
|
||||
# local build
|
||||
return "{}/{}-addon-{}".format(
|
||||
addon_data[ATTR_REPOSITORY], self.arch, addon_data[ATTR_SLUG])
|
||||
|
||||
def map_config(self, addon):
|
||||
"""Return True if config map is needed."""
|
||||
return MAP_CONFIG in self._system_data[addon][ATTR_MAP]
|
||||
def need_build(self, addon):
|
||||
"""Return True if this addon need a local build."""
|
||||
addon_data = self._system_data.get(
|
||||
addon, self._addons_cache.get(addon)
|
||||
)
|
||||
return ATTR_IMAGE not in addon_data
|
||||
|
||||
def map_ssl(self, addon):
|
||||
"""Return True if ssl map is needed."""
|
||||
return MAP_SSL in self._system_data[addon][ATTR_MAP]
|
||||
def map_volumes(self, addon):
|
||||
"""Return a dict of {volume: policy} from addon."""
|
||||
volumes = {}
|
||||
for volume in self._system_data[addon][ATTR_MAP]:
|
||||
result = RE_VOLUME.match(volume)
|
||||
volumes[result.group(1)] = result.group(2) or 'ro'
|
||||
|
||||
def map_addons(self, addon):
|
||||
"""Return True if addons map is needed."""
|
||||
return MAP_ADDONS in self._system_data[addon][ATTR_MAP]
|
||||
|
||||
def map_backup(self, addon):
|
||||
"""Return True if backup map is needed."""
|
||||
return MAP_BACKUP in self._system_data[addon][ATTR_MAP]
|
||||
return volumes
|
||||
|
||||
def path_data(self, addon):
|
||||
"""Return addon data path inside supervisor."""
|
||||
@@ -323,12 +350,16 @@ class AddonsData(Config):
|
||||
|
||||
def path_extern_data(self, addon):
|
||||
"""Return addon data path external for docker."""
|
||||
return str(PurePath(self.config.path_extern_addons_data, addon))
|
||||
return PurePath(self.config.path_extern_addons_data, addon)
|
||||
|
||||
def path_addon_options(self, addon):
|
||||
"""Return path to addons options."""
|
||||
return Path(self.path_data(addon), "options.json")
|
||||
|
||||
def path_addon_location(self, addon):
|
||||
"""Return path to this addon."""
|
||||
return Path(self._addons_cache[addon][ATTR_LOCATON])
|
||||
|
||||
def write_addon_options(self, addon):
|
||||
"""Return True if addon options is written to data."""
|
||||
schema = self.get_schema(addon)
|
||||
|
@@ -4,8 +4,12 @@ import voluptuous as vol
|
||||
from ..const import (
|
||||
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_STARTUP,
|
||||
ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, STARTUP_ONCE, STARTUP_AFTER,
|
||||
STARTUP_BEFORE, BOOT_AUTO, BOOT_MANUAL, ATTR_SCHEMA, ATTR_IMAGE, MAP_SSL,
|
||||
MAP_CONFIG, MAP_ADDONS, MAP_BACKUP, ATTR_URL, ATTR_MAINTAINER)
|
||||
STARTUP_BEFORE, BOOT_AUTO, BOOT_MANUAL, ATTR_SCHEMA, ATTR_IMAGE,
|
||||
ATTR_URL, ATTR_MAINTAINER, ATTR_ARCH, ATTR_DEVICES, ATTR_ENVIRONMENT,
|
||||
ARCH_ARMHF, ARCH_AARCH64, ARCH_AMD64, ARCH_I386)
|
||||
|
||||
|
||||
MAP_VOLUME = r"^(config|ssl|addons|backup|share|mnt)(?::(rw|:ro))?$"
|
||||
|
||||
V_STR = 'str'
|
||||
V_INT = 'int'
|
||||
@@ -16,21 +20,26 @@ V_URL = 'url'
|
||||
|
||||
ADDON_ELEMENT = vol.In([V_STR, V_INT, V_FLOAT, V_BOOL, V_EMAIL, V_URL])
|
||||
|
||||
ARCH_ALL = [
|
||||
ARCH_ARMHF, ARCH_AARCH64, ARCH_AMD64, ARCH_I386
|
||||
]
|
||||
|
||||
# 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.Optional(ATTR_URL): vol.Url(),
|
||||
vol.Optional(ATTR_ARCH, default=ARCH_ALL): [vol.In(ARCH_ALL)],
|
||||
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, default=[]): [
|
||||
vol.In([MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP])
|
||||
],
|
||||
vol.Optional(ATTR_URL): vol.Url(),
|
||||
vol.Optional(ATTR_DEVICES): [vol.Match(r"^(.*):(.*):([rwm]{1,3})$")],
|
||||
vol.Optional(ATTR_MAP, default=[]): [vol.Match(MAP_VOLUME)],
|
||||
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)},
|
||||
vol.Required(ATTR_OPTIONS): dict,
|
||||
vol.Required(ATTR_SCHEMA): {
|
||||
vol.Coerce(str): vol.Any(ADDON_ELEMENT, [
|
||||
@@ -64,10 +73,10 @@ def validate_options(raw_schema):
|
||||
try:
|
||||
if isinstance(typ, list):
|
||||
# nested value
|
||||
options[key] = _nested_validate(typ[0], value)
|
||||
options[key] = _nested_validate(typ[0], value, key)
|
||||
else:
|
||||
# normal value
|
||||
options[key] = _single_validate(typ, value)
|
||||
options[key] = _single_validate(typ, value, key)
|
||||
except (IndexError, KeyError):
|
||||
raise vol.Invalid(
|
||||
"Type error for {}.".format(key)) from None
|
||||
@@ -78,12 +87,12 @@ def validate_options(raw_schema):
|
||||
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
def _single_validate(typ, value):
|
||||
def _single_validate(typ, value, key):
|
||||
"""Validate a single element."""
|
||||
try:
|
||||
# if required argument
|
||||
if value is None:
|
||||
raise vol.Invalid("A required argument is not set!")
|
||||
raise vol.Invalid("Missing required option '{}'.".format(key))
|
||||
|
||||
if typ == V_STR:
|
||||
return str(value)
|
||||
@@ -98,13 +107,13 @@ def _single_validate(typ, value):
|
||||
elif typ == V_URL:
|
||||
return vol.Url()(value)
|
||||
|
||||
raise vol.Invalid("Fatal error for {}.".format(value))
|
||||
raise vol.Invalid("Fatal error for {} type {}.".format(key, typ))
|
||||
except ValueError:
|
||||
raise vol.Invalid(
|
||||
"Type {} error for {}.".format(typ, value)) from None
|
||||
"Type {} error for '{}' on {}.".format(typ, value, key)) from None
|
||||
|
||||
|
||||
def _nested_validate(typ, data_list):
|
||||
def _nested_validate(typ, data_list, key):
|
||||
"""Validate nested items."""
|
||||
options = []
|
||||
|
||||
@@ -117,10 +126,10 @@ def _nested_validate(typ, data_list):
|
||||
raise vol.Invalid(
|
||||
"Unknown nested options {}.".format(c_key))
|
||||
|
||||
c_options[c_key] = _single_validate(typ[c_key], c_value)
|
||||
c_options[c_key] = _single_validate(typ[c_key], c_value, c_key)
|
||||
options.append(c_options)
|
||||
# normal list
|
||||
else:
|
||||
options.append(_single_validate(typ, element))
|
||||
options.append(_single_validate(typ, element, key))
|
||||
|
||||
return options
|
||||
|
@@ -1,5 +1,6 @@
|
||||
"""Init file for HassIO rest api."""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
@@ -8,6 +9,7 @@ from .homeassistant import APIHomeAssistant
|
||||
from .host import APIHost
|
||||
from .network import APINetwork
|
||||
from .supervisor import APISupervisor
|
||||
from .security import APISecurity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -64,6 +66,7 @@ class RestAPI(object):
|
||||
|
||||
self.webapp.router.add_get('/homeassistant/info', api_hass.info)
|
||||
self.webapp.router.add_post('/homeassistant/update', api_hass.update)
|
||||
self.webapp.router.add_post('/homeassistant/restart', api_hass.restart)
|
||||
self.webapp.router.add_get('/homeassistant/logs', api_hass.logs)
|
||||
|
||||
def register_addons(self, addons):
|
||||
@@ -77,12 +80,30 @@ class RestAPI(object):
|
||||
'/addons/{addon}/uninstall', api_addons.uninstall)
|
||||
self.webapp.router.add_post('/addons/{addon}/start', api_addons.start)
|
||||
self.webapp.router.add_post('/addons/{addon}/stop', api_addons.stop)
|
||||
self.webapp.router.add_post(
|
||||
'/addons/{addon}/restart', api_addons.restart)
|
||||
self.webapp.router.add_post(
|
||||
'/addons/{addon}/update', api_addons.update)
|
||||
self.webapp.router.add_post(
|
||||
'/addons/{addon}/options', api_addons.options)
|
||||
self.webapp.router.add_get('/addons/{addon}/logs', api_addons.logs)
|
||||
|
||||
def register_security(self):
|
||||
"""Register security function."""
|
||||
api_security = APISecurity(self.config, self.loop)
|
||||
|
||||
self.webapp.router.add_get('/security/info', api_security.info)
|
||||
self.webapp.router.add_post('/security/options', api_security.options)
|
||||
self.webapp.router.add_post('/security/totp', api_security.totp)
|
||||
self.webapp.router.add_post('/security/session', api_security.session)
|
||||
|
||||
def register_panel(self):
|
||||
"""Register panel for homeassistant."""
|
||||
panel_dir = Path(__file__).parents[1].joinpath('panel')
|
||||
|
||||
self.webapp.router.register_resource(
|
||||
web.StaticResource('/panel', str(panel_dir)))
|
||||
|
||||
async def start(self):
|
||||
"""Run rest api webserver."""
|
||||
self._handler = self.webapp.make_handler(loop=self.loop)
|
||||
|
@@ -8,7 +8,8 @@ from voluptuous.humanize import humanize_error
|
||||
from .util import api_process, api_process_raw, api_validate
|
||||
from ..const import (
|
||||
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS,
|
||||
ATTR_URL, STATE_STOPPED, STATE_STARTED, BOOT_AUTO, BOOT_MANUAL)
|
||||
ATTR_URL, ATTR_DESCRIPTON, ATTR_DETACHED, ATTR_NAME, ATTR_REPOSITORY,
|
||||
ATTR_BUILD, STATE_STOPPED, STATE_STARTED, BOOT_AUTO, BOOT_MANUAL)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -48,12 +49,17 @@ class APIAddons(object):
|
||||
addon = self._extract_addon(request)
|
||||
|
||||
return {
|
||||
ATTR_NAME: self.addons.get_name(addon),
|
||||
ATTR_DESCRIPTON: self.addons.get_description(addon),
|
||||
ATTR_VERSION: self.addons.version_installed(addon),
|
||||
ATTR_REPOSITORY: self.addons.get_repository(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),
|
||||
ATTR_URL: self.addons.get_url(addon),
|
||||
ATTR_DETACHED: addon in self.addons.list_detached,
|
||||
ATTR_BUILD: self.addons.need_build(addon),
|
||||
}
|
||||
|
||||
@api_process
|
||||
@@ -83,6 +89,11 @@ class APIAddons(object):
|
||||
version = body.get(
|
||||
ATTR_VERSION, self.addons.get_last_version(addon))
|
||||
|
||||
# check if arch supported
|
||||
if self.addons.arch not in self.addons.get_arch(addon):
|
||||
raise RuntimeError(
|
||||
"Addon is not supported on {}".format(self.addons.arch))
|
||||
|
||||
return await asyncio.shield(
|
||||
self.addons.install(addon, version), loop=self.loop)
|
||||
|
||||
@@ -138,6 +149,12 @@ class APIAddons(object):
|
||||
return await asyncio.shield(
|
||||
self.addons.update(addon, version), loop=self.loop)
|
||||
|
||||
@api_process
|
||||
async def restart(self, request):
|
||||
"""Restart addon."""
|
||||
addon = self._extract_addon(request)
|
||||
return await asyncio.shield(self.addons.restart(addon), loop=self.loop)
|
||||
|
||||
@api_process_raw
|
||||
def logs(self, request):
|
||||
"""Return logs from addon."""
|
||||
|
@@ -26,16 +26,14 @@ class APIHomeAssistant(object):
|
||||
@api_process
|
||||
async def info(self, request):
|
||||
"""Return host information."""
|
||||
info = {
|
||||
return {
|
||||
ATTR_VERSION: self.homeassistant.version,
|
||||
ATTR_LAST_VERSION: self.config.last_homeassistant,
|
||||
}
|
||||
|
||||
return info
|
||||
|
||||
@api_process
|
||||
async def update(self, request):
|
||||
"""Update host OS."""
|
||||
"""Update homeassistant."""
|
||||
body = await api_validate(SCHEMA_VERSION, request)
|
||||
version = body.get(ATTR_VERSION, self.config.last_homeassistant)
|
||||
|
||||
@@ -48,6 +46,15 @@ class APIHomeAssistant(object):
|
||||
return await asyncio.shield(
|
||||
self.homeassistant.update(version), loop=self.loop)
|
||||
|
||||
@api_process
|
||||
async def restart(self, request):
|
||||
"""Restart homeassistant."""
|
||||
if self.homeassistant.in_progress:
|
||||
raise RuntimeError("Other task is in progress")
|
||||
|
||||
return await asyncio.shield(
|
||||
self.homeassistant.restart(), loop=self.loop)
|
||||
|
||||
@api_process_raw
|
||||
def logs(self, request):
|
||||
"""Return homeassistant docker logs.
|
||||
|
102
hassio/api/security.py
Normal file
102
hassio/api/security.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""Init file for HassIO security rest api."""
|
||||
from datetime import datetime, timedelta
|
||||
import io
|
||||
import logging
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
import pyotp
|
||||
import pyqrcode
|
||||
|
||||
from .util import api_process, api_validate, hash_password
|
||||
from ..const import ATTR_INITIALIZE, ATTR_PASSWORD, ATTR_TOTP, ATTR_SESSION
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCHEMA_PASSWORD = vol.Schema({
|
||||
vol.Required(ATTR_PASSWORD): vol.Coerce(str),
|
||||
})
|
||||
|
||||
SCHEMA_SESSION = SCHEMA_PASSWORD.extend({
|
||||
vol.Optional(ATTR_TOTP, default=None): vol.Coerce(str),
|
||||
})
|
||||
|
||||
|
||||
class APISecurity(object):
|
||||
"""Handle rest api for security functions."""
|
||||
|
||||
def __init__(self, config, loop):
|
||||
"""Initialize security rest api part."""
|
||||
self.config = config
|
||||
self.loop = loop
|
||||
|
||||
def _check_password(self, body):
|
||||
"""Check if password is valid and security is initialize."""
|
||||
if not self.config.security_initialize:
|
||||
raise RuntimeError("First set a password")
|
||||
|
||||
password = hash_password(body[ATTR_PASSWORD])
|
||||
if password != self.config.security_password:
|
||||
raise RuntimeError("Wrong password")
|
||||
|
||||
@api_process
|
||||
async def info(self, request):
|
||||
"""Return host information."""
|
||||
return {
|
||||
ATTR_INITIALIZE: self.config.security_initialize,
|
||||
ATTR_TOTP: self.config.security_totp is not None,
|
||||
}
|
||||
|
||||
@api_process
|
||||
async def options(self, request):
|
||||
"""Set options / password."""
|
||||
body = await api_validate(SCHEMA_PASSWORD, request)
|
||||
|
||||
if self.config.security_initialize:
|
||||
raise RuntimeError("Password is already set!")
|
||||
|
||||
self.config.security_password = hash_password(body[ATTR_PASSWORD])
|
||||
self.config.security_initialize = True
|
||||
return True
|
||||
|
||||
@api_process
|
||||
async def totp(self, request):
|
||||
"""Set and initialze TOTP."""
|
||||
body = await api_validate(SCHEMA_PASSWORD, request)
|
||||
self._check_password(body)
|
||||
|
||||
# generate TOTP
|
||||
totp_init_key = pyotp.random_base32()
|
||||
totp = pyotp.TOTP(totp_init_key)
|
||||
|
||||
# init qrcode
|
||||
buff = io.BytesIO()
|
||||
|
||||
qrcode = pyqrcode.create(totp.provisioning_uri("Hass.IO"))
|
||||
qrcode.svg(buff)
|
||||
|
||||
# finish
|
||||
self.config.security_totp = totp_init_key
|
||||
return web.Response(body=buff.getvalue(), content_type='image/svg+xml')
|
||||
|
||||
@api_process
|
||||
async def session(self, request):
|
||||
"""Set and initialze session."""
|
||||
body = await api_validate(SCHEMA_SESSION, request)
|
||||
self._check_password(body)
|
||||
|
||||
# check TOTP
|
||||
if self.config.security_totp:
|
||||
totp = pyotp.TOTP(self.config.security_totp)
|
||||
if body[ATTR_TOTP] != totp.now():
|
||||
raise RuntimeError("Invalid TOTP token!")
|
||||
|
||||
# create session
|
||||
valid_until = datetime.now() + timedelta(days=1)
|
||||
session = hashlib.sha256(os.urandom(54)).hexdigest()
|
||||
|
||||
# store session
|
||||
self.config.security_sessions = (session, valid_until)
|
||||
return {ATTR_SESSION: session}
|
@@ -10,7 +10,8 @@ from ..const import (
|
||||
ATTR_ADDONS, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_BETA_CHANNEL,
|
||||
HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES, ATTR_REPOSITORIES,
|
||||
ATTR_REPOSITORY, ATTR_DESCRIPTON, ATTR_NAME, ATTR_SLUG, ATTR_INSTALLED,
|
||||
ATTR_DETACHED, ATTR_SOURCE, ATTR_MAINTAINER, ATTR_URL)
|
||||
ATTR_DETACHED, ATTR_SOURCE, ATTR_MAINTAINER, ATTR_URL, ATTR_ARCH,
|
||||
ATTR_BUILD)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,22 +42,23 @@ class APISupervisor(object):
|
||||
detached = self.addons.list_detached
|
||||
|
||||
if only_installed:
|
||||
addons = self.addons.data_installed
|
||||
addons = self.addons.list_installed
|
||||
else:
|
||||
addons = self.addons.data_all
|
||||
addons = self.addons.list_all
|
||||
|
||||
data = []
|
||||
for addon, values in addons.items():
|
||||
i_version = self.addons.version_installed(addon)
|
||||
|
||||
for addon in addons:
|
||||
data.append({
|
||||
ATTR_NAME: values[ATTR_NAME],
|
||||
ATTR_NAME: self.addons.get_name(addon),
|
||||
ATTR_SLUG: addon,
|
||||
ATTR_DESCRIPTON: values[ATTR_DESCRIPTON],
|
||||
ATTR_VERSION: values[ATTR_VERSION],
|
||||
ATTR_INSTALLED: i_version,
|
||||
ATTR_DESCRIPTON: self.addons.get_description(addon),
|
||||
ATTR_VERSION: self.addons.get_last_version(addon),
|
||||
ATTR_INSTALLED: self.addons.version_installed(addon),
|
||||
ATTR_ARCH: self.addons.get_arch(addon),
|
||||
ATTR_DETACHED: addon in detached,
|
||||
ATTR_REPOSITORY: values[ATTR_REPOSITORY],
|
||||
ATTR_REPOSITORY: self.addons.get_repository(addon),
|
||||
ATTR_BUILD: self.addons.need_build(addon),
|
||||
ATTR_URL: self.addons.get_url(addon),
|
||||
})
|
||||
|
||||
return data
|
||||
@@ -89,6 +91,7 @@ class APISupervisor(object):
|
||||
ATTR_VERSION: HASSIO_VERSION,
|
||||
ATTR_LAST_VERSION: self.config.last_hassio,
|
||||
ATTR_BETA_CHANNEL: self.config.upstream_beta,
|
||||
ATTR_ARCH: self.addons.arch,
|
||||
ATTR_ADDONS: self._addons_list(only_installed=True),
|
||||
ATTR_ADDONS_REPOSITORIES: self.config.addons_repositories,
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
"""Init file for HassIO util for rest api."""
|
||||
import json
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
@@ -32,6 +33,8 @@ def api_process(method):
|
||||
|
||||
if isinstance(answer, dict):
|
||||
return api_return_ok(data=answer)
|
||||
if isinstance(answer, web.Response):
|
||||
return answer
|
||||
elif answer:
|
||||
return api_return_ok()
|
||||
return api_return_error()
|
||||
@@ -101,3 +104,9 @@ async def api_validate(schema, request):
|
||||
raise RuntimeError(humanize_error(data, ex)) from None
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def hash_password(password):
|
||||
"""Hash and salt our passwords."""
|
||||
key = ")*()*SALT_HASSIO2123{}6554547485HSKA!!*JSLAfdasda$".format(password)
|
||||
return hashlib.sha256(key.encode()).hexdigest()
|
||||
|
@@ -21,33 +21,42 @@ def initialize_system_data(websession):
|
||||
"Create Home-Assistant config folder %s", config.path_config)
|
||||
config.path_config.mkdir()
|
||||
|
||||
# homeassistant ssl folder
|
||||
# hassio ssl folder
|
||||
if not config.path_ssl.is_dir():
|
||||
_LOGGER.info("Create Home-Assistant ssl folder %s", config.path_ssl)
|
||||
_LOGGER.info("Create hassio ssl folder %s", config.path_ssl)
|
||||
config.path_ssl.mkdir()
|
||||
|
||||
# homeassistant addon data folder
|
||||
# hassio addon data folder
|
||||
if not config.path_addons_data.is_dir():
|
||||
_LOGGER.info("Create Home-Assistant addon data folder %s",
|
||||
config.path_addons_data)
|
||||
_LOGGER.info(
|
||||
"Create hassio addon data folder %s", config.path_addons_data)
|
||||
config.path_addons_data.mkdir(parents=True)
|
||||
|
||||
if not config.path_addons_local.is_dir():
|
||||
_LOGGER.info("Create Home-Assistant addon local repository folder %s",
|
||||
_LOGGER.info("Create hassio addon local repository folder %s",
|
||||
config.path_addons_local)
|
||||
config.path_addons_local.mkdir(parents=True)
|
||||
|
||||
if not config.path_addons_git.is_dir():
|
||||
_LOGGER.info("Create Home-Assistant addon git repositories folder %s",
|
||||
_LOGGER.info("Create hassio addon git repositories folder %s",
|
||||
config.path_addons_git)
|
||||
config.path_addons_git.mkdir(parents=True)
|
||||
|
||||
# homeassistant backup folder
|
||||
if not config.path_addons_build.is_dir():
|
||||
_LOGGER.info("Create Home-Assistant addon build folder %s",
|
||||
config.path_addons_build)
|
||||
config.path_addons_build.mkdir(parents=True)
|
||||
|
||||
# hassio backup folder
|
||||
if not config.path_backup.is_dir():
|
||||
_LOGGER.info("Create Home-Assistant backup folder %s",
|
||||
config.path_backup)
|
||||
_LOGGER.info("Create hassio backup folder %s", config.path_backup)
|
||||
config.path_backup.mkdir()
|
||||
|
||||
# share folder
|
||||
if not config.path_share.is_dir():
|
||||
_LOGGER.info("Create hassio share folder %s", config.path_share)
|
||||
config.path_share.mkdir()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Bootstrap HassIO."""
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
@@ -13,6 +14,8 @@ from .tools import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATETIME_FORMAT = "%Y%m%d %H:%M:%S"
|
||||
|
||||
HOMEASSISTANT_CONFIG = PurePath("homeassistant")
|
||||
HOMEASSISTANT_LAST = 'homeassistant_last'
|
||||
|
||||
@@ -24,14 +27,22 @@ ADDONS_CORE = PurePath("addons/core")
|
||||
ADDONS_LOCAL = PurePath("addons/local")
|
||||
ADDONS_GIT = PurePath("addons/git")
|
||||
ADDONS_DATA = PurePath("addons/data")
|
||||
ADDONS_BUILD = PurePath("addons/build")
|
||||
ADDONS_CUSTOM_LIST = 'addons_custom_list'
|
||||
|
||||
BACKUP_DATA = PurePath("backup")
|
||||
|
||||
SHARE_DATA = PurePath("share")
|
||||
|
||||
UPSTREAM_BETA = 'upstream_beta'
|
||||
|
||||
API_ENDPOINT = 'api_endpoint'
|
||||
|
||||
SECURITY_INITIALIZE = 'security_initialize'
|
||||
SECURITY_TOTP = 'security_totp'
|
||||
SECURITY_PASSWORD = 'security_password'
|
||||
SECURITY_SESSIONS = 'security_sessions'
|
||||
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
SCHEMA_CONFIG = vol.Schema({
|
||||
@@ -41,6 +52,11 @@ SCHEMA_CONFIG = vol.Schema({
|
||||
vol.Optional(HASSIO_LAST): vol.Coerce(str),
|
||||
vol.Optional(HASSIO_CLEANUP): vol.Coerce(str),
|
||||
vol.Optional(ADDONS_CUSTOM_LIST, default=[]): [vol.Url()],
|
||||
vol.Optional(SECURITY_INITIALIZE, default=False): vol.Boolean(),
|
||||
vol.Optional(SECURITY_TOTP): vol.Coerce(str),
|
||||
vol.Optional(SECURITY_PASSWORD): vol.Coerce(str),
|
||||
vol.Optional(SECURITY_SESSIONS, default={}):
|
||||
{vol.Coerce(str): vol.Coerce(str)},
|
||||
}, extra=vol.REMOVE_EXTRA)
|
||||
|
||||
|
||||
@@ -192,7 +208,7 @@ class CoreConfig(Config):
|
||||
@property
|
||||
def path_extern_addons_local(self):
|
||||
"""Return path for customs addons."""
|
||||
return str(PurePath(self.path_extern_hassio, ADDONS_LOCAL))
|
||||
return PurePath(self.path_extern_hassio, ADDONS_LOCAL)
|
||||
|
||||
@property
|
||||
def path_addons_data(self):
|
||||
@@ -202,7 +218,12 @@ class CoreConfig(Config):
|
||||
@property
|
||||
def path_extern_addons_data(self):
|
||||
"""Return root addon data folder extern for docker."""
|
||||
return str(PurePath(self.path_extern_hassio, ADDONS_DATA))
|
||||
return PurePath(self.path_extern_hassio, ADDONS_DATA)
|
||||
|
||||
@property
|
||||
def path_addons_build(self):
|
||||
"""Return root addon build folder."""
|
||||
return Path(HASSIO_SHARE, ADDONS_BUILD)
|
||||
|
||||
@property
|
||||
def path_backup(self):
|
||||
@@ -212,7 +233,17 @@ class CoreConfig(Config):
|
||||
@property
|
||||
def path_extern_backup(self):
|
||||
"""Return root backup data folder extern for docker."""
|
||||
return str(PurePath(self.path_extern_hassio, BACKUP_DATA))
|
||||
return PurePath(self.path_extern_hassio, BACKUP_DATA)
|
||||
|
||||
@property
|
||||
def path_share(self):
|
||||
"""Return root share data folder."""
|
||||
return Path(HASSIO_SHARE, SHARE_DATA)
|
||||
|
||||
@property
|
||||
def path_extern_share(self):
|
||||
"""Return root share data folder extern for docker."""
|
||||
return PurePath(self.path_extern_hassio, SHARE_DATA)
|
||||
|
||||
@property
|
||||
def addons_repositories(self):
|
||||
@@ -235,3 +266,55 @@ class CoreConfig(Config):
|
||||
|
||||
self._data[ADDONS_CUSTOM_LIST].remove(repo)
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def security_initialize(self):
|
||||
"""Return is security was initialize."""
|
||||
return self._data[SECURITY_INITIALIZE]
|
||||
|
||||
@security_initialize.setter
|
||||
def security_initialize(self, value):
|
||||
"""Set is security initialize."""
|
||||
self._data[SECURITY_INITIALIZE] = value
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def security_totp(self):
|
||||
"""Return the TOTP key."""
|
||||
return self._data.get(SECURITY_TOTP)
|
||||
|
||||
@security_totp.setter
|
||||
def security_totp(self, value):
|
||||
"""Set the TOTP key."""
|
||||
self._data[SECURITY_TOTP] = value
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def security_password(self):
|
||||
"""Return the password key."""
|
||||
return self._data.get(SECURITY_PASSWORD)
|
||||
|
||||
@security_password.setter
|
||||
def security_password(self, value):
|
||||
"""Set the password key."""
|
||||
self._data[SECURITY_PASSWORD] = value
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def security_sessions(self):
|
||||
"""Return api sessions."""
|
||||
return {session: datetime.strptime(until, DATETIME_FORMAT) for
|
||||
session, until in self._data[SECURITY_SESSIONS].items()}
|
||||
|
||||
@security_sessions.setter
|
||||
def security_sessions(self, value):
|
||||
"""Set the a new session."""
|
||||
session, valid = value
|
||||
if valid is None:
|
||||
self._data[SECURITY_SESSIONS].pop(session, None)
|
||||
else:
|
||||
self._data[SECURITY_SESSIONS].update(
|
||||
{session: valid.strftime(DATETIME_FORMAT)}
|
||||
)
|
||||
|
||||
self.save()
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""Const file for HassIO."""
|
||||
from pathlib import Path
|
||||
|
||||
HASSIO_VERSION = '0.22'
|
||||
HASSIO_VERSION = '0.29'
|
||||
|
||||
URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/'
|
||||
'hassio/master/version.json')
|
||||
@@ -10,13 +10,13 @@ URL_HASSIO_VERSION_BETA = ('https://raw.githubusercontent.com/home-assistant/'
|
||||
|
||||
URL_HASSIO_ADDONS = 'https://github.com/home-assistant/hassio-addons'
|
||||
|
||||
DOCKER_REPO = "homeassistant"
|
||||
|
||||
HASSIO_SHARE = Path("/data")
|
||||
|
||||
RUN_UPDATE_INFO_TASKS = 28800
|
||||
RUN_UPDATE_SUPERVISOR_TASKS = 29100
|
||||
RUN_RELOAD_ADDONS_TASKS = 28800
|
||||
RUN_WATCHDOG_HOMEASSISTANT = 15
|
||||
RUN_CLEANUP_API_SESSIONS = 900
|
||||
|
||||
RESTART_EXIT_CODE = 100
|
||||
|
||||
@@ -26,6 +26,14 @@ FILE_HASSIO_CONFIG = Path(HASSIO_SHARE, "config.json")
|
||||
SOCKET_DOCKER = Path("/var/run/docker.sock")
|
||||
SOCKET_HC = Path("/var/run/hassio-hc.sock")
|
||||
|
||||
LABEL_VERSION = 'io.hass.version'
|
||||
LABEL_ARCH = 'io.hass.arch'
|
||||
LABEL_TYPE = 'io.hass.type'
|
||||
|
||||
META_ADDON = 'addon'
|
||||
META_SUPERVISOR = 'supervisor'
|
||||
META_HOMEASSISTANT = 'homeassistant'
|
||||
|
||||
JSON_RESULT = 'result'
|
||||
JSON_DATA = 'data'
|
||||
JSON_MESSAGE = 'message'
|
||||
@@ -33,6 +41,7 @@ JSON_MESSAGE = 'message'
|
||||
RESULT_ERROR = 'error'
|
||||
RESULT_OK = 'ok'
|
||||
|
||||
ATTR_ARCH = 'arch'
|
||||
ATTR_HOSTNAME = 'hostname'
|
||||
ATTR_OS = 'os'
|
||||
ATTR_TYPE = 'type'
|
||||
@@ -60,6 +69,14 @@ ATTR_REPOSITORY = 'repository'
|
||||
ATTR_REPOSITORIES = 'repositories'
|
||||
ATTR_URL = 'url'
|
||||
ATTR_MAINTAINER = 'maintainer'
|
||||
ATTR_PASSWORD = 'password'
|
||||
ATTR_TOTP = 'totp'
|
||||
ATTR_INITIALIZE = 'initialize'
|
||||
ATTR_SESSION = 'session'
|
||||
ATTR_LOCATON = 'location'
|
||||
ATTR_BUILD = 'build'
|
||||
ATTR_DEVICES = 'devices'
|
||||
ATTR_ENVIRONMENT = 'environment'
|
||||
|
||||
STARTUP_BEFORE = 'before'
|
||||
STARTUP_AFTER = 'after'
|
||||
@@ -75,3 +92,10 @@ MAP_CONFIG = 'config'
|
||||
MAP_SSL = 'ssl'
|
||||
MAP_ADDONS = 'addons'
|
||||
MAP_BACKUP = 'backup'
|
||||
MAP_SHARE = 'share'
|
||||
MAP_MNT = 'mnt'
|
||||
|
||||
ARCH_ARMHF = 'armhf'
|
||||
ARCH_AARCH64 = 'aarch64'
|
||||
ARCH_AMD64 = 'amd64'
|
||||
ARCH_I386 = 'i386'
|
||||
|
@@ -11,10 +11,14 @@ from .api import RestAPI
|
||||
from .host_control import HostControl
|
||||
from .const import (
|
||||
SOCKET_DOCKER, RUN_UPDATE_INFO_TASKS, RUN_RELOAD_ADDONS_TASKS,
|
||||
RUN_UPDATE_SUPERVISOR_TASKS, STARTUP_AFTER, STARTUP_BEFORE)
|
||||
RUN_UPDATE_SUPERVISOR_TASKS, RUN_WATCHDOG_HOMEASSISTANT,
|
||||
RUN_CLEANUP_API_SESSIONS, STARTUP_AFTER, STARTUP_BEFORE)
|
||||
from .scheduler import Scheduler
|
||||
from .dock.homeassistant import DockerHomeAssistant
|
||||
from .dock.supervisor import DockerSupervisor
|
||||
from .tasks import (
|
||||
hassio_update, homeassistant_watchdog, homeassistant_setup,
|
||||
api_sessions_cleanup)
|
||||
from .tools import get_arch_from_image, get_local_ip
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -69,6 +73,13 @@ class HassIO(object):
|
||||
self.supervisor, self.addons, self.host_control)
|
||||
self.api.register_homeassistant(self.homeassistant)
|
||||
self.api.register_addons(self.addons)
|
||||
self.api.register_security()
|
||||
self.api.register_panel()
|
||||
|
||||
# schedule api session cleanup
|
||||
self.scheduler.register_task(
|
||||
api_sessions_cleanup(self.config), RUN_CLEANUP_API_SESSIONS,
|
||||
now=True)
|
||||
|
||||
# schedule update info tasks
|
||||
self.scheduler.register_task(
|
||||
@@ -78,7 +89,8 @@ class HassIO(object):
|
||||
# first start of supervisor?
|
||||
if not await self.homeassistant.exists():
|
||||
_LOGGER.info("No HomeAssistant docker found.")
|
||||
await self._setup_homeassistant()
|
||||
await homeassistant_setup(
|
||||
self.config, self.loop, self.homeassistant)
|
||||
|
||||
# Load addons
|
||||
arch = get_arch_from_image(self.supervisor.image)
|
||||
@@ -90,7 +102,8 @@ class HassIO(object):
|
||||
|
||||
# schedule self update task
|
||||
self.scheduler.register_task(
|
||||
self._hassio_update, RUN_UPDATE_SUPERVISOR_TASKS)
|
||||
hassio_update(self.config, self.supervisor),
|
||||
RUN_UPDATE_SUPERVISOR_TASKS)
|
||||
|
||||
async def start(self):
|
||||
"""Start HassIO orchestration."""
|
||||
@@ -98,19 +111,26 @@ class HassIO(object):
|
||||
await self.api.start()
|
||||
_LOGGER.info("Start hassio api on %s", self.config.api_endpoint)
|
||||
|
||||
# HomeAssistant is already running / supervisor have only reboot
|
||||
if await self.homeassistant.is_running():
|
||||
_LOGGER.info("HassIO reboot detected")
|
||||
return
|
||||
try:
|
||||
# HomeAssistant is already running / supervisor have only reboot
|
||||
if await self.homeassistant.is_running():
|
||||
_LOGGER.info("HassIO reboot detected")
|
||||
return
|
||||
|
||||
# start addon mark as before
|
||||
await self.addons.auto_boot(STARTUP_BEFORE)
|
||||
# start addon mark as before
|
||||
await self.addons.auto_boot(STARTUP_BEFORE)
|
||||
|
||||
# run HomeAssistant
|
||||
await self.homeassistant.run()
|
||||
# run HomeAssistant
|
||||
await self.homeassistant.run()
|
||||
|
||||
# start addon mark as after
|
||||
await self.addons.auto_boot(STARTUP_AFTER)
|
||||
# start addon mark as after
|
||||
await self.addons.auto_boot(STARTUP_AFTER)
|
||||
|
||||
finally:
|
||||
# schedule homeassistant watchdog
|
||||
self.scheduler.register_task(
|
||||
homeassistant_watchdog(self.loop, self.homeassistant),
|
||||
RUN_WATCHDOG_HOMEASSISTANT)
|
||||
|
||||
async def stop(self, exit_code=0):
|
||||
"""Stop a running orchestration."""
|
||||
@@ -123,28 +143,3 @@ class HassIO(object):
|
||||
|
||||
self.exit_code = exit_code
|
||||
self.loop.stop()
|
||||
|
||||
async def _setup_homeassistant(self):
|
||||
"""Install a homeassistant docker container."""
|
||||
while True:
|
||||
# read homeassistant tag and install it
|
||||
if not self.config.last_homeassistant:
|
||||
await self.config.fetch_update_infos()
|
||||
|
||||
tag = self.config.last_homeassistant
|
||||
if tag and await self.homeassistant.install(tag):
|
||||
break
|
||||
_LOGGER.warning("Error on setup HomeAssistant. Retry in 60.")
|
||||
await asyncio.sleep(60, loop=self.loop)
|
||||
|
||||
# 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)
|
||||
|
@@ -5,6 +5,7 @@ import logging
|
||||
|
||||
import docker
|
||||
|
||||
from ..const import LABEL_VERSION
|
||||
from ..tools import get_version_from_env
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -33,6 +34,19 @@ class DockerBase(object):
|
||||
"""Return True if a task is in progress."""
|
||||
return self._lock.locked()
|
||||
|
||||
def process_metadata(self, metadata=None, force=False):
|
||||
"""Read metadata and set it to object."""
|
||||
if not force and self.version:
|
||||
return
|
||||
|
||||
# read metadata
|
||||
metadata = metadata or self.container.attrs
|
||||
if LABEL_VERSION in metadata['Config']['Labels']:
|
||||
self.version = metadata['Config']['Labels'][LABEL_VERSION]
|
||||
else:
|
||||
# dedicated
|
||||
self.version = get_version_from_env(metadata['Config']['Env'])
|
||||
|
||||
async def install(self, tag):
|
||||
"""Pull docker image."""
|
||||
if self._lock.locked():
|
||||
@@ -52,12 +66,12 @@ class DockerBase(object):
|
||||
image = self.dock.images.pull("{}:{}".format(self.image, tag))
|
||||
|
||||
image.tag(self.image, tag='latest')
|
||||
self.version = get_version_from_env(image.attrs['Config']['Env'])
|
||||
_LOGGER.info("Tag image %s with version %s as latest",
|
||||
self.image, self.version)
|
||||
self.process_metadata(metadata=image.attrs, force=True)
|
||||
except docker.errors.APIError as err:
|
||||
_LOGGER.error("Can't install %s:%s -> %s.", self.image, tag, err)
|
||||
return False
|
||||
|
||||
_LOGGER.info("Tag image %s with version %s as latest", self.image, tag)
|
||||
return True
|
||||
|
||||
def exists(self):
|
||||
@@ -74,7 +88,7 @@ class DockerBase(object):
|
||||
"""
|
||||
try:
|
||||
image = self.dock.images.get(self.image)
|
||||
self.version = get_version_from_env(image.attrs['Config']['Env'])
|
||||
self.process_metadata(metadata=image.attrs)
|
||||
except docker.errors.DockerException:
|
||||
return False
|
||||
|
||||
@@ -95,8 +109,7 @@ class DockerBase(object):
|
||||
if not self.container:
|
||||
try:
|
||||
self.container = self.dock.containers.get(self.docker_name)
|
||||
self.version = get_version_from_env(
|
||||
self.container.attrs['Config']['Env'])
|
||||
self.process_metadata()
|
||||
except docker.errors.DockerException:
|
||||
return False
|
||||
else:
|
||||
@@ -121,8 +134,7 @@ class DockerBase(object):
|
||||
try:
|
||||
self.container = self.dock.containers.get(self.docker_name)
|
||||
self.image = self.container.attrs['Config']['Image']
|
||||
self.version = get_version_from_env(
|
||||
self.container.attrs['Config']['Env'])
|
||||
self.process_metadata()
|
||||
_LOGGER.info("Attach to image %s with version %s",
|
||||
self.image, self.version)
|
||||
except (docker.errors.DockerException, KeyError):
|
||||
@@ -199,12 +211,14 @@ class DockerBase(object):
|
||||
self.image, self.version)
|
||||
|
||||
try:
|
||||
self.dock.images.remove(
|
||||
image="{}:latest".format(self.image), force=True)
|
||||
self.dock.images.remove(
|
||||
image="{}:{}".format(self.image, self.version), force=True)
|
||||
except docker.errors.ImageNotFound:
|
||||
return True
|
||||
with suppress(docker.errors.ImageNotFound):
|
||||
self.dock.images.remove(
|
||||
image="{}:latest".format(self.image), force=True)
|
||||
|
||||
with suppress(docker.errors.ImageNotFound):
|
||||
self.dock.images.remove(
|
||||
image="{}:{}".format(self.image, self.version), force=True)
|
||||
|
||||
except docker.errors.DockerException as err:
|
||||
_LOGGER.warning("Can't remove image %s -> %s", self.image, err)
|
||||
return False
|
||||
@@ -264,3 +278,30 @@ class DockerBase(object):
|
||||
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)
|
||||
|
||||
async def restart(self):
|
||||
"""Restart docker container."""
|
||||
if self._lock.locked():
|
||||
_LOGGER.error("Can't excute restart while a task is in progress")
|
||||
return False
|
||||
|
||||
async with self._lock:
|
||||
return await self.loop.run_in_executor(None, self._restart)
|
||||
|
||||
def _restart(self):
|
||||
"""Restart docker container.
|
||||
|
||||
Need run inside executor.
|
||||
"""
|
||||
if not self.container:
|
||||
return False
|
||||
|
||||
_LOGGER.info("Restart %s", self.image)
|
||||
|
||||
try:
|
||||
self.container.restart(timeout=30)
|
||||
except docker.errors.DockerException as err:
|
||||
_LOGGER.warning("Can't restart %s -> %s", self.image, err)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@@ -1,15 +1,18 @@
|
||||
"""Init file for HassIO addon docker object."""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
import docker
|
||||
|
||||
from . import DockerBase
|
||||
from ..tools import get_version_from_env
|
||||
from .util import dockerfile_template
|
||||
from ..const import (
|
||||
META_ADDON, MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, MAP_SHARE,
|
||||
MAP_MNT)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HASS_DOCKER_NAME = 'homeassistant'
|
||||
|
||||
|
||||
class DockerAddon(DockerBase):
|
||||
"""Docker hassio wrapper for HomeAssistant."""
|
||||
@@ -30,32 +33,46 @@ class DockerAddon(DockerBase):
|
||||
def volumes(self):
|
||||
"""Generate volumes for mappings."""
|
||||
volumes = {
|
||||
self.addons_data.path_extern_data(self.addon): {
|
||||
str(self.addons_data.path_extern_data(self.addon)): {
|
||||
'bind': '/data', 'mode': 'rw'
|
||||
}}
|
||||
|
||||
if self.addons_data.map_config(self.addon):
|
||||
addon_mapping = self.addons_data.map_volumes(self.addon)
|
||||
|
||||
if MAP_CONFIG in addon_mapping:
|
||||
volumes.update({
|
||||
self.config.path_extern_config: {
|
||||
'bind': '/config', 'mode': 'rw'
|
||||
str(self.config.path_extern_config): {
|
||||
'bind': '/config', 'mode': addon_mapping[MAP_CONFIG]
|
||||
}})
|
||||
|
||||
if self.addons_data.map_ssl(self.addon):
|
||||
if MAP_SSL in addon_mapping:
|
||||
volumes.update({
|
||||
self.config.path_extern_ssl: {
|
||||
'bind': '/ssl', 'mode': 'rw'
|
||||
str(self.config.path_extern_ssl): {
|
||||
'bind': '/ssl', 'mode': addon_mapping[MAP_SSL]
|
||||
}})
|
||||
|
||||
if self.addons_data.map_addons(self.addon):
|
||||
if MAP_ADDONS in addon_mapping:
|
||||
volumes.update({
|
||||
self.config.path_extern_addons_local: {
|
||||
'bind': '/addons', 'mode': 'rw'
|
||||
str(self.config.path_extern_addons_local): {
|
||||
'bind': '/addons', 'mode': addon_mapping[MAP_ADDONS]
|
||||
}})
|
||||
|
||||
if self.addons_data.map_backup(self.addon):
|
||||
if MAP_BACKUP in addon_mapping:
|
||||
volumes.update({
|
||||
self.config.path_extern_backup: {
|
||||
'bind': '/backup', 'mode': 'rw'
|
||||
str(self.config.path_extern_backup): {
|
||||
'bind': '/backup', 'mode': addon_mapping[MAP_BACKUP]
|
||||
}})
|
||||
|
||||
if MAP_SHARE in addon_mapping:
|
||||
volumes.update({
|
||||
str(self.config.path_extern_share): {
|
||||
'bind': '/share', 'mode': addon_mapping[MAP_SHARE]
|
||||
}})
|
||||
|
||||
if MAP_MNT in addon_mapping:
|
||||
volumes.update({
|
||||
'/mnt': {
|
||||
'bind': '/mnt', 'mode': addon_mapping[MAP_MNT]
|
||||
}})
|
||||
|
||||
return volumes
|
||||
@@ -78,12 +95,12 @@ class DockerAddon(DockerBase):
|
||||
detach=True,
|
||||
network_mode='bridge',
|
||||
ports=self.addons_data.get_ports(self.addon),
|
||||
devices=self.addons_data.get_devices(self.addon),
|
||||
environment=self.addons_data.get_environment(self.addon),
|
||||
volumes=self.volumes,
|
||||
)
|
||||
|
||||
self.version = get_version_from_env(
|
||||
self.container.attrs['Config']['Env'])
|
||||
|
||||
self.process_metadata()
|
||||
_LOGGER.info("Start docker addon %s with version %s",
|
||||
self.image, self.version)
|
||||
|
||||
@@ -98,11 +115,87 @@ class DockerAddon(DockerBase):
|
||||
|
||||
Need run inside executor.
|
||||
"""
|
||||
# read container
|
||||
try:
|
||||
self.container = self.dock.containers.get(self.docker_name)
|
||||
self.version = get_version_from_env(
|
||||
self.container.attrs['Config']['Env'])
|
||||
self.process_metadata()
|
||||
|
||||
_LOGGER.info("Attach to container %s with version %s",
|
||||
self.image, self.version)
|
||||
return
|
||||
except (docker.errors.DockerException, KeyError):
|
||||
pass
|
||||
|
||||
# read image
|
||||
try:
|
||||
image = self.dock.images.get(self.image)
|
||||
self.process_metadata(metadata=image.attrs)
|
||||
|
||||
_LOGGER.info("Attach to image %s with version %s",
|
||||
self.image, self.version)
|
||||
except (docker.errors.DockerException, KeyError):
|
||||
pass
|
||||
_LOGGER.error("No container/image found for %s", self.image)
|
||||
|
||||
def _install(self, tag):
|
||||
"""Pull docker image or build it.
|
||||
|
||||
Need run inside executor.
|
||||
"""
|
||||
if self.addons_data.need_build(self.addon):
|
||||
return self._build(tag)
|
||||
|
||||
return super()._install(tag)
|
||||
|
||||
async def build(self, tag):
|
||||
"""Build a docker container."""
|
||||
if self._lock.locked():
|
||||
_LOGGER.error("Can't excute build while a task is in progress")
|
||||
return False
|
||||
|
||||
async with self._lock:
|
||||
return await self.loop.run_in_executor(None, self._build, tag)
|
||||
|
||||
def _build(self, tag):
|
||||
"""Build a docker container.
|
||||
|
||||
Need run inside executor.
|
||||
"""
|
||||
build_dir = Path(self.config.path_addons_build, self.addon)
|
||||
try:
|
||||
# prepare temporary addon build folder
|
||||
try:
|
||||
source = self.addons_data.path_addon_location(self.addon)
|
||||
shutil.copytree(str(source), str(build_dir))
|
||||
except shutil.Error as err:
|
||||
_LOGGER.error("Can't copy %s to temporary build folder -> %s",
|
||||
source, build_dir)
|
||||
return False
|
||||
|
||||
# prepare Dockerfile
|
||||
try:
|
||||
dockerfile_template(
|
||||
Path(build_dir, 'Dockerfile'), self.addons_data.arch,
|
||||
tag, META_ADDON)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Can't prepare dockerfile -> %s", err)
|
||||
|
||||
# run docker build
|
||||
try:
|
||||
build_tag = "{}:{}".format(self.image, tag)
|
||||
|
||||
_LOGGER.info("Start build %s on %s", build_tag, build_dir)
|
||||
image = self.dock.images.build(
|
||||
path=str(build_dir), tag=build_tag, pull=True)
|
||||
|
||||
image.tag(self.image, tag='latest')
|
||||
self.process_metadata(metadata=image.attrs, force=True)
|
||||
|
||||
except (docker.errors.DockerException, TypeError) as err:
|
||||
_LOGGER.error("Can't build %s -> %s", build_tag, err)
|
||||
return False
|
||||
|
||||
_LOGGER.info("Build %s done", build_tag)
|
||||
return True
|
||||
|
||||
finally:
|
||||
shutil.rmtree(str(build_dir), ignore_errors=True)
|
||||
|
@@ -4,7 +4,6 @@ import logging
|
||||
import docker
|
||||
|
||||
from . import DockerBase
|
||||
from ..tools import get_version_from_env
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -45,14 +44,15 @@ class DockerHomeAssistant(DockerBase):
|
||||
'HASSIO': self.config.api_endpoint,
|
||||
},
|
||||
volumes={
|
||||
self.config.path_extern_config:
|
||||
str(self.config.path_extern_config):
|
||||
{'bind': '/config', 'mode': 'rw'},
|
||||
self.config.path_extern_ssl:
|
||||
{'bind': '/ssl', 'mode': 'rw'},
|
||||
str(self.config.path_extern_ssl):
|
||||
{'bind': '/ssl', 'mode': 'ro'},
|
||||
str(self.config.path_extern_share):
|
||||
{'bind': '/share', 'mode': 'rw'},
|
||||
})
|
||||
|
||||
self.version = get_version_from_env(
|
||||
self.container.attrs['Config']['Env'])
|
||||
self.process_metadata()
|
||||
|
||||
_LOGGER.info("Start docker addon %s with version %s",
|
||||
self.image, self.version)
|
||||
|
@@ -81,3 +81,7 @@ class DockerSupervisor(DockerBase):
|
||||
async def remove(self):
|
||||
"""Remove docker image."""
|
||||
raise RuntimeError("Not support on supervisor docker container!")
|
||||
|
||||
async def restart(self):
|
||||
"""Restart docker container."""
|
||||
raise RuntimeError("Not support on supervisor docker container!")
|
||||
|
40
hassio/dock/util.py
Normal file
40
hassio/dock/util.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""HassIO docker utilitys."""
|
||||
import re
|
||||
|
||||
from ..const import ARCH_AARCH64, ARCH_ARMHF, ARCH_I386, ARCH_AMD64
|
||||
|
||||
|
||||
RESIN_BASE_IMAGE = {
|
||||
ARCH_ARMHF: "resin/armhf-alpine:3.5",
|
||||
ARCH_AARCH64: "resin/aarch64-alpine:3.5",
|
||||
ARCH_I386: "resin/i386-alpine:3.5",
|
||||
ARCH_AMD64: "resin/amd64-alpine:3.5",
|
||||
}
|
||||
|
||||
TMPL_IMAGE = re.compile(r"%%BASE_IMAGE%%")
|
||||
|
||||
|
||||
def dockerfile_template(dockerfile, arch, version, meta_type):
|
||||
"""Prepare a Hass.IO dockerfile."""
|
||||
buff = []
|
||||
resin_image = RESIN_BASE_IMAGE[arch]
|
||||
|
||||
# read docker
|
||||
with dockerfile.open('r') as dock_input:
|
||||
for line in dock_input:
|
||||
line = TMPL_IMAGE.sub(resin_image, line)
|
||||
buff.append(line)
|
||||
|
||||
# add metadata
|
||||
buff.append(create_metadata(version, arch, meta_type))
|
||||
|
||||
# write docker
|
||||
with dockerfile.open('w') as dock_output:
|
||||
dock_output.writelines(buff)
|
||||
|
||||
|
||||
def create_metadata(version, arch, meta_type):
|
||||
"""Generate docker label layer for hassio."""
|
||||
return ('LABEL io.hass.version="{}" '
|
||||
'io.hass.arch="{}" '
|
||||
'io.hass.type="{}"').format(version, arch, meta_type)
|
20
hassio/panel/hassio-main.html
Normal file
20
hassio/panel/hassio-main.html
Normal file
File diff suppressed because one or more lines are too long
BIN
hassio/panel/hassio-main.html.gz
Normal file
BIN
hassio/panel/hassio-main.html.gz
Normal file
Binary file not shown.
60
hassio/tasks.py
Normal file
60
hassio/tasks.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Multible tasks."""
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def api_sessions_cleanup(config):
|
||||
"""Create scheduler task for cleanup api sessions."""
|
||||
async def _api_sessions_cleanup():
|
||||
"""Cleanup old api sessions."""
|
||||
now = datetime.now()
|
||||
for session, until_valid in config.security_sessions.items():
|
||||
if now >= until_valid:
|
||||
config.security_sessions = (session, None)
|
||||
|
||||
return _api_sessions_cleanup
|
||||
|
||||
|
||||
def hassio_update(config, supervisor):
|
||||
"""Create scheduler task for update of supervisor hassio."""
|
||||
async def _hassio_update():
|
||||
"""Check and run update of supervisor hassio."""
|
||||
if config.last_hassio == supervisor.version:
|
||||
return
|
||||
|
||||
_LOGGER.info("Found new HassIO version %s.", config.last_hassio)
|
||||
await supervisor.update(config.last_hassio)
|
||||
|
||||
return _hassio_update
|
||||
|
||||
|
||||
def homeassistant_watchdog(loop, homeassistant):
|
||||
"""Create scheduler task for montoring running state."""
|
||||
async def _homeassistant_watchdog():
|
||||
"""Check running state and start if they is close."""
|
||||
if homeassistant.in_progress or await homeassistant.is_running():
|
||||
return
|
||||
|
||||
loop.create_task(homeassistant.run())
|
||||
|
||||
return _homeassistant_watchdog
|
||||
|
||||
|
||||
async def homeassistant_setup(config, loop, homeassistant):
|
||||
"""Install a homeassistant docker container."""
|
||||
while True:
|
||||
# read homeassistant tag and install it
|
||||
if not config.last_homeassistant:
|
||||
await config.fetch_update_infos()
|
||||
|
||||
tag = config.last_homeassistant
|
||||
if tag and await homeassistant.install(tag):
|
||||
break
|
||||
_LOGGER.warning("Error on setup HomeAssistant. Retry in 60.")
|
||||
await asyncio.sleep(60, loop=loop)
|
||||
|
||||
# store version
|
||||
_LOGGER.info("HomeAssistant docker now installed.")
|
1
home-assistant-polymer
Submodule
1
home-assistant-polymer
Submodule
Submodule home-assistant-polymer added at a341ccf944
BIN
misc/security.png
Normal file
BIN
misc/security.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
1
misc/security.xml
Normal file
1
misc/security.xml
Normal file
@@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:53.0) Gecko/20100101 Firefox/53.0" version="6.5.8" editor="www.draw.io" type="device"><diagram name="Page-1">5Vxdd5s4EP01fmwOkgCbx9hp2j7sNrvpnnYfiVFsTjDyghwn++tXGMmAxileEB9O+9BjBhjM3GHmzjXKhCw2L58Sf7v+jQU0mmAreJmQmwnGU4eI/zPDa24gyM0NqyQMchMqDPfhv1QaLWndhQFNKwdyxiIebqvGJYtjuuQVm58kbF897JFF1atu/RUFhvulH0Hr9zDg69w6w25h/0zD1VpdGblevufBXz6tEraL5fUmmDwe/uW7N77yJW80XfsB25dM5OOELBLGeP5p87KgURZaFbb8vNs39h6/d0Jjfs4JOD/h2Y928tZvwyTlwnTP/YTLL8lfVWA4fRF+52u+iYQBiY8pT9gTXbCIJcISs1gcOX8Mo0gz+VG4isXmUnwzKuzzZ5rwUIT8Wu7YhEGQXWa+X4ec3m/9ZXbNvcivzCGL+b38Go7aztMGeWIb3rcMRXYV+lIyyTh8omxDefIqDpF7ySw/Q6asKxHaF/gjS9rWJewVkr5MudXRcRF28UFG/jQKBKDwVypipAe/FPUtC2N+uKIznzg3mYUmobhwFtoblvA1W7HYj+4KawcxQhgGyT0Vo5mBINkgSJ/9NB1hkDAiw0XJAVFaiyhdffk6wkDZ7oCBckGg2JbGh1uKs2b2drT0wvXAOGcbsYPGwXXWfDJbxJZPP4uSqK4ryiuZTYNKU4JhK4VFRSChkc/D52rbOhUW6e0uQ7pAwNOeZ1sLbMp2yZLKk8ptRPMjoNMc4aqj/HaBowNIxzs8C7cpwE2ckdLlLgm5uNPbMH5kvaLnDIYenmrPj9sQPuLUODIH3wzCNxVxFtdz/9llrGcexiEvtibkOiNwfpTS7KjpTVtsD085mQd+uqaBPE/slmRilm29hPyH+PzBurIcuf232LauCFH7S5XwxvpZpuQQVDKlyaPfMlNsy60AjK2mmYJrHJnLFA9kip8+ZfsP+WHdfe8+E856/kk/EOqsApOGECJS48gchGqcK2GYUm4Sw8vss7hpoT5GVDlyvM6wg6NhtdGyLQ9ZLAi4G2WF+kHMK+7qULK1gr4VBHTPkkAv6nrJt7b70iFGir1Kj/K4iC6vsWPPUGMHjgzmCxxiq/mS0jQVCfNGvvyvZOk1VxQdQFcWmlbowNRtRQfsMacc0XWNpikHHL2RcgIG/7V0mJxJWyYlFA306lSk5Rv5Jg94oq+mM66egDSqW31xSm16J9OmGTOrcWSwSEF5xMi43xGSA1FL0rTd6NQSODKIJNRvfmfJxodQvmPJGlfZoN2nZo2gEHMZorWDYJQ6UxkR1DsuRLXuN0xw2L8c2brXSGE4Ug+mW6vkHn6gdpqKIbpw7RDcVcc6JtpolGv11I1g3HAcQ+MGcGQQwBOKyBnaNU/E0XhROY4zvn2fGrfKqUZ1wrDK7TSWTXCNI4NJBWWTXOYejb6tiF7fU4jbVIHQpxDgyCB6UF/IZ4Xete3x9GK3aSnXxW3X7kzcPvHrfzdi5SAypVuVKV3itqros1EzhykyxByAoz6FylOvNbx7obI3XqANbNPG70nMahwZrFBQOBizUjkUSZjqM3VTkgAcGYQSihuXoZR5fQobBAobF6KU9RsmqCJcjlLWb6TguD6YUqaSe3h27plSyrzulDJS9ypB70qZeupGwHc9U0oZcGQQwPqf3dsoZflxFy6UkTZlwrBQ5pkSyoAjgzkFf7ovhLLbb1+/3XWfDGfVCnzubGyYCiPLlGAGPRmEESovZcXMCJAX2pqRZUo5Q1Z30hmpW4DRjXSWdYVDLzgcNcu64gVqaSrZRsotEDIlpkFPfapppH6VyftT03ojD/qqvebLjmZ1ngyWLSjCjFlPG4xEIFOCGvRkDky1TPHEy3+iSooiia2TPOLXeRVw5kqeVWoauKtXAW2oSY1U4LQ1noQ9G4SpuwXsGIRptAqnM2ScoPwzZolz0FBBouMvRTvwOT3WQJ2GywJZEHAzHLrgzIpB54wZ2a0Ys32iOaoHaQDGfHyd+rjQXWld7ZfMqwbaQb+E5Kc6s0mVzeDANsR6LNIy1fCJVDt3CUYXw5lWWWyvYaoRp85Tn8OZA8nbH39+WLCAts2YrtZTnVtuWg9Wem1pysXJTAPcsc8DvAmckPyNHM5z9ZbWo5UOgtvw+UWkzpNBOCFJ/ZKvzv7lJiqtPx8LV3l1lXpNp+VIJTaLv/mWo1b8XT3y8T8=</diagram></mxfile>
|
2
setup.py
2
setup.py
@@ -38,5 +38,7 @@ setup(
|
||||
'colorlog',
|
||||
'voluptuous',
|
||||
'gitpython',
|
||||
'pyotp',
|
||||
'pyqrcode'
|
||||
]
|
||||
)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"hassio": "0.22",
|
||||
"homeassistant": "0.44.1",
|
||||
"hassio": "0.29",
|
||||
"homeassistant": "0.44.2",
|
||||
"resinos": "0.7",
|
||||
"resinhup": "0.1",
|
||||
"generic": "0.3"
|
||||
|
Reference in New Issue
Block a user