Compare commits

..

131 Commits
0.55 ... 0.75

Author SHA1 Message Date
Pascal Vizeli
6c75957578 Fix version merge conflict 2017-11-24 22:18:13 +01:00
Pascal Vizeli
3a8307acfe Fix panel (#258) 2017-11-24 21:54:56 +01:00
Pascal Vizeli
f20c7d42ee Update home-assistant to version 0.58.1 2017-11-24 14:13:16 +01:00
Pascal Vizeli
9419fbff94 Add new pannel (#257) 2017-11-24 14:01:17 +01:00
Pascal Vizeli
3ac6c03637 Update Hass.io to 0.75 2017-11-22 16:46:14 +01:00
Pascal Vizeli
a95274f1b3 Use tini for home-assistant docker (#255) 2017-11-21 15:55:10 +01:00
Markus
9d2fb87cec typo (#254) 2017-11-20 13:48:04 +01:00
Paulus Schoutsen
ce9c3565b6 Hassio panel split (#249)
* Allow serving two types of panels

* Remove old panel

* Make backwards compatible

* Add comment
2017-11-20 13:44:04 +01:00
Pascal Vizeli
b0ec58ed1b Update Home-Assistant to 0.57.3 2017-11-13 20:47:36 +01:00
Pascal Vizeli
893a5f8dd3 Update Home-Assistant to 0.57.3 2017-11-13 18:44:12 +01:00
Pascal Vizeli
98064f6a90 Update Home-Assistant to 0.57.2 2017-11-06 11:25:25 +01:00
Pascal Vizeli
5146f89354 Update Home-Assistant to 0.57.2 2017-11-06 07:24:04 +01:00
Pascal Vizeli
fb46592d48 Pump version to 0.75 2017-11-05 00:35:04 +01:00
Pascal Vizeli
b4fb5ac681 Fix merge conflict 2017-11-05 00:32:17 +01:00
Pascal Vizeli
4b7201dc59 Update Home-Assistant to 0.57.1 2017-11-05 00:13:11 +01:00
Pascal Vizeli
3a5a4e4c27 Update hass.io to 0.74 2017-11-05 00:12:49 +01:00
Pascal Vizeli
70104a9280 Set api token for access requirements (#238)
* Set api token for access requirements

* fix uuid

* make robust

* fix names
2017-11-05 00:07:49 +01:00
Pascal Vizeli
efbc7b17a1 Use init system for add-ons (#237)
* Use init system for add-ons

* Update const.py

* Update validate.py

* Update addon.py

* Update addon.py

* remove options

* remove options p2

* remove options p3

* Update addon.py
2017-11-04 21:52:41 +01:00
Pascal Vizeli
64c5e20fc4 Update Home-Assistant to 0.57 2017-11-04 11:43:04 +01:00
Pascal Vizeli
13498afa97 Update Home-Assistant to 0.57 2017-11-04 11:33:55 +01:00
Pascal Vizeli
f6375f1bd6 Pump version to 0.74 2017-10-25 12:12:13 +02:00
pvizeli
8fd1599173 Fix merge conflict version 2017-10-25 12:09:33 +02:00
Pascal Vizeli
63302b73b0 Update hass.io to 0.73 2017-10-25 12:01:27 +02:00
Pascal Vizeli
f591f67a2a Show hardware GPIO interface (#233)
* Update hardware.py

* Update host.py

* Update API.md

* Update API.md

* fix lint
2017-10-25 11:50:00 +02:00
Pascal Vizeli
cda3184a55 Add support for legacy mode (#232)
* Add support for legacy mode

* Update const.py

* add legacy mode

* Update addon.py

* Update addon.py

* Update addon.py

* Update addon.py
2017-10-24 17:23:33 +02:00
Pascal Vizeli
afc811e975 Update Home-Assistant to 0.56.2 2017-10-24 06:59:19 +02:00
Pascal Vizeli
2e169dcb42 Update Home-Assistant to 0.56.2 2017-10-24 00:08:57 +02:00
Pascal Vizeli
34e24e184f Update Home-Assistant to 0.56.1 2017-10-23 00:26:49 +02:00
Pascal Vizeli
2e4751ed7d Update version.json 2017-10-23 00:16:38 +02:00
Pascal Vizeli
8c82c467d4 Fix aiohttp 2.3.1 (#231) 2017-10-22 14:05:45 +02:00
Pascal Vizeli
f3f6771534 Add a static entry for hassio api (#230) 2017-10-22 13:53:41 +02:00
Pascal Vizeli
0a75a4dcbc Update Home-Assistant to 0.56 2017-10-22 11:16:51 +02:00
Pascal Vizeli
1a4542fc4e Update Home-Assistant to 0.56 2017-10-22 10:34:31 +02:00
Pascal Vizeli
7e0525749e Pump version to 0.73 2017-10-17 22:49:35 +02:00
Pascal Vizeli
b33b26018d fix version conflict 2017-10-17 22:47:20 +02:00
Pascal Vizeli
66c93e7176 Minimize downtime to 1 sec (#223) 2017-10-17 22:25:29 +02:00
Pascal Vizeli
5674d32bad Update hass.io version 0.72 2017-10-17 21:54:10 +02:00
Pascal Vizeli
7a84972770 Better close/loop handling (#221)
* Better close/loop handling

* Update bootstrap.py

* Update __main__.py

* Update core.py

* Update __main__.py

* Update __main__.py

* Update supervisor.py

* Update supervisor.py

* Update const.py

* fix lint
2017-10-17 16:04:37 +02:00
Pascal Vizeli
638f0f5371 Update Home-Assistant to 0.55.2 2017-10-17 00:07:42 +02:00
Pascal Vizeli
dca1b6f1d3 Update Home-Assistant to 0.55.2 2017-10-17 00:02:17 +02:00
Pascal Vizeli
2b0ee109d6 Rollback Home-Assistant 0.55.1 2017-10-16 19:06:17 +02:00
Pascal Vizeli
e7430d87d7 Update docker image validate (#220) 2017-10-16 17:15:09 +02:00
Pascal Vizeli
9751c1de79 Update Home-Assistant to 0.55.1 2017-10-16 15:50:48 +02:00
Pascal Vizeli
c497167b64 Update Home-Assistant to 0.55.1 2017-10-16 15:50:08 +02:00
Pascal Vizeli
7fb2aca88b Pump version to 0.72 2017-10-13 22:28:15 +02:00
Pascal Vizeli
0d544845b1 Update hass.io to version 0.71 2017-10-13 22:14:11 +02:00
Pascal Vizeli
602eb472f9 Allow to set a option als optional (#218)
* Update validate.py

* Update validate.py

* Update validate.py

* Update validate.py

* Update validate.py

* fix bug

* Extend schema

* Update validate.py

* fix lint

* Update validate.py

* Update validate.py

* Fix deepmerge

* Update setup.py

* Update validate.py
2017-10-13 22:02:41 +02:00
Pascal Vizeli
f22fa46bdb Pump version to 0.71 2017-10-10 07:24:35 +02:00
Pascal Vizeli
4171a28260 Update Hass.io to 0.70 2017-10-10 07:10:01 +02:00
Pascal Vizeli
55365a631a Bugfix weburl (#217) 2017-10-10 07:08:56 +02:00
Pascal Vizeli
547415b30b Pump version to 0.70 2017-10-09 15:38:49 +02:00
pvizeli
cbf79f1fab Fix version merge conflict 2017-10-09 15:25:30 +02:00
Pascal Vizeli
31cc1dce82 Update Hass.io to version 0.69 2017-10-09 15:15:30 +02:00
Pascal Vizeli
8a11e6c845 Check if a option is missing inside nested lists (#216)
* Update validate.py

* fix lint
2017-10-09 14:08:29 +02:00
Pascal Vizeli
2df4f80aa5 More log output (#214)
* Update snapshot.py

* Update __init__.py

* Update snapshot.py

* Update snapshot.py

* fix lint
2017-10-09 13:30:15 +02:00
Pascal Vizeli
68566ee9e1 Update homeassistant.py (#215) 2017-10-09 13:21:21 +02:00
Pascal Vizeli
fe04b7ec59 Remove dedicated API calls (#212)
* Update addons.py

* Update API.md

* Update addon.py

* Update addon.py

* Update addons.py
2017-10-09 10:48:17 +02:00
Pascal Vizeli
38f96d7ddd Remove unknown options from input (#213)
* Update validate.py

* Update validate.py

* Cleanup unneeded code
2017-10-09 10:09:43 +02:00
Pascal Vizeli
2b2edd6e98 Update Home-Assistant to version 0.55 2017-10-08 09:49:06 +02:00
Pascal Vizeli
361969aca2 Update Home-Assistant to version 0.55 2017-10-08 09:43:07 +02:00
Pascal Vizeli
e61e7f41f2 Pump version to 0.69 2017-10-03 17:37:48 +02:00
Pascal Vizeli
75150fd149 Update hass.io to 0.68 2017-10-03 17:12:40 +02:00
Pascal Vizeli
bd1c8be1e1 Support new API for add-on STDIN support (#207)
* Add function to write data to add-on stdin with API

* Update API Doc

* Add to api

* Update addon.py
2017-10-03 16:44:48 +02:00
Pascal Vizeli
f167197640 New config for homeassistant_api & upgrade snapshot (#206)
* New config for homeassistant_api & upgrade snapshot

* fix lint
2017-10-03 11:47:35 +02:00
Pascal Vizeli
f084ecc007 Pump version to 0.68 2017-10-03 00:59:12 +02:00
Pascal Vizeli
65becbd0ae Update hass.io to 0.67 2017-10-03 00:41:30 +02:00
Pascal Vizeli
f38e28a4d9 Add interface and home-assistant api proxy (#205)
* Add initial for hass interface

* For better compatibility, remove extra options for cleanup old stuff

* Add new functions to api

* Add api proxy to home-assistant

* use const

* fix lint

* fix lint

* Add check_api_state function

* Add api watchdog

* Fix lint

* update output

* fix url

* Fix API call

* fix API documentation

* remove password

* fix api call to hass api only

* fix problem with config missmatch

* test

* Detect wrong ssl settings

* disable watchdog & add options

* Update API
2017-10-03 00:31:14 +02:00
Pascal Vizeli
2998cd94ff Allow dynamic handling of proto part (#203)
* Allow dynamic handling of proto part

* Fix lint

* fix bug
2017-10-01 00:03:06 +02:00
Pascal Vizeli
79e2f3e8ab Pump version to 0.67 2017-09-30 12:09:22 +02:00
Pascal Vizeli
13291f52f2 Update hass.io to 0.66 2017-09-30 12:03:37 +02:00
Pascal Vizeli
4baa80c3de Map sysfs devices/platform/soc data 2017-09-30 11:04:27 +02:00
Pascal Vizeli
be28a6b012 fix spell 2017-09-29 23:31:07 +02:00
Pascal Vizeli
d94ada6216 Pump version to 0.65 2017-09-29 22:12:52 +02:00
Pascal Vizeli
b2d7743e06 Update hass.io to 0.65 2017-09-29 21:56:15 +02:00
Pascal Vizeli
40324beb72 Add support for kernel gpio interface (#202)
* Add support for kernel gpio interface

* Update addon.py

* fix git python module change

* Update git.py
2017-09-29 21:42:33 +02:00
Pascal Vizeli
c02f6913b3 Extend label schema (#200)
* Update build.py

* Update build.py

* fix lint
2017-09-29 16:29:23 +02:00
Pascal Vizeli
d56af22d5e Dockerfiles for new build system 2017-09-27 17:17:08 +02:00
Pascal Vizeli
1795103086 Pump version to 0.65 2017-09-26 09:10:56 +02:00
pvizeli
02e1689dd1 Fix version confict 2017-09-26 08:48:31 +02:00
Pascal Vizeli
ab4d96331f Update Home-Assistant to 0.54 2017-09-23 12:03:08 +02:00
Pascal Vizeli
cb881cba28 Update Home-Assistant to 0.54 2017-09-23 12:02:49 +02:00
Pascal Vizeli
44b247f397 Update hass.io to 0.64 2017-09-19 22:06:48 +02:00
Pascal Vizeli
8bb43daf91 Remove support for custom configs / configs for other hass.io versions (#197)
* Remove support for custom configs

* not need since supervisor is autoupdate
2017-09-19 21:52:18 +02:00
Pascal Vizeli
a7e65613d6 Update validate.py (#196) 2017-09-19 20:43:44 +02:00
Pascal Vizeli
3c04c71401 Update build system to origin docker (#191)
* Update build system to origin docker

* Rename build env

* fix lint p1

* fix bug & add more log info for snapshot/restore

* fix exception

* Log build info

* revert last change

* fix regex
2017-09-19 18:06:34 +02:00
Pascal Vizeli
1353d52bd1 Reset json file to default on schema error (#193) 2017-09-19 17:51:16 +02:00
Pascal Vizeli
7701457791 Update resinos to 1.1 2017-09-18 22:05:46 +02:00
Pascal Vizeli
b7820bc6a6 Update resinos to 1.1 2017-09-18 22:05:11 +02:00
Pascal Vizeli
df66102de0 Pump version to 0.64 2017-09-17 14:42:35 +02:00
Pascal Vizeli
4b308d0de1 Fix version conflict 2017-09-17 14:40:20 +02:00
Pascal Vizeli
4448ba886b Update hass.io to version 0.63 2017-09-17 14:25:02 +02:00
Pascal Vizeli
f39006be01 Change flow and start landingpage faster (#189)
* Change flow and start landingpage faster

* run homeassistant after install

* Update homeassistant.py
2017-09-16 14:45:36 +02:00
Pascal Vizeli
e5204eef8a Update Home-Assistant to 0.53.1 2017-09-14 01:28:01 +02:00
Pascal Vizeli
1f07d47fd6 Update Home-Assistant to 0.53.1 2017-09-14 01:27:36 +02:00
Pascal Vizeli
ba352abf0b Pump version to 0.63 2017-09-12 20:13:41 +02:00
Pascal Vizeli
2bf440a744 Update hass.io to version 0.62 2017-09-12 20:10:15 +02:00
Pascal Vizeli
3b26136636 More schema options (#187)
* Extend the addon schema options

* convert range to float

* convert match to string

* fix lint

* cleanup

* fix lint

* fix options name
2017-09-12 19:38:26 +02:00
Pascal Vizeli
8249f042c0 Pump version to 0.62 2017-09-11 14:42:23 +02:00
Pascal Vizeli
84bbaeee5f Fix merge conflicts with versions 2017-09-11 14:41:12 +02:00
Pascal Vizeli
b7620b7adf Update version.json 2017-09-11 14:24:48 +02:00
Pascal Vizeli
5a80be9fd4 Allow stop/start home-assistant & flow of startup (#182)
* Allow config boot

* Read boot settings

* Use internal boot time for detect reboot

* Check if Home-Assistant need to watch

* Make datetime string and parse_datetime

* Add api calls

* fix lint p1

* Use new datetime parser for sessions and make a real default boot time

* fix lint p2

* only start docker if they is running

* convert to int (timestamp)

* add boot flag
2017-09-11 14:14:26 +02:00
Pascal Vizeli
a733886803 Pump version to 0.61 2017-09-11 10:03:15 +02:00
Pascal Vizeli
834fd29fab Update HomeAssistant to 0.53 2017-09-10 16:33:37 +02:00
Pascal Vizeli
fd1caf8aa6 Update HomeAssistant to 0.53 2017-09-10 16:33:17 +02:00
Pascal Vizeli
975c9e8061 Update Home-Assistant 0.52.1 2017-08-28 23:38:43 +02:00
Pascal Vizeli
0b3c5885ec Update Home-Assistant 0.52.1 2017-08-28 23:38:17 +02:00
Pascal Vizeli
711b63e2d0 Update Home-Assistant to version 0.52 (#173) 2017-08-26 19:42:55 +02:00
Pascal Vizeli
c7b833b5eb Update Home-Assistant 0.52 2017-08-26 13:49:11 +02:00
Pascal Vizeli
fd472b3084 Update Hass.io to 0.60 2017-08-25 16:47:06 +02:00
Pascal Vizeli
dcbb6a2160 Fix socat spawn (#172)
* Update dns.py

* Update dns.py
2017-08-25 16:41:48 +02:00
Pascal Vizeli
56fa1550d2 Pump version to 0.60 2017-08-24 22:40:19 +02:00
Pascal Vizeli
e1f97860ee Update hass.io to 0.59 2017-08-24 22:27:42 +02:00
Pascal Vizeli
6ab3fe18d9 Allow to see log also if there some process (#170) 2017-08-24 22:23:24 +02:00
Pascal Vizeli
7969f3dfd7 Remove default bridge (#168)
* Remove default bridge

* rename bridge
2017-08-24 21:49:36 +02:00
Pascal Vizeli
6f05b90e4e Pump hass.io to 0.59 2017-08-24 17:17:34 +02:00
Pascal Vizeli
3aa53d99d7 Update hass.io to 0.58 2017-08-24 17:07:26 +02:00
Pascal Vizeli
3525f5a02f Cleanup network mode & fix port mapping (#166)
* Cleanup network mode & fix port mapping

* Fix lint
2017-08-24 16:39:06 +02:00
Pascal Vizeli
04514a9f5c WIP: Network docker hassio (#159)
* Create hassio network layer / allow linking

* rename docker

* fix lint

* fix lint p2

* Set network options

* First version of network code

* Finish network layer

* Remove old api_endpoint stuff

* Add DNS forwarding

* Fix DNS recorder

* Fix lint p1

* Fix lint p2

* Fix lint p3

* Fix spell

* Fix ipam struct

* Fix ip to str

* Fix ip to str v2

* Fix spell

* Fix hass on host

* Fix host attach to network

* Cleanup network code

* Fix lint & add debug

* fix link

* Remove log

* Fix network

* fix reattach of supervisor

* set options

* Fix containers

* Fix remapping & add a test

* Fix dict bug

* Fix prop

* Test with run container

* Fix problem
2017-08-24 14:57:13 +02:00
Pascal Vizeli
1c915ef4cd Pump hass.io version to 0.58 2017-08-22 16:29:10 +02:00
pvizeli
b03a2c5c5f Merge remote-tracking branch 'origin/dev' 2017-08-22 16:16:01 +02:00
Pascal Vizeli
64988b285e Update hass.io to version 0.57 2017-08-22 16:15:17 +02:00
Pascal Vizeli
5c69dca7b3 New panel with poly2 (#163) 2017-08-22 15:44:14 +02:00
Pascal Vizeli
dfda7dc748 Better cleanup for local build add-ons (#161)
* Better cleanup for local build add-ons

* fix lint
2017-08-20 23:02:58 +02:00
Pascal Vizeli
cb7710c23f Add a error message if that is not a local build addon (#162) 2017-08-20 22:33:27 +02:00
Pascal Vizeli
f9b12a2eb2 Allow rebuild for local build addons (#158)
* Allow rebuild for local build addons

* fix lint
2017-08-19 22:44:39 +02:00
Pascal Vizeli
6a7617faad Use deepmerge for options (#157)
Add an optional extended description…
2017-08-18 15:57:13 +02:00
Pascal Vizeli
05554ccf7e Pump version to 0.57 2017-08-16 11:38:43 +02:00
pvizeli
a94e6c5303 Merge remote-tracking branch 'origin/dev' 2017-08-16 11:38:07 +02:00
Pascal Vizeli
d6fc8892db Update hass.io to 0.56 2017-08-16 11:36:50 +02:00
Pascal Vizeli
fa9b3b939e Use exit code to detect wrong configs (#156)
* Update homeassistant.py

* Update homeassistant.py

* Update util.py

* add support for yaml errors
2017-08-16 11:25:38 +02:00
Pascal Vizeli
70685c41be Update const.py 2017-08-16 02:18:33 +02:00
44 changed files with 1859 additions and 731 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
# General files
.git
.github
# Test related files
.tox
# Temporary files
**/__pycache__

54
API.md
View File

@@ -243,6 +243,7 @@ Optional:
"serial": ["/dev/xy"],
"input": ["Input device name"],
"disk": ["/dev/sdax"],
"gpio": ["gpiochip0", "gpiochip100"],
"audio": {
"CARD_ID": {
"name": "xy",
@@ -283,7 +284,11 @@ Optional:
"last_version": "LAST_VERSION",
"devices": [""],
"image": "str",
"custom": "bool -> if custom image"
"custom": "bool -> if custom image",
"boot": "bool",
"port": 8123,
"ssl": "bool",
"watchdog": "bool"
}
```
@@ -302,19 +307,30 @@ Optional:
Output is the raw Docker log.
- POST `/homeassistant/restart`
- POST `/homeassistant/options`
- POST `/homeassistant/check`
- POST `/homeassistant/start`
- POST `/homeassistant/stop`
- POST `/homeassistant/options`
```json
{
"devices": [],
"image": "Optional|null",
"last_version": "Optional for custom image|null"
"last_version": "Optional for custom image|null",
"port": "port for access hass",
"ssl": "bool",
"password": "",
"watchdog": "bool"
}
```
Image with `null` and last_version with `null` reset this options.
- POST/GET `/homeassistant/api`
Proxy to real home-assistant instance.
### RESTful for API addons
- GET `/addons`
@@ -339,7 +355,10 @@ Get all available addons.
"url": "null|url",
"logo": "bool",
"audio": "bool",
"hassio_api": "bool"
"gpio": "bool",
"stdin": "bool",
"hassio_api": "bool",
"homeassistant_api": "bool"
}
],
"repositories": [
@@ -377,7 +396,10 @@ Get all available addons.
"devices": ["/dev/xy"],
"logo": "bool",
"hassio_api": "bool",
"homeassistant_api": "bool",
"stdin": "bool",
"webui": "null|http(s)://[HOST]:port/xy/zx",
"gpio": "bool",
"audio": "bool",
"audio_input": "null|0,0",
"audio_output": "null|0,0"
@@ -409,32 +431,24 @@ For reset custom network/audio settings, set it `null`.
- POST `/addons/{addon}/install`
Optional:
```json
{
"version": "VERSION"
}
```
- POST `/addons/{addon}/uninstall`
- POST `/addons/{addon}/update`
Optional:
```json
{
"version": "VERSION"
}
```
- GET `/addons/{addon}/logs`
Output is the raw Docker log.
- POST `/addons/{addon}/restart`
- POST `/addons/{addon}/rebuild`
Only supported for local build addons
- POST `/addons/{addon}/stdin`
Write data to add-on stdin
## Host Control
Communicate over UNIX socket with a host daemon.

23
Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
ARG BUILD_FROM
FROM $BUILD_FROM
# add env
ENV LANG C.UTF-8
# setup base
RUN apk add --no-cache python3 python3-dev \
libressl libressl-dev \
libffi libffi-dev \
musl musl-dev \
gcc libstdc++ \
git socat \
&& pip3 install --no-cache-dir --upgrade pip \
&& pip3 install --no-cache-dir --upgrade cryptography jwcrypto \
&& apk del python3-dev libressl-dev libffi-dev musl-dev gcc
# install HassIO
COPY . /usr/src/hassio
RUN pip3 install --no-cache-dir /usr/src/hassio \
&& rm -rf /usr/src/hassio
CMD [ "python3", "-m", "hassio" ]

View File

@@ -13,11 +13,12 @@ _LOGGER = logging.getLogger(__name__)
# pylint: disable=invalid-name
if __name__ == "__main__":
bootstrap.initialize_logging()
loop = asyncio.get_event_loop()
if not bootstrap.check_environment():
exit(1)
sys.exit(1)
loop = asyncio.get_event_loop()
# init executor pool
executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker")
loop.set_default_executor(executor)
@@ -27,19 +28,20 @@ if __name__ == "__main__":
bootstrap.migrate_system_env(config)
_LOGGER.info("Run Hassio setup")
_LOGGER.info("Setup HassIO")
loop.run_until_complete(hassio.setup())
_LOGGER.info("Start Hassio")
loop.call_soon_threadsafe(loop.create_task, hassio.start())
loop.call_soon_threadsafe(bootstrap.reg_signal, loop, hassio)
loop.call_soon_threadsafe(bootstrap.reg_signal, loop)
_LOGGER.info("Run Hassio loop")
loop.run_forever()
_LOGGER.info("Cleanup system")
executor.shutdown(wait=False)
loop.close()
try:
_LOGGER.info("Run HassIO")
loop.run_forever()
finally:
_LOGGER.info("Stopping HassIO")
loop.run_until_complete(hassio.stop())
executor.shutdown(wait=False)
loop.close()
_LOGGER.info("Close Hassio")
sys.exit(hassio.exit_code)
sys.exit(0)

View File

@@ -15,11 +15,11 @@ BUILTIN_REPOSITORIES = set((REPOSITORY_CORE, REPOSITORY_LOCAL))
class AddonManager(object):
"""Manage addons inside HassIO."""
def __init__(self, config, loop, dock):
def __init__(self, config, loop, docker):
"""Initialize docker base wrapper."""
self.loop = loop
self.config = config
self.dock = dock
self.docker = docker
self.data = Data(config)
self.addons = {}
self.repositories = {}
@@ -108,7 +108,7 @@ class AddonManager(object):
tasks = []
for addon_slug in add_addons:
addon = Addon(
self.config, self.loop, self.dock, self.data, addon_slug)
self.config, self.loop, self.docker, self.data, addon_slug)
tasks.append(addon.load())
self.addons[addon_slug] = addon

View File

@@ -12,36 +12,38 @@ import voluptuous as vol
from voluptuous.humanize import humanize_error
from .validate import (
validate_options, SCHEMA_ADDON_SNAPSHOT, MAP_VOLUME)
validate_options, SCHEMA_ADDON_SNAPSHOT, RE_VOLUME)
from ..const import (
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP,
ATTR_OPTIONS, ATTR_PORTS, ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY,
ATTR_URL, ATTR_ARCH, ATTR_LOCATON, ATTR_DEVICES, ATTR_ENVIRONMENT,
ATTR_HOST_NETWORK, ATTR_TMPFS, ATTR_PRIVILEGED, ATTR_STARTUP,
ATTR_HOST_NETWORK, ATTR_TMPFS, ATTR_PRIVILEGED, ATTR_STARTUP, ATTR_UUID,
STATE_STARTED, STATE_STOPPED, STATE_NONE, ATTR_USER, ATTR_SYSTEM,
ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_WEBUI,
ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT)
ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY)
from .util import check_installed
from ..dock.addon import DockerAddon
from ..tools import write_json_file, read_json_file
_LOGGER = logging.getLogger(__name__)
RE_VOLUME = re.compile(MAP_VOLUME)
RE_WEBUI = re.compile(r"^(.*\[HOST\]:)\[PORT:(\d+)\](.*)$")
RE_WEBUI = re.compile(
r"^(?:(?P<s_prefix>https?)|\[PROTO:(?P<t_proto>\w+)\])"
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$")
class Addon(object):
"""Hold data for addon inside HassIO."""
def __init__(self, config, loop, dock, data, slug):
def __init__(self, config, loop, docker, data, slug):
"""Initialize data holder."""
self.loop = loop
self.config = config
self.data = data
self._id = slug
self.docker = DockerAddon(config, loop, dock, self)
self.docker = DockerAddon(config, loop, docker, self)
async def load(self):
"""Async initialize of object."""
@@ -106,7 +108,7 @@ class Addon(object):
if self.is_installed:
return {
**self.data.system[self._id][ATTR_OPTIONS],
**self.data.user[self._id][ATTR_OPTIONS],
**self.data.user[self._id][ATTR_OPTIONS]
}
return self.data.cache[self._id][ATTR_OPTIONS]
@@ -151,6 +153,12 @@ class Addon(object):
"""Return timeout of addon for docker stop."""
return self._mesh[ATTR_TIMEOUT]
@property
def api_token(self):
"""Return a API token for this add-on."""
if self.is_installed:
return self.data.user[self._id][ATTR_UUID]
@property
def description(self):
"""Return description of addon."""
@@ -176,8 +184,8 @@ class Addon(object):
@property
def ports(self):
"""Return ports of addon."""
if self.network_mode != 'bridge' or ATTR_PORTS not in self._mesh:
return
if self.host_network or ATTR_PORTS not in self._mesh:
return None
if not self.is_installed or \
ATTR_NETWORK not in self.data.user[self._id]:
@@ -203,27 +211,37 @@ class Addon(object):
def webui(self):
"""Return URL to webui or None."""
if ATTR_WEBUI not in self._mesh:
return
return None
webui = RE_WEBUI.match(self._mesh[ATTR_WEBUI])
webui = self._mesh[ATTR_WEBUI]
dock_port = RE_WEBUI.sub(r"\2", webui)
# extract arguments
t_port = webui.group('t_port')
t_proto = webui.group('t_proto')
s_prefix = webui.group('s_prefix') or ""
s_suffix = webui.group('s_suffix') or ""
# search host port for this docker port
if self.ports is None:
real_port = dock_port
port = t_port
else:
real_port = self.ports.get("{}/tcp".format(dock_port), dock_port)
port = self.ports.get("{}/tcp".format(t_port), t_port)
# for interface config or port lists
if isinstance(real_port, (tuple, list)):
real_port = real_port[-1]
if isinstance(port, (tuple, list)):
port = port[-1]
return RE_WEBUI.sub(r"\g<1>{}\g<3>".format(real_port), webui)
# lookup the correct protocol from config
if t_proto:
proto = 'https' if self.options[t_proto] else 'http'
else:
proto = s_prefix
return "{}://[HOST]:{}{}".format(proto, port, s_suffix)
@property
def network_mode(self):
"""Return network mode of addon."""
if self._mesh[ATTR_HOST_NETWORK]:
return 'host'
return 'bridge'
def host_network(self):
"""Return True if addon run on host network."""
return self._mesh[ATTR_HOST_NETWORK]
@property
def devices(self):
@@ -246,10 +264,30 @@ class Addon(object):
return self._mesh.get(ATTR_PRIVILEGED)
@property
def use_hassio_api(self):
def legacy(self):
"""Return if the add-on don't support hass labels."""
return self._mesh.get(ATTR_LEGACY)
@property
def access_hassio_api(self):
"""Return True if the add-on access to hassio api."""
return self._mesh[ATTR_HASSIO_API]
@property
def access_homeassistant_api(self):
"""Return True if the add-on access to Home-Assistant api proxy."""
return self._mesh[ATTR_HOMEASSISTANT_API]
@property
def with_stdin(self):
"""Return True if the add-on access use stdin input."""
return self._mesh[ATTR_STDIN]
@property
def with_gpio(self):
"""Return True if the add-on access to gpio interface."""
return self._mesh[ATTR_GPIO]
@property
def with_audio(self):
"""Return True if the add-on access to audio."""
@@ -259,7 +297,7 @@ class Addon(object):
def audio_output(self):
"""Return ALSA config for output or None."""
if not self.with_audio:
return
return None
setting = self.config.audio_output
if self.is_installed and ATTR_AUDIO_OUTPUT in self.data.user[self._id]:
@@ -417,7 +455,7 @@ class Addon(object):
return False
return True
async def install(self, version=None):
async def install(self):
"""Install a addon."""
if self.config.arch not in self.supported_arch:
_LOGGER.error(
@@ -433,11 +471,10 @@ class Addon(object):
"Create Home-Assistant addon data folder %s", self.path_data)
self.path_data.mkdir()
version = version or self.last_version
if not await self.docker.install(version):
if not await self.docker.install(self.last_version):
return False
self._set_install(version)
self._set_install(self.last_version)
return True
@check_installed
@@ -480,19 +517,18 @@ class Addon(object):
return self.docker.stop()
@check_installed
async def update(self, version=None):
async def update(self):
"""Update addon."""
version = version or self.last_version
last_state = await self.state()
if version == self.version_installed:
if self.last_version == self.version_installed:
_LOGGER.warning(
"Addon %s is already installed in %s", self._id, version)
"No update available for Addon %s", self._id)
return False
if not await self.docker.update(version):
if not await self.docker.update(self.last_version):
return False
self._set_update(version)
self._set_update(self.last_version)
# restore state
if last_state == STATE_STARTED:
@@ -515,6 +551,39 @@ class Addon(object):
"""
return self.docker.logs()
@check_installed
async def rebuild(self):
"""Performe a rebuild of local build addon."""
last_state = await self.state()
if not self.need_build:
_LOGGER.error("Can't rebuild a none local build addon!")
return False
# remove docker container but not addon config
if not await self.docker.remove():
return False
if not await self.docker.install(self.version_installed):
return False
# restore state
if last_state == STATE_STARTED:
await self.docker.run()
return True
@check_installed
async def write_stdin(self, data):
"""Write data to add-on stdin.
Return a coroutine.
"""
if not self.with_stdin:
_LOGGER.error("Add-on don't support write to stdin!")
return False
return await self.docker.write_stdin(data)
@check_installed
async def snapshot(self, tar_file):
"""Snapshot a state of a addon."""
@@ -545,11 +614,13 @@ class Addon(object):
snapshot.add(self.path_data, arcname="data")
try:
_LOGGER.info("Build snapshot for addon %s", self._id)
await self.loop.run_in_executor(None, _create_tar)
except tarfile.TarError as err:
_LOGGER.error("Can't write tarfile %s -> %s", tar_file, err)
return False
_LOGGER.info("Finish snapshot for addon %s", self._id)
return True
async def restore(self, tar_file):
@@ -604,6 +675,7 @@ class Addon(object):
shutil.copytree(str(Path(temp, "data")), str(self.path_data))
try:
_LOGGER.info("Restore data for addon %s", self._id)
await self.loop.run_in_executor(None, _restore_data)
except shutil.Error as err:
_LOGGER.error("Can't restore origin data -> %s", err)
@@ -613,4 +685,5 @@ class Addon(object):
if data[ATTR_STATE] == STATE_STARTED:
return await self.start()
_LOGGER.info("Finish restore for addon %s", self._id)
return True

65
hassio/addons/build.py Normal file
View File

@@ -0,0 +1,65 @@
"""HassIO addons build environment."""
from pathlib import Path
from .validate import SCHEMA_BUILD_CONFIG
from ..const import ATTR_SQUASH, ATTR_BUILD_FROM, ATTR_ARGS, META_ADDON
from ..tools import JsonConfig
class AddonBuild(JsonConfig):
"""Handle build options for addons."""
def __init__(self, config, addon):
"""Initialize addon builder."""
self.config = config
self.addon = addon
super().__init__(
Path(addon.path_location, 'build.json'), SCHEMA_BUILD_CONFIG)
def save(self):
"""Ignore save function."""
pass
@property
def base_image(self):
"""Base images for this addon."""
return self._data[ATTR_BUILD_FROM][self.config.arch]
@property
def squash(self):
"""Return True or False if squash is active."""
return self._data[ATTR_SQUASH]
@property
def additional_args(self):
"""Return additional docker build arguments."""
return self._data[ATTR_ARGS]
def get_docker_args(self, version):
"""Create a dict with docker build arguments."""
args = {
'path': str(self.addon.path_location),
'tag': "{}:{}".format(self.addon.image, version),
'pull': True,
'forcerm': True,
'squash': self.squash,
'labels': {
'io.hass.version': version,
'io.hass.arch': self.config.arch,
'io.hass.type': META_ADDON,
'io.hass.name': self.addon.name,
'io.hass.description': self.addon.description,
},
'buildargs': {
'BUILD_FROM': self.base_image,
'BUILD_VERSION': version,
'BUILD_ARCH': self.config.arch,
**self.additional_args,
}
}
if self.addon.url:
args['labels']['io.hass.url'] = self.addon.url
return args

View File

@@ -3,15 +3,13 @@ import copy
import logging
import json
from pathlib import Path
import re
import voluptuous as vol
from voluptuous.humanize import humanize_error
from .util import extract_hash_from_path
from .validate import (
SCHEMA_ADDON_CONFIG, SCHEMA_ADDON_FILE, SCHEMA_REPOSITORY_CONFIG,
MAP_VOLUME)
SCHEMA_ADDON_CONFIG, SCHEMA_ADDON_FILE, SCHEMA_REPOSITORY_CONFIG)
from ..const import (
FILE_HASSIO_ADDONS, ATTR_VERSION, ATTR_SLUG, ATTR_REPOSITORY, ATTR_LOCATON,
REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_USER, ATTR_SYSTEM)
@@ -19,8 +17,6 @@ from ..tools import JsonConfig, read_json_file
_LOGGER = logging.getLogger(__name__)
RE_VOLUME = re.compile(MAP_VOLUME)
class Data(JsonConfig):
"""Hold data for addons inside HassIO."""

View File

@@ -73,7 +73,7 @@ class GitRepo(object):
None, self.repo.remotes.origin.pull)
except (git.InvalidGitRepositoryError, git.NoSuchPathError,
git.exc.GitCommandError) as err:
git.GitCommandError) as err:
_LOGGER.error("Can't pull %s repo: %s.", self.url, err)
return False

View File

@@ -1,4 +1,8 @@
"""Validate addons options schema."""
import logging
import re
import uuid
import voluptuous as vol
from ..const import (
@@ -9,13 +13,16 @@ from ..const import (
ATTR_ARCH, ATTR_DEVICES, ATTR_ENVIRONMENT, ATTR_HOST_NETWORK, ARCH_ARMHF,
ARCH_AARCH64, ARCH_AMD64, ARCH_I386, ATTR_TMPFS, ATTR_PRIVILEGED,
ATTR_USER, ATTR_STATE, ATTR_SYSTEM, STATE_STARTED, STATE_STOPPED,
ATTR_LOCATON, ATTR_REPOSITORY, ATTR_TIMEOUT, ATTR_NETWORK,
ATTR_LOCATON, ATTR_REPOSITORY, ATTR_TIMEOUT, ATTR_NETWORK, ATTR_UUID,
ATTR_AUTO_UPDATE, ATTR_WEBUI, ATTR_AUDIO, ATTR_AUDIO_INPUT,
ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API)
ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_BUILD_FROM, ATTR_SQUASH,
ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY)
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_CHANNEL
_LOGGER = logging.getLogger(__name__)
MAP_VOLUME = r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$"
RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$")
V_STR = 'str'
V_INT = 'int'
@@ -24,8 +31,18 @@ V_BOOL = 'bool'
V_EMAIL = 'email'
V_URL = 'url'
V_PORT = 'port'
V_MATCH = 'match'
ADDON_ELEMENT = vol.In([V_STR, V_INT, V_FLOAT, V_BOOL, V_EMAIL, V_URL, V_PORT])
RE_SCHEMA_ELEMENT = re.compile(
r"^(?:"
r"|str|bool|email|url|port"
r"|int(?:\((?P<i_min>\d+)?,(?P<i_max>\d+)?\))?"
r"|float(?:\((?P<f_min>[\d\.]+)?,(?P<f_max>[\d\.]+)?\))?"
r"|match\((?P<match>.*)\)"
r")\??$"
)
SCHEMA_ELEMENT = vol.Match(RE_SCHEMA_ELEMENT)
ARCH_ALL = [
ARCH_ARMHF, ARCH_AARCH64, ARCH_AMD64, ARCH_I386
@@ -42,6 +59,13 @@ PRIVILEGED_ALL = [
"SYS_RAWIO"
]
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",
}
def _simple_startup(value):
"""Simple startup schema."""
@@ -66,26 +90,35 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_PORTS): DOCKER_PORTS,
vol.Optional(ATTR_WEBUI):
vol.Match(r"^(?:https?):\/\/\[HOST\]:\[PORT:\d+\].*$"),
vol.Match(r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"),
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_MAP, default=[]): [vol.Match(RE_VOLUME)],
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)},
vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)],
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
vol.Optional(ATTR_GPIO, default=False): vol.Boolean(),
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),
vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(),
vol.Optional(ATTR_STDIN, default=False): vol.Boolean(),
vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(),
vol.Required(ATTR_OPTIONS): dict,
vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({
vol.Coerce(str): vol.Any(ADDON_ELEMENT, [
vol.Any(ADDON_ELEMENT, {vol.Coerce(str): ADDON_ELEMENT})
], vol.Schema({vol.Coerce(str): ADDON_ELEMENT}))
vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [
vol.Any(
SCHEMA_ELEMENT,
{vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [SCHEMA_ELEMENT])}
),
], vol.Schema({
vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [SCHEMA_ELEMENT])
}))
}), False),
vol.Optional(ATTR_IMAGE): vol.Match(r"\w*/\w*"),
vol.Optional(ATTR_IMAGE): vol.Match(r"^[\w{}]+/[\-\w{}]+$"),
vol.Optional(ATTR_TIMEOUT, default=10):
vol.All(vol.Coerce(int), vol.Range(min=10, max=120))
}, extra=vol.ALLOW_EXTRA)
vol.All(vol.Coerce(int), vol.Range(min=10, max=120)),
}, extra=vol.REMOVE_EXTRA)
# pylint: disable=no-value-for-parameter
@@ -93,12 +126,26 @@ SCHEMA_REPOSITORY_CONFIG = vol.Schema({
vol.Required(ATTR_NAME): vol.Coerce(str),
vol.Optional(ATTR_URL): vol.Url(),
vol.Optional(ATTR_MAINTAINER): vol.Coerce(str),
}, extra=vol.ALLOW_EXTRA)
}, extra=vol.REMOVE_EXTRA)
# pylint: disable=no-value-for-parameter
SCHEMA_BUILD_CONFIG = vol.Schema({
vol.Optional(ATTR_BUILD_FROM, default=BASE_IMAGE): vol.Schema({
vol.In(ARCH_ALL): vol.Match(r"(?:^[\w{}]+/)?[\-\w{}]+:[\.\-\w{}]+$"),
}),
vol.Optional(ATTR_SQUASH, default=False): vol.Boolean(),
vol.Optional(ATTR_ARGS, default={}): vol.Schema({
vol.Coerce(str): vol.Coerce(str)
}),
}, extra=vol.REMOVE_EXTRA)
# pylint: disable=no-value-for-parameter
SCHEMA_ADDON_USER = vol.Schema({
vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex):
vol.Match(r"^[0-9a-f]{32}$"),
vol.Optional(ATTR_OPTIONS, default={}): dict,
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
vol.Optional(ATTR_BOOT):
@@ -106,7 +153,7 @@ SCHEMA_ADDON_USER = vol.Schema({
vol.Optional(ATTR_NETWORK): DOCKER_PORTS,
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL,
vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL,
})
}, extra=vol.REMOVE_EXTRA)
SCHEMA_ADDON_SYSTEM = SCHEMA_ADDON_CONFIG.extend({
@@ -141,8 +188,10 @@ def validate_options(raw_schema):
# read options
for key, value in struct.items():
# Ignore unknown options / remove from list
if key not in raw_schema:
raise vol.Invalid("Unknown options {}.".format(key))
_LOGGER.warning("Unknown options %s", key)
continue
typ = raw_schema[key]
try:
@@ -159,6 +208,7 @@ def validate_options(raw_schema):
raise vol.Invalid(
"Type error for {}.".format(key)) from None
_check_missing_options(raw_schema, options, 'root')
return options
return validate
@@ -167,30 +217,38 @@ def validate_options(raw_schema):
# pylint: disable=no-value-for-parameter
def _single_validate(typ, value, key):
"""Validate a single element."""
try:
# if required argument
if value is None:
raise vol.Invalid("Missing required option '{}'.".format(key))
# if required argument
if value is None:
raise vol.Invalid("Missing required option '{}'.".format(key))
if typ == V_STR:
return str(value)
elif typ == V_INT:
return int(value)
elif typ == V_FLOAT:
return float(value)
elif typ == V_BOOL:
return vol.Boolean()(value)
elif typ == V_EMAIL:
return vol.Email()(value)
elif typ == V_URL:
return vol.Url()(value)
elif typ == V_PORT:
return NETWORK_PORT(value)
# parse extend data from type
match = RE_SCHEMA_ELEMENT.match(typ)
raise vol.Invalid("Fatal error for {} type {}".format(key, typ))
except ValueError:
raise vol.Invalid(
"Type {} error for '{}' on {}.".format(typ, value, key)) from None
# prepare range
range_args = {}
for group_name in ('i_min', 'i_max', 'f_min', 'f_max'):
group_value = match.group(group_name)
if group_value:
range_args[group_name[2:]] = float(group_value)
if typ.startswith(V_STR):
return str(value)
elif typ.startswith(V_INT):
return vol.All(vol.Coerce(int), vol.Range(**range_args))(value)
elif typ.startswith(V_FLOAT):
return vol.All(vol.Coerce(float), vol.Range(**range_args))(value)
elif typ.startswith(V_BOOL):
return vol.Boolean()(value)
elif typ.startswith(V_EMAIL):
return vol.Email()(value)
elif typ.startswith(V_URL):
return vol.Url()(value)
elif typ.startswith(V_PORT):
return NETWORK_PORT(value)
elif typ.startswith(V_MATCH):
return vol.Match(match.group('match'))(str(value))
raise vol.Invalid("Fatal error for {} type {}".format(key, typ))
def _nested_validate_list(typ, data_list, key):
@@ -198,17 +256,10 @@ def _nested_validate_list(typ, data_list, key):
options = []
for element in data_list:
# dict list
# Nested?
if isinstance(typ, dict):
c_options = {}
for c_key, c_value in element.items():
if c_key not in typ:
raise vol.Invalid(
"Unknown nested options {}".format(c_key))
c_options[c_key] = _single_validate(typ[c_key], c_value, c_key)
c_options = _nested_validate_dict(typ, element, key)
options.append(c_options)
# normal list
else:
options.append(_single_validate(typ, element, key))
@@ -220,9 +271,28 @@ def _nested_validate_dict(typ, data_dict, key):
options = {}
for c_key, c_value in data_dict.items():
# Ignore unknown options / remove from list
if c_key not in typ:
raise vol.Invalid("Unknow nested dict options {}".format(c_key))
_LOGGER.warning("Unknown options %s", c_key)
continue
options[c_key] = _single_validate(typ[c_key], c_value, c_key)
# Nested?
if isinstance(typ[c_key], list):
options[c_key] = _nested_validate_list(typ[c_key][0],
c_value, c_key)
else:
options[c_key] = _single_validate(typ[c_key], c_value, c_key)
_check_missing_options(typ, options, key)
return options
def _check_missing_options(origin, exists, root):
"""Check if all options are exists."""
missing = set(origin) - set(exists)
for miss_opt in missing:
if isinstance(origin[miss_opt], str) and \
origin[miss_opt].endswith("?"):
continue
raise vol.Invalid(
"Missing option {} in {}".format(miss_opt, root))

View File

@@ -72,7 +72,13 @@ class RestAPI(object):
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_post('/homeassistant/stop', api_hass.stop)
self.webapp.router.add_post('/homeassistant/start', api_hass.start)
self.webapp.router.add_post('/homeassistant/check', api_hass.check)
self.webapp.router.add_post(
'/homeassistant/api/{path:.+}', api_hass.api)
self.webapp.router.add_get(
'/homeassistant/api/{path:.+}', api_hass.api)
def register_addons(self, addons):
"""Register homeassistant function."""
@@ -94,8 +100,11 @@ class RestAPI(object):
'/addons/{addon}/update', api_addons.update)
self.webapp.router.add_post(
'/addons/{addon}/options', api_addons.options)
self.webapp.router.add_post(
'/addons/{addon}/rebuild', api_addons.rebuild)
self.webapp.router.add_get('/addons/{addon}/logs', api_addons.logs)
self.webapp.router.add_get('/addons/{addon}/logo', api_addons.logo)
self.webapp.router.add_post('/addons/{addon}/stdin', api_addons.stdin)
def register_security(self):
"""Register security function."""
@@ -130,13 +139,18 @@ class RestAPI(object):
def register_panel(self):
"""Register panel for homeassistant."""
panel = Path(__file__).parents[1].joinpath('panel/hassio-main.html')
def create_panel_response(build_type):
"""Create a function to generate a response."""
path = Path(__file__).parents[1].joinpath(
'panel/hassio-main-{}.html'.format(build_type))
def get_panel(request):
"""Return file response with panel."""
return web.FileResponse(panel)
return lambda request: web.FileResponse(path)
self.webapp.router.add_get('/panel', get_panel)
# This route is for backwards compatibility with HA < 0.58
self.webapp.router.add_get('/panel', create_panel_response('es5'))
self.webapp.router.add_get('/panel_es5', create_panel_response('es5'))
self.webapp.router.add_get(
'/panel_latest', create_panel_response('latest'))
async def start(self):
"""Run rest api webserver."""
@@ -157,5 +171,5 @@ class RestAPI(object):
await self.webapp.shutdown()
if self._handler:
await self._handler.finish_connections(60)
await self._handler.shutdown(60)
await self.webapp.cleanup()

View File

@@ -13,7 +13,8 @@ from ..const import (
ATTR_SOURCE, ATTR_REPOSITORIES, ATTR_ADDONS, ATTR_ARCH, ATTR_MAINTAINER,
ATTR_INSTALLED, ATTR_LOGO, ATTR_WEBUI, ATTR_DEVICES, ATTR_PRIVILEGED,
ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API,
BOOT_AUTO, BOOT_MANUAL, CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY)
ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, BOOT_AUTO, BOOT_MANUAL,
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY)
from ..validate import DOCKER_PORTS
_LOGGER = logging.getLogger(__name__)
@@ -77,8 +78,11 @@ class APIAddons(object):
ATTR_DEVICES: self._pretty_devices(addon),
ATTR_URL: addon.url,
ATTR_LOGO: addon.with_logo,
ATTR_HASSIO_API: addon.use_hassio_api,
ATTR_STDIN: addon.with_stdin,
ATTR_HASSIO_API: addon.access_hassio_api,
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
ATTR_AUDIO: addon.with_audio,
ATTR_GPIO: addon.with_gpio,
})
data_repositories = []
@@ -121,12 +125,15 @@ class APIAddons(object):
ATTR_DETACHED: addon.is_detached,
ATTR_BUILD: addon.need_build,
ATTR_NETWORK: addon.ports,
ATTR_HOST_NETWORK: addon.network_mode == 'host',
ATTR_HOST_NETWORK: addon.host_network,
ATTR_PRIVILEGED: addon.privileged,
ATTR_DEVICES: self._pretty_devices(addon),
ATTR_LOGO: addon.with_logo,
ATTR_WEBUI: addon.webui,
ATTR_HASSIO_API: addon.use_hassio_api,
ATTR_STDIN: addon.with_stdin,
ATTR_HASSIO_API: addon.access_hassio_api,
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
ATTR_GPIO: addon.with_gpio,
ATTR_AUDIO: addon.with_audio,
ATTR_AUDIO_INPUT: addon.audio_input,
ATTR_AUDIO_OUTPUT: addon.audio_output,
@@ -159,14 +166,10 @@ class APIAddons(object):
return True
@api_process
async def install(self, request):
def install(self, request):
"""Install addon."""
body = await api_validate(SCHEMA_VERSION, request)
addon = self._extract_addon(request, check_installed=False)
version = body.get(ATTR_VERSION, addon.last_version)
return await asyncio.shield(
addon.install(version=version), loop=self.loop)
return asyncio.shield(addon.install(), loop=self.loop)
@api_process
def uninstall(self, request):
@@ -195,17 +198,14 @@ class APIAddons(object):
return asyncio.shield(addon.stop(), loop=self.loop)
@api_process
async def update(self, request):
def update(self, request):
"""Update addon."""
body = await api_validate(SCHEMA_VERSION, request)
addon = self._extract_addon(request)
version = body.get(ATTR_VERSION, addon.last_version)
if version == addon.version_installed:
raise RuntimeError("Version %s is already in use", version)
if addon.last_version == addon.version_installed:
raise RuntimeError("No update available!")
return await asyncio.shield(
addon.update(version=version), loop=self.loop)
return asyncio.shield(addon.update(), loop=self.loop)
@api_process
def restart(self, request):
@@ -213,6 +213,15 @@ class APIAddons(object):
addon = self._extract_addon(request)
return asyncio.shield(addon.restart(), loop=self.loop)
@api_process
def rebuild(self, request):
"""Rebuild local build addon."""
addon = self._extract_addon(request)
if not addon.need_build:
raise RuntimeError("Only local build addons are supported")
return asyncio.shield(addon.rebuild(), loop=self.loop)
@api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request):
"""Return logs from addon."""
@@ -228,3 +237,13 @@ class APIAddons(object):
with addon.path_logo.open('rb') as png:
return png.read()
@api_process
async def stdin(self, request):
"""Write to stdin of addon."""
addon = self._extract_addon(request)
if not addon.with_stdin:
raise RuntimeError("STDIN not supported by addons")
data = await request.read()
return await asyncio.shield(addon.write_stdin(data), loop=self.loop)

View File

@@ -2,22 +2,34 @@
import asyncio
import logging
import aiohttp
from aiohttp import web
from aiohttp.web_exceptions import HTTPBadGateway
from aiohttp.hdrs import CONTENT_TYPE
import async_timeout
import voluptuous as vol
from .util import api_process, api_process_raw, api_validate
from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_DEVICES, ATTR_IMAGE, ATTR_CUSTOM,
CONTENT_TYPE_BINARY)
from ..validate import HASS_DEVICES
ATTR_BOOT, ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG,
CONTENT_TYPE_BINARY, HEADER_HA_ACCESS)
from ..validate import HASS_DEVICES, NETWORK_PORT
_LOGGER = logging.getLogger(__name__)
# pylint: disable=no-value-for-parameter
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_DEVICES): HASS_DEVICES,
vol.Optional(ATTR_BOOT): vol.Boolean(),
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Any(None, vol.Coerce(str)),
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'):
vol.Any(None, vol.Coerce(str)),
vol.Optional(ATTR_PORT): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
vol.Optional(ATTR_SSL): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
})
SCHEMA_VERSION = vol.Schema({
@@ -34,6 +46,45 @@ class APIHomeAssistant(object):
self.loop = loop
self.homeassistant = homeassistant
async def homeassistant_proxy(self, path, request):
"""Return a client request with proxy origin for Home-Assistant."""
url = "{}/api/{}".format(self.homeassistant.api_url, path)
try:
data = None
headers = {}
method = getattr(
self.homeassistant.websession, request.method.lower())
# read data
with async_timeout.timeout(10, loop=self.loop):
data = await request.read()
if data:
headers.update({CONTENT_TYPE: request.content_type})
# need api password?
if self.homeassistant.api_password:
headers = {HEADER_HA_ACCESS: self.homeassistant.api_password}
# reset headers
if not headers:
headers = None
client = await method(
url, data=data, headers=headers, timeout=300
)
return client
except aiohttp.ClientError as err:
_LOGGER.error("Client error on api %s request %s.", path, err)
except asyncio.TimeoutError:
_LOGGER.error("Client timeout error on api request %s.", path)
raise HTTPBadGateway()
@api_process
async def info(self, request):
"""Return host information."""
@@ -43,6 +94,10 @@ class APIHomeAssistant(object):
ATTR_IMAGE: self.homeassistant.image,
ATTR_DEVICES: self.homeassistant.devices,
ATTR_CUSTOM: self.homeassistant.is_custom_image,
ATTR_BOOT: self.homeassistant.boot,
ATTR_PORT: self.homeassistant.api_port,
ATTR_SSL: self.homeassistant.api_ssl,
ATTR_WATCHDOG: self.homeassistant.watchdog,
}
@api_process
@@ -57,6 +112,21 @@ class APIHomeAssistant(object):
self.homeassistant.set_custom(
body[ATTR_IMAGE], body[ATTR_LAST_VERSION])
if ATTR_BOOT in body:
self.homeassistant.boot = body[ATTR_BOOT]
if ATTR_PORT in body:
self.homeassistant.api_port = body[ATTR_PORT]
if ATTR_PASSWORD in body:
self.homeassistant.api_password = body[ATTR_PASSWORD]
if ATTR_SSL in body:
self.homeassistant.api_ssl = body[ATTR_SSL]
if ATTR_WATCHDOG in body:
self.homeassistant.watchdog = body[ATTR_WATCHDOG]
return True
@api_process
@@ -71,6 +141,16 @@ class APIHomeAssistant(object):
return await asyncio.shield(
self.homeassistant.update(version), loop=self.loop)
@api_process
def stop(self, request):
"""Stop homeassistant."""
return asyncio.shield(self.homeassistant.stop(), loop=self.loop)
@api_process
def start(self, request):
"""Start homeassistant."""
return asyncio.shield(self.homeassistant.run(), loop=self.loop)
@api_process
def restart(self, request):
"""Restart homeassistant."""
@@ -89,3 +169,14 @@ class APIHomeAssistant(object):
raise RuntimeError(message)
return True
async def api(self, request):
"""Proxy API request to Home-Assistant."""
path = request.match_info.get('path')
client = await self.homeassistant_proxy(path, request)
return web.Response(
body=await client.read(),
status=client.status,
content_type=client.content_type
)

View File

@@ -8,7 +8,7 @@ from .util import api_process_hostcontrol, api_process, api_validate
from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_TYPE, ATTR_HOSTNAME, ATTR_FEATURES,
ATTR_OS, ATTR_SERIAL, ATTR_INPUT, ATTR_DISK, ATTR_AUDIO, ATTR_AUDIO_INPUT,
ATTR_AUDIO_OUTPUT)
ATTR_AUDIO_OUTPUT, ATTR_GPIO)
from ..validate import ALSA_CHANNEL
_LOGGER = logging.getLogger(__name__)
@@ -83,8 +83,9 @@ class APIHost(object):
async def hardware(self, request):
"""Return local hardware infos."""
return {
ATTR_SERIAL: self.local_hw.serial_devices,
ATTR_INPUT: self.local_hw.input_devices,
ATTR_DISK: self.local_hw.disk_devices,
ATTR_SERIAL: list(self.local_hw.serial_devices),
ATTR_INPUT: list(self.local_hw.input_devices),
ATTR_DISK: list(self.local_hw.disk_devices),
ATTR_GPIO: list(self.local_hw.gpio_devices),
ATTR_AUDIO: self.local_hw.audio_devices,
}

View File

@@ -87,9 +87,6 @@ def api_process_raw(content):
def api_return_error(message=None):
"""Return a API error message."""
if message:
_LOGGER.error(message)
return web.json_response({
JSON_RESULT: RESULT_ERROR,
JSON_MESSAGE: message,

View File

@@ -2,6 +2,7 @@
import logging
import os
import signal
import shutil
from pathlib import Path
from colorlog import ColoredFormatter
@@ -100,6 +101,7 @@ def initialize_logging():
def check_environment():
"""Check if all environment are exists."""
# check environment variables
for key in ('SUPERVISOR_SHARE', 'SUPERVISOR_NAME',
'HOMEASSISTANT_REPOSITORY'):
try:
@@ -108,29 +110,35 @@ def check_environment():
_LOGGER.fatal("Can't find %s in env!", key)
return False
# check docker socket
if not SOCKET_DOCKER.is_socket():
_LOGGER.fatal("Can't find docker socket!")
return False
# check socat exec
if not shutil.which('socat'):
_LOGGER.fatal("Can0t find socat program!")
return False
return True
def reg_signal(loop, hassio):
def reg_signal(loop):
"""Register SIGTERM, SIGKILL to stop system."""
try:
loop.add_signal_handler(
signal.SIGTERM, lambda: loop.create_task(hassio.stop()))
signal.SIGTERM, lambda: loop.call_soon(loop.stop))
except (ValueError, RuntimeError):
_LOGGER.warning("Could not bind to SIGTERM")
try:
loop.add_signal_handler(
signal.SIGHUP, lambda: loop.create_task(hassio.stop()))
signal.SIGHUP, lambda: loop.call_soon(loop.stop))
except (ValueError, RuntimeError):
_LOGGER.warning("Could not bind to SIGHUP")
try:
loop.add_signal_handler(
signal.SIGINT, lambda: loop.create_task(hassio.stop()))
signal.SIGINT, lambda: loop.call_soon(loop.stop))
except (ValueError, RuntimeError):
_LOGGER.warning("Could not bind to SIGINT")

View File

@@ -6,15 +6,13 @@ from pathlib import Path, PurePath
from .const import (
FILE_HASSIO_CONFIG, HASSIO_DATA, ATTR_SECURITY, ATTR_SESSIONS,
ATTR_PASSWORD, ATTR_TOTP, ATTR_TIMEZONE, ATTR_API_ENDPOINT,
ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT)
from .tools import JsonConfig
ATTR_PASSWORD, ATTR_TOTP, ATTR_TIMEZONE, ATTR_ADDONS_CUSTOM_LIST,
ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_LAST_BOOT)
from .tools import JsonConfig, parse_datetime
from .validate import SCHEMA_HASSIO_CONFIG
_LOGGER = logging.getLogger(__name__)
DATETIME_FORMAT = "%Y%m%d %H:%M:%S"
HOMEASSISTANT_CONFIG = PurePath("homeassistant")
HASSIO_SSL = PurePath("ssl")
@@ -28,6 +26,8 @@ BACKUP_DATA = PurePath("backup")
SHARE_DATA = PurePath("share")
TMP_DATA = PurePath("tmp")
DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat()
class CoreConfig(JsonConfig):
"""Hold all core config data."""
@@ -37,16 +37,6 @@ class CoreConfig(JsonConfig):
super().__init__(FILE_HASSIO_CONFIG, SCHEMA_HASSIO_CONFIG)
self.arch = None
@property
def api_endpoint(self):
"""Return IP address of api endpoint."""
return self._data[ATTR_API_ENDPOINT]
@api_endpoint.setter
def api_endpoint(self, value):
"""Store IP address of api endpoint."""
self._data[ATTR_API_ENDPOINT] = value
@property
def timezone(self):
"""Return system timezone."""
@@ -58,6 +48,22 @@ class CoreConfig(JsonConfig):
self._data[ATTR_TIMEZONE] = value
self.save()
@property
def last_boot(self):
"""Return last boot datetime."""
boot_str = self._data.get(ATTR_LAST_BOOT, DEFAULT_BOOT_TIME)
boot_time = parse_datetime(boot_str)
if not boot_time:
return datetime.utcfromtimestamp(1)
return boot_time
@last_boot.setter
def last_boot(self, value):
"""Set last boot datetime."""
self._data[ATTR_LAST_BOOT] = value.isoformat()
self.save()
@property
def path_hassio(self):
"""Return hassio data path."""
@@ -201,14 +207,14 @@ class CoreConfig(JsonConfig):
def security_sessions(self):
"""Return api sessions."""
return {
session: datetime.strptime(until, DATETIME_FORMAT) for
session: parse_datetime(until) for
session, until in self._data[ATTR_SESSIONS].items()
}
def add_security_session(self, session, valid):
"""Set the a new session."""
self._data[ATTR_SESSIONS].update(
{session: valid.strftime(DATETIME_FORMAT)}
{session: valid.isoformat()}
)
self.save()

View File

@@ -1,7 +1,8 @@
"""Const file for HassIO."""
from pathlib import Path
from ipaddress import ip_network
HASSIO_VERSION = '0.55'
HASSIO_VERSION = '0.75'
URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/'
'hassio/{}/version.json')
@@ -15,11 +16,10 @@ RUN_UPDATE_SUPERVISOR_TASKS = 29100
RUN_UPDATE_ADDONS_TASKS = 57600
RUN_RELOAD_ADDONS_TASKS = 28800
RUN_RELOAD_SNAPSHOTS_TASKS = 72000
RUN_WATCHDOG_HOMEASSISTANT = 15
RUN_WATCHDOG_HOMEASSISTANT_DOCKER = 15
RUN_WATCHDOG_HOMEASSISTANT_API = 300
RUN_CLEANUP_API_SESSIONS = 900
RESTART_EXIT_CODE = 100
FILE_HASSIO_ADDONS = Path(HASSIO_DATA, "addons.json")
FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json")
FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json")
@@ -28,6 +28,10 @@ FILE_HASSIO_UPDATER = Path(HASSIO_DATA, "updater.json")
SOCKET_DOCKER = Path("/var/run/docker.sock")
SOCKET_HC = Path("/var/run/hassio-hc.sock")
DOCKER_NETWORK = 'hassio'
DOCKER_NETWORK_MASK = ip_network('172.30.32.0/23')
DOCKER_NETWORK_RANGE = ip_network('172.30.33.0/24')
LABEL_VERSION = 'io.hass.version'
LABEL_ARCH = 'io.hass.arch'
LABEL_TYPE = 'io.hass.type'
@@ -45,17 +49,22 @@ RESULT_OK = 'ok'
CONTENT_TYPE_BINARY = 'application/octet-stream'
CONTENT_TYPE_PNG = 'image/png'
CONTENT_TYPE_JSON = 'application/json'
HEADER_HA_ACCESS = 'x-ha-access'
ATTR_WATCHDOG = 'watchdog'
ATTR_DATE = 'date'
ATTR_ARCH = 'arch'
ATTR_HOSTNAME = 'hostname'
ATTR_TIMEZONE = 'timezone'
ATTR_ARGS = 'args'
ATTR_OS = 'os'
ATTR_TYPE = 'type'
ATTR_SOURCE = 'source'
ATTR_FEATURES = 'features'
ATTR_ADDONS = 'addons'
ATTR_VERSION = 'version'
ATTR_LAST_BOOT = 'last_boot'
ATTR_LAST_VERSION = 'last_version'
ATTR_BETA_CHANNEL = 'beta_channel'
ATTR_NAME = 'name'
@@ -64,6 +73,8 @@ ATTR_DESCRIPTON = 'description'
ATTR_STARTUP = 'startup'
ATTR_BOOT = 'boot'
ATTR_PORTS = 'ports'
ATTR_PORT = 'port'
ATTR_SSL = 'ssl'
ATTR_MAP = 'map'
ATTR_WEBUI = 'webui'
ATTR_OPTIONS = 'options'
@@ -73,6 +84,7 @@ ATTR_STATE = 'state'
ATTR_SCHEMA = 'schema'
ATTR_IMAGE = 'image'
ATTR_LOGO = 'logo'
ATTR_STDIN = 'stdin'
ATTR_ADDONS_REPOSITORIES = 'addons_repositories'
ATTR_REPOSITORY = 'repository'
ATTR_REPOSITORIES = 'repositories'
@@ -97,6 +109,8 @@ ATTR_SNAPSHOTS = 'snapshots'
ATTR_HOMEASSISTANT = 'homeassistant'
ATTR_HASSIO = 'hassio'
ATTR_HASSIO_API = 'hassio_api'
ATTR_HOMEASSISTANT_API = 'homeassistant_api'
ATTR_UUID = 'uuid'
ATTR_FOLDERS = 'folders'
ATTR_SIZE = 'size'
ATTR_TYPE = 'type'
@@ -111,7 +125,10 @@ ATTR_OUTPUT = 'output'
ATTR_DISK = 'disk'
ATTR_SERIAL = 'serial'
ATTR_SECURITY = 'security'
ATTR_API_ENDPOINT = 'api_endpoint'
ATTR_BUILD_FROM = 'build_from'
ATTR_SQUASH = 'squash'
ATTR_GPIO = 'gpio'
ATTR_LEGACY = 'legacy'
ATTR_ADDONS_CUSTOM_LIST = 'addons_custom_list'
STARTUP_INITIALIZE = 'initialize'

View File

@@ -3,26 +3,28 @@ import asyncio
import logging
import aiohttp
import docker
from .addons import AddonManager
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,
RUN_UPDATE_INFO_TASKS, RUN_RELOAD_ADDONS_TASKS,
RUN_UPDATE_SUPERVISOR_TASKS, RUN_WATCHDOG_HOMEASSISTANT_DOCKER,
RUN_CLEANUP_API_SESSIONS, STARTUP_SYSTEM, STARTUP_SERVICES,
STARTUP_APPLICATION, STARTUP_INITIALIZE, RUN_RELOAD_SNAPSHOTS_TASKS,
RUN_UPDATE_ADDONS_TASKS)
from .hardware import Hardware
from .homeassistant import HomeAssistant
from .scheduler import Scheduler
from .dock import DockerAPI
from .dock.supervisor import DockerSupervisor
from .dns import DNSForward
from .snapshots import SnapshotsManager
from .updater import Updater
from .tasks import (
hassio_update, homeassistant_watchdog, api_sessions_cleanup, addons_update)
from .tools import get_local_ip, fetch_timezone
hassio_update, homeassistant_watchdog_docker, api_sessions_cleanup,
addons_update)
from .tools import fetch_timezone
_LOGGER = logging.getLogger(__name__)
@@ -40,21 +42,22 @@ class HassIO(object):
self.scheduler = Scheduler(loop)
self.api = RestAPI(config, loop)
self.hardware = Hardware()
self.dock = docker.DockerClient(
base_url="unix:/{}".format(str(SOCKET_DOCKER)), version='auto')
self.docker = DockerAPI()
self.dns = DNSForward()
# init basic docker container
self.supervisor = DockerSupervisor(config, loop, self.dock, self.stop)
self.supervisor = DockerSupervisor(
config, loop, self.docker, self.stop)
# init homeassistant
self.homeassistant = HomeAssistant(
config, loop, self.dock, self.updater)
config, loop, self.docker, self.updater)
# init HostControl
self.host_control = HostControl(loop)
# init addon system
self.addons = AddonManager(config, loop, self.dock)
self.addons = AddonManager(config, loop, self.docker)
# init snapshot system
self.snapshots = SnapshotsManager(
@@ -64,15 +67,12 @@ class HassIO(object):
"""Setup HassIO orchestration."""
# supervisor
if not await self.supervisor.attach():
_LOGGER.fatal("Can't attach to supervisor docker container!")
_LOGGER.fatal("Can't setup supervisor docker container!")
await self.supervisor.cleanup()
# set running arch
self.config.arch = self.supervisor.arch
# 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)
@@ -122,6 +122,9 @@ class HassIO(object):
self.scheduler.register_task(
self.snapshots.reload, RUN_RELOAD_SNAPSHOTS_TASKS, now=True)
# start dns forwarding
self.loop.create_task(self.dns.start())
# start addon mark as initialize
await self.addons.auto_boot(STARTUP_INITIALIZE)
@@ -136,11 +139,11 @@ class HassIO(object):
# start api
await self.api.start()
_LOGGER.info("Start hassio api on %s", self.config.api_endpoint)
_LOGGER.info("Start hassio api on %s", self.docker.network.supervisor)
try:
# HomeAssistant is already running / supervisor have only reboot
if await self.homeassistant.is_running():
if self.hardware.last_boot == self.config.last_boot:
_LOGGER.info("HassIO reboot detected")
return
@@ -151,29 +154,37 @@ class HassIO(object):
await self.addons.auto_boot(STARTUP_SERVICES)
# run HomeAssistant
await self.homeassistant.run()
if self.homeassistant.boot:
await self.homeassistant.run()
# start addon mark as application
await self.addons.auto_boot(STARTUP_APPLICATION)
# store new last boot
self.config.last_boot = self.hardware.last_boot
finally:
# schedule homeassistant watchdog
self.scheduler.register_task(
homeassistant_watchdog(self.loop, self.homeassistant),
RUN_WATCHDOG_HOMEASSISTANT)
homeassistant_watchdog_docker(self.loop, self.homeassistant),
RUN_WATCHDOG_HOMEASSISTANT_DOCKER)
# self.scheduler.register_task(
# homeassistant_watchdog_api(self.loop, self.homeassistant),
# RUN_WATCHDOG_HOMEASSISTANT_API)
# If landingpage / run upgrade in background
if self.homeassistant.version == 'landingpage':
self.loop.create_task(self.homeassistant.install())
async def stop(self, exit_code=0):
async def stop(self):
"""Stop a running orchestration."""
# don't process scheduler anymore
self.scheduler.suspend = True
# process stop tasks
self.websession.close()
await self.api.stop()
self.homeassistant.websession.close()
self.exit_code = exit_code
self.loop.stop()
# process async stop tasks
await asyncio.wait([self.api.stop(), self.dns.stop()], loop=self.loop)

40
hassio/dns.py Normal file
View File

@@ -0,0 +1,40 @@
"""Setup the internal DNS service for host applications."""
import asyncio
import logging
import shlex
_LOGGER = logging.getLogger(__name__)
COMMAND = "socat UDP-RECVFROM:53,fork UDP-SENDTO:127.0.0.11:53"
class DNSForward(object):
"""Manage DNS forwarding to internal DNS."""
def __init__(self):
"""Initialize DNS forwarding."""
self.proc = None
async def start(self):
"""Start DNS forwarding."""
try:
self.proc = await asyncio.create_subprocess_exec(
*shlex.split(COMMAND),
stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
except OSError as err:
_LOGGER.error("Can't start DNS forwarding -> %s", err)
else:
_LOGGER.info("Start DNS port forwarding for host add-ons")
async def stop(self):
"""Stop DNS forwarding."""
if not self.proc:
_LOGGER.warning("DNS forwarding is not running!")
return
self.proc.kill()
await self.proc.wait()
_LOGGER.info("Stop DNS forwarding")

View File

@@ -1,324 +1,108 @@
"""Init file for HassIO docker object."""
import asyncio
from contextlib import suppress
import logging
import docker
from .util import docker_process
from ..const import LABEL_VERSION, LABEL_ARCH
from .network import DockerNetwork
from ..const import SOCKET_DOCKER
_LOGGER = logging.getLogger(__name__)
class DockerBase(object):
"""Docker hassio wrapper."""
class DockerAPI(object):
"""Docker hassio wrapper.
def __init__(self, config, loop, dock, image=None, timeout=30):
This class is not AsyncIO safe!
"""
def __init__(self):
"""Initialize docker base wrapper."""
self.config = config
self.loop = loop
self.dock = dock
self.image = image
self.timeout = timeout
self.version = None
self.arch = None
self._lock = asyncio.Lock(loop=loop)
self.docker = docker.DockerClient(
base_url="unix:/{}".format(str(SOCKET_DOCKER)), version='auto')
self.network = DockerNetwork(self.docker)
@property
def name(self):
"""Return name of docker container."""
return None
def images(self):
"""Return api images."""
return self.docker.images
@property
def in_progress(self):
"""Return True if a task is in progress."""
return self._lock.locked()
def containers(self):
"""Return api containers."""
return self.docker.containers
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']
@property
def api(self):
"""Return api containers."""
return self.docker.api
# read version
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)
# read arch
need_arch = force or not self.arch
if need_arch and LABEL_ARCH in metadata['Config']['Labels']:
self.arch = metadata['Config']['Labels'][LABEL_ARCH]
@docker_process
def install(self, tag):
"""Pull docker image."""
return self.loop.run_in_executor(None, self._install, tag)
def _install(self, tag):
"""Pull docker image.
def run(self, image, **kwargs):
""""Create a docker and run it.
Need run inside executor.
"""
name = kwargs.get('name', image)
network_mode = kwargs.get('network_mode')
hostname = kwargs.get('hostname')
# setup network
if network_mode:
kwargs['dns'] = [str(self.network.supervisor)]
else:
kwargs['network'] = None
# create container
try:
_LOGGER.info("Pull image %s tag %s.", self.image, tag)
image = self.dock.images.pull("{}:{}".format(self.image, tag))
image.tag(self.image, tag='latest')
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)
container = self.docker.containers.create(image, **kwargs)
except docker.errors.DockerException as err:
_LOGGER.error("Can't create container from %s -> %s", name, err)
return False
_LOGGER.info("Tag image %s with version %s as latest", self.image, tag)
return True
def exists(self):
"""Return True if docker image exists in local repo."""
return self.loop.run_in_executor(None, self._exists)
def _exists(self):
"""Return True if docker image exists in local repo.
Need run inside executor.
"""
try:
self.dock.images.get(self.image)
except docker.errors.DockerException:
return False
return True
def is_running(self):
"""Return True if docker is Running.
Return a Future.
"""
return self.loop.run_in_executor(None, self._is_running)
def _is_running(self):
"""Return True if docker is Running.
Need run inside executor.
"""
try:
container = self.dock.containers.get(self.name)
image = self.dock.images.get(self.image)
except docker.errors.DockerException:
return False
# 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
@docker_process
def attach(self):
"""Attach to running docker container."""
return self.loop.run_in_executor(None, self._attach)
def _attach(self):
"""Attach to running docker container.
Need run inside executor.
"""
try:
if self.image:
obj_data = self.dock.images.get(self.image).attrs
# attach network
if not network_mode:
alias = [hostname] if hostname else None
if self.network.attach_container(container, alias=alias):
self.network.detach_default_bridge(container)
else:
obj_data = self.dock.containers.get(self.name).attrs
except docker.errors.DockerException:
return False
_LOGGER.warning("Can't attach %s to hassio-net!", name)
self.process_metadata(obj_data)
_LOGGER.info(
"Attach to image %s with version %s", self.image, self.version)
return True
@docker_process
def run(self):
"""Run docker image."""
return self.loop.run_in_executor(None, self._run)
def _run(self):
"""Run docker image.
Need run inside executor.
"""
raise NotImplementedError()
@docker_process
def stop(self):
"""Stop/remove docker container."""
return self.loop.run_in_executor(None, self._stop)
def _stop(self):
"""Stop/remove and remove docker container.
Need run inside executor.
"""
# run container
try:
container = self.dock.containers.get(self.name)
except docker.errors.DockerException:
return False
if container.status == 'running':
_LOGGER.info("Stop %s docker application", self.image)
with suppress(docker.errors.DockerException):
container.stop(timeout=self.timeout)
with suppress(docker.errors.DockerException):
_LOGGER.info("Clean %s docker application", self.image)
container.remove(force=True)
return True
@docker_process
def remove(self):
"""Remove docker images."""
return self.loop.run_in_executor(None, self._remove)
def _remove(self):
"""remove docker images.
Need run inside executor.
"""
# cleanup container
self._stop()
_LOGGER.info(
"Remove docker %s with latest and %s", self.image, self.version)
try:
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)
container.start()
except docker.errors.DockerException as err:
_LOGGER.warning("Can't remove image %s -> %s", self.image, err)
return False
# clean metadata
self.version = None
self.arch = None
return True
@docker_process
def update(self, tag):
"""Update a docker image."""
return self.loop.run_in_executor(None, self._update, tag)
def _update(self, tag):
"""Update a docker image.
Need run inside executor.
"""
_LOGGER.info(
"Update docker %s with %s:%s", self.version, self.image, tag)
# update docker image
if not self._install(tag):
return False
# stop container & cleanup
self._stop()
self._cleanup()
return True
@docker_process
def logs(self):
"""Return docker logs of container."""
return self.loop.run_in_executor(None, self._logs)
def _logs(self):
"""Return docker logs of container.
Need run inside executor.
"""
try:
container = self.dock.containers.get(self.name)
except docker.errors.DockerException:
return b""
try:
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)
@docker_process
def restart(self):
"""Restart docker container."""
return self.loop.run_in_executor(None, self._restart)
def _restart(self):
"""Restart docker container.
Need run inside executor.
"""
try:
container = self.dock.containers.get(self.name)
except docker.errors.DockerException:
return False
_LOGGER.info("Restart %s", self.image)
try:
container.restart(timeout=self.timeout)
except docker.errors.DockerException as err:
_LOGGER.warning("Can't restart %s -> %s", self.image, err)
_LOGGER.error("Can't start %s -> %s", name, err)
return False
return True
@docker_process
def cleanup(self):
"""Check if old version exists and cleanup."""
return 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 False
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)
return True
@docker_process
def execute_command(self, command):
"""Create a temporary container and run command."""
return self.loop.run_in_executor(None, self._execute_command, command)
def _execute_command(self, command):
def run_command(self, image, command=None, **kwargs):
"""Create a temporary container and run command.
Need run inside executor.
"""
raise NotImplementedError()
stdout = kwargs.get('stdout', True)
stderr = kwargs.get('stderr', True)
_LOGGER.info("Run command '%s' on %s", command, image)
try:
container = self.docker.containers.run(
image,
command=command,
network=self.network.name,
**kwargs
)
# wait until command is done
exit_code = container.wait()
output = container.logs(stdout=stdout, stderr=stderr)
except docker.errors.DockerException as err:
_LOGGER.error("Can't execute command -> %s", err)
return (None, b"")
# cleanup container
with suppress(docker.errors.DockerException):
container.remove(force=True)
return (exit_code, output)

View File

@@ -1,30 +1,45 @@
"""Init file for HassIO addon docker object."""
import logging
from pathlib import Path
import shutil
import os
import docker
import requests
from . import DockerBase
from .util import dockerfile_template, docker_process
from .interface import DockerInterface
from .util import docker_process
from ..addons.build import AddonBuild
from ..const import (
META_ADDON, MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, MAP_SHARE)
MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, MAP_SHARE)
_LOGGER = logging.getLogger(__name__)
AUDIO_DEVICE = "/dev/snd:/dev/snd:rwm"
class DockerAddon(DockerBase):
class DockerAddon(DockerInterface):
"""Docker hassio wrapper for HomeAssistant."""
def __init__(self, config, loop, dock, addon):
def __init__(self, config, loop, api, addon):
"""Initialize docker homeassistant wrapper."""
super().__init__(
config, loop, dock, image=addon.image, timeout=addon.timeout)
config, loop, api, image=addon.image, timeout=addon.timeout)
self.addon = addon
def process_metadata(self, metadata, force=False):
"""Use addon data instead meta data with legacy."""
if not self.addon.legacy:
return super().process_metadata(metadata, force=force)
# set meta data
if not self.version or force:
if force: # called on install/update/build
self.version = self.addon.last_version
else:
self.version = self.addon.version_installed
if not self.arch:
self.arch = self.config.arch
@property
def name(self):
"""Return name of docker container."""
@@ -45,6 +60,10 @@ class DockerAddon(DockerBase):
'ALSA_INPUT': self.addon.audio_input,
})
# Set api token if any API access is needed
if self.addon.access_hassio_api or self.addon.access_homeassistant_api:
addon_env['API_TOKEN'] = self.addon.api_token
return {
**addon_env,
'TZ': self.config.timezone,
@@ -64,6 +83,18 @@ class DockerAddon(DockerBase):
return devices
return None
@property
def ports(self):
"""Filter None from addon ports."""
if not self.addon.ports:
return None
return {
container_port: host_port
for container_port, host_port in self.addon.ports.items()
if host_port
}
@property
def tmpfs(self):
"""Return tmpfs for docker add-on."""
@@ -73,15 +104,20 @@ class DockerAddon(DockerBase):
return None
@property
def mapping(self):
def network_mapping(self):
"""Return hosts mapping."""
if not self.addon.use_hassio_api:
return None
return {
'hassio': self.config.api_endpoint,
'homeassistant': self.docker.network.gateway,
'hassio': self.docker.network.supervisor,
}
@property
def network_mode(self):
"""Return network mode for addon."""
if self.addon.host_network:
return 'host'
return None
@property
def volumes(self):
"""Generate volumes for mappings."""
@@ -92,6 +128,7 @@ class DockerAddon(DockerBase):
addon_mapping = self.addon.map_volumes
# setup config mappings
if MAP_CONFIG in addon_mapping:
volumes.update({
str(self.config.path_extern_config): {
@@ -122,6 +159,17 @@ class DockerAddon(DockerBase):
'bind': '/share', 'mode': addon_mapping[MAP_SHARE]
}})
# init other hardware mappings
if self.addon.with_gpio:
volumes.update({
'/sys/class/gpio': {
'bind': '/sys/class/gpio', 'mode': "rw"
},
'/sys/devices/platform/soc': {
'bind': '/sys/devices/platform/soc', 'mode': "rw"
},
})
return volumes
def _run(self):
@@ -139,29 +187,28 @@ class DockerAddon(DockerBase):
if not self.addon.write_options():
return False
try:
self.dock.containers.run(
self.image,
name=self.name,
hostname=self.hostname,
detach=True,
network_mode=self.addon.network_mode,
ports=self.addon.ports,
extra_hosts=self.mapping,
devices=self.devices,
cap_add=self.addon.privileged,
environment=self.environment,
volumes=self.volumes,
tmpfs=self.tmpfs
)
ret = self.docker.run(
self.image,
name=self.name,
hostname=self.hostname,
detach=True,
init=True,
stdin_open=self.addon.with_stdin,
network_mode=self.network_mode,
ports=self.ports,
extra_hosts=self.network_mapping,
devices=self.devices,
cap_add=self.addon.privileged,
environment=self.environment,
volumes=self.volumes,
tmpfs=self.tmpfs
)
except docker.errors.DockerException as err:
_LOGGER.error("Can't run %s -> %s", self.image, err)
return False
if ret:
_LOGGER.info("Start docker addon %s with version %s",
self.image, self.version)
_LOGGER.info(
"Start docker addon %s with version %s", self.image, self.version)
return True
return ret
def _install(self, tag):
"""Pull docker image or build it.
@@ -178,45 +225,21 @@ class DockerAddon(DockerBase):
Need run inside executor.
"""
build_dir = Path(self.config.path_tmp, self.addon.slug)
build_env = AddonBuild(self.config, self.addon)
_LOGGER.info("Start build %s:%s", self.image, tag)
try:
# prepare temporary addon build folder
try:
source = self.addon.path_location
shutil.copytree(str(source), str(build_dir))
except shutil.Error as err:
_LOGGER.error("Can't copy %s to temporary build folder -> %s",
source, err)
return False
image = self.docker.images.build(**build_env.get_docker_args(tag))
# prepare Dockerfile
try:
dockerfile_template(
Path(build_dir, 'Dockerfile'), self.config.arch,
tag, META_ADDON)
except OSError as err:
_LOGGER.error("Can't prepare dockerfile -> %s", err)
image.tag(self.image, tag='latest')
self.process_metadata(image.attrs, force=True)
# run docker build
try:
build_tag = "{}:{}".format(self.image, tag)
except (docker.errors.DockerException) as err:
_LOGGER.error("Can't build %s:%s -> %s", self.image, tag, err)
return False
_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)
_LOGGER.info("Build %s:%s done", self.image, tag)
return True
@docker_process
def export_image(self, path):
@@ -229,7 +252,7 @@ class DockerAddon(DockerBase):
Need run inside executor.
"""
try:
image = self.dock.api.get_image(self.image)
image = self.docker.api.get_image(self.image)
except docker.errors.DockerException as err:
_LOGGER.error("Can't fetch image %s -> %s", self.image, err)
return False
@@ -257,9 +280,9 @@ class DockerAddon(DockerBase):
"""
try:
with tar_file.open("rb") as read_tar:
self.dock.api.load_image(read_tar)
self.docker.api.load_image(read_tar)
image = self.dock.images.get(self.image)
image = self.docker.images.get(self.image)
image.tag(self.image, tag=tag)
except (docker.errors.DockerException, OSError) as err:
_LOGGER.error("Can't import image %s -> %s", self.image, err)
@@ -278,3 +301,35 @@ class DockerAddon(DockerBase):
"""
self._stop()
return self._run()
@docker_process
def write_stdin(self, data):
"""Write to add-on stdin."""
return self.loop.run_in_executor(None, self._write_stdin, data)
def _write_stdin(self, data):
"""Write to add-on stdin.
Need run inside executor.
"""
if not self._is_running():
return False
try:
# load needed docker objects
container = self.docker.containers.get(self.name)
socket = container.attach_socket(params={'stdin': 1, 'stream': 1})
except docker.errors.DockerException as err:
_LOGGER.error("Can't attach to %s stdin -> %s", self.name, err)
return False
try:
# write to stdin
data += b"\n"
os.write(socket.fileno(), data)
socket.close()
except OSError as err:
_LOGGER.error("Can't write to %s stdin -> %s", self.name, err)
return False
return True

View File

@@ -1,22 +1,21 @@
"""Init file for HassIO docker object."""
from contextlib import suppress
import logging
import docker
from . import DockerBase
from .interface import DockerInterface
_LOGGER = logging.getLogger(__name__)
HASS_DOCKER_NAME = 'homeassistant'
class DockerHomeAssistant(DockerBase):
class DockerHomeAssistant(DockerInterface):
"""Docker hassio wrapper for HomeAssistant."""
def __init__(self, config, loop, dock, data):
def __init__(self, config, loop, api, data):
"""Initialize docker homeassistant wrapper."""
super().__init__(config, loop, dock, image=data.image)
super().__init__(config, loop, api, image=data.image)
self.data = data
@property
@@ -47,71 +46,69 @@ class DockerHomeAssistant(DockerBase):
# cleanup
self._stop()
try:
self.dock.containers.run(
self.image,
name=self.name,
hostname=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': 'ro'},
str(self.config.path_extern_share):
{'bind': '/share', 'mode': 'rw'},
}
)
ret = self.docker.run(
self.image,
name=self.name,
hostname=self.name,
detach=True,
privileged=True,
init=True,
devices=self.devices,
network_mode='host',
environment={
'HASSIO': self.docker.network.supervisor,
'TZ': self.config.timezone,
},
volumes={
str(self.config.path_extern_config):
{'bind': '/config', 'mode': 'rw'},
str(self.config.path_extern_ssl):
{'bind': '/ssl', 'mode': 'ro'},
str(self.config.path_extern_share):
{'bind': '/share', 'mode': 'rw'},
}
)
except docker.errors.DockerException as err:
_LOGGER.error("Can't run %s -> %s", self.image, err)
return False
if ret:
_LOGGER.info("Start homeassistant %s with version %s",
self.image, self.version)
_LOGGER.info(
"Start homeassistant %s with version %s", self.image, self.version)
return True
return ret
def _execute_command(self, command):
"""Create a temporary container and run command.
Need run inside executor.
"""
_LOGGER.info("Run command '%s' on %s", command, self.image)
return self.docker.run_command(
self.image,
command,
detach=True,
stdout=True,
stderr=True,
environment={
'TZ': self.config.timezone,
},
volumes={
str(self.config.path_extern_config):
{'bind': '/config', 'mode': 'ro'},
str(self.config.path_extern_ssl):
{'bind': '/ssl', 'mode': 'ro'},
}
)
def is_initialize(self):
"""Return True if docker container exists."""
return self.loop.run_in_executor(None, self._is_initialize)
def _is_initialize(self):
"""Return True if docker container exists.
Need run inside executor.
"""
try:
container = self.dock.containers.run(
self.image,
command=command,
detach=True,
stdout=True,
stderr=True,
environment={
'TZ': self.config.timezone,
},
volumes={
str(self.config.path_extern_config):
{'bind': '/config', 'mode': 'ro'},
str(self.config.path_extern_ssl):
{'bind': '/ssl', 'mode': 'ro'},
}
)
self.docker.containers.get(self.name)
except docker.errors.DockerException:
return False
# wait until command is done
container.wait()
output = container.logs()
except docker.errors.DockerException as err:
_LOGGER.error("Can't execute command -> %s", err)
return b""
# cleanup container
with suppress(docker.errors.DockerException):
container.remove(force=True)
return output
return True

327
hassio/dock/interface.py Normal file
View File

@@ -0,0 +1,327 @@
"""Interface class for HassIO docker object."""
import asyncio
from contextlib import suppress
import logging
import docker
from .util import docker_process
from ..const import LABEL_VERSION, LABEL_ARCH
_LOGGER = logging.getLogger(__name__)
class DockerInterface(object):
"""Docker hassio interface."""
def __init__(self, config, loop, api, image=None, timeout=30):
"""Initialize docker base wrapper."""
self.config = config
self.loop = loop
self.docker = api
self.image = image
self.timeout = timeout
self.version = None
self.arch = None
self._lock = asyncio.Lock(loop=loop)
@property
def name(self):
"""Return name of docker container."""
return None
@property
def in_progress(self):
"""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 version
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)
# read arch
need_arch = force or not self.arch
if need_arch and LABEL_ARCH in metadata['Config']['Labels']:
self.arch = metadata['Config']['Labels'][LABEL_ARCH]
@docker_process
def install(self, tag):
"""Pull docker image."""
return self.loop.run_in_executor(None, self._install, tag)
def _install(self, tag):
"""Pull docker image.
Need run inside executor.
"""
try:
_LOGGER.info("Pull image %s tag %s.", self.image, tag)
image = self.docker.images.pull("{}:{}".format(self.image, tag))
image.tag(self.image, tag='latest')
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):
"""Return True if docker image exists in local repo."""
return self.loop.run_in_executor(None, self._exists)
def _exists(self):
"""Return True if docker image exists in local repo.
Need run inside executor.
"""
try:
self.docker.images.get(self.image)
except docker.errors.DockerException:
return False
return True
def is_running(self):
"""Return True if docker is Running.
Return a Future.
"""
return self.loop.run_in_executor(None, self._is_running)
def _is_running(self):
"""Return True if docker is Running.
Need run inside executor.
"""
try:
container = self.docker.containers.get(self.name)
image = self.docker.images.get(self.image)
except docker.errors.DockerException:
return False
# 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
@docker_process
def attach(self):
"""Attach to running docker container."""
return self.loop.run_in_executor(None, self._attach)
def _attach(self):
"""Attach to running docker container.
Need run inside executor.
"""
try:
if self.image:
obj_data = self.docker.images.get(self.image).attrs
else:
obj_data = self.docker.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
@docker_process
def run(self):
"""Run docker image."""
return self.loop.run_in_executor(None, self._run)
def _run(self):
"""Run docker image.
Need run inside executor.
"""
raise NotImplementedError()
@docker_process
def stop(self):
"""Stop/remove docker container."""
return self.loop.run_in_executor(None, self._stop)
def _stop(self):
"""Stop/remove and remove docker container.
Need run inside executor.
"""
try:
container = self.docker.containers.get(self.name)
except docker.errors.DockerException:
return False
if container.status == 'running':
_LOGGER.info("Stop %s docker application", self.image)
with suppress(docker.errors.DockerException):
container.stop(timeout=self.timeout)
with suppress(docker.errors.DockerException):
_LOGGER.info("Clean %s docker application", self.image)
container.remove(force=True)
return True
@docker_process
def remove(self):
"""Remove docker images."""
return self.loop.run_in_executor(None, self._remove)
def _remove(self):
"""remove docker images.
Need run inside executor.
"""
# cleanup container
self._stop()
_LOGGER.info(
"Remove docker %s with latest and %s", self.image, self.version)
try:
with suppress(docker.errors.ImageNotFound):
self.docker.images.remove(
image="{}:latest".format(self.image), force=True)
with suppress(docker.errors.ImageNotFound):
self.docker.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
# clean metadata
self.version = None
self.arch = None
return True
@docker_process
def update(self, tag):
"""Update a docker image."""
return self.loop.run_in_executor(None, self._update, tag)
def _update(self, tag):
"""Update a docker image.
Need run inside executor.
"""
_LOGGER.info(
"Update docker %s with %s:%s", self.version, self.image, tag)
# update docker image
if not self._install(tag):
return False
# stop container & cleanup
self._stop()
self._cleanup()
return True
def logs(self):
"""Return docker logs of container.
Return a Future.
"""
return self.loop.run_in_executor(None, self._logs)
def _logs(self):
"""Return docker logs of container.
Need run inside executor.
"""
try:
container = self.docker.containers.get(self.name)
except docker.errors.DockerException:
return b""
try:
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)
@docker_process
def restart(self):
"""Restart docker container."""
return self.loop.run_in_executor(None, self._restart)
def _restart(self):
"""Restart docker container.
Need run inside executor.
"""
try:
container = self.docker.containers.get(self.name)
except docker.errors.DockerException:
return False
_LOGGER.info("Restart %s", self.image)
try:
container.restart(timeout=self.timeout)
except docker.errors.DockerException as err:
_LOGGER.warning("Can't restart %s -> %s", self.image, err)
return False
return True
@docker_process
def cleanup(self):
"""Check if old version exists and cleanup."""
return 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.docker.images.get(self.image)
except docker.errors.DockerException:
_LOGGER.warning("Can't find %s for cleanup", self.image)
return False
for image in self.docker.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.docker.images.remove(image.id, force=True)
return True
@docker_process
def execute_command(self, command):
"""Create a temporary container and run command."""
return self.loop.run_in_executor(None, self._execute_command, command)
def _execute_command(self, command):
"""Create a temporary container and run command.
Need run inside executor.
"""
raise NotImplementedError()

89
hassio/dock/network.py Normal file
View File

@@ -0,0 +1,89 @@
"""Internal network manager for HassIO."""
import logging
import docker
from ..const import DOCKER_NETWORK_MASK, DOCKER_NETWORK, DOCKER_NETWORK_RANGE
_LOGGER = logging.getLogger(__name__)
class DockerNetwork(object):
"""Internal HassIO Network."""
def __init__(self, dock):
"""Initialize internal hassio network."""
self.docker = dock
self.network = self._get_network()
@property
def name(self):
"""Return name of network."""
return DOCKER_NETWORK
@property
def containers(self):
"""Return of connected containers from network."""
return self.network.containers
@property
def gateway(self):
"""Return gateway of the network."""
return DOCKER_NETWORK_MASK[1]
@property
def supervisor(self):
"""Return supervisor of the network."""
return DOCKER_NETWORK_MASK[2]
def _get_network(self):
"""Get HassIO network."""
try:
return self.docker.networks.get(DOCKER_NETWORK)
except docker.errors.NotFound:
_LOGGER.info("Can't find HassIO network, create new network")
ipam_pool = docker.types.IPAMPool(
subnet=str(DOCKER_NETWORK_MASK),
gateway=str(self.gateway),
iprange=str(DOCKER_NETWORK_RANGE)
)
ipam_config = docker.types.IPAMConfig(pool_configs=[ipam_pool])
return self.docker.networks.create(
DOCKER_NETWORK, driver='bridge', ipam=ipam_config, options={
"com.docker.network.bridge.name": DOCKER_NETWORK,
})
def attach_container(self, container, alias=None, ipv4=None):
"""Attach container to hassio network.
Need run inside executor.
"""
ipv4 = str(ipv4) if ipv4 else None
try:
self.network.connect(container, aliases=alias, ipv4_address=ipv4)
except docker.errors.APIError as err:
_LOGGER.error("Can't link container to hassio-net -> %s", err)
return False
self.network.reload()
return True
def detach_default_bridge(self, container):
"""Detach default docker bridge.
Need run inside executor.
"""
try:
default_network = self.docker.networks.get('bridge')
default_network.disconnect(container)
except docker.errors.NotFound:
return
except docker.errors.APIError as err:
_LOGGER.warning(
"Can't disconnect container from default -> %s", err)

View File

@@ -2,19 +2,20 @@
import logging
import os
from . import DockerBase
import docker
from .interface import DockerInterface
from .util import docker_process
from ..const import RESTART_EXIT_CODE
_LOGGER = logging.getLogger(__name__)
class DockerSupervisor(DockerBase):
class DockerSupervisor(DockerInterface):
"""Docker hassio wrapper for HomeAssistant."""
def __init__(self, config, loop, dock, stop_callback, image=None):
def __init__(self, config, loop, api, stop_callback, image=None):
"""Initialize docker base wrapper."""
super().__init__(config, loop, dock, image=image)
super().__init__(config, loop, api, image=image)
self.stop_callback = stop_callback
@property
@@ -22,13 +23,35 @@ class DockerSupervisor(DockerBase):
"""Return name of docker container."""
return os.environ['SUPERVISOR_NAME']
def _attach(self):
"""Attach to running docker container.
Need run inside executor.
"""
try:
container = self.docker.containers.get(self.name)
except docker.errors.DockerException:
return False
self.process_metadata(container.attrs)
_LOGGER.info("Attach to supervisor %s with version %s",
self.image, self.version)
# if already attach
if container in self.docker.network.containers:
return True
# attach to network
return self.docker.network.attach_container(
container, alias=['hassio'], ipv4=self.docker.network.supervisor)
@docker_process
async def update(self, tag):
"""Update a supervisor docker image."""
_LOGGER.info("Update supervisor docker to %s:%s", self.image, tag)
if await self.loop.run_in_executor(None, self._install, tag):
self.loop.create_task(self.stop_callback(RESTART_EXIT_CODE))
self.loop.call_later(1, self.loop.stop)
return True
return False

View File

@@ -1,48 +1,8 @@
"""HassIO docker utilitys."""
import logging
import re
from ..const import ARCH_AARCH64, ARCH_ARMHF, ARCH_I386, ARCH_AMD64
_LOGGER = logging.getLogger(__name__)
HASSIO_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 = []
hassio_image = HASSIO_BASE_IMAGE[arch]
custom_image = re.compile(r"^#{}:FROM".format(arch))
# read docker
with dockerfile.open('r') as dock_input:
for line in dock_input:
line = TMPL_IMAGE.sub(hassio_image, line)
line = custom_image.sub("FROM", 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)
# pylint: disable=protected-access
def docker_process(method):

View File

@@ -1,4 +1,5 @@
"""Read hardware info from system."""
from datetime import datetime
import logging
from pathlib import Path
import re
@@ -15,6 +16,11 @@ RE_CARDS = re.compile(r"(\d+) \[(\w*) *\]: (.*\w)")
ASOUND_DEVICES = Path("/proc/asound/devices")
RE_DEVICES = re.compile(r"\[.*(\d+)- (\d+).*\]: ([\w ]*)")
PROC_STAT = Path("/proc/stat")
RE_BOOT_TIME = re.compile(r"btime (\d+)")
GPIO_DEVICES = Path("/sys/class/gpio")
class Hardware(object):
"""Represent a interface to procfs, sysfs and udev."""
@@ -31,7 +37,7 @@ class Hardware(object):
if 'ID_VENDOR' in device:
dev_list.add(device.device_node)
return list(dev_list)
return dev_list
@property
def input_devices(self):
@@ -41,7 +47,7 @@ class Hardware(object):
if 'NAME' in device:
dev_list.add(device['NAME'].replace('"', ''))
return list(dev_list)
return dev_list
@property
def disk_devices(self):
@@ -51,7 +57,7 @@ class Hardware(object):
if device.device_node.startswith('/dev/sd'):
dev_list.add(device.device_node)
return list(dev_list)
return dev_list
@property
def audio_devices(self):
@@ -85,3 +91,30 @@ class Hardware(object):
continue
return audio_list
@property
def gpio_devices(self):
"""Return list of GPIO interface on device."""
dev_list = set()
for interface in GPIO_DEVICES.glob("gpio*"):
dev_list.add(interface.name)
return dev_list
@property
def last_boot(self):
"""Return last boot time."""
try:
with PROC_STAT.open("r") as stat_file:
stats = stat_file.read()
except OSError as err:
_LOGGER.error("Can't read stat data -> %s", err)
return
# parse stat file
found = RE_BOOT_TIME.search(stats)
if not found:
_LOGGER.error("Can't found last boot time!")
return
return datetime.utcfromtimestamp(int(found.group(1)))

View File

@@ -4,28 +4,36 @@ import logging
import os
import re
import aiohttp
from aiohttp.hdrs import CONTENT_TYPE
import async_timeout
from .const import (
FILE_HASSIO_HOMEASSISTANT, ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION,
ATTR_VERSION)
ATTR_VERSION, ATTR_BOOT, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG,
HEADER_HA_ACCESS, CONTENT_TYPE_JSON)
from .dock.homeassistant import DockerHomeAssistant
from .tools import JsonConfig, convert_to_ascii
from .validate import SCHEMA_HASS_CONFIG
_LOGGER = logging.getLogger(__name__)
RE_CONFIG_CHECK = re.compile(r"error", re.IGNORECASE)
RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml")
class HomeAssistant(JsonConfig):
"""Hass core object for handle it."""
def __init__(self, config, loop, dock, updater):
def __init__(self, config, loop, docker, updater):
"""Initialize hass object."""
super().__init__(FILE_HASSIO_HOMEASSISTANT, SCHEMA_HASS_CONFIG)
self.config = config
self.loop = loop
self.updater = updater
self.docker = DockerHomeAssistant(config, loop, dock, self)
self.docker = DockerHomeAssistant(config, loop, docker, self)
self.api_ip = docker.network.gateway
self.websession = aiohttp.ClientSession(
connector=aiohttp.TCPConnector(verify_ssl=False), loop=loop)
async def prepare(self):
"""Prepare HomeAssistant object."""
@@ -38,6 +46,57 @@ class HomeAssistant(JsonConfig):
else:
await self.docker.attach()
@property
def api_port(self):
"""Return network port to home-assistant instance."""
return self._data[ATTR_PORT]
@api_port.setter
def api_port(self, value):
"""Set network port for home-assistant instance."""
self._data[ATTR_PORT] = value
self.save()
@property
def api_password(self):
"""Return password for home-assistant instance."""
return self._data.get(ATTR_PASSWORD)
@api_password.setter
def api_password(self, value):
"""Set password for home-assistant instance."""
self._data[ATTR_PASSWORD] = value
self.save()
@property
def api_ssl(self):
"""Return if we need ssl to home-assistant instance."""
return self._data[ATTR_SSL]
@api_ssl.setter
def api_ssl(self, value):
"""Set SSL for home-assistant instance."""
self._data[ATTR_SSL] = value
self.save()
@property
def api_url(self):
"""Return API url to Home-Assistant."""
return "{}://{}:{}".format(
'https' if self.api_ssl else 'http', self.api_ip, self.api_port
)
@property
def watchdog(self):
"""Return True if the watchdog should protect Home-Assistant."""
return self._data[ATTR_WATCHDOG]
@watchdog.setter
def watchdog(self, value):
"""Return True if the watchdog should protect Home-Assistant."""
self._data[ATTR_WATCHDOG] = value
self.save()
@property
def version(self):
"""Return version of running homeassistant."""
@@ -73,6 +132,17 @@ class HomeAssistant(JsonConfig):
self._data[ATTR_DEVICES] = value
self.save()
@property
def boot(self):
"""Return True if home-assistant boot is enabled."""
return self._data[ATTR_BOOT]
@boot.setter
def boot(self, value):
"""Set home-assistant boot options."""
self._data[ATTR_BOOT] = value
self.save()
def set_custom(self, image, version):
"""Set a custom image for homeassistant."""
# reset
@@ -98,6 +168,9 @@ class HomeAssistant(JsonConfig):
_LOGGER.warning("Fails install landingpage, retry after 60sec")
await asyncio.sleep(60, loop=self.loop)
# run landingpage after installation
await self.docker.run()
async def install(self):
"""Install a landingpage."""
_LOGGER.info("Setup HomeAssistant")
@@ -112,13 +185,16 @@ class HomeAssistant(JsonConfig):
_LOGGER.warning("Error on install HomeAssistant. Retry in 60sec")
await asyncio.sleep(60, loop=self.loop)
# store version
# finishing
_LOGGER.info("HomeAssistant docker now installed")
if self.boot:
await self.docker.run()
await self.docker.cleanup()
async def update(self, version=None):
"""Update HomeAssistant version."""
version = version or self.last_version
running = await self.docker.is_running()
if version == self.docker.version:
_LOGGER.warning("Version %s is already installed", version)
@@ -127,7 +203,8 @@ class HomeAssistant(JsonConfig):
try:
return await self.docker.update(version)
finally:
await self.docker.run()
if running:
await self.docker.run()
def run(self):
"""Run HomeAssistant docker.
@@ -164,6 +241,13 @@ class HomeAssistant(JsonConfig):
"""
return self.docker.is_running()
def is_initialize(self):
"""Return True if a docker container is exists.
Return a coroutine.
"""
return self.docker.is_initialize()
@property
def in_progress(self):
"""Return True if a task is in progress."""
@@ -171,17 +255,36 @@ class HomeAssistant(JsonConfig):
async def check_config(self):
"""Run homeassistant config check."""
log = await self.docker.execute_command(
exit_code, log = await self.docker.execute_command(
"python3 -m homeassistant -c /config --script check_config"
)
# if not valid
if not log:
if exit_code is None:
return (False, "")
# parse output
log = convert_to_ascii(log)
if RE_CONFIG_CHECK.search(log):
if exit_code != 0 or RE_YAML_ERROR.search(log):
return (False, log)
return (True, log)
async def check_api_state(self):
"""Check if Home-Assistant up and running."""
url = "{}/api/".format(self.api_url)
header = {CONTENT_TYPE: CONTENT_TYPE_JSON}
if self.api_password:
header.update({HEADER_HA_ACCESS: self.api_password})
try:
async with async_timeout.timeout(30, loop=self.loop):
async with self.websession.get(url, headers=header) as request:
status = request.status
except (asyncio.TimeoutError, aiohttp.ClientError):
return False
if status not in (200, 201):
_LOGGER.warning("Home-Assistant API config missmatch")
return True

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -197,6 +197,8 @@ class SnapshotsManager(object):
await snapshot.restore_folders()
# start homeassistant restore
_LOGGER.info("Full-Restore %s restore Home-Assistant",
snapshot.slug)
snapshot.restore_homeassistant(self.homeassistant)
task_hass = self.loop.create_task(
self.homeassistant.update(snapshot.homeassistant_version))
@@ -279,6 +281,8 @@ class SnapshotsManager(object):
await snapshot.restore_folders(folders)
if homeassistant:
_LOGGER.info("Partial-Restore %s restore Home-Assistant",
snapshot.slug)
snapshot.restore_homeassistant(self.homeassistant)
tasks.append(self.homeassistant.update(
snapshot.homeassistant_version))

View File

@@ -14,7 +14,7 @@ from .util import remove_folder
from ..const import (
ATTR_SLUG, ATTR_NAME, ATTR_DATE, ATTR_ADDONS, ATTR_REPOSITORIES,
ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_VERSION, ATTR_TYPE, ATTR_DEVICES,
ATTR_IMAGE)
ATTR_IMAGE, ATTR_PORT, ATTR_SSL, ATTR_PASSWORD, ATTR_WATCHDOG, ATTR_BOOT)
from ..tools import write_json_file
_LOGGER = logging.getLogger(__name__)
@@ -101,6 +101,56 @@ class Snapshot(object):
"""Set snapshot homeassistant custom image."""
self._data[ATTR_HOMEASSISTANT][ATTR_IMAGE] = value
@property
def homeassistant_ssl(self):
"""Return snapshot homeassistant api ssl."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_SSL)
@homeassistant_ssl.setter
def homeassistant_ssl(self, value):
"""Set snapshot homeassistant api ssl."""
self._data[ATTR_HOMEASSISTANT][ATTR_SSL] = value
@property
def homeassistant_port(self):
"""Return snapshot homeassistant api port."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_PORT)
@homeassistant_port.setter
def homeassistant_port(self, value):
"""Set snapshot homeassistant api port."""
self._data[ATTR_HOMEASSISTANT][ATTR_PORT] = value
@property
def homeassistant_password(self):
"""Return snapshot homeassistant api password."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_PASSWORD)
@homeassistant_password.setter
def homeassistant_password(self, value):
"""Set snapshot homeassistant api password."""
self._data[ATTR_HOMEASSISTANT][ATTR_PASSWORD] = value
@property
def homeassistant_watchdog(self):
"""Return snapshot homeassistant watchdog options."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_WATCHDOG)
@homeassistant_watchdog.setter
def homeassistant_watchdog(self, value):
"""Set snapshot homeassistant watchdog options."""
self._data[ATTR_HOMEASSISTANT][ATTR_WATCHDOG] = value
@property
def homeassistant_boot(self):
"""Return snapshot homeassistant boot options."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_BOOT)
@homeassistant_boot.setter
def homeassistant_boot(self, value):
"""Set snapshot homeassistant boot options."""
self._data[ATTR_HOMEASSISTANT][ATTR_BOOT] = value
@property
def size(self):
"""Return snapshot size."""
@@ -126,20 +176,34 @@ class Snapshot(object):
"""Read all data from homeassistant object."""
self.homeassistant_version = homeassistant.version
self.homeassistant_devices = homeassistant.devices
self.homeassistant_watchdog = homeassistant.watchdog
self.homeassistant_boot = homeassistant.boot
# custom image
if homeassistant.is_custom_image:
self.homeassistant_image = homeassistant.image
# api
self.homeassistant_port = homeassistant.api_port
self.homeassistant_ssl = homeassistant.api_ssl
self.homeassistant_password = homeassistant.api_password
def restore_homeassistant(self, homeassistant):
"""Write all data to homeassistant object."""
homeassistant.devices = self.homeassistant_devices
homeassistant.watchdog = self.homeassistant_watchdog
homeassistant.boot = self.homeassistant_boot
# custom image
if self.homeassistant_image:
homeassistant.set_custom(
self.homeassistant_image, self.homeassistant_version)
# api
homeassistant.api_port = self.homeassistant_port
homeassistant.api_ssl = self.homeassistant_ssl
homeassistant.api_password = self.homeassistant_password
async def load(self):
"""Read snapshot.json from tar file."""
if not self.tar_file.is_file():
@@ -197,7 +261,8 @@ class Snapshot(object):
"""Async context to close a snapshot."""
# exists snapshot or exception on build
if self.tar_file.is_file() or exception_type is not None:
return self._tmp.cleanup()
self._tmp.cleanup()
return
# validate data
try:
@@ -219,7 +284,6 @@ class Snapshot(object):
_LOGGER.error("Can't write snapshot.json")
self._tmp.cleanup()
self._tmp = None
async def import_addon(self, addon):
"""Add a addon into snapshot."""
@@ -259,9 +323,11 @@ class Snapshot(object):
origin_dir = Path(self.config.path_hassio, name)
try:
_LOGGER.info("Snapshot folder %s", name)
with tarfile.open(snapshot_tar, "w:gz",
compresslevel=1) as tar_file:
tar_file.add(origin_dir, arcname=".")
_LOGGER.info("Snapshot folder %s done", name)
self._data[ATTR_FOLDERS].append(name)
except tarfile.TarError as err:
@@ -288,8 +354,10 @@ class Snapshot(object):
remove_folder(origin_dir)
try:
_LOGGER.info("Restore folder %s", name)
with tarfile.open(snapshot_tar, "r:gz") as tar_file:
tar_file.extractall(path=origin_dir)
_LOGGER.info("Restore folder %s done", name)
except tarfile.TarError as err:
_LOGGER.warning("Can't restore folder %s -> %s", name, err)

View File

@@ -5,9 +5,10 @@ import voluptuous as vol
from ..const import (
ATTR_REPOSITORIES, ATTR_ADDONS, ATTR_NAME, ATTR_SLUG, ATTR_DATE,
ATTR_VERSION, ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_TYPE, ATTR_DEVICES,
ATTR_IMAGE, FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL,
ATTR_IMAGE, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG, ATTR_BOOT,
FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL,
SNAPSHOT_FULL, SNAPSHOT_PARTIAL)
from ..validate import HASS_DEVICES
from ..validate import HASS_DEVICES, NETWORK_PORT
ALL_FOLDERS = [FOLDER_HOMEASSISTANT, FOLDER_SHARE, FOLDER_ADDONS, FOLDER_SSL]
@@ -21,6 +22,11 @@ SCHEMA_SNAPSHOT = vol.Schema({
vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Optional(ATTR_DEVICES, default=[]): HASS_DEVICES,
vol.Optional(ATTR_IMAGE): vol.Coerce(str),
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
}),
vol.Optional(ATTR_FOLDERS, default=[]): [vol.In(ALL_FOLDERS)],
vol.Optional(ATTR_ADDONS, default=[]): [vol.Schema({

View File

@@ -62,13 +62,54 @@ def hassio_update(supervisor, updater):
return _hassio_update
def homeassistant_watchdog(loop, homeassistant):
"""Create scheduler task for montoring running state."""
async def _homeassistant_watchdog():
"""Check running state and start if they is close."""
def homeassistant_watchdog_docker(loop, homeassistant):
"""Create scheduler task for montoring running state of docker."""
async def _homeassistant_watchdog_docker():
"""Check running state of docker and start if they is close."""
# if Home-Assistant is active
if not await homeassistant.is_initialize() or \
not homeassistant.watchdog:
return
# if Home-Assistant is running
if homeassistant.in_progress or await homeassistant.is_running():
return
loop.create_task(homeassistant.run())
_LOGGER.error("Watchdog found a problem with Home-Assistant docker!")
return _homeassistant_watchdog
return _homeassistant_watchdog_docker
def homeassistant_watchdog_api(loop, homeassistant):
"""Create scheduler task for montoring running state of API.
Try 2 times to call API before we restart Home-Assistant. Maybe we had a
delay in our system.
"""
retry_scan = 0
async def _homeassistant_watchdog_api():
"""Check running state of API and start if they is close."""
nonlocal retry_scan
# if Home-Assistant is active
if not await homeassistant.is_initialize() or \
not homeassistant.watchdog:
return
# if Home-Assistant API is up
if homeassistant.in_progress or await homeassistant.check_api_state():
return
retry_scan += 1
# Retry active
if retry_scan == 1:
_LOGGER.warning("Watchdog miss API response from Home-Assistant")
return
loop.create_task(homeassistant.restart())
_LOGGER.error("Watchdog found a problem with Home-Assistant API!")
retry_scan = 0
return _homeassistant_watchdog_api

View File

@@ -1,14 +1,14 @@
"""Tools file for HassIO."""
import asyncio
from contextlib import suppress
from datetime import datetime
from datetime import datetime, timedelta, timezone
import json
import logging
import socket
import re
import aiohttp
import async_timeout
import pytz
import voluptuous as vol
from voluptuous.humanize import humanize_error
@@ -18,27 +18,15 @@ FREEGEOIP_URL = "https://freegeoip.io/json/"
RE_STRING = re.compile(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))")
def get_local_ip(loop):
"""Retrieve local IP address.
Return a future.
"""
def local_ip():
"""Return local ip."""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Use Google Public DNS server to determine own IP
sock.connect(('8.8.8.8', 80))
return sock.getsockname()[0]
except socket.error:
return socket.gethostbyname(socket.gethostname())
finally:
sock.close()
return loop.run_in_executor(None, local_ip)
# Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved.
# https://github.com/django/django/blob/master/LICENSE
DATETIME_RE = re.compile(
r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
r'(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$'
)
def write_json_file(jsonfile, data):
@@ -76,6 +64,42 @@ def convert_to_ascii(raw):
return RE_STRING.sub("", raw.decode())
# Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved.
# https://github.com/django/django/blob/master/LICENSE
def parse_datetime(dt_str):
"""Parse a string and return a datetime.datetime.
This function supports time zone offsets. When the input contains one,
the output uses a timezone with a fixed offset from UTC.
Raises ValueError if the input is well formatted but not a valid datetime.
Returns None if the input isn't well formatted.
"""
match = DATETIME_RE.match(dt_str)
if not match:
return None
kws = match.groupdict() # type: Dict[str, Any]
if kws['microsecond']:
kws['microsecond'] = kws['microsecond'].ljust(6, '0')
tzinfo_str = kws.pop('tzinfo')
tzinfo = None # type: Optional[dt.tzinfo]
if tzinfo_str == 'Z':
tzinfo = pytz.utc
elif tzinfo_str is not None:
offset_mins = int(tzinfo_str[-2:]) if len(tzinfo_str) > 3 else 0
offset_hours = int(tzinfo_str[1:3])
offset = timedelta(hours=offset_hours, minutes=offset_mins)
if tzinfo_str[0] == '-':
offset = -offset
tzinfo = timezone(offset)
else:
tzinfo = None
kws = {k: int(v) for k, v in kws.items() if v is not None}
kws['tzinfo'] = tzinfo
return datetime(**kws)
class JsonConfig(object):
"""Hass core object for handle it."""
@@ -99,6 +123,8 @@ class JsonConfig(object):
except vol.Invalid as ex:
_LOGGER.error("Can't parse %s -> %s",
self._file, humanize_error(self._data, ex))
# reset data to default
self._data = self._schema({})
def save(self):
"""Store data to config file."""

View File

@@ -6,8 +6,9 @@ import pytz
from .const import (
ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_SESSIONS, ATTR_PASSWORD,
ATTR_TOTP, ATTR_SECURITY, ATTR_BETA_CHANNEL, ATTR_TIMEZONE,
ATTR_API_ENDPOINT, ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO_OUTPUT,
ATTR_AUDIO_INPUT, ATTR_HOMEASSISTANT, ATTR_HASSIO)
ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
ATTR_HOMEASSISTANT, ATTR_HASSIO, ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL,
ATTR_PORT, ATTR_WATCHDOG)
NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
@@ -55,11 +56,17 @@ DOCKER_PORTS = vol.Schema({
})
# pylint: disable=no-value-for-parameter
SCHEMA_HASS_CONFIG = vol.Schema({
vol.Optional(ATTR_DEVICES, default=[]): HASS_DEVICES,
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Coerce(str),
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
})
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
}, extra=vol.REMOVE_EXTRA)
# pylint: disable=no-value-for-parameter
@@ -67,13 +74,13 @@ SCHEMA_UPDATER_CONFIG = vol.Schema({
vol.Optional(ATTR_BETA_CHANNEL, default=False): vol.Boolean(),
vol.Optional(ATTR_HOMEASSISTANT): vol.Coerce(str),
vol.Optional(ATTR_HASSIO): vol.Coerce(str),
})
}, extra=vol.REMOVE_EXTRA)
# pylint: disable=no-value-for-parameter
SCHEMA_HASSIO_CONFIG = vol.Schema({
vol.Optional(ATTR_API_ENDPOINT): vol.Coerce(str),
vol.Optional(ATTR_TIMEZONE, default='UTC'): validate_timezone,
vol.Optional(ATTR_LAST_BOOT): vol.Coerce(str),
vol.Optional(ATTR_ADDONS_CUSTOM_LIST, default=[]): [vol.Url()],
vol.Optional(ATTR_SECURITY, default=False): vol.Boolean(),
vol.Optional(ATTR_TOTP): vol.Coerce(str),

View File

@@ -12,7 +12,7 @@ setup(
url='https://home-assistant.io/',
description=('Open-source private cloud os for Home-Assistant'
' based on ResinOS'),
long_description=('A maintenainless private cloud operator system that'
long_description=('A maintainless private cloud operator system that'
'setup a Home-Assistant instance. Based on ResinOS'),
classifiers=[
'Intended Audience :: End Users/Desktop',

View File

@@ -1,7 +1,7 @@
{
"hassio": "0.55",
"homeassistant": "0.51.2",
"resinos": "1.0",
"hassio": "0.75",
"homeassistant": "0.57.3",
"resinos": "1.1",
"resinhup": "0.3",
"generic": "0.3",
"cluster": "0.1"