Merge pull request #94 from pvizeli/extend_hass

Add landingpage / cleanup homeassistant core handling
This commit is contained in:
Pascal Vizeli 2017-07-12 00:12:12 +02:00 committed by GitHub
commit bcfd76d33c
15 changed files with 301 additions and 144 deletions

8
API.md
View File

@ -269,7 +269,9 @@ Optional:
{
"version": "INSTALL_VERSION",
"last_version": "LAST_VERSION",
"devices": []
"devices": [""],
"image": "str",
"custom": "bool -> if custom image"
}
```
@ -291,9 +293,13 @@ Output the raw docker log
```json
{
"devices": [],
"image": "Optional|null",
"last_version": "Optional for custom image|null"
}
```
Image with `null` and last_version with `null` reset this options.
### REST API addons
- POST `/addons/reload`

View File

@ -15,53 +15,22 @@ from .validate import (
from ..const import (
FILE_HASSIO_ADDONS, ATTR_VERSION, ATTR_SLUG, ATTR_REPOSITORY, ATTR_LOCATON,
REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_USER, ATTR_SYSTEM)
from ..tools import read_json_file, write_json_file
from ..tools import JsonConfig, read_json_file
_LOGGER = logging.getLogger(__name__)
RE_VOLUME = re.compile(MAP_VOLUME)
class Data(object):
class Data(JsonConfig):
"""Hold data for addons inside HassIO."""
def __init__(self, config):
"""Initialize data holder."""
self._file = FILE_HASSIO_ADDONS
self._data = {}
super().__init__(FILE_HASSIO_ADDONS, SCHEMA_ADDON_FILE)
self.config = config
self._cache = {}
self._repositories = {}
# init or load data
if self._file.is_file():
try:
self._data = read_json_file(self._file)
except (OSError, json.JSONDecodeError):
_LOGGER.warning("Can't read %s", self._file)
self._data = {}
# validate
try:
self._data = SCHEMA_ADDON_FILE(self._data)
except vol.Invalid as ex:
_LOGGER.error("Can't parse addons.json -> %s",
humanize_error(self._data, ex))
def save(self):
"""Store data to config file."""
# validate
try:
self._data = SCHEMA_ADDON_FILE(self._data)
except vol.Invalid as ex:
_LOGGER.error("Can't parse addons data -> %s",
humanize_error(self._data, ex))
return False
if not write_json_file(self._file, self._data):
_LOGGER.error("Can't store config in %s", self._file)
return False
return True
self._cache = {}
@property
def user(self):

View File

@ -5,7 +5,8 @@ import logging
import voluptuous as vol
from .util import api_process, api_process_raw, api_validate
from ..const import ATTR_VERSION, ATTR_LAST_VERSION, ATTR_DEVICES
from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_DEVICES, ATTR_IMAGE, ATTR_CUSTOM)
from ..validate import HASS_DEVICES
_LOGGER = logging.getLogger(__name__)
@ -13,6 +14,9 @@ _LOGGER = logging.getLogger(__name__)
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_DEVICES): HASS_DEVICES,
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)),
})
SCHEMA_VERSION = vol.Schema({
@ -34,8 +38,10 @@ class APIHomeAssistant(object):
"""Return host information."""
return {
ATTR_VERSION: self.homeassistant.version,
ATTR_LAST_VERSION: self.config.last_homeassistant,
ATTR_DEVICES: self.config.homeassistant_devices,
ATTR_LAST_VERSION: self.homeassistant.last_version,
ATTR_IMAGE: self.homeassistant.image,
ATTR_DEVICES: self.homeassistant.devices,
ATTR_CUSTOM: self.homeassistant.is_custom_image,
}
@api_process
@ -44,7 +50,11 @@ class APIHomeAssistant(object):
body = await api_validate(SCHEMA_OPTIONS, request)
if ATTR_DEVICES in body:
self.config.homeassistant_devices = body[ATTR_DEVICES]
self.homeassistant.devices = body[ATTR_DEVICES]
if ATTR_IMAGE in body:
self.homeassistant.set_custom(
body[ATTR_IMAGE], body[ATTR_LAST_VERSION])
return True
@ -57,9 +67,6 @@ class APIHomeAssistant(object):
if self.homeassistant.in_progress:
raise RuntimeError("Other task is in progress")
if version == self.homeassistant.version:
raise RuntimeError("Version is already in use")
return await asyncio.shield(
self.homeassistant.update(version), loop=self.loop)

View File

@ -1,17 +1,13 @@
"""Bootstrap HassIO."""
from datetime import datetime
import logging
import json
import os
from pathlib import Path, PurePath
import voluptuous as vol
from voluptuous.humanize import humanize_error
from .const import FILE_HASSIO_CONFIG, HASSIO_DATA
from .tools import (
fetch_last_versions, write_json_file, read_json_file, validate_timezone)
from .validate import HASS_DEVICES
from .tools import fetch_last_versions, JsonConfig, validate_timezone
_LOGGER = logging.getLogger(__name__)
@ -19,7 +15,6 @@ DATETIME_FORMAT = "%Y%m%d %H:%M:%S"
HOMEASSISTANT_CONFIG = PurePath("homeassistant")
HOMEASSISTANT_LAST = 'homeassistant_last'
HOMEASSISTANT_DEVICES = 'homeassistant_devices'
HASSIO_SSL = PurePath("ssl")
HASSIO_LAST = 'hassio_last'
@ -50,7 +45,6 @@ SCHEMA_CONFIG = vol.Schema({
vol.Optional(API_ENDPOINT): vol.Coerce(str),
vol.Optional(TIMEZONE, default='UTC'): validate_timezone,
vol.Optional(HOMEASSISTANT_LAST): vol.Coerce(str),
vol.Optional(HOMEASSISTANT_DEVICES, default=[]): HASS_DEVICES,
vol.Optional(HASSIO_LAST): vol.Coerce(str),
vol.Optional(ADDONS_CUSTOM_LIST, default=[]): [vol.Url()],
vol.Optional(SECURITY_INITIALIZE, default=False): vol.Boolean(),
@ -61,48 +55,13 @@ SCHEMA_CONFIG = vol.Schema({
}, extra=vol.REMOVE_EXTRA)
class CoreConfig(object):
class CoreConfig(JsonConfig):
"""Hold all core config data."""
def __init__(self):
"""Initialize config object."""
super().__init__(FILE_HASSIO_CONFIG, SCHEMA_CONFIG)
self.arch = None
self._file = FILE_HASSIO_CONFIG
self._data = {}
# init or load data
if self._file.is_file():
try:
self._data = read_json_file(self._file)
except (OSError, json.JSONDecodeError):
_LOGGER.warning("Can't read %s", self._file)
self._data = {}
# validate data
if not self._validate_config():
self._data = SCHEMA_CONFIG({})
def _validate_config(self):
"""Validate config and return True or False."""
# validate data
try:
self._data = SCHEMA_CONFIG(self._data)
except vol.Invalid as ex:
_LOGGER.warning(
"Invalid config %s", humanize_error(self._data, ex))
return False
return True
def save(self):
"""Store data to config file."""
if not self._validate_config():
return False
if not write_json_file(self._file, self._data):
_LOGGER.error("Can't store config in %s", self._file)
return False
return True
async def fetch_update_infos(self, websession):
"""Read current versions from web."""
@ -150,22 +109,6 @@ class CoreConfig(object):
self._data[TIMEZONE] = value
self.save()
@property
def homeassistant_devices(self):
"""Return list of special device to map into homeassistant."""
return self._data[HOMEASSISTANT_DEVICES]
@homeassistant_devices.setter
def homeassistant_devices(self, value):
"""Set list of special device."""
self._data[HOMEASSISTANT_DEVICES] = value
self.save()
@property
def homeassistant_image(self):
"""Return docker homeassistant repository."""
return os.environ['HOMEASSISTANT_REPOSITORY']
@property
def last_homeassistant(self):
"""Actual version of homeassistant."""

View File

@ -24,6 +24,7 @@ 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")
SOCKET_DOCKER = Path("/var/run/docker.sock")
SOCKET_HC = Path("/var/run/hassio-hc.sock")
@ -94,6 +95,7 @@ ATTR_SIZE = 'size'
ATTR_TYPE = 'type'
ATTR_TIMEOUT = 'timeout'
ATTR_AUTO_UPDATE = 'auto_update'
ATTR_CUSTOM = 'custom'
STARTUP_INITIALIZE = 'initialize'
STARTUP_BEFORE = 'before'

View File

@ -13,13 +13,12 @@ from .const import (
RUN_UPDATE_SUPERVISOR_TASKS, RUN_WATCHDOG_HOMEASSISTANT,
RUN_CLEANUP_API_SESSIONS, STARTUP_AFTER, STARTUP_BEFORE,
STARTUP_INITIALIZE, RUN_RELOAD_SNAPSHOTS_TASKS, RUN_UPDATE_ADDONS_TASKS)
from .homeassistant import HomeAssistant
from .scheduler import Scheduler
from .dock.homeassistant import DockerHomeAssistant
from .dock.supervisor import DockerSupervisor
from .snapshots import SnapshotsManager
from .tasks import (
hassio_update, homeassistant_watchdog, homeassistant_setup,
api_sessions_cleanup, addons_update)
hassio_update, homeassistant_watchdog, api_sessions_cleanup, addons_update)
from .tools import get_local_ip, fetch_timezone
_LOGGER = logging.getLogger(__name__)
@ -41,7 +40,10 @@ class HassIO(object):
# init basic docker container
self.supervisor = DockerSupervisor(config, loop, self.dock, self.stop)
self.homeassistant = DockerHomeAssistant(config, loop, self.dock)
# init homeassistant
self.homeassistant = HomeAssistant(
config, loop, self.dock, self.websession)
# init HostControl
self.host_control = HostControl(loop)
@ -94,13 +96,8 @@ class HassIO(object):
api_sessions_cleanup(self.config), RUN_CLEANUP_API_SESSIONS,
now=True)
# first start of supervisor?
if not await self.homeassistant.exists():
_LOGGER.info("No HomeAssistant docker found.")
await homeassistant_setup(
self.config, self.loop, self.homeassistant, self.websession)
else:
await self.homeassistant.attach()
# Load homeassistant
await self.homeassistant.prepare()
# Load addons
await self.addons.prepare()
@ -157,6 +154,10 @@ class HassIO(object):
homeassistant_watchdog(self.loop, self.homeassistant),
RUN_WATCHDOG_HOMEASSISTANT)
# 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):
"""Stop a running orchestration."""
# don't process scheduler anymore

View File

@ -260,9 +260,13 @@ class DockerBase(object):
if not self._install(tag):
return False
# cleanup old stuff
# run or cleanup container
if was_running:
self._run()
else:
self._stop()
# cleanup images
self._cleanup()
return True

View File

@ -13,9 +13,10 @@ HASS_DOCKER_NAME = 'homeassistant'
class DockerHomeAssistant(DockerBase):
"""Docker hassio wrapper for HomeAssistant."""
def __init__(self, config, loop, dock):
def __init__(self, config, loop, dock, data):
"""Initialize docker homeassistant wrapper."""
super().__init__(config, loop, dock, image=config.homeassistant_image)
super().__init__(config, loop, dock, image=data.image)
self.data = data
@property
def name(self):
@ -25,11 +26,11 @@ class DockerHomeAssistant(DockerBase):
@property
def devices(self):
"""Create list of special device to map into docker."""
if not self.config.homeassistant_devices:
if not self.data.devices:
return
devices = []
for device in self.config.homeassistant_devices:
for device in self.data.devices:
devices.append("/dev/{0}:/dev/{0}:rwm".format(device))
return devices

162
hassio/homeassistant.py Normal file
View File

@ -0,0 +1,162 @@
"""HomeAssistant control object."""
import asyncio
import logging
import os
from .const import (
FILE_HASSIO_HOMEASSISTANT, ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION,
ATTR_VERSION)
from .dock.homeassistant import DockerHomeAssistant
from .tools import JsonConfig
from .validate import SCHEMA_HASS_CONFIG
_LOGGER = logging.getLogger(__name__)
class HomeAssistant(JsonConfig):
"""Hass core object for handle it."""
def __init__(self, config, loop, dock, websession):
"""Initialize hass object."""
super().__init__(FILE_HASSIO_HOMEASSISTANT, SCHEMA_HASS_CONFIG)
self.config = config
self.loop = loop
self.websession = websession
self.docker = DockerHomeAssistant(config, loop, dock, self)
async def prepare(self):
"""Prepare HomeAssistant object."""
if not await self.docker.exists():
_LOGGER.info("No HomeAssistant docker %s found.", self.image)
if self.is_custom_image:
await self.install()
else:
await self.install_landingpage()
else:
await self.docker.attach()
@property
def version(self):
"""Return version of running homeassistant."""
return self.docker.version
@property
def last_version(self):
"""Return last available version of homeassistant."""
if self.is_custom_image:
return self._data.get(ATTR_LAST_VERSION)
return self.config.last_homeassistant
@property
def image(self):
"""Return image name of hass containter."""
if ATTR_IMAGE in self._data:
return self._data[ATTR_IMAGE]
return os.environ['HOMEASSISTANT_REPOSITORY']
@property
def is_custom_image(self):
"""Return True if a custom image is used."""
return ATTR_IMAGE in self._data
@property
def devices(self):
"""Return extend device mapping."""
return self._data[ATTR_DEVICES]
@devices.setter
def devices(self, value):
"""Set extend device mapping."""
self._data[ATTR_DEVICES] = value
self.save()
def set_custom(self, image, version):
"""Set a custom image for homeassistant."""
# reset
if image is None and version is None:
self._data.pop(ATTR_IMAGE, None)
self._data.pop(ATTR_VERSION, None)
self.docker.image = self.image
else:
if image:
self._data[ATTR_IMAGE] = image
self.docker.image = image
if version:
self._data[ATTR_VERSION] = version
self.save()
async def install_landingpage(self):
"""Install a landingpage."""
_LOGGER.info("Setup HomeAssistant landingpage")
while True:
if await self.docker.install('landingpage'):
break
_LOGGER.warning("Fails install landingpage, retry after 60sec")
await asyncio.sleep(60, loop=self.loop)
async def install(self):
"""Install a landingpage."""
_LOGGER.info("Setup HomeAssistant")
while True:
# read homeassistant tag and install it
if not self.last_version:
await self.config.fetch_update_infos(self.websession)
tag = self.last_version
if tag and await self.docker.install(tag):
break
_LOGGER.warning("Error on install HomeAssistant. Retry in 60sec")
await asyncio.sleep(60, loop=self.loop)
# store version
_LOGGER.info("HomeAssistant docker now installed")
await self.docker.cleanup()
def update(self, version=None):
"""Update HomeAssistant version.
Return a coroutine.
"""
version = version or self.last_version
return self.docker.update(version)
def run(self):
"""Run HomeAssistant docker.
Return a coroutine.
"""
return self.docker.run()
def stop(self):
"""Stop HomeAssistant docker.
Return a coroutine.
"""
return self.docker.stop()
def restart(self):
"""Restart HomeAssistant docker.
Return a coroutine.
"""
return self.docker.restart()
def logs(self):
"""Get HomeAssistant docker logs.
Return a coroutine.
"""
return self.docker.logs()
def is_running(self):
"""Return True if docker container is running.
Return a coroutine.
"""
return self.docker.is_running()
@property
def in_progress(self):
"""Return True if a task is in progress."""
return self.docker.in_progress

View File

@ -46,8 +46,7 @@ class SnapshotsManager(object):
snapshot.create(slug, name, date_str, sys_type)
# set general data
snapshot.homeassistant_version = self.homeassistant.version
snapshot.homeassistant_devices = self.config.homeassistant_devices
snapshot.snapshot_homeassistant(self.homeassistant)
snapshot.repositories = self.config.addons_repositories
return snapshot
@ -198,8 +197,7 @@ class SnapshotsManager(object):
await snapshot.restore_folders()
# start homeassistant restore
self.config.homeassistant_devices = \
snapshot.homeassistant_devices
snapshot.restore_homeassistant(self.homeassistant)
task_hass = self.loop.create_task(
self.homeassistant.update(snapshot.homeassistant_version))
@ -281,8 +279,7 @@ class SnapshotsManager(object):
await snapshot.restore_folders(folders)
if homeassistant:
self.config.homeassistant_devices = \
snapshot.homeassistant_devices
snapshot.restore_homeassistant(self.homeassistant)
tasks.append(self.homeassistant.update(
snapshot.homeassistant_version))

View File

@ -13,7 +13,8 @@ from .validate import SCHEMA_SNAPSHOT, ALL_FOLDERS
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_HOMEASSISTANT, ATTR_FOLDERS, ATTR_VERSION, ATTR_TYPE, ATTR_DEVICES,
ATTR_IMAGE)
from ..tools import write_json_file
_LOGGER = logging.getLogger(__name__)
@ -90,6 +91,16 @@ class Snapshot(object):
"""Set snapshot homeassistant devices."""
self._data[ATTR_HOMEASSISTANT][ATTR_DEVICES] = value
@property
def homeassistant_image(self):
"""Return snapshot homeassistant custom image."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_IMAGE)
@homeassistant_image.setter
def homeassistant_image(self, value):
"""Set snapshot homeassistant custom image."""
self._data[ATTR_HOMEASSISTANT][ATTR_IMAGE] = value
@property
def size(self):
"""Return snapshot size."""
@ -111,6 +122,24 @@ class Snapshot(object):
self._data[ATTR_REPOSITORIES] = []
self._data[ATTR_FOLDERS] = []
def snapshot_homeassistant(self, homeassistant):
"""Read all data from homeassistant object."""
self.homeassistant_version = homeassistant.version
self.homeassistant_devices = homeassistant.devices
# custom image
if homeassistant.is_custom_image:
self.homeassistant_image = homeassistant.image
def restore_homeassistant(self, homeassistant):
"""Write all data to homeassistant object."""
homeassistant.devices = self.homeassistant_devices
# custom image
if self.homeassistant_image:
homeassistant.set_custom(
self.homeassistant_image, self.homeassistant_version)
async def load(self):
"""Read snapshot.json from tar file."""
if not self.tar_file.is_file():

View File

@ -5,7 +5,7 @@ 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,
FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL,
ATTR_IMAGE, FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL,
SNAPSHOT_FULL, SNAPSHOT_PARTIAL)
from ..validate import HASS_DEVICES
@ -20,6 +20,7 @@ SCHEMA_SNAPSHOT = vol.Schema({
vol.Required(ATTR_HOMEASSISTANT): 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_FOLDERS, default=[]): [vol.In(ALL_FOLDERS)],
vol.Optional(ATTR_ADDONS, default=[]): [vol.Schema({

View File

@ -66,20 +66,3 @@ def homeassistant_watchdog(loop, homeassistant):
loop.create_task(homeassistant.run())
return _homeassistant_watchdog
async def homeassistant_setup(config, loop, homeassistant, websession):
"""Install a homeassistant docker container."""
while True:
# read homeassistant tag and install it
if not config.last_homeassistant:
await config.fetch_update_infos(websession)
tag = config.last_homeassistant
if tag and await homeassistant.install(tag):
break
_LOGGER.warning("Error on setup HomeAssistant. Retry in 60.")
await asyncio.sleep(60, loop=loop)
# store version
_LOGGER.info("HomeAssistant docker now installed.")

View File

@ -9,6 +9,7 @@ import aiohttp
import async_timeout
import pytz
import voluptuous as vol
from voluptuous.humanize import humanize_error
from .const import URL_HASSIO_VERSION, URL_HASSIO_VERSION_BETA
@ -98,3 +99,44 @@ async def fetch_timezone(websession):
data = await request.json()
return data.get('time_zone', 'UTC')
class JsonConfig(object):
"""Hass core object for handle it."""
def __init__(self, json_file, schema):
"""Initialize hass object."""
self._file = json_file
self._schema = schema
self._data = {}
# init or load data
if self._file.is_file():
try:
self._data = read_json_file(self._file)
except (OSError, json.JSONDecodeError):
_LOGGER.warning("Can't read %s", self._file)
self._data = {}
# validate
try:
self._data = self._schema(self._data)
except vol.Invalid as ex:
_LOGGER.error("Can't parse %s -> %s",
self._file, humanize_error(self._data, ex))
def save(self):
"""Store data to config file."""
# validate
try:
self._data = self._schema(self._data)
except vol.Invalid as ex:
_LOGGER.error("Can't parse data -> %s",
humanize_error(self._data, ex))
return False
# write
if not write_json_file(self._file, self._data):
_LOGGER.error("Can't store config in %s", self._file)
return False
return True

View File

@ -1,6 +1,9 @@
"""Validate functions."""
import voluptuous as vol
from .const import ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION
NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
HASS_DEVICES = [vol.Match(r"^[^/]*$")]
@ -30,3 +33,10 @@ DOCKER_PORTS = vol.Schema({
vol.All(vol.Coerce(str), vol.Match(r"^\d+(?:/tcp|/udp)?$")):
convert_to_docker_ports,
})
SCHEMA_HASS_CONFIG = vol.Schema({
vol.Optional(ATTR_DEVICES, default=[]): HASS_DEVICES,
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Coerce(str),
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
})