Compare commits

...

95 Commits
0.24 ... 0.37

Author SHA1 Message Date
Pascal Vizeli
8233083392 Merge remote-tracking branch 'origin/dev' 2017-06-17 23:55:24 +02:00
Pascal Vizeli
106378d1d0 Update version.json 2017-06-17 23:47:02 +02:00
Pascal Vizeli
01d18d5ff3 Allow custom options without validate (#80) 2017-06-17 22:47:56 +02:00
Pascal Vizeli
6d23f3bd1c Use our new base image 2017-06-14 16:55:03 +02:00
Pascal Vizeli
ef96579a29 Update HomeAssistant 0.46.1 2017-06-10 00:55:17 +02:00
Pascal Vizeli
44f0a9f21a Update HomeAssistant 0.46.1 2017-06-10 00:54:49 +02:00
Pascal Vizeli
d854307acb Remove container before delete images (#78) 2017-06-08 17:11:14 +02:00
Pascal Vizeli
334b41de71 Pump version to 0.37 2017-06-05 13:02:13 +02:00
Pascal Vizeli
1da50eab7a Fix versions 2017-06-05 12:59:44 +02:00
Pascal Vizeli
b119a42f4d Update Hass.IO 0.36 2017-06-05 12:56:14 +02:00
Pascal Vizeli
99aa438817 Allow extend cap for addons (#77)
* Allow extend cap for addons

* cleanup
2017-06-05 12:46:45 +02:00
Pascal Vizeli
99fa91f480 Add better validator for device 2017-06-05 12:45:43 +02:00
Pascal Vizeli
93969d264d Update HomeAssistant 0.46 2017-06-04 09:32:55 +02:00
Pascal Vizeli
711e199977 Update HomeAssistant 0.46 2017-06-04 09:11:46 +02:00
Pascal Vizeli
4e645332c3 Pump version to 0.36 2017-06-03 00:15:46 +02:00
Pascal Vizeli
df8afb3337 Merge pull request #76 from home-assistant/dev
Release 0.35
2017-06-03 00:01:16 +02:00
Pascal Vizeli
255a33fc08 Update Hass.IO to 0.35 2017-06-02 23:52:48 +02:00
Pascal Vizeli
d15b6f0294 Allow map special device to homeassistant docker (#75)
* Allow map special device to homeassistant docker

* fix lint

* fix '/dev/'
2017-06-02 23:39:54 +02:00
Pascal Vizeli
1aa24e40ae Update README.md 2017-06-02 10:59:55 +02:00
Pascal Vizeli
c0bde4a488 Next version 2017-06-01 00:39:39 +02:00
Pascal Vizeli
2a09b70294 Merge pull request #74 from home-assistant/dev
Release 0.34
2017-06-01 00:01:48 +02:00
Pascal Vizeli
e35b0a54c1 Pump version to 0.34 2017-05-31 23:53:22 +02:00
Pascal Vizeli
8287330c67 cleanup 2017-05-31 23:44:05 +02:00
Pascal Vizeli
6b16da93cd WIP: Refactory / Cleanup docker base (#73)
* Refactory / Cleanup docker base

* Check ID of running image

* Small bugs / lint

* Add log info

* Fix lint

* Add a real cleanup solution

* fix unused import

* Cleanup restart after updates

* Use restart callback

* rename callback

* Add info log for cleanup & fix lint

* Fix lint

* fix wrong id

* fix set addon as install
2017-05-31 23:41:04 +02:00
bestlibre
c1cd9bba45 Adding tmpfs to addon config (#72)
* Adding tmpfs to addon config

* Adding vol.Match and correcting syntax error

* Missing import and linting

* Update addon.py

* Revert "Update addon.py"

This reverts commit 82798c8f2d.

* optimaze code
2017-05-31 09:39:22 +02:00
Pascal Vizeli
e33420f26e Pump version to 0.34 2017-05-24 00:09:47 +02:00
Pascal Vizeli
abd9683e11 Fix merge conflicts 2017-05-24 00:07:49 +02:00
Pascal Vizeli
8cbeabbe21 Pump Hass.IO version 2017-05-23 23:58:20 +02:00
Pascal Vizeli
df7d988d2f Try to fetch timezone on startup (#70) 2017-05-23 23:17:20 +02:00
Pascal Vizeli
544c009b9c Fix rewrite config to addon on restart (#71) 2017-05-23 22:52:28 +02:00
Pascal Vizeli
b2e0babc60 Update HomeAssistant to 0.45.1 2017-05-23 00:04:55 +02:00
Pascal Vizeli
f7c79cbd3a Update HomeAssistant to 0.45.1 2017-05-22 23:55:47 +02:00
Pascal Vizeli
587e9618da Pump version 2017-05-22 23:22:06 +02:00
Pascal Vizeli
cb2dd3b81c Fix 2017-05-22 23:17:02 +02:00
Pascal Vizeli
8d4dd7de3f Update version.json 2017-05-22 23:07:23 +02:00
Pascal Vizeli
6927c989d0 Timezone (#69)
* Add Timezone support

* finish PR

* fix lint
2017-05-22 22:56:44 +02:00
Pascal Vizeli
97853d1691 Add support for hostname change. (#68)
* Add support for hostname change.

* Fix lint

* Fix lint p2

* Update network.py
2017-05-22 21:59:19 +02:00
Pascal Vizeli
0cdef0d118 Allow add-on to run on host network (#67)
* Allow add-on to run on host network

* cleanup name

* fix lint
2017-05-22 21:52:37 +02:00
Justin Weberg
0b17ffc243 Update readme (#66)
Update installation section for appearance and ease of understanding.
2017-05-21 17:13:36 -07:00
Pascal Vizeli
c516d46f16 Update HomeAssistant 0.45 2017-05-21 08:18:48 +02:00
Pascal Vizeli
cb8ec22b6d Update HomeAssistant 0.45 2017-05-21 08:13:51 +02:00
Pascal Vizeli
4a5fbd79c1 Update ResinOS v0.8 2017-05-20 16:38:09 +02:00
Pascal Vizeli
b636a03567 Update ResinOS v0.8 2017-05-20 16:37:32 +02:00
Pascal Vizeli
c96faf7c0a Pump version 0.32 2017-05-20 01:13:28 +02:00
Pascal Vizeli
2e1cd4076a Merge pull request #64 from home-assistant/dev
Release 0.31
2017-05-20 00:21:40 +02:00
Pascal Vizeli
9984a638ba fix lint 2017-05-20 00:19:48 +02:00
Pascal Vizeli
a492bccc03 Update Hass.IO 0.31 2017-05-20 00:16:50 +02:00
Pascal Vizeli
e7a0e0f565 update pannel 2017-05-20 00:08:11 +02:00
Pascal Vizeli
030e081d45 Update frontend (#63) 2017-05-19 23:59:25 +02:00
Pascal Vizeli
8537536368 Delete hassio-main.html.gz 2017-05-19 23:47:55 +02:00
Pascal Vizeli
f03f323aac Add files via upload 2017-05-19 23:47:34 +02:00
Pascal Vizeli
58c0c67796 Update __init__.py 2017-05-19 23:26:41 +02:00
Pascal Vizeli
f5e196a663 wrap panel into function 2017-05-19 23:24:02 +02:00
Pascal Vizeli
808df68e57 fix panel v2 2017-05-19 23:18:09 +02:00
Pascal Vizeli
fa51c2e6e9 use pathlib 2017-05-19 22:51:46 +02:00
Pascal Vizeli
ba3760e770 Revert API change 2017-05-19 22:48:17 +02:00
Pascal Vizeli
ad1a8557b8 Fix panel & new startup type (#62)
* Fix pannel

* Add new startup  type
2017-05-19 22:31:34 +02:00
Pascal Vizeli
fe91f812d9 Add hostname support for hc protocol 2017-05-19 09:56:30 +02:00
Pascal Vizeli
4cc11305c7 Pump version to 0.31 2017-05-19 09:47:25 +02:00
Pascal Vizeli
898c0330c8 Merge pull request #61 from home-assistant/dev
Release 0.30
2017-05-18 18:14:44 +02:00
Pascal Vizeli
33e5f94f1f cleanup 2017-05-18 18:10:34 +02:00
Pascal Vizeli
da4ee63890 Update version.json 2017-05-18 18:08:29 +02:00
Pascal Vizeli
d34203b133 Remove mnt mount (#60)
* Remove mnt mount

* fix lint
2017-05-18 17:45:44 +02:00
Pascal Vizeli
23addfb9a6 Pump version 2017-05-18 17:40:02 +02:00
Pascal Vizeli
81e1227a7b Merge pull request #59 from home-assistant/dev
Release 0.29
2017-05-17 23:43:09 +02:00
Pascal Vizeli
75be8666a6 Update version.json 2017-05-17 23:41:50 +02:00
Pascal Vizeli
6031a60084 Add addon share and allow to mount host mnt (#58)
* Add addon share and allow to mount host mnt

* fix comments logs
2017-05-17 17:21:54 +02:00
Pascal Vizeli
39d5785118 Allow config.json to manipulate docker env. (#57)
Allow config.json to manipulate docker env.
2017-05-17 14:45:02 +02:00
Pascal Vizeli
bddcdcadb2 Pump version 2017-05-17 14:25:57 +02:00
Pascal Vizeli
3eac6a3366 Merge pull request #56 from home-assistant/dev
Release 0.28
2017-05-16 22:31:47 +02:00
Pascal Vizeli
3c7b962cf9 update hass.io 2017-05-16 22:17:19 +02:00
Pascal Vizeli
bd756e2a9c fix dev regex 2017-05-16 22:11:42 +02:00
Pascal Vizeli
e7920bee2a Fix group regex 2017-05-16 22:03:51 +02:00
Pascal Vizeli
ebcc21370e Add policy for mappings (#55)
* Add policy for mappings

* fix travis
2017-05-16 17:15:35 +02:00
Pascal Vizeli
34c4acf199 Add device support (#54)
Add device support
2017-05-16 14:50:47 +02:00
Pascal Vizeli
47e45dfc9f Pump version 2017-05-16 11:45:20 +02:00
Pascal Vizeli
2ecea7c1b4 Merge pull request #53 from home-assistant/dev
Release 0.27
2017-05-16 00:20:29 +02:00
Pascal Vizeli
5c0eccd12f Bugfix attach container/image (#52) 2017-05-16 00:07:43 +02:00
Pascal Vizeli
f34ab9402b Fix remove (#51) 2017-05-15 23:39:34 +02:00
Pascal Vizeli
2569a82caf Update Hass.IO version 2017-05-15 23:27:18 +02:00
Pascal Vizeli
4bdd256000 Use label instead env, cleanup build (#50)
* Use label instead env, cleanup build

* Update const.py

* fix lint

* add space

* fix lint

* use dynamic type

* fix lint

* fix path

* fix label read

* fix bug
2017-05-15 23:19:35 +02:00
Pascal Vizeli
6f4f6338c5 Pump version 2017-05-15 16:35:30 +02:00
Pascal Vizeli
7cb72b55a8 Merge pull request #49 from home-assistant/dev
Release 0.26
2017-05-15 00:26:30 +02:00
Pascal Vizeli
1a9a08cbfb Update version.json 2017-05-15 00:17:59 +02:00
Pascal Vizeli
237ee0363d Update error message (#48) 2017-05-15 00:08:15 +02:00
Pascal Vizeli
86180ddc34 Allow every repository to make a local build (#47)
* Allow every repository to make a local build

* store version of build

* cleanup code

* fix lint
2017-05-14 23:32:54 +02:00
Pascal Vizeli
eed41d30ec Update const.py 2017-05-13 23:17:23 +02:00
Pascal Vizeli
0b0fd6b910 Add files via upload 2017-05-13 19:14:54 +02:00
Pascal Vizeli
1f887b47ab register panel on core 2017-05-13 17:44:16 +02:00
Pascal Vizeli
affd8057ca WIP Add panel to hass.io (#46)
* Add poliymare

* Add commit

* add static route

* fix name

* add panel
2017-05-13 17:41:46 +02:00
Pascal Vizeli
7a8ee2c46a Merge pull request #45 from home-assistant/dev
Release 0.25
2017-05-12 16:20:12 +02:00
Pascal Vizeli
35fe1f464c Update Hass.IO 0.25 2017-05-12 16:15:18 +02:00
Pascal Vizeli
0955bafebd Update data handling of addons (#44)
* Update data handling of addons

* Update addons api

* Update data.py

* Update data.py

* Add url fix bug
2017-05-12 16:14:49 +02:00
Pascal Vizeli
2e0c540c63 Pump version 2017-05-12 08:53:20 +02:00
Pascal Vizeli
6e9ef17a28 Merge pull request #43 from home-assistant/dev
Release 0.24
2017-05-12 01:47:48 +02:00
27 changed files with 568 additions and 327 deletions

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "home-assistant-polymer"]
path = home-assistant-polymer
url = https://github.com/home-assistant/home-assistant-polymer

31
API.md
View File

@@ -34,6 +34,7 @@ The addons from `addons` are only installed one.
"last_version": "LAST_VERSION",
"arch": "armhf|aarch64|i386|amd64",
"beta_channel": "true|false",
"timezone": "TIMEZONE",
"addons": [
{
"name": "xy bla",
@@ -43,7 +44,9 @@ The addons from `addons` are only installed one.
"repository": "12345678|null",
"version": "LAST_VERSION",
"installed": "INSTALL_VERSION",
"detached": "bool"
"detached": "bool",
"build": "bool",
"url": "null|url"
}
],
"addons_repositories": [
@@ -67,7 +70,9 @@ Get all available addons
"repository": "core|local|REP_ID",
"version": "LAST_VERSION",
"installed": "none|INSTALL_VERSION",
"detached": "bool"
"detached": "bool",
"build": "bool",
"url": "null|url"
}
],
"repositories": [
@@ -94,6 +99,7 @@ Optional:
```json
{
"beta_channel": "true|false",
"timezone": "TIMEZONE",
"addons_repositories": [
"REPO_URL"
]
@@ -172,6 +178,11 @@ Optional:
### Network
- GET `/network/info`
```json
{
"hostname": ""
}
```
- POST `/network/options`
```json
@@ -192,7 +203,8 @@ Optional:
```json
{
"version": "INSTALL_VERSION",
"last_version": "LAST_VERSION"
"last_version": "LAST_VERSION",
"devices": []
}
```
@@ -210,6 +222,13 @@ Output the raw docker log
- POST `/homeassistant/restart`
- POST `/homeassistant/options`
```json
{
"devices": [],
}
```
### REST API addons
- GET `/addons/{addon}/info`
@@ -224,6 +243,7 @@ Output the raw docker log
"last_version": "LAST_VERSION",
"state": "started|stopped",
"boot": "auto|manual",
"build": "bool",
"options": {},
}
```
@@ -276,8 +296,10 @@ Communicate over unix socket with a host daemon.
# shutdown
# host-update [v]
# hostname xy
# network info
# network hostname xy
-> {}
# network wlan ssd xy
# network wlan password xy
# network int ip xy
@@ -289,6 +311,7 @@ features:
- shutdown
- reboot
- update
- hostname
- network_info
- network_control

View File

@@ -9,18 +9,6 @@ Hass.io is a Docker based system for managing your Home Assistant installation a
**HassIO is under active development and is not ready yet for production use.**
## Installing Hass.io
## Installation
Looks to our [website](https://home-assistant.io/hassio).
# HomeAssistant
## SSL
All addons that create SSL certs follow the same file structure. If you use one, put follow lines in your `configuration.yaml`.
```yaml
http:
ssl_certificate: /ssl/fullchain.pem
ssl_key: /ssl/privkey.pem
```
Installation instructions can be found at [https://home-assistant.io/hassio](https://home-assistant.io/hassio).

View File

@@ -191,15 +191,13 @@ class AddonManager(AddonsData):
return False
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_addon_update(addon, version)
if is_running:
await self.start(addon)
return True
return False
if not await self.dockers[addon].update(version):
return False
self.set_addon_update(addon, version)
return True
async def restart(self, addon):
"""Restart addon."""
@@ -207,6 +205,10 @@ class AddonManager(AddonsData):
_LOGGER.error("No docker found for addon %s", addon)
return False
if not self.write_addon_options(addon):
_LOGGER.error("Can't write options for addon %s", addon)
return False
return await self.dockers[addon].restart()
async def logs(self, addon):

View File

@@ -3,18 +3,21 @@ 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_ARCH, ATTR_LOCATON)
ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY, ATTR_URL, ATTR_ARCH,
ATTR_LOCATON, ATTR_DEVICES, ATTR_ENVIRONMENT, ATTR_HOST_NETWORK,
ATTR_TMPFS, ATTR_PRIVILEGED)
from ..config import Config
from ..tools import read_json_file, write_json_file
@@ -26,6 +29,8 @@ USER = 'user'
REPOSITORY_CORE = 'core'
REPOSITORY_LOCAL = 'local'
RE_VOLUME = re.compile(MAP_VOLUME)
class AddonsData(Config):
"""Hold data for addons inside HassIO."""
@@ -149,12 +154,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)
@@ -166,20 +172,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."""
@@ -271,35 +269,65 @@ 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_network_mode(self, addon):
"""Return network mode of addon."""
if self._system_data[addon][ATTR_HOST_NETWORK]:
return 'host'
return 'bridge'
def get_devices(self, addon):
"""Return devices of addon."""
return self._system_data[addon].get(ATTR_DEVICES)
def get_tmpfs(self, addon):
"""Return tmpfs of addon."""
return self._system_data[addon].get(ATTR_TMPFS)
def get_environment(self, addon):
"""Return environment of addon."""
return self._system_data[addon].get(ATTR_ENVIRONMENT)
def get_privileged(self, addon):
"""Return list of privilege."""
return self._system_data[addon].get(ATTR_PRIVILEGED)
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 not in self._addons_cache:
return self._system_data[addon][ATTR_ARCH]
return self._addons_cache[addon][ATTR_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."""
@@ -307,44 +335,29 @@ class AddonsData(Config):
addon, self._addons_cache.get(addon)
)
# core repository
if addon_data[ATTR_REPOSITORY] == REPOSITORY_CORE:
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)
# Local build addon
if addon_data[ATTR_REPOSITORY] == REPOSITORY_LOCAL:
return "local/{}-addon-{}".format(self.arch, addon_data[ATTR_SLUG])
_LOGGER.error("No image for %s", addon)
# local build
return "{}/{}-addon-{}".format(
addon_data[ATTR_REPOSITORY], self.arch, addon_data[ATTR_SLUG])
def need_build(self, addon):
"""Return True if this addon need a local build."""
"""Return True if this addon need a local build."""
addon_data = self._system_data.get(
addon, self._addons_cache.get(addon)
)
return addon_data[ATTR_REPOSITORY] == REPOSITORY_LOCAL \
and not addon_data.get(ATTR_IMAGE)
return ATTR_IMAGE not in addon_data
def map_config(self, addon):
"""Return True if config map is needed."""
return MAP_CONFIG 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_ssl(self, addon):
"""Return True if ssl map is needed."""
return MAP_SSL in self._system_data[addon][ATTR_MAP]
def map_addons(self, addon):
"""Return True if addons map is needed."""
return MAP_ADDONS in self._system_data[addon][ATTR_MAP]
def map_backup(self, addon):
"""Return True if backup map is needed."""
return MAP_BACKUP in self._system_data[addon][ATTR_MAP]
return volumes
def path_data(self, addon):
"""Return addon data path inside supervisor."""
@@ -380,5 +393,7 @@ class AddonsData(Config):
"""Create a schema for addon options."""
raw_schema = self._system_data[addon][ATTR_SCHEMA]
schema = vol.Schema(vol.All(dict, validate_options(raw_schema)))
return schema
if isinstance(raw_schema, bool):
return vol.Schema(dict)
return vol.Schema(vol.All(dict, validate_options(raw_schema)))

View File

@@ -4,9 +4,13 @@ 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, ATTR_ARCH,
ARCH_ARMHF, ARCH_AARCH64, ARCH_AMD64, ARCH_I386)
STARTUP_BEFORE, STARTUP_INITIALIZE, BOOT_AUTO, BOOT_MANUAL, ATTR_SCHEMA,
ATTR_IMAGE, ATTR_URL, ATTR_MAINTAINER, ATTR_ARCH, ATTR_DEVICES,
ATTR_ENVIRONMENT, ATTR_HOST_NETWORK, ARCH_ARMHF, ARCH_AARCH64, ARCH_AMD64,
ARCH_I386, ATTR_TMPFS, ATTR_PRIVILEGED)
MAP_VOLUME = r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$"
V_STR = 'str'
V_INT = 'int'
@@ -21,8 +25,23 @@ ARCH_ALL = [
ARCH_ARMHF, ARCH_AARCH64, ARCH_AMD64, ARCH_I386
]
PRIVILEGE_ALL = [
"NET_ADMIN"
]
def check_network(data):
"""Validate network settings."""
host_network = data[ATTR_HOST_NETWORK]
if ATTR_PORTS in data and host_network:
raise vol.Invalid("Hostnetwork & ports are not allow!")
return data
# pylint: disable=no-value-for-parameter
SCHEMA_ADDON_CONFIG = vol.Schema({
SCHEMA_ADDON_CONFIG = vol.Schema(vol.All({
vol.Required(ATTR_NAME): vol.Coerce(str),
vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Required(ATTR_SLUG): vol.Coerce(str),
@@ -30,21 +49,26 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
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.In([STARTUP_BEFORE, STARTUP_AFTER, STARTUP_ONCE,
STARTUP_INITIALIZE]),
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_HOST_NETWORK, default=False): vol.Boolean(),
vol.Optional(ATTR_DEVICES): [vol.Match(r"^(.*):(.*):([rwm]{1,3})$")],
vol.Optional(ATTR_TMPFS):
vol.Match(r"^size=(\d)*[kmg](,uid=\d{1,4})?(,rw)?$"),
vol.Optional(ATTR_MAP, default=[]): [vol.Match(MAP_VOLUME)],
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)},
vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGE_ALL)],
vol.Required(ATTR_OPTIONS): dict,
vol.Required(ATTR_SCHEMA): {
vol.Required(ATTR_SCHEMA): vol.Any({
vol.Coerce(str): vol.Any(ADDON_ELEMENT, [
vol.Any(ADDON_ELEMENT, {vol.Coerce(str): ADDON_ELEMENT})
])
},
}, False),
vol.Optional(ATTR_IMAGE): vol.Match(r"\w*/\w*"),
}, extra=vol.ALLOW_EXTRA)
}, check_network), extra=vol.ALLOW_EXTRA)
# pylint: disable=no-value-for-parameter
@@ -70,10 +94,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
@@ -84,12 +108,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)
@@ -104,13 +128,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 = []
@@ -123,10 +147,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

View File

@@ -1,5 +1,6 @@
"""Init file for HassIO rest api."""
import logging
from pathlib import Path
from aiohttp import web
@@ -64,6 +65,7 @@ class RestAPI(object):
api_hass = APIHomeAssistant(self.config, self.loop, dock_homeassistant)
self.webapp.router.add_get('/homeassistant/info', api_hass.info)
self.webapp.router.add_post('/homeassistant/options', api_hass.options)
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)
@@ -96,6 +98,16 @@ class RestAPI(object):
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 = Path(__file__).parents[1].joinpath('panel/hassio-main.html')
def get_panel(request):
"""Return file response with panel."""
return web.FileResponse(panel)
self.webapp.router.add_get('/panel', get_panel)
async def start(self):
"""Run rest api webserver."""
self._handler = self.webapp.make_handler(loop=self.loop)

View File

@@ -9,7 +9,7 @@ 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, ATTR_DESCRIPTON, ATTR_DETACHED, ATTR_NAME, ATTR_REPOSITORY,
STATE_STOPPED, STATE_STARTED, BOOT_AUTO, BOOT_MANUAL)
ATTR_BUILD, STATE_STOPPED, STATE_STARTED, BOOT_AUTO, BOOT_MANUAL)
_LOGGER = logging.getLogger(__name__)
@@ -59,6 +59,7 @@ class APIAddons(object):
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

View File

@@ -5,10 +5,15 @@ import logging
import voluptuous as vol
from .util import api_process, api_process_raw, api_validate
from ..const import ATTR_VERSION, ATTR_LAST_VERSION
from ..const import ATTR_VERSION, ATTR_LAST_VERSION, ATTR_DEVICES
_LOGGER = logging.getLogger(__name__)
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_DEVICES): [vol.Match(r"^[^/]*$")],
})
SCHEMA_VERSION = vol.Schema({
vol.Optional(ATTR_VERSION): vol.Coerce(str),
})
@@ -29,8 +34,19 @@ class APIHomeAssistant(object):
return {
ATTR_VERSION: self.homeassistant.version,
ATTR_LAST_VERSION: self.config.last_homeassistant,
ATTR_DEVICES: self.config.homeassistant_devices,
}
@api_process
async def options(self, request):
"""Set homeassistant options."""
body = await api_validate(SCHEMA_OPTIONS, request)
if ATTR_DEVICES in body:
self.config.homeassistant_devices = body[ATTR_DEVICES]
return True
@api_process
async def update(self, request):
"""Update homeassistant."""

View File

@@ -1,11 +1,19 @@
"""Init file for HassIO network rest api."""
import logging
from .util import api_process_hostcontrol
import voluptuous as vol
from .util import api_process, api_process_hostcontrol, api_validate
from ..const import ATTR_HOSTNAME
_LOGGER = logging.getLogger(__name__)
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_HOSTNAME): vol.Coerce(str),
})
class APINetwork(object):
"""Handle rest api for network functions."""
@@ -15,12 +23,21 @@ class APINetwork(object):
self.loop = loop
self.host_control = host_control
@api_process_hostcontrol
def info(self, request):
@api_process
async def info(self, request):
"""Show network settings."""
pass
return {
ATTR_HOSTNAME: self.host_control.hostname,
}
@api_process_hostcontrol
def options(self, request):
async def options(self, request):
"""Edit network settings."""
pass
body = await api_validate(SCHEMA_OPTIONS, request)
# hostname
if ATTR_HOSTNAME in body:
if self.host_control.hostname != body[ATTR_HOSTNAME]:
await self.host_control.set_hostname(body[ATTR_HOSTNAME])
return True

View File

@@ -10,7 +10,9 @@ 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_ARCH)
ATTR_DETACHED, ATTR_SOURCE, ATTR_MAINTAINER, ATTR_URL, ATTR_ARCH,
ATTR_BUILD, ATTR_TIMEZONE)
from ..tools import validate_timezone
_LOGGER = logging.getLogger(__name__)
@@ -18,6 +20,7 @@ SCHEMA_OPTIONS = vol.Schema({
# pylint: disable=no-value-for-parameter
vol.Optional(ATTR_BETA_CHANNEL): vol.Boolean(),
vol.Optional(ATTR_ADDONS_REPOSITORIES): [vol.Url()],
vol.Optional(ATTR_TIMEZONE): validate_timezone,
})
SCHEMA_VERSION = vol.Schema({
@@ -41,23 +44,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_ARCH: values[ATTR_ARCH],
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
@@ -91,6 +94,7 @@ class APISupervisor(object):
ATTR_LAST_VERSION: self.config.last_hassio,
ATTR_BETA_CHANNEL: self.config.upstream_beta,
ATTR_ARCH: self.addons.arch,
ATTR_TIMEZONE: self.config.timezone,
ATTR_ADDONS: self._addons_list(only_installed=True),
ATTR_ADDONS_REPOSITORIES: self.config.addons_repositories,
}
@@ -111,6 +115,9 @@ class APISupervisor(object):
if ATTR_BETA_CHANNEL in body:
self.config.upstream_beta = body[ATTR_BETA_CHANNEL]
if ATTR_TIMEZONE in body:
self.config.timezone = body[ATTR_TIMEZONE]
if ATTR_ADDONS_REPOSITORIES in body:
new = set(body[ATTR_ADDONS_REPOSITORIES])
old = set(self.config.addons_repositories)

View File

@@ -21,24 +21,24 @@ 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)
@@ -47,12 +47,16 @@ def initialize_system_data(websession):
config.path_addons_build)
config.path_addons_build.mkdir(parents=True)
# homeassistant backup folder
# 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

View File

@@ -10,7 +10,7 @@ from voluptuous.humanize import humanize_error
from .const import FILE_HASSIO_CONFIG, HASSIO_SHARE
from .tools import (
fetch_last_versions, write_json_file, read_json_file)
fetch_last_versions, write_json_file, read_json_file, validate_timezone)
_LOGGER = logging.getLogger(__name__)
@@ -18,10 +18,10 @@ DATETIME_FORMAT = "%Y%m%d %H:%M:%S"
HOMEASSISTANT_CONFIG = PurePath("homeassistant")
HOMEASSISTANT_LAST = 'homeassistant_last'
HOMEASSISTANT_DEVICES = 'homeassistant_devices'
HASSIO_SSL = PurePath("ssl")
HASSIO_LAST = 'hassio_last'
HASSIO_CLEANUP = 'hassio_cleanup'
ADDONS_CORE = PurePath("addons/core")
ADDONS_LOCAL = PurePath("addons/local")
@@ -32,9 +32,11 @@ ADDONS_CUSTOM_LIST = 'addons_custom_list'
BACKUP_DATA = PurePath("backup")
UPSTREAM_BETA = 'upstream_beta'
SHARE_DATA = PurePath("share")
UPSTREAM_BETA = 'upstream_beta'
API_ENDPOINT = 'api_endpoint'
TIMEZONE = 'timezone'
SECURITY_INITIALIZE = 'security_initialize'
SECURITY_TOTP = 'security_totp'
@@ -46,9 +48,10 @@ SECURITY_SESSIONS = 'security_sessions'
SCHEMA_CONFIG = vol.Schema({
vol.Optional(UPSTREAM_BETA, default=False): vol.Boolean(),
vol.Optional(API_ENDPOINT): vol.Coerce(str),
vol.Optional(TIMEZONE, default='UTC'): validate_timezone,
vol.Optional(HOMEASSISTANT_LAST): vol.Coerce(str),
vol.Optional(HOMEASSISTANT_DEVICES, default=[]): [vol.Coerce(str)],
vol.Optional(HASSIO_LAST): vol.Coerce(str),
vol.Optional(HASSIO_CLEANUP): vol.Coerce(str),
vol.Optional(ADDONS_CUSTOM_LIST, default=[]): [vol.Url()],
vol.Optional(SECURITY_INITIALIZE, default=False): vol.Boolean(),
vol.Optional(SECURITY_TOTP): vol.Coerce(str),
@@ -133,19 +136,28 @@ class CoreConfig(Config):
def upstream_beta(self, value):
"""Set beta upstream mode."""
self._data[UPSTREAM_BETA] = bool(value)
self.save()
@property
def hassio_cleanup(self):
"""Return Version they need to cleanup."""
return self._data.get(HASSIO_CLEANUP)
def timezone(self):
"""Return system timezone."""
return self._data[TIMEZONE]
@hassio_cleanup.setter
def hassio_cleanup(self, version):
"""Set or remove cleanup flag."""
if version is None:
self._data.pop(HASSIO_CLEANUP, None)
else:
self._data[HASSIO_CLEANUP] = version
@timezone.setter
def timezone(self, value):
"""Set system timezone."""
self._data[TIMEZONE] = value
self.save()
@property
def homeassistant_devices(self):
"""Return list of special device to map into homeassistant."""
return self._data[HOMEASSISTANT_DEVICES]
@homeassistant_devices.setter
def homeassistant_devices(self, value):
"""Set list of special device."""
self._data[HOMEASSISTANT_DEVICES] = value
self.save()
@property
@@ -233,6 +245,16 @@ class CoreConfig(Config):
"""Return root backup data folder extern for docker."""
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):
"""Return list of addons custom repositories."""

View File

@@ -1,7 +1,7 @@
"""Const file for HassIO."""
from pathlib import Path
HASSIO_VERSION = '0.24'
HASSIO_VERSION = '0.37'
URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/'
'hassio/master/version.json')
@@ -10,8 +10,6 @@ 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
@@ -28,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'
@@ -37,6 +43,7 @@ RESULT_OK = 'ok'
ATTR_ARCH = 'arch'
ATTR_HOSTNAME = 'hostname'
ATTR_TIMEZONE = 'timezone'
ATTR_OS = 'os'
ATTR_TYPE = 'type'
ATTR_SOURCE = 'source'
@@ -68,7 +75,14 @@ ATTR_TOTP = 'totp'
ATTR_INITIALIZE = 'initialize'
ATTR_SESSION = 'session'
ATTR_LOCATON = 'location'
ATTR_BUILD = 'build'
ATTR_DEVICES = 'devices'
ATTR_ENVIRONMENT = 'environment'
ATTR_HOST_NETWORK = 'host_network'
ATTR_TMPFS = 'tmpfs'
ATTR_PRIVILEGED = 'privileged'
STARTUP_INITIALIZE = 'initialize'
STARTUP_BEFORE = 'before'
STARTUP_AFTER = 'after'
STARTUP_ONCE = 'once'
@@ -83,6 +97,7 @@ MAP_CONFIG = 'config'
MAP_SSL = 'ssl'
MAP_ADDONS = 'addons'
MAP_BACKUP = 'backup'
MAP_SHARE = 'share'
ARCH_ARMHF = 'armhf'
ARCH_AARCH64 = 'aarch64'

View File

@@ -12,14 +12,15 @@ from .host_control import HostControl
from .const import (
SOCKET_DOCKER, RUN_UPDATE_INFO_TASKS, RUN_RELOAD_ADDONS_TASKS,
RUN_UPDATE_SUPERVISOR_TASKS, RUN_WATCHDOG_HOMEASSISTANT,
RUN_CLEANUP_API_SESSIONS, STARTUP_AFTER, STARTUP_BEFORE)
RUN_CLEANUP_API_SESSIONS, STARTUP_AFTER, STARTUP_BEFORE,
STARTUP_INITIALIZE)
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
from .tools import get_arch_from_image, get_local_ip, fetch_timezone
_LOGGER = logging.getLogger(__name__)
@@ -40,7 +41,7 @@ class HassIO(object):
# init basic docker container
self.supervisor = DockerSupervisor(
self.config, self.loop, self.dock, self)
self.config, self.loop, self.dock, self.stop)
self.homeassistant = DockerHomeAssistant(
self.config, self.loop, self.dock)
@@ -53,19 +54,24 @@ class HassIO(object):
async def setup(self):
"""Setup HassIO orchestration."""
# supervisor
await self.supervisor.attach()
if not await self.supervisor.attach():
_LOGGER.fatal("Can't attach to supervisor docker container!")
await self.supervisor.cleanup()
# set api endpoint
self.config.api_endpoint = await get_local_ip(self.loop)
# update timezone
if self.config.timezone == 'UTC':
self.config.timezone = await fetch_timezone(self.websession)
# hostcontrol
await self.host_control.load()
# schedule update info tasks
self.scheduler.register_task(
self.host_control.load, RUN_UPDATE_INFO_TASKS)
self.host_control.load, RUN_UPDATE_INFO_TASKS)
# rest api views
self.api.register_host(self.host_control)
self.api.register_network(self.host_control)
@@ -74,6 +80,7 @@ class HassIO(object):
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(
@@ -90,6 +97,8 @@ class HassIO(object):
_LOGGER.info("No HomeAssistant docker found.")
await homeassistant_setup(
self.config, self.loop, self.homeassistant)
else:
await self.homeassistant.attach()
# Load addons
arch = get_arch_from_image(self.supervisor.image)
@@ -104,6 +113,9 @@ class HassIO(object):
hassio_update(self.config, self.supervisor),
RUN_UPDATE_SUPERVISOR_TASKS)
# start addon mark as initialize
await self.addons.auto_boot(STARTUP_INITIALIZE)
async def start(self):
"""Start HassIO orchestration."""
# start api

View File

@@ -5,7 +5,7 @@ import logging
import docker
from ..tools import get_version_from_env
from ..const import LABEL_VERSION
_LOGGER = logging.getLogger(__name__)
@@ -19,12 +19,11 @@ class DockerBase(object):
self.loop = loop
self.dock = dock
self.image = image
self.container = None
self.version = None
self._lock = asyncio.Lock(loop=loop)
@property
def docker_name(self):
def name(self):
"""Return name of docker container."""
return None
@@ -33,6 +32,19 @@ class DockerBase(object):
"""Return True if a task is in progress."""
return self._lock.locked()
def process_metadata(self, metadata, force=False):
"""Read metadata and set it to object."""
# read image
if not self.image:
self.image = metadata['Config']['Image']
# read metadata
need_version = force or not self.version
if need_version and LABEL_VERSION in metadata['Config']['Labels']:
self.version = metadata['Config']['Labels'][LABEL_VERSION]
elif need_version:
_LOGGER.warning("Can't read version from %s", self.name)
async def install(self, tag):
"""Pull docker image."""
if self._lock.locked():
@@ -52,12 +64,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(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):
@@ -73,8 +85,7 @@ class DockerBase(object):
Need run inside executor.
"""
try:
image = self.dock.images.get(self.image)
self.version = get_version_from_env(image.attrs['Config']['Env'])
self.dock.images.get(self.image)
except docker.errors.DockerException:
return False
@@ -92,17 +103,21 @@ class DockerBase(object):
Need run inside executor.
"""
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'])
except docker.errors.DockerException:
return False
else:
self.container.reload()
try:
container = self.dock.containers.get(self.name)
image = self.dock.images.get(self.image)
except docker.errors.DockerException:
return False
return self.container.status == 'running'
# container is not running
if container.status != 'running':
return False
# we run on a old image, stop and start it
if container.image.id != image.id:
return False
return True
async def attach(self):
"""Attach to running docker container."""
@@ -119,17 +134,17 @@ class DockerBase(object):
Need run inside executor.
"""
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'])
_LOGGER.info("Attach to image %s with version %s",
self.image, self.version)
except (docker.errors.DockerException, KeyError):
_LOGGER.fatal(
"Can't attach to %s docker container!", self.docker_name)
if self.image:
obj_data = self.dock.images.get(self.image).attrs
else:
obj_data = self.dock.containers.get(self.name).attrs
except docker.errors.DockerException:
return False
self.process_metadata(obj_data)
_LOGGER.info(
"Attach to image %s with version %s", self.image, self.version)
return True
async def run(self):
@@ -163,20 +178,19 @@ class DockerBase(object):
Need run inside executor.
"""
if not self.container:
try:
container = self.dock.containers.get(self.name)
except docker.errors.DockerException:
return
_LOGGER.info("Stop %s docker application", self.image)
self.container.reload()
if self.container.status == 'running':
if container.status == 'running':
with suppress(docker.errors.DockerException):
self.container.stop()
container.stop()
with suppress(docker.errors.DockerException):
self.container.remove(force=True)
self.container = None
container.remove(force=True)
async def remove(self):
"""Remove docker container."""
@@ -192,19 +206,21 @@ class DockerBase(object):
Need run inside executor.
"""
if self._is_running():
self._stop()
# cleanup container
self._stop()
_LOGGER.info("Remove docker %s with latest and %s",
self.image, self.version)
_LOGGER.info(
"Remove docker %s with latest and %s", self.image, self.version)
try:
self.dock.images.remove(
image="{}:latest".format(self.image), force=True)
self.dock.images.remove(
image="{}:{}".format(self.image, self.version), force=True)
except docker.errors.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
@@ -225,23 +241,21 @@ class DockerBase(object):
Need run inside executor.
"""
old_image = "{}:{}".format(self.image, self.version)
was_running = self._is_running()
_LOGGER.info("Update docker %s with %s:%s",
old_image, self.image, tag)
_LOGGER.info(
"Update docker %s with %s:%s", self.version, self.image, tag)
# update docker image
if self._install(tag):
_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)
return True
if not self._install(tag):
return False
return False
# cleanup old stuff
if was_running:
self._run()
self._cleanup()
return True
async def logs(self):
"""Return docker logs of container."""
@@ -257,11 +271,13 @@ class DockerBase(object):
Need run inside executor.
"""
if not self.container:
return
try:
container = self.dock.containers.get(self.name)
except docker.errors.DockerException:
return b""
try:
return self.container.logs(tail=100, stdout=True, stderr=True)
return 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)
@@ -279,15 +295,45 @@ class DockerBase(object):
Need run inside executor.
"""
if not self.container:
try:
container = self.dock.containers.get(self.name)
except docker.errors.DockerException:
return False
_LOGGER.info("Restart %s", self.image)
try:
self.container.restart(timeout=30)
container.restart(timeout=30)
except docker.errors.DockerException as err:
_LOGGER.warning("Can't restart %s -> %s", self.image, err)
return False
return True
async def cleanup(self):
"""Check if old version exists and cleanup."""
if self._lock.locked():
_LOGGER.error("Can't excute cleanup while a task is in progress")
return False
async with self._lock:
await self.loop.run_in_executor(None, self._cleanup)
def _cleanup(self):
"""Check if old version exists and cleanup.
Need run inside executor.
"""
try:
latest = self.dock.images.get(self.image)
except docker.errors.DockerException:
_LOGGER.warning("Can't find %s for cleanup", self.image)
return
for image in self.dock.images.list(name=self.image):
if latest.id == image.id:
continue
with suppress(docker.errors.DockerException):
_LOGGER.info("Cleanup docker images: %s", image.tags)
self.dock.images.remove(image.id, force=True)

View File

@@ -7,7 +7,8 @@ import docker
from . import DockerBase
from .util import dockerfile_template
from ..tools import get_version_from_env
from ..const import (
META_ADDON, MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, MAP_SHARE)
_LOGGER = logging.getLogger(__name__)
@@ -23,10 +24,28 @@ class DockerAddon(DockerBase):
self.addons_data = addons_data
@property
def docker_name(self):
def name(self):
"""Return name of docker container."""
return "addon_{}".format(self.addon)
@property
def environment(self):
"""Return environment for docker add-on."""
addon_env = self.addons_data.get_environment(self.addon) or {}
return {
**addon_env,
'TZ': self.config.timezone,
}
@property
def tmpfs(self):
"""Return tmpfs for docker add-on."""
options = self.addons_data.get_tmpfs(self.addon)
if options:
return {"/tmpfs": "{}".format(options)}
return None
@property
def volumes(self):
"""Generate volumes for mappings."""
@@ -35,28 +54,36 @@ class DockerAddon(DockerBase):
'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({
str(self.config.path_extern_config): {
'bind': '/config', 'mode': 'rw'
'bind': '/config', 'mode': addon_mapping[MAP_CONFIG]
}})
if self.addons_data.map_ssl(self.addon):
if MAP_SSL in addon_mapping:
volumes.update({
str(self.config.path_extern_ssl): {
'bind': '/ssl', 'mode': 'rw'
'bind': '/ssl', 'mode': addon_mapping[MAP_SSL]
}})
if self.addons_data.map_addons(self.addon):
if MAP_ADDONS in addon_mapping:
volumes.update({
str(self.config.path_extern_addons_local): {
'bind': '/addons', 'mode': 'rw'
'bind': '/addons', 'mode': addon_mapping[MAP_ADDONS]
}})
if self.addons_data.map_backup(self.addon):
if MAP_BACKUP in addon_mapping:
volumes.update({
str(self.config.path_extern_backup): {
'bind': '/backup', 'mode': 'rw'
'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]
}})
return volumes
@@ -69,46 +96,31 @@ class DockerAddon(DockerBase):
if self._is_running():
return
# cleanup old container
# cleanup
self._stop()
try:
self.container = self.dock.containers.run(
self.dock.containers.run(
self.image,
name=self.docker_name,
name=self.name,
detach=True,
network_mode='bridge',
network_mode=self.addons_data.get_network_mode(self.addon),
ports=self.addons_data.get_ports(self.addon),
devices=self.addons_data.get_devices(self.addon),
cap_add=self.addons_data.get_privileged(self.addon),
environment=self.environment,
volumes=self.volumes,
tmpfs=self.tmpfs
)
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
_LOGGER.info(
"Start docker addon %s with version %s", self.image, self.version)
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
def _install(self, tag):
"""Pull docker image or build it.
@@ -147,7 +159,8 @@ class DockerAddon(DockerBase):
# prepare Dockerfile
try:
dockerfile_template(
Path(build_dir, 'Dockerfile'), self.addons_data.arch, tag)
Path(build_dir, 'Dockerfile'), self.addons_data.arch,
tag, META_ADDON)
except OSError as err:
_LOGGER.error("Can't prepare dockerfile -> %s", err)
@@ -159,12 +172,14 @@ class DockerAddon(DockerBase):
image = self.dock.images.build(
path=str(build_dir), tag=build_tag, pull=True)
_LOGGER.info("Build %s done", build_tag)
image.tag(self.image, tag='latest')
self.process_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:

View File

@@ -4,7 +4,6 @@ import logging
import docker
from . import DockerBase
from ..tools import get_version_from_env
_LOGGER = logging.getLogger(__name__)
@@ -19,10 +18,22 @@ class DockerHomeAssistant(DockerBase):
super().__init__(config, loop, dock, image=config.homeassistant_image)
@property
def docker_name(self):
def name(self):
"""Return name of docker container."""
return HASS_DOCKER_NAME
@property
def devices(self):
"""Create list of special device to map into docker."""
if not self.config.homeassistant_devices:
return
devices = []
for device in self.config.homeassistant_devices:
devices.append("/dev/{0}:/dev/{0}:rwm".format(device))
return devices
def _run(self):
"""Run docker image.
@@ -31,47 +42,34 @@ class DockerHomeAssistant(DockerBase):
if self._is_running():
return
# cleanup old container
# cleanup
self._stop()
try:
self.container = self.dock.containers.run(
self.dock.containers.run(
self.image,
name=self.docker_name,
name=self.name,
detach=True,
privileged=True,
devices=self.devices,
network_mode='host',
environment={
'HASSIO': self.config.api_endpoint,
'TZ': self.config.timezone,
},
volumes={
str(self.config.path_extern_config):
{'bind': '/config', 'mode': 'rw'},
str(self.config.path_extern_ssl):
{'bind': '/ssl', 'mode': 'rw'},
{'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'])
_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
_LOGGER.info(
"Start homeassistant %s with version %s", self.image, self.version)
return True
async def update(self, tag):
"""Update homeassistant docker image."""
if self._lock.locked():
_LOGGER.error("Can't excute update while a task is in progress")
return False
async with self._lock:
if await self.loop.run_in_executor(None, self._update, tag):
await self.loop.run_in_executor(None, self._run)
return True
return False

View File

@@ -2,8 +2,6 @@
import logging
import os
import docker
from . import DockerBase
from ..const import RESTART_EXIT_CODE
@@ -13,14 +11,13 @@ _LOGGER = logging.getLogger(__name__)
class DockerSupervisor(DockerBase):
"""Docker hassio wrapper for HomeAssistant."""
def __init__(self, config, loop, dock, hassio, image=None):
def __init__(self, config, loop, dock, stop_callback, image=None):
"""Initialize docker base wrapper."""
super().__init__(config, loop, dock, image=image)
self.hassio = hassio
self.stop_callback = stop_callback
@property
def docker_name(self):
def name(self):
"""Return name of docker container."""
return os.environ['SUPERVISOR_NAME']
@@ -31,41 +28,14 @@ class DockerSupervisor(DockerBase):
return False
_LOGGER.info("Update supervisor docker to %s:%s", self.image, tag)
old_version = self.version
async with self._lock:
if await self.loop.run_in_executor(None, self._install, tag):
self.config.hassio_cleanup = old_version
self.loop.create_task(self.hassio.stop(RESTART_EXIT_CODE))
self.loop.create_task(self.stop_callback(RESTART_EXIT_CODE))
return True
return False
async def cleanup(self):
"""Check if old supervisor version exists and cleanup."""
if not self.config.hassio_cleanup:
return
async with self._lock:
if await self.loop.run_in_executor(None, self._cleanup):
self.config.hassio_cleanup = None
def _cleanup(self):
"""Remove old image.
Need run inside executor.
"""
old_image = "{}:{}".format(self.image, self.config.hassio_cleanup)
_LOGGER.info("Old supervisor docker found %s", old_image)
try:
self.dock.images.remove(image=old_image, force=True)
except docker.errors.DockerException as err:
_LOGGER.warning("Can't remove old image %s -> %s", old_image, err)
return False
return True
async def run(self):
"""Run docker image."""
raise RuntimeError("Not support on supervisor docker container!")

View File

@@ -5,17 +5,16 @@ 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",
ARCH_ARMHF: "homeassistant/armhf-base:latest",
ARCH_AARCH64: "homeassistant/aarch64-base:latest",
ARCH_I386: "homeassistant/i386-base:latest",
ARCH_AMD64: "homeassistant/amd64-base:latest",
}
TMPL_VERSION = re.compile(r"%%VERSION%%")
TMPL_IMAGE = re.compile(r"%%BASE_IMAGE%%")
def dockerfile_template(dockerfile, arch, version):
def dockerfile_template(dockerfile, arch, version, meta_type):
"""Prepare a Hass.IO dockerfile."""
buff = []
resin_image = RESIN_BASE_IMAGE[arch]
@@ -23,10 +22,19 @@ def dockerfile_template(dockerfile, arch, version):
# read docker
with dockerfile.open('r') as dock_input:
for line in dock_input:
line = TMPL_VERSION.sub(version, line)
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)

View File

@@ -17,6 +17,7 @@ UNKNOWN = 'unknown'
FEATURES_SHUTDOWN = 'shutdown'
FEATURES_REBOOT = 'reboot'
FEATURES_UPDATE = 'update'
FEATURES_HOSTNAME = 'hostname'
FEATURES_NETWORK_INFO = 'network_info'
FEATURES_NETWORK_CONTROL = 'network_control'
@@ -117,3 +118,7 @@ class HostControl(object):
if version:
return self._send_command("update {}".format(version))
return self._send_command("update")
def set_hostname(self, hostname):
"""Update hostname on host."""
return self._send_command("hostname {}".format(hostname))

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1,5 +1,6 @@
"""Tools file for HassIO."""
import asyncio
from contextlib import suppress
import json
import logging
import re
@@ -7,11 +8,15 @@ import socket
import aiohttp
import async_timeout
import pytz
import voluptuous as vol
from .const import URL_HASSIO_VERSION, URL_HASSIO_VERSION_BETA
_LOGGER = logging.getLogger(__name__)
FREEGEOIP_URL = "https://freegeoip.io/json/"
_RE_VERSION = re.compile(r"VERSION=(.*)")
_IMAGE_ARCH = re.compile(r".*/([a-z0-9]*)-hassio-supervisor")
@@ -41,17 +46,6 @@ def get_arch_from_image(image):
return found.group(1)
def get_version_from_env(env_list):
"""Extract Version from ENV list."""
for env in env_list:
found = _RE_VERSION.match(env)
if found:
return found.group(1)
_LOGGER.error("Can't find VERSION in env")
return None
def get_local_ip(loop):
"""Retrieve local IP address.
@@ -90,3 +84,28 @@ def read_json_file(jsonfile):
"""Read a json file and return a dict."""
with jsonfile.open('r') as cfile:
return json.loads(cfile.read())
def validate_timezone(timezone):
"""Validate voluptuous timezone."""
try:
pytz.timezone(timezone)
except pytz.exceptions.UnknownTimeZoneError:
raise vol.Invalid(
"Invalid time zone passed in. Valid options can be found here: "
"http://en.wikipedia.org/wiki/List_of_tz_database_time_zones") \
from None
return timezone
async def fetch_timezone(websession):
"""Read timezone from freegeoip."""
data = {}
with suppress(aiohttp.ClientError, asyncio.TimeoutError,
json.JSONDecodeError, KeyError):
with async_timeout.timeout(10, loop=websession.loop):
async with websession.get(FREEGEOIP_URL) as request:
data = await request.json()
return data.get('time_zone', 'UTC')

View File

@@ -39,6 +39,7 @@ setup(
'voluptuous',
'gitpython',
'pyotp',
'pyqrcode'
'pyqrcode',
'pytz'
]
)

View File

@@ -1,7 +1,7 @@
{
"hassio": "0.24",
"homeassistant": "0.44.2",
"resinos": "0.7",
"hassio": "0.37",
"homeassistant": "0.46.1",
"resinos": "0.8",
"resinhup": "0.1",
"generic": "0.3"
}