Compare commits

...

102 Commits
0.23 ... 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
Pascal Vizeli
eb3cdbfeb9 update Hass.IO 0.24 2017-05-12 01:37:42 +02:00
Pascal Vizeli
f4cb16ad09 WIP: Add support for build docker on local repository (#42)
* Add support for build docker on local repository

* Add docker support

* finish build

* change api

* add dockerfile generator

* finish it

* fix lint

* fix path

* fix path

* fix copy

* add debug stuff

* fix docker template

* cleanups

* fix addons

* change handling

* fix lint / cleanup code

* fix lint

* tag
2017-05-12 01:37:03 +02:00
Pascal Vizeli
956af2bd62 Add security api and TOTP on supervisor (#41)
* Add security api and TOTP on supervisor

* finish security api

* fix lint

* fix lint p2

* add new api view to init

* Task session cleanup / fix hass wachdog

* fix lint

* fix api return

* fix check
2017-05-10 22:02:47 +02:00
Pascal Vizeli
b76cd5c004 Add files via upload 2017-05-10 17:01:35 +02:00
Pascal Vizeli
61d9301dcc Add files via upload 2017-05-10 11:06:34 +02:00
Pascal Vizeli
2ded05be83 Add files via upload 2017-05-10 10:39:00 +02:00
Pascal Vizeli
899d6766c5 Pump version to 0.24 2017-05-10 00:30:11 +02:00
32 changed files with 953 additions and 333 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

65
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"
]
@@ -108,6 +114,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`
@@ -138,6 +178,11 @@ Optional:
### Network
- GET `/network/info`
```json
{
"hostname": ""
}
```
- POST `/network/options`
```json
@@ -158,7 +203,8 @@ Optional:
```json
{
"version": "INSTALL_VERSION",
"last_version": "LAST_VERSION"
"last_version": "LAST_VERSION",
"devices": []
}
```
@@ -176,6 +222,13 @@ Output the raw docker log
- POST `/homeassistant/restart`
- POST `/homeassistant/options`
```json
{
"devices": [],
}
```
### REST API addons
- GET `/addons/{addon}/info`
@@ -190,6 +243,7 @@ Output the raw docker log
"last_version": "LAST_VERSION",
"state": "started|stopped",
"boot": "auto|manual",
"build": "bool",
"options": {},
}
```
@@ -242,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
@@ -255,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_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."""
@@ -109,6 +114,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 +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)
@@ -165,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."""
@@ -270,62 +269,95 @@ 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."""
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."""
@@ -333,12 +365,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)
@@ -357,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
@@ -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__)
@@ -63,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)
@@ -86,6 +89,25 @@ class RestAPI(object):
'/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 = 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),
})
@@ -26,12 +31,21 @@ 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,
ATTR_DEVICES: self.config.homeassistant_devices,
}
return info
@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):

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

102
hassio/api/security.py Normal file
View 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}

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

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

View File

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

View File

@@ -1,4 +1,5 @@
"""Bootstrap HassIO."""
from datetime import datetime
import logging
import json
import os
@@ -9,38 +10,54 @@ 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__)
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")
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")
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'
SECURITY_PASSWORD = 'security_password'
SECURITY_SESSIONS = 'security_sessions'
# pylint: disable=no-value-for-parameter
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),
vol.Optional(SECURITY_PASSWORD): vol.Coerce(str),
vol.Optional(SECURITY_SESSIONS, default={}):
{vol.Coerce(str): vol.Coerce(str)},
}, extra=vol.REMOVE_EXTRA)
@@ -119,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
@@ -192,7 +218,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 +228,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 +243,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 +276,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()

View File

@@ -1,7 +1,7 @@
"""Const file for HassIO."""
from pathlib import Path
HASSIO_VERSION = '0.23'
HASSIO_VERSION = '0.37'
URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/'
'hassio/master/version.json')
@@ -10,14 +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
@@ -27,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'
@@ -36,6 +43,7 @@ RESULT_OK = 'ok'
ATTR_ARCH = 'arch'
ATTR_HOSTNAME = 'hostname'
ATTR_TIMEZONE = 'timezone'
ATTR_OS = 'os'
ATTR_TYPE = 'type'
ATTR_SOURCE = 'source'
@@ -62,7 +70,19 @@ 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'
ATTR_HOST_NETWORK = 'host_network'
ATTR_TMPFS = 'tmpfs'
ATTR_PRIVILEGED = 'privileged'
STARTUP_INITIALIZE = 'initialize'
STARTUP_BEFORE = 'before'
STARTUP_AFTER = 'after'
STARTUP_ONCE = 'once'
@@ -77,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

@@ -11,13 +11,16 @@ 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, RUN_WATCHDOG_HOMEASSISTANT, STARTUP_AFTER,
STARTUP_BEFORE)
RUN_UPDATE_SUPERVISOR_TASKS, RUN_WATCHDOG_HOMEASSISTANT,
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
from .tools import get_arch_from_image, get_local_ip
from .tasks import (
hassio_update, homeassistant_watchdog, homeassistant_setup,
api_sessions_cleanup)
from .tools import get_arch_from_image, get_local_ip, fetch_timezone
_LOGGER = logging.getLogger(__name__)
@@ -38,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)
@@ -51,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)
@@ -71,6 +79,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(
@@ -82,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)
@@ -96,30 +113,35 @@ 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
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()
# schedule homeassistant watchdog
self.scheduler.register_task(
homeassistant_watchdog(self.loop, self.homeassistant),
RUN_WATCHDOG_HOMEASSISTANT)
# 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."""

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

@@ -1,15 +1,17 @@
"""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)
_LOGGER = logging.getLogger(__name__)
HASS_DOCKER_NAME = 'homeassistant'
class DockerAddon(DockerBase):
"""Docker hassio wrapper for HomeAssistant."""
@@ -22,40 +24,66 @@ 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."""
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]
}})
return volumes
@@ -68,41 +96,91 @@ 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.
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:
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
# 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(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)

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={
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'])
_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!")

40
hassio/dock/util.py Normal file
View 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: "homeassistant/armhf-base:latest",
ARCH_AARCH64: "homeassistant/aarch64-base:latest",
ARCH_I386: "homeassistant/i386-base:latest",
ARCH_AMD64: "homeassistant/amd64-base:latest",
}
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)

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,10 +1,23 @@
"""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():

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')

BIN
misc/security.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

1
misc/security.xml Normal file
View 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>

View File

@@ -38,5 +38,8 @@ setup(
'colorlog',
'voluptuous',
'gitpython',
'pyotp',
'pyqrcode',
'pytz'
]
)

View File

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