Merge pull request #728 from home-assistant/dev

Release 132
This commit is contained in:
Pascal Vizeli 2018-09-30 20:16:08 +02:00 committed by GitHub
commit 74e03a9a2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
95 changed files with 1624 additions and 1225 deletions

3
.gitignore vendored
View File

@ -90,3 +90,6 @@ ENV/
# pylint # pylint
.pylint.d/ .pylint.d/
# VS Code
.vscode/

44
API.md
View File

@ -428,11 +428,11 @@ Get all available addons.
"name": "xy bla", "name": "xy bla",
"slug": "xy", "slug": "xy",
"description": "description", "description": "description",
"arch": ["armhf", "aarch64", "i386", "amd64"],
"repository": "core|local|REP_ID", "repository": "core|local|REP_ID",
"version": "LAST_VERSION", "version": "LAST_VERSION",
"installed": "none|INSTALL_VERSION", "installed": "none|INSTALL_VERSION",
"detached": "bool", "detached": "bool",
"available": "bool",
"build": "bool", "build": "bool",
"url": "null|url", "url": "null|url",
"icon": "bool", "icon": "bool",
@ -463,6 +463,9 @@ Get all available addons.
"auto_update": "bool", "auto_update": "bool",
"url": "null|url of addon", "url": "null|url of addon",
"detached": "bool", "detached": "bool",
"available": "bool",
"arch": ["armhf", "aarch64", "i386", "amd64"],
"machine": "[raspberrypi2, tinker]",
"repository": "12345678|null", "repository": "12345678|null",
"version": "null|VERSION_INSTALLED", "version": "null|VERSION_INSTALLED",
"last_version": "LAST_VERSION", "last_version": "LAST_VERSION",
@ -496,8 +499,8 @@ Get all available addons.
"audio": "bool", "audio": "bool",
"audio_input": "null|0,0", "audio_input": "null|0,0",
"audio_output": "null|0,0", "audio_output": "null|0,0",
"services": "null|['mqtt']", "services_role": "['service:access']",
"discovery": "null|['component/platform']" "discovery": "['service']"
} }
``` ```
@ -573,12 +576,13 @@ Write data to add-on stdin
### Service discovery ### Service discovery
- GET `/services/discovery` - GET `/discovery`
```json ```json
{ {
"discovery": [ "discovery": [
{ {
"provider": "name", "addon": "slug",
"service": "name",
"uuid": "uuid", "uuid": "uuid",
"component": "component", "component": "component",
"platform": "null|platform", "platform": "null|platform",
@ -588,10 +592,11 @@ Write data to add-on stdin
} }
``` ```
- GET `/services/discovery/{UUID}` - GET `/discovery/{UUID}`
```json ```json
{ {
"provider": "name", "addon": "slug",
"service": "name",
"uuid": "uuid", "uuid": "uuid",
"component": "component", "component": "component",
"platform": "null|platform", "platform": "null|platform",
@ -599,9 +604,10 @@ Write data to add-on stdin
} }
``` ```
- POST `/services/discovery` - POST `/discovery`
```json ```json
{ {
"service": "name",
"component": "component", "component": "component",
"platform": "null|platform", "platform": "null|platform",
"config": {} "config": {}
@ -615,7 +621,7 @@ return:
} }
``` ```
- DEL `/services/discovery/{UUID}` - DEL `/discovery/{UUID}`
- GET `/services` - GET `/services`
```json ```json
@ -624,7 +630,7 @@ return:
{ {
"slug": "name", "slug": "name",
"available": "bool", "available": "bool",
"provider": "null|name|list" "providers": "list"
} }
] ]
} }
@ -632,12 +638,10 @@ return:
#### MQTT #### MQTT
This service performs an auto discovery to Home-Assistant.
- GET `/services/mqtt` - GET `/services/mqtt`
```json ```json
{ {
"provider": "name", "addon": "name",
"host": "xy", "host": "xy",
"port": "8883", "port": "8883",
"ssl": "bool", "ssl": "bool",
@ -660,3 +664,17 @@ This service performs an auto discovery to Home-Assistant.
``` ```
- DEL `/services/mqtt` - DEL `/services/mqtt`
### Misc
- GET `/version`
```json
{
"supervisor": "version",
"homeassistant": "version",
"hassos": "null|version",
"machine": "type",
"arch": "arch",
"channel": "stable|beta|dev"
}
```

View File

@ -1,4 +1,4 @@
"""Init file for HassIO addons.""" """Init file for Hass.io add-ons."""
import asyncio import asyncio
import logging import logging
@ -14,10 +14,10 @@ BUILTIN_REPOSITORIES = set((REPOSITORY_CORE, REPOSITORY_LOCAL))
class AddonManager(CoreSysAttributes): class AddonManager(CoreSysAttributes):
"""Manage addons inside HassIO.""" """Manage add-ons inside Hass.io."""
def __init__(self, coresys): def __init__(self, coresys):
"""Initialize docker base wrapper.""" """Initialize Docker base wrapper."""
self.coresys = coresys self.coresys = coresys
self.data = AddonsData(coresys) self.data = AddonsData(coresys)
self.addons_obj = {} self.addons_obj = {}
@ -25,51 +25,44 @@ class AddonManager(CoreSysAttributes):
@property @property
def list_addons(self): def list_addons(self):
"""Return a list of all addons.""" """Return a list of all add-ons."""
return list(self.addons_obj.values()) return list(self.addons_obj.values())
@property @property
def list_installed(self): def list_installed(self):
"""Return a list of installed addons.""" """Return a list of installed add-ons."""
return [addon for addon in self.addons_obj.values() return [addon for addon in self.addons_obj.values()
if addon.is_installed] if addon.is_installed]
@property @property
def list_repositories(self): def list_repositories(self):
"""Return list of addon repositories.""" """Return list of add-on repositories."""
return list(self.repositories_obj.values()) return list(self.repositories_obj.values())
def get(self, addon_slug): def get(self, addon_slug):
"""Return an add-on from slug.""" """Return an add-on from slug."""
return self.addons_obj.get(addon_slug) return self.addons_obj.get(addon_slug)
def from_uuid(self, uuid):
"""Return an add-on from uuid."""
for addon in self.list_addons:
if addon.is_installed and uuid == addon.uuid:
return addon
return None
def from_token(self, token): def from_token(self, token):
"""Return an add-on from hassio token.""" """Return an add-on from Hass.io token."""
for addon in self.list_addons: for addon in self.list_addons:
if addon.is_installed and token == addon.hassio_token: if addon.is_installed and token == addon.hassio_token:
return addon return addon
return None return None
async def load(self): async def load(self):
"""Startup addon management.""" """Start up add-on management."""
self.data.reload() self.data.reload()
# init hassio built-in repositories # Init Hass.io built-in repositories
repositories = \ repositories = \
set(self.sys_config.addons_repositories) | BUILTIN_REPOSITORIES set(self.sys_config.addons_repositories) | BUILTIN_REPOSITORIES
# init custom repositories & load addons # Init custom repositories and load add-ons
await self.load_repositories(repositories) await self.load_repositories(repositories)
async def reload(self): async def reload(self):
"""Update addons from repo and reload list.""" """Update add-ons from repository and reload list."""
tasks = [repository.update() for repository in tasks = [repository.update() for repository in
self.repositories_obj.values()] self.repositories_obj.values()]
if tasks: if tasks:
@ -113,14 +106,14 @@ class AddonManager(CoreSysAttributes):
await self.load_addons() await self.load_addons()
async def load_addons(self): async def load_addons(self):
"""Update/add internal addon store.""" """Update/add internal add-on store."""
all_addons = set(self.data.system) | set(self.data.cache) all_addons = set(self.data.system) | set(self.data.cache)
# calc diff # calc diff
add_addons = all_addons - set(self.addons_obj) add_addons = all_addons - set(self.addons_obj)
del_addons = set(self.addons_obj) - all_addons del_addons = set(self.addons_obj) - all_addons
_LOGGER.info("Load addons: %d all - %d new - %d remove", _LOGGER.info("Load add-ons: %d all - %d new - %d remove",
len(all_addons), len(add_addons), len(del_addons)) len(all_addons), len(add_addons), len(del_addons))
# new addons # new addons
@ -139,14 +132,14 @@ class AddonManager(CoreSysAttributes):
self.addons_obj.pop(addon_slug) self.addons_obj.pop(addon_slug)
async def boot(self, stage): async def boot(self, stage):
"""Boot addons with mode auto.""" """Boot add-ons with mode auto."""
tasks = [] tasks = []
for addon in self.addons_obj.values(): for addon in self.addons_obj.values():
if addon.is_installed and addon.boot == BOOT_AUTO and \ if addon.is_installed and addon.boot == BOOT_AUTO and \
addon.startup == stage: addon.startup == stage:
tasks.append(addon.start()) tasks.append(addon.start())
_LOGGER.info("Startup %s run %d addons", stage, len(tasks)) _LOGGER.info("Startup %s run %d add-ons", stage, len(tasks))
if tasks: if tasks:
await asyncio.wait(tasks) await asyncio.wait(tasks)
await asyncio.sleep(self.sys_config.wait_boot) await asyncio.sleep(self.sys_config.wait_boot)
@ -160,6 +153,6 @@ class AddonManager(CoreSysAttributes):
addon.startup == stage: addon.startup == stage:
tasks.append(addon.stop()) tasks.append(addon.stop())
_LOGGER.info("Shutdown %s stop %d addons", stage, len(tasks)) _LOGGER.info("Shutdown %s stop %d add-ons", stage, len(tasks))
if tasks: if tasks:
await asyncio.wait(tasks) await asyncio.wait(tasks)

View File

@ -1,4 +1,4 @@
"""Init file for HassIO addons.""" """Init file for Hass.io add-ons."""
from contextlib import suppress from contextlib import suppress
from copy import deepcopy from copy import deepcopy
import logging import logging
@ -13,7 +13,8 @@ import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
from .validate import ( from .validate import (
validate_options, SCHEMA_ADDON_SNAPSHOT, RE_VOLUME, RE_SERVICE) validate_options, SCHEMA_ADDON_SNAPSHOT, RE_VOLUME, RE_SERVICE,
MACHINE_ALL)
from .utils import check_installed, remove_data from .utils import check_installed, remove_data
from ..const import ( from ..const import (
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP,
@ -27,6 +28,7 @@ from ..const import (
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES, ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES,
ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_FULL_ACCESS, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_FULL_ACCESS,
ATTR_PROTECTED, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE, ATTR_PROTECTED, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE,
ATTR_MACHINE,
SECURITY_PROFILE, SECURITY_DISABLE, SECURITY_DEFAULT) SECURITY_PROFILE, SECURITY_DISABLE, SECURITY_DEFAULT)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..docker.addon import DockerAddon from ..docker.addon import DockerAddon
@ -43,7 +45,7 @@ RE_WEBUI = re.compile(
class Addon(CoreSysAttributes): class Addon(CoreSysAttributes):
"""Hold data for addon inside HassIO.""" """Hold data for add-on inside Hass.io."""
def __init__(self, coresys, slug): def __init__(self, coresys, slug):
"""Initialize data holder.""" """Initialize data holder."""
@ -59,29 +61,38 @@ class Addon(CoreSysAttributes):
@property @property
def slug(self): def slug(self):
"""Return slug/id of addon.""" """Return slug/id of add-on."""
return self._id return self._id
@property @property
def _mesh(self): def _mesh(self):
"""Return addon data from system or cache.""" """Return add-on data from system or cache."""
return self._data.system.get(self._id, self._data.cache.get(self._id)) return self._data.system.get(self._id, self._data.cache.get(self._id))
@property @property
def _data(self): def _data(self):
"""Return addons data storage.""" """Return add-ons data storage."""
return self.sys_addons.data return self.sys_addons.data
@property @property
def is_installed(self): def is_installed(self):
"""Return True if an addon is installed.""" """Return True if an add-on is installed."""
return self._id in self._data.system return self._id in self._data.system
@property @property
def is_detached(self): def is_detached(self):
"""Return True if addon is detached.""" """Return True if add-on is detached."""
return self._id not in self._data.cache return self._id not in self._data.cache
@property
def available(self):
"""Return True if this add-on is available on this platform."""
if self.sys_arch not in self.supported_arch:
return False
if self.sys_machine not in self.supported_machine:
return False
return True
@property @property
def version_installed(self): def version_installed(self):
"""Return installed version.""" """Return installed version."""
@ -97,19 +108,19 @@ class Addon(CoreSysAttributes):
self._data.save_data() self._data.save_data()
def _set_uninstall(self): def _set_uninstall(self):
"""Set addon as uninstalled.""" """Set add-on as uninstalled."""
self._data.system.pop(self._id, None) self._data.system.pop(self._id, None)
self._data.user.pop(self._id, None) self._data.user.pop(self._id, None)
self._data.save_data() self._data.save_data()
def _set_update(self, version): def _set_update(self, version):
"""Update version of addon.""" """Update version of add-on."""
self._data.system[self._id] = deepcopy(self._data.cache[self._id]) self._data.system[self._id] = deepcopy(self._data.cache[self._id])
self._data.user[self._id][ATTR_VERSION] = version self._data.user[self._id][ATTR_VERSION] = version
self._data.save_data() self._data.save_data()
def _restore_data(self, user, system): def _restore_data(self, user, system):
"""Restore data to addon.""" """Restore data to add-on."""
self._data.user[self._id] = deepcopy(user) self._data.user[self._id] = deepcopy(user)
self._data.system[self._id] = deepcopy(system) self._data.system[self._id] = deepcopy(system)
self._data.save_data() self._data.save_data()
@ -126,7 +137,7 @@ class Addon(CoreSysAttributes):
@options.setter @options.setter
def options(self, value): def options(self, value):
"""Store user addon options.""" """Store user add-on options."""
if value is None: if value is None:
self._data.user[self._id][ATTR_OPTIONS] = {} self._data.user[self._id][ATTR_OPTIONS] = {}
else: else:
@ -158,7 +169,7 @@ class Addon(CoreSysAttributes):
@property @property
def name(self): def name(self):
"""Return name of addon.""" """Return name of add-on."""
return self._mesh[ATTR_NAME] return self._mesh[ATTR_NAME]
@property @property
@ -175,14 +186,14 @@ class Addon(CoreSysAttributes):
@property @property
def hassio_token(self): def hassio_token(self):
"""Return access token for hass.io API.""" """Return access token for Hass.io API."""
if self.is_installed: if self.is_installed:
return self._data.user[self._id].get(ATTR_ACCESS_TOKEN) return self._data.user[self._id].get(ATTR_ACCESS_TOKEN)
return None return None
@property @property
def description(self): def description(self):
"""Return description of addon.""" """Return description of add-on."""
return self._mesh[ATTR_DESCRIPTON] return self._mesh[ATTR_DESCRIPTON]
@property @property
@ -200,56 +211,55 @@ class Addon(CoreSysAttributes):
@property @property
def repository(self): def repository(self):
"""Return repository of addon.""" """Return repository of add-on."""
return self._mesh[ATTR_REPOSITORY] return self._mesh[ATTR_REPOSITORY]
@property @property
def last_version(self): def last_version(self):
"""Return version of addon.""" """Return version of add-on."""
if self._id in self._data.cache: if self._id in self._data.cache:
return self._data.cache[self._id][ATTR_VERSION] return self._data.cache[self._id][ATTR_VERSION]
return self.version_installed return self.version_installed
@property @property
def protected(self): def protected(self):
"""Return if addon is in protected mode.""" """Return if add-on is in protected mode."""
if self.is_installed: if self.is_installed:
return self._data.user[self._id][ATTR_PROTECTED] return self._data.user[self._id][ATTR_PROTECTED]
return True return True
@protected.setter @protected.setter
def protected(self, value): def protected(self, value):
"""Set addon in protected mode.""" """Set add-on in protected mode."""
self._data.user[self._id][ATTR_PROTECTED] = value self._data.user[self._id][ATTR_PROTECTED] = value
@property @property
def startup(self): def startup(self):
"""Return startup type of addon.""" """Return startup type of add-on."""
return self._mesh.get(ATTR_STARTUP) return self._mesh.get(ATTR_STARTUP)
@property @property
def services(self): def services_role(self):
"""Return dict of services with rights.""" """Return dict of services with rights."""
raw_services = self._mesh.get(ATTR_SERVICES) raw_services = self._mesh.get(ATTR_SERVICES)
if not raw_services: if not raw_services:
return None return {}
formated_services = {} services = {}
for data in raw_services: for data in raw_services:
service = RE_SERVICE.match(data) service = RE_SERVICE.match(data)
formated_services[service.group('service')] = \ services[service.group('service')] = service.group('rights')
service.group('rights') or 'ro'
return formated_services return services
@property @property
def discovery(self): def discovery(self):
"""Return list of discoverable components/platforms.""" """Return list of discoverable components/platforms."""
return self._mesh.get(ATTR_DISCOVERY) return self._mesh.get(ATTR_DISCOVERY, [])
@property @property
def ports(self): def ports(self):
"""Return ports of addon.""" """Return ports of add-on."""
if self.host_network or ATTR_PORTS not in self._mesh: if self.host_network or ATTR_PORTS not in self._mesh:
return None return None
@ -260,7 +270,7 @@ class Addon(CoreSysAttributes):
@ports.setter @ports.setter
def ports(self, value): def ports(self, value):
"""Set custom ports of addon.""" """Set custom ports of add-on."""
if value is None: if value is None:
self._data.user[self._id].pop(ATTR_NETWORK, None) self._data.user[self._id].pop(ATTR_NETWORK, None)
else: else:
@ -304,42 +314,42 @@ class Addon(CoreSysAttributes):
@property @property
def host_network(self): def host_network(self):
"""Return True if addon run on host network.""" """Return True if add-on run on host network."""
return self._mesh[ATTR_HOST_NETWORK] return self._mesh[ATTR_HOST_NETWORK]
@property @property
def host_pid(self): def host_pid(self):
"""Return True if addon run on host PID namespace.""" """Return True if add-on run on host PID namespace."""
return self._mesh[ATTR_HOST_PID] return self._mesh[ATTR_HOST_PID]
@property @property
def host_ipc(self): def host_ipc(self):
"""Return True if addon run on host IPC namespace.""" """Return True if add-on run on host IPC namespace."""
return self._mesh[ATTR_HOST_IPC] return self._mesh[ATTR_HOST_IPC]
@property @property
def host_dbus(self): def host_dbus(self):
"""Return True if addon run on host DBUS.""" """Return True if add-on run on host D-BUS."""
return self._mesh[ATTR_HOST_DBUS] return self._mesh[ATTR_HOST_DBUS]
@property @property
def devices(self): def devices(self):
"""Return devices of addon.""" """Return devices of add-on."""
return self._mesh.get(ATTR_DEVICES) return self._mesh.get(ATTR_DEVICES)
@property @property
def auto_uart(self): def auto_uart(self):
"""Return True if we should map all uart device.""" """Return True if we should map all UART device."""
return self._mesh.get(ATTR_AUTO_UART) return self._mesh.get(ATTR_AUTO_UART)
@property @property
def tmpfs(self): def tmpfs(self):
"""Return tmpfs of addon.""" """Return tmpfs of add-on."""
return self._mesh.get(ATTR_TMPFS) return self._mesh.get(ATTR_TMPFS)
@property @property
def environment(self): def environment(self):
"""Return environment of addon.""" """Return environment of add-on."""
return self._mesh.get(ATTR_ENVIRONMENT) return self._mesh.get(ATTR_ENVIRONMENT)
@property @property
@ -349,7 +359,7 @@ class Addon(CoreSysAttributes):
@property @property
def apparmor(self): def apparmor(self):
"""Return True if apparmor is enabled.""" """Return True if AppArmor is enabled."""
if not self._mesh.get(ATTR_APPARMOR): if not self._mesh.get(ATTR_APPARMOR):
return SECURITY_DISABLE return SECURITY_DISABLE
elif self.sys_host.apparmor.exists(self.slug): elif self.sys_host.apparmor.exists(self.slug):
@ -358,22 +368,22 @@ class Addon(CoreSysAttributes):
@property @property
def legacy(self): def legacy(self):
"""Return if the add-on don't support hass labels.""" """Return if the add-on don't support Home Assistant labels."""
return self._mesh.get(ATTR_LEGACY) return self._mesh.get(ATTR_LEGACY)
@property @property
def access_docker_api(self): def access_docker_api(self):
"""Return if the add-on need read-only docker API access.""" """Return if the add-on need read-only Docker API access."""
return self._mesh.get(ATTR_DOCKER_API) return self._mesh.get(ATTR_DOCKER_API)
@property @property
def access_hassio_api(self): def access_hassio_api(self):
"""Return True if the add-on access to hassio api.""" """Return True if the add-on access to Hass.io REASTful API."""
return self._mesh[ATTR_HASSIO_API] return self._mesh[ATTR_HASSIO_API]
@property @property
def access_homeassistant_api(self): def access_homeassistant_api(self):
"""Return True if the add-on access to Home-Assistant api proxy.""" """Return True if the add-on access to Home Assistant API proxy."""
return self._mesh[ATTR_HOMEASSISTANT_API] return self._mesh[ATTR_HOMEASSISTANT_API]
@property @property
@ -388,7 +398,7 @@ class Addon(CoreSysAttributes):
@property @property
def with_gpio(self): def with_gpio(self):
"""Return True if the add-on access to gpio interface.""" """Return True if the add-on access to GPIO interface."""
return self._mesh[ATTR_GPIO] return self._mesh[ATTR_GPIO]
@property @property
@ -445,7 +455,7 @@ class Addon(CoreSysAttributes):
@property @property
def url(self): def url(self):
"""Return url of addon.""" """Return URL of add-on."""
return self._mesh.get(ATTR_URL) return self._mesh.get(ATTR_URL)
@property @property
@ -468,12 +478,17 @@ class Addon(CoreSysAttributes):
"""Return list of supported arch.""" """Return list of supported arch."""
return self._mesh[ATTR_ARCH] return self._mesh[ATTR_ARCH]
@property
def supported_machine(self):
"""Return list of supported machine."""
return self._mesh.get(ATTR_MACHINE) or MACHINE_ALL
@property @property
def image(self): def image(self):
"""Return image name of addon.""" """Return image name of add-on."""
addon_data = self._mesh addon_data = self._mesh
# Repository with dockerhub images # Repository with Dockerhub images
if ATTR_IMAGE in addon_data: if ATTR_IMAGE in addon_data:
return addon_data[ATTR_IMAGE].format(arch=self.sys_arch) return addon_data[ATTR_IMAGE].format(arch=self.sys_arch)
@ -484,12 +499,12 @@ class Addon(CoreSysAttributes):
@property @property
def need_build(self): def need_build(self):
"""Return True if this addon need a local build.""" """Return True if this add-on need a local build."""
return ATTR_IMAGE not in self._mesh return ATTR_IMAGE not in self._mesh
@property @property
def map_volumes(self): def map_volumes(self):
"""Return a dict of {volume: policy} from addon.""" """Return a dict of {volume: policy} from add-on."""
volumes = {} volumes = {}
for volume in self._mesh[ATTR_MAP]: for volume in self._mesh[ATTR_MAP]:
result = RE_VOLUME.match(volume) result = RE_VOLUME.match(volume)
@ -499,37 +514,37 @@ class Addon(CoreSysAttributes):
@property @property
def path_data(self): def path_data(self):
"""Return addon data path inside supervisor.""" """Return add-on data path inside Supervisor."""
return Path(self.sys_config.path_addons_data, self._id) return Path(self.sys_config.path_addons_data, self._id)
@property @property
def path_extern_data(self): def path_extern_data(self):
"""Return addon data path external for docker.""" """Return add-on data path external for Docker."""
return PurePath(self.sys_config.path_extern_addons_data, self._id) return PurePath(self.sys_config.path_extern_addons_data, self._id)
@property @property
def path_options(self): def path_options(self):
"""Return path to addons options.""" """Return path to add-on options."""
return Path(self.path_data, "options.json") return Path(self.path_data, "options.json")
@property @property
def path_location(self): def path_location(self):
"""Return path to this addon.""" """Return path to this add-on."""
return Path(self._mesh[ATTR_LOCATON]) return Path(self._mesh[ATTR_LOCATON])
@property @property
def path_icon(self): def path_icon(self):
"""Return path to addon icon.""" """Return path to add-on icon."""
return Path(self.path_location, 'icon.png') return Path(self.path_location, 'icon.png')
@property @property
def path_logo(self): def path_logo(self):
"""Return path to addon logo.""" """Return path to add-on logo."""
return Path(self.path_location, 'logo.png') return Path(self.path_location, 'logo.png')
@property @property
def path_changelog(self): def path_changelog(self):
"""Return path to addon changelog.""" """Return path to add-on changelog."""
return Path(self.path_location, 'CHANGELOG.md') return Path(self.path_location, 'CHANGELOG.md')
@property @property
@ -544,15 +559,15 @@ class Addon(CoreSysAttributes):
@property @property
def path_extern_asound(self): def path_extern_asound(self):
"""Return path to asound config for docker.""" """Return path to asound config for Docker."""
return Path(self.sys_config.path_extern_tmp, f"{self.slug}_asound") return Path(self.sys_config.path_extern_tmp, f"{self.slug}_asound")
def save_data(self): def save_data(self):
"""Save data of addon.""" """Save data of add-on."""
self.sys_addons.data.save_data() self.sys_addons.data.save_data()
def write_options(self): def write_options(self):
"""Return True if addon options is written to data.""" """Return True if add-on options is written to data."""
schema = self.schema schema = self.schema
options = self.options options = self.options
@ -560,15 +575,22 @@ class Addon(CoreSysAttributes):
schema(options) schema(options)
write_json_file(self.path_options, options) write_json_file(self.path_options, options)
except vol.Invalid as ex: except vol.Invalid as ex:
_LOGGER.error("Addon %s have wrong options: %s", self._id, _LOGGER.error("Add-on %s have wrong options: %s", self._id,
humanize_error(options, ex)) humanize_error(options, ex))
except (OSError, json.JSONDecodeError) as err: except (OSError, json.JSONDecodeError) as err:
_LOGGER.error("Addon %s can't write options: %s", self._id, err) _LOGGER.error("Add-on %s can't write options: %s", self._id, err)
else: else:
return True return True
return False return False
def remove_discovery(self):
"""Remove all discovery message from add-on."""
for message in self.sys_discovery.list_messages:
if message.addon != self.slug:
continue
self.sys_discovery.remove(message)
def write_asound(self): def write_asound(self):
"""Write asound config to file and return True on success.""" """Write asound config to file and return True on success."""
asound_config = self.sys_host.alsa.asound( asound_config = self.sys_host.alsa.asound(
@ -578,7 +600,7 @@ class Addon(CoreSysAttributes):
with self.path_asound.open('w') as config_file: with self.path_asound.open('w') as config_file:
config_file.write(asound_config) config_file.write(asound_config)
except OSError as err: except OSError as err:
_LOGGER.error("Addon %s can't write asound: %s", self._id, err) _LOGGER.error("Add-on %s can't write asound: %s", self._id, err)
return False return False
return True return True
@ -606,7 +628,7 @@ class Addon(CoreSysAttributes):
@property @property
def schema(self): def schema(self):
"""Create a schema for addon options.""" """Create a schema for add-on options."""
raw_schema = self._mesh[ATTR_SCHEMA] raw_schema = self._mesh[ATTR_SCHEMA]
if isinstance(raw_schema, bool): if isinstance(raw_schema, bool):
@ -614,7 +636,7 @@ class Addon(CoreSysAttributes):
return vol.Schema(vol.All(dict, validate_options(raw_schema))) return vol.Schema(vol.All(dict, validate_options(raw_schema)))
def test_update_schema(self): def test_update_schema(self):
"""Check if the exists config valid after update.""" """Check if the existing configuration is valid after update."""
if not self.is_installed or self.is_detached: if not self.is_installed or self.is_detached:
return True return True
@ -644,19 +666,19 @@ class Addon(CoreSysAttributes):
return True return True
async def install(self): async def install(self):
"""Install an addon.""" """Install an add-on."""
if self.sys_arch not in self.supported_arch: if not self.available:
_LOGGER.error( _LOGGER.error(
"Addon %s not supported on %s", self._id, self.sys_arch) "Add-on %s not supported on %s", self._id, self.sys_arch)
return False return False
if self.is_installed: if self.is_installed:
_LOGGER.error("Addon %s is already installed", self._id) _LOGGER.error("Add-on %s is already installed", self._id)
return False return False
if not self.path_data.is_dir(): if not self.path_data.is_dir():
_LOGGER.info( _LOGGER.info(
"Create Home-Assistant addon data folder %s", self.path_data) "Create Home Assistant add-on data folder %s", self.path_data)
self.path_data.mkdir() self.path_data.mkdir()
# Setup/Fix AppArmor profile # Setup/Fix AppArmor profile
@ -670,13 +692,13 @@ class Addon(CoreSysAttributes):
@check_installed @check_installed
async def uninstall(self): async def uninstall(self):
"""Remove an addon.""" """Remove an add-on."""
if not await self.instance.remove(): if not await self.instance.remove():
return False return False
if self.path_data.is_dir(): if self.path_data.is_dir():
_LOGGER.info( _LOGGER.info(
"Remove Home-Assistant addon data folder %s", self.path_data) "Remove Home Assistant add-on data folder %s", self.path_data)
await remove_data(self.path_data) await remove_data(self.path_data)
# Cleanup audio settings # Cleanup audio settings
@ -684,16 +706,19 @@ class Addon(CoreSysAttributes):
with suppress(OSError): with suppress(OSError):
self.path_asound.unlink() self.path_asound.unlink()
# Cleanup apparmor profile # Cleanup AppArmor profile
if self.sys_host.apparmor.exists(self.slug): if self.sys_host.apparmor.exists(self.slug):
with suppress(HostAppArmorError): with suppress(HostAppArmorError):
await self.sys_host.apparmor.remove_profile(self.slug) await self.sys_host.apparmor.remove_profile(self.slug)
# Remove discovery messages
self.remove_discovery()
self._set_uninstall() self._set_uninstall()
return True return True
async def state(self): async def state(self):
"""Return running state of addon.""" """Return running state of add-on."""
if not self.is_installed: if not self.is_installed:
return STATE_NONE return STATE_NONE
@ -703,9 +728,9 @@ class Addon(CoreSysAttributes):
@check_installed @check_installed
async def start(self): async def start(self):
"""Set options and start addon.""" """Set options and start add-on."""
if await self.instance.is_running(): if await self.instance.is_running():
_LOGGER.warning("%s allready running!", self.slug) _LOGGER.warning("%s already running!", self.slug)
return return
# Access Token # Access Token
@ -724,7 +749,7 @@ class Addon(CoreSysAttributes):
@check_installed @check_installed
def stop(self): def stop(self):
"""Stop addon. """Stop add-on.
Return a coroutine. Return a coroutine.
""" """
@ -732,11 +757,11 @@ class Addon(CoreSysAttributes):
@check_installed @check_installed
async def update(self): async def update(self):
"""Update addon.""" """Update add-on."""
last_state = await self.state() last_state = await self.state()
if self.last_version == self.version_installed: if self.last_version == self.version_installed:
_LOGGER.warning("No update available for Addon %s", self._id) _LOGGER.warning("No update available for add-on %s", self._id)
return False return False
if not await self.instance.update(self.last_version): if not await self.instance.update(self.last_version):
@ -753,13 +778,13 @@ class Addon(CoreSysAttributes):
@check_installed @check_installed
async def restart(self): async def restart(self):
"""Restart addon.""" """Restart add-on."""
await self.stop() await self.stop()
return await self.start() return await self.start()
@check_installed @check_installed
def logs(self): def logs(self):
"""Return addons log output. """Return add-ons log output.
Return a coroutine. Return a coroutine.
""" """
@ -775,11 +800,11 @@ class Addon(CoreSysAttributes):
@check_installed @check_installed
async def rebuild(self): async def rebuild(self):
"""Performe a rebuild of local build addon.""" """Perform a rebuild of local build add-on."""
last_state = await self.state() last_state = await self.state()
if not self.need_build: if not self.need_build:
_LOGGER.error("Can't rebuild a none local build addon!") _LOGGER.error("Can't rebuild a none local build add-on!")
return False return False
# remove docker container but not addon config # remove docker container but not addon config
@ -808,7 +833,7 @@ class Addon(CoreSysAttributes):
@check_installed @check_installed
async def snapshot(self, tar_file): async def snapshot(self, tar_file):
"""Snapshot state of an addon.""" """Snapshot state of an add-on."""
with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp: with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp:
# store local image # store local image
if self.need_build and not await \ if self.need_build and not await \
@ -822,7 +847,7 @@ class Addon(CoreSysAttributes):
ATTR_STATE: await self.state(), ATTR_STATE: await self.state(),
} }
# store local configs/state # Store local configs/state
try: try:
write_json_file(Path(temp, 'addon.json'), data) write_json_file(Path(temp, 'addon.json'), data)
except (OSError, json.JSONDecodeError) as err: except (OSError, json.JSONDecodeError) as err:
@ -846,7 +871,7 @@ class Addon(CoreSysAttributes):
snapshot.add(self.path_data, arcname="data") snapshot.add(self.path_data, arcname="data")
try: try:
_LOGGER.info("Build snapshot for addon %s", self._id) _LOGGER.info("Build snapshot for add-on %s", self._id)
await self.sys_run_in_executor(_write_tarfile) await self.sys_run_in_executor(_write_tarfile)
except (tarfile.TarError, OSError) as err: except (tarfile.TarError, OSError) as err:
_LOGGER.error("Can't write tarfile %s: %s", tar_file, err) _LOGGER.error("Can't write tarfile %s: %s", tar_file, err)
@ -856,7 +881,7 @@ class Addon(CoreSysAttributes):
return True return True
async def restore(self, tar_file): async def restore(self, tar_file):
"""Restore state of an addon.""" """Restore state of an add-on."""
with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp: with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp:
# extract snapshot # extract snapshot
def _extract_tarfile(): def _extract_tarfile():
@ -870,13 +895,13 @@ class Addon(CoreSysAttributes):
_LOGGER.error("Can't read tarfile %s: %s", tar_file, err) _LOGGER.error("Can't read tarfile %s: %s", tar_file, err)
return False return False
# read snapshot data # Read snapshot data
try: try:
data = read_json_file(Path(temp, 'addon.json')) data = read_json_file(Path(temp, 'addon.json'))
except (OSError, json.JSONDecodeError) as err: except (OSError, json.JSONDecodeError) as err:
_LOGGER.error("Can't read addon.json: %s", err) _LOGGER.error("Can't read addon.json: %s", err)
# validate # Validate
try: try:
data = SCHEMA_ADDON_SNAPSHOT(data) data = SCHEMA_ADDON_SNAPSHOT(data)
except vol.Invalid as err: except vol.Invalid as err:
@ -884,11 +909,11 @@ class Addon(CoreSysAttributes):
self._id, humanize_error(data, err)) self._id, humanize_error(data, err))
return False return False
# restore data / reload addon # Restore data or reload add-on
_LOGGER.info("Restore config for addon %s", self._id) _LOGGER.info("Restore config for addon %s", self._id)
self._restore_data(data[ATTR_USER], data[ATTR_SYSTEM]) self._restore_data(data[ATTR_USER], data[ATTR_SYSTEM])
# check version / restore image # Check version / restore image
version = data[ATTR_VERSION] version = data[ATTR_VERSION]
if not await self.instance.exists(): if not await self.instance.exists():
_LOGGER.info("Restore image for addon %s", self._id) _LOGGER.info("Restore image for addon %s", self._id)
@ -902,7 +927,7 @@ class Addon(CoreSysAttributes):
else: else:
await self.instance.stop() await self.instance.stop()
# restore data # Restore data
def _restore_data(): def _restore_data():
"""Restore data.""" """Restore data."""
shutil.copytree(str(Path(temp, "data")), str(self.path_data)) shutil.copytree(str(Path(temp, "data")), str(self.path_data))
@ -926,9 +951,9 @@ class Addon(CoreSysAttributes):
_LOGGER.error("Can't restore AppArmor profile") _LOGGER.error("Can't restore AppArmor profile")
return False return False
# run addon # Run add-on
if data[ATTR_STATE] == STATE_STARTED: if data[ATTR_STATE] == STATE_STARTED:
return await self.start() return await self.start()
_LOGGER.info("Finish restore for addon %s", self._id) _LOGGER.info("Finish restore for add-on %s", self._id)
return True return True

View File

@ -1,4 +1,4 @@
"""HassIO addons build environment.""" """Hass.io add-on build environment."""
from pathlib import Path from pathlib import Path
from .validate import SCHEMA_BUILD_CONFIG, BASE_IMAGE from .validate import SCHEMA_BUILD_CONFIG, BASE_IMAGE
@ -8,10 +8,10 @@ from ..utils.json import JsonConfig
class AddonBuild(JsonConfig, CoreSysAttributes): class AddonBuild(JsonConfig, CoreSysAttributes):
"""Handle build options for addons.""" """Handle build options for add-ons."""
def __init__(self, coresys, slug): def __init__(self, coresys, slug):
"""Initialize addon builder.""" """Initialize Hass.io add-on builder."""
self.coresys = coresys self.coresys = coresys
self._id = slug self._id = slug
@ -24,12 +24,12 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
@property @property
def addon(self): def addon(self):
"""Return addon of build data.""" """Return add-on of build data."""
return self.sys_addons.get(self._id) return self.sys_addons.get(self._id)
@property @property
def base_image(self): def base_image(self):
"""Base images for this addon.""" """Base images for this add-on."""
return self._data[ATTR_BUILD_FROM].get( return self._data[ATTR_BUILD_FROM].get(
self.sys_arch, BASE_IMAGE[self.sys_arch]) self.sys_arch, BASE_IMAGE[self.sys_arch])
@ -40,11 +40,11 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
@property @property
def additional_args(self): def additional_args(self):
"""Return additional docker build arguments.""" """Return additional Docker build arguments."""
return self._data[ATTR_ARGS] return self._data[ATTR_ARGS]
def get_docker_args(self, version): def get_docker_args(self, version):
"""Create a dict with docker build arguments.""" """Create a dict with Docker build arguments."""
args = { args = {
'path': str(self.addon.path_location), 'path': str(self.addon.path_location),
'tag': f"{self.addon.image}:{version}", 'tag': f"{self.addon.image}:{version}",

View File

@ -1,4 +1,4 @@
"""Init file for HassIO addons.""" """Init file for Hass.io add-on data."""
import logging import logging
import json import json
from pathlib import Path from pathlib import Path
@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
class AddonsData(JsonConfig, CoreSysAttributes): class AddonsData(JsonConfig, CoreSysAttributes):
"""Hold data for addons inside HassIO.""" """Hold data for Add-ons inside Hass.io."""
def __init__(self, coresys): def __init__(self, coresys):
"""Initialize data holder.""" """Initialize data holder."""
@ -30,26 +30,26 @@ class AddonsData(JsonConfig, CoreSysAttributes):
@property @property
def user(self): def user(self):
"""Return local addon user data.""" """Return local add-on user data."""
return self._data[ATTR_USER] return self._data[ATTR_USER]
@property @property
def system(self): def system(self):
"""Return local addon data.""" """Return local add-on data."""
return self._data[ATTR_SYSTEM] return self._data[ATTR_SYSTEM]
@property @property
def cache(self): def cache(self):
"""Return addon data from cache/repositories.""" """Return add-on data from cache/repositories."""
return self._cache return self._cache
@property @property
def repositories(self): def repositories(self):
"""Return addon data from repositories.""" """Return add-on data from repositories."""
return self._repositories return self._repositories
def reload(self): def reload(self):
"""Read data from addons repository.""" """Read data from add-on repository."""
self._cache = {} self._cache = {}
self._repositories = {} self._repositories = {}
@ -94,7 +94,7 @@ class AddonsData(JsonConfig, CoreSysAttributes):
self._read_addons_folder(path, slug) self._read_addons_folder(path, slug)
def _read_addons_folder(self, path, repository): def _read_addons_folder(self, path, repository):
"""Read data from addons folder.""" """Read data from add-ons folder."""
for addon in path.glob("**/config.json"): for addon in path.glob("**/config.json"):
try: try:
addon_config = read_json_file(addon) addon_config = read_json_file(addon)

View File

@ -1,4 +1,4 @@
"""Init file for HassIO addons git.""" """Init file for Hass.io add-on Git."""
import asyncio import asyncio
import logging import logging
import functools as ft import functools as ft
@ -16,10 +16,10 @@ _LOGGER = logging.getLogger(__name__)
class GitRepo(CoreSysAttributes): class GitRepo(CoreSysAttributes):
"""Manage addons git repo.""" """Manage Add-on Git repository."""
def __init__(self, coresys, path, url): def __init__(self, coresys, path, url):
"""Initialize git base wrapper.""" """Initialize Git base wrapper."""
self.coresys = coresys self.coresys = coresys
self.repo = None self.repo = None
self.path = path self.path = path
@ -38,13 +38,13 @@ class GitRepo(CoreSysAttributes):
return self._data[ATTR_BRANCH] return self._data[ATTR_BRANCH]
async def load(self): async def load(self):
"""Init git addon repo.""" """Init Git add-on repository."""
if not self.path.is_dir(): if not self.path.is_dir():
return await self.clone() return await self.clone()
async with self.lock: async with self.lock:
try: try:
_LOGGER.info("Load addon %s repository", self.path) _LOGGER.info("Load add-on %s repository", self.path)
self.repo = await self.sys_run_in_executor( self.repo = await self.sys_run_in_executor(
git.Repo, str(self.path)) git.Repo, str(self.path))
@ -57,7 +57,7 @@ class GitRepo(CoreSysAttributes):
return True return True
async def clone(self): async def clone(self):
"""Clone git addon repo.""" """Clone git add-on repository."""
async with self.lock: async with self.lock:
git_args = { git_args = {
attribute: value attribute: value
@ -70,7 +70,7 @@ class GitRepo(CoreSysAttributes):
} }
try: try:
_LOGGER.info("Clone addon %s repository", self.url) _LOGGER.info("Clone add-on %s repository", self.url)
self.repo = await self.sys_run_in_executor(ft.partial( self.repo = await self.sys_run_in_executor(ft.partial(
git.Repo.clone_from, self.url, str(self.path), git.Repo.clone_from, self.url, str(self.path),
**git_args **git_args
@ -78,20 +78,20 @@ class GitRepo(CoreSysAttributes):
except (git.InvalidGitRepositoryError, git.NoSuchPathError, except (git.InvalidGitRepositoryError, git.NoSuchPathError,
git.GitCommandError) as err: git.GitCommandError) as err:
_LOGGER.error("Can't clone %s repo: %s.", self.url, err) _LOGGER.error("Can't clone %s repository: %s.", self.url, err)
self._remove() self._remove()
return False return False
return True return True
async def pull(self): async def pull(self):
"""Pull git addon repo.""" """Pull Git add-on repo."""
if self.lock.locked(): if self.lock.locked():
_LOGGER.warning("It is already a task in progress.") _LOGGER.warning("It is already a task in progress")
return False return False
async with self.lock: async with self.lock:
_LOGGER.info("Update addon %s repository", self.url) _LOGGER.info("Update add-on %s repository", self.url)
branch = self.repo.active_branch.name branch = self.repo.active_branch.name
try: try:
@ -130,19 +130,19 @@ class GitRepo(CoreSysAttributes):
class GitRepoHassIO(GitRepo): class GitRepoHassIO(GitRepo):
"""HassIO addons repository.""" """Hass.io add-ons repository."""
def __init__(self, coresys): def __init__(self, coresys):
"""Initialize git hassio addon repository.""" """Initialize Git Hass.io add-on repository."""
super().__init__( super().__init__(
coresys, coresys.config.path_addons_core, URL_HASSIO_ADDONS) coresys, coresys.config.path_addons_core, URL_HASSIO_ADDONS)
class GitRepoCustom(GitRepo): class GitRepoCustom(GitRepo):
"""Custom addons repository.""" """Custom add-ons repository."""
def __init__(self, coresys, url): def __init__(self, coresys, url):
"""Initialize git hassio addon repository.""" """Initialize custom Git Hass.io addo-n repository."""
path = Path( path = Path(
coresys.config.path_addons_git, coresys.config.path_addons_git,
get_hash_from_repository(url)) get_hash_from_repository(url))
@ -151,5 +151,5 @@ class GitRepoCustom(GitRepo):
def remove(self): def remove(self):
"""Remove a custom repository.""" """Remove a custom repository."""
_LOGGER.info("Remove custom addon repository %s", self.url) _LOGGER.info("Remove custom add-on repository %s", self.url)
self._remove() self._remove()

View File

@ -1,15 +1,16 @@
"""Represent a HassIO repository.""" """Represent a Hass.io repository."""
from .git import GitRepoHassIO, GitRepoCustom from .git import GitRepoHassIO, GitRepoCustom
from .utils import get_hash_from_repository from .utils import get_hash_from_repository
from ..const import ( from ..const import (
REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_NAME, ATTR_URL, ATTR_MAINTAINER) REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_NAME, ATTR_URL, ATTR_MAINTAINER)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIError
UNKNOWN = 'unknown' UNKNOWN = 'unknown'
class Repository(CoreSysAttributes): class Repository(CoreSysAttributes):
"""Repository in HassIO.""" """Repository in Hass.io."""
def __init__(self, coresys, repository): def __init__(self, coresys, repository):
"""Initialize repository object.""" """Initialize repository object."""
@ -44,7 +45,7 @@ class Repository(CoreSysAttributes):
@property @property
def url(self): def url(self):
"""Return url of repository.""" """Return URL of repository."""
return self._mesh.get(ATTR_URL, self.source) return self._mesh.get(ATTR_URL, self.source)
@property @property
@ -59,14 +60,14 @@ class Repository(CoreSysAttributes):
return True return True
async def update(self): async def update(self):
"""Update addon repository.""" """Update add-on repository."""
if self.git: if self.git:
return await self.git.pull() return await self.git.pull()
return True return True
def remove(self): def remove(self):
"""Remove addon repository.""" """Remove add-on repository."""
if self._id in (REPOSITORY_CORE, REPOSITORY_LOCAL): if self._id in (REPOSITORY_CORE, REPOSITORY_LOCAL):
raise RuntimeError("Can't remove built-in repositories!") raise APIError("Can't remove built-in repositories!")
self.git.remove() self.git.remove()

View File

@ -1,4 +1,4 @@
"""Util addons functions.""" """Util add-ons functions."""
import asyncio import asyncio
import hashlib import hashlib
import logging import logging
@ -28,10 +28,6 @@ def rating_security(addon):
elif addon.apparmor == SECURITY_PROFILE: elif addon.apparmor == SECURITY_PROFILE:
rating += 1 rating += 1
# API Access
if addon.access_hassio_api or addon.access_homeassistant_api:
rating += -1
# Privileged options # Privileged options
if addon.privileged in (PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN, if addon.privileged in (PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN,
PRIVILEGED_SYS_RAWIO, PRIVILEGED_SYS_PTRACE): PRIVILEGED_SYS_RAWIO, PRIVILEGED_SYS_PTRACE):
@ -78,7 +74,7 @@ def extract_hash_from_path(path):
def check_installed(method): def check_installed(method):
"""Wrap function with check if addon is installed.""" """Wrap function with check if add-on is installed."""
async def wrap_check(addon, *args, **kwargs): async def wrap_check(addon, *args, **kwargs):
"""Return False if not installed or the function.""" """Return False if not installed or the function."""
if not addon.is_installed: if not addon.is_installed:

View File

@ -1,4 +1,4 @@
"""Validate addons options schema.""" """Validate add-ons options schema."""
import logging import logging
import re import re
import uuid import uuid
@ -20,18 +20,19 @@ from ..const import (
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY, ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY,
ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_PROTECTED, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_PROTECTED,
ATTR_FULL_ACCESS, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE, ATTR_FULL_ACCESS, ATTR_ACCESS_TOKEN, ATTR_HOST_PID, ATTR_HASSIO_ROLE,
ATTR_MACHINE,
PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO, PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO,
PRIVILEGED_IPC_LOCK, PRIVILEGED_SYS_TIME, PRIVILEGED_SYS_NICE, PRIVILEGED_IPC_LOCK, PRIVILEGED_SYS_TIME, PRIVILEGED_SYS_NICE,
PRIVILEGED_SYS_RESOURCE, PRIVILEGED_SYS_PTRACE, PRIVILEGED_SYS_RESOURCE, PRIVILEGED_SYS_PTRACE,
ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_MANAGER, ROLE_ADMIN) ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_MANAGER, ROLE_ADMIN)
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE, UUID_MATCH
from ..services.validate import DISCOVERY_SERVICES
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$") RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$")
RE_SERVICE = re.compile(r"^(?P<service>mqtt)(?::(?P<rights>rw|:ro))?$") RE_SERVICE = re.compile(r"^(?P<service>mqtt):(?P<rights>provide|want|need)$")
RE_DISCOVERY = re.compile(r"^(?P<component>\w*)(?:/(?P<platform>\w*>))?$")
V_STR = 'str' V_STR = 'str'
V_INT = 'int' V_INT = 'int'
@ -57,6 +58,12 @@ ARCH_ALL = [
ARCH_ARMHF, ARCH_AARCH64, ARCH_AMD64, ARCH_I386 ARCH_ARMHF, ARCH_AARCH64, ARCH_AMD64, ARCH_I386
] ]
MACHINE_ALL = [
'intel-nuc', 'qemux86', 'qemux86-64', 'qemuarm', 'qemuarm-64',
'raspberrypi', 'raspberrypi2', 'raspberrypi3', 'raspberrypi3-64',
'odroid-cu2', 'odroid-xu',
]
STARTUP_ALL = [ STARTUP_ALL = [
STARTUP_ONCE, STARTUP_INITIALIZE, STARTUP_SYSTEM, STARTUP_SERVICES, STARTUP_ONCE, STARTUP_INITIALIZE, STARTUP_SYSTEM, STARTUP_SERVICES,
STARTUP_APPLICATION STARTUP_APPLICATION
@ -105,6 +112,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Required(ATTR_DESCRIPTON): vol.Coerce(str), vol.Required(ATTR_DESCRIPTON): vol.Coerce(str),
vol.Optional(ATTR_URL): vol.Url(), vol.Optional(ATTR_URL): vol.Url(),
vol.Optional(ATTR_ARCH, default=ARCH_ALL): [vol.In(ARCH_ALL)], vol.Optional(ATTR_ARCH, default=ARCH_ALL): [vol.In(ARCH_ALL)],
vol.Optional(ATTR_MACHINE): [vol.In(MACHINE_ALL)],
vol.Required(ATTR_STARTUP): vol.Required(ATTR_STARTUP):
vol.All(_simple_startup, vol.In(STARTUP_ALL)), vol.All(_simple_startup, vol.In(STARTUP_ALL)),
vol.Required(ATTR_BOOT): vol.Required(ATTR_BOOT):
@ -135,7 +143,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(), vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(),
vol.Optional(ATTR_DOCKER_API, default=False): vol.Boolean(), vol.Optional(ATTR_DOCKER_API, default=False): vol.Boolean(),
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)], vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
vol.Optional(ATTR_DISCOVERY): [vol.Match(RE_DISCOVERY)], vol.Optional(ATTR_DISCOVERY): [vol.In(DISCOVERY_SERVICES)],
vol.Required(ATTR_OPTIONS): dict, vol.Required(ATTR_OPTIONS): dict,
vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({ vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({
vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [ vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [
@ -177,8 +185,7 @@ SCHEMA_BUILD_CONFIG = vol.Schema({
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
SCHEMA_ADDON_USER = vol.Schema({ SCHEMA_ADDON_USER = vol.Schema({
vol.Required(ATTR_VERSION): vol.Coerce(str), vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH,
vol.Match(r"^[0-9a-f]{32}$"),
vol.Optional(ATTR_ACCESS_TOKEN): vol.Match(r"^[0-9a-f]{64}$"), vol.Optional(ATTR_ACCESS_TOKEN): vol.Match(r"^[0-9a-f]{64}$"),
vol.Optional(ATTR_OPTIONS, default=dict): dict, vol.Optional(ATTR_OPTIONS, default=dict): dict,
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(), vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
@ -218,7 +225,7 @@ SCHEMA_ADDON_SNAPSHOT = vol.Schema({
def validate_options(raw_schema): def validate_options(raw_schema):
"""Validate schema.""" """Validate schema."""
def validate(struct): def validate(struct):
"""Create schema validator for addons options.""" """Create schema validator for add-ons options."""
options = {} options = {}
# read options # read options

View File

@ -1,4 +1,4 @@
"""Init file for HassIO rest api.""" """Init file for Hass.io RESTful API."""
import logging import logging
from pathlib import Path from pathlib import Path
@ -14,6 +14,7 @@ from .proxy import APIProxy
from .supervisor import APISupervisor from .supervisor import APISupervisor
from .snapshots import APISnapshots from .snapshots import APISnapshots
from .services import APIServices from .services import APIServices
from .version import APIVersion
from .security import SecurityMiddleware from .security import SecurityMiddleware
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
@ -21,10 +22,10 @@ _LOGGER = logging.getLogger(__name__)
class RestAPI(CoreSysAttributes): class RestAPI(CoreSysAttributes):
"""Handle rest api for hassio.""" """Handle RESTful API for Hass.io."""
def __init__(self, coresys): def __init__(self, coresys):
"""Initialize docker base wrapper.""" """Initialize Docker base wrapper."""
self.coresys = coresys self.coresys = coresys
self.security = SecurityMiddleware(coresys) self.security = SecurityMiddleware(coresys)
self.webapp = web.Application( self.webapp = web.Application(
@ -47,9 +48,10 @@ class RestAPI(CoreSysAttributes):
self._register_snapshots() self._register_snapshots()
self._register_discovery() self._register_discovery()
self._register_services() self._register_services()
self._register_version()
def _register_host(self): def _register_host(self):
"""Register hostcontrol function.""" """Register hostcontrol functions."""
api_host = APIHost() api_host = APIHost()
api_host.coresys = self.coresys api_host.coresys = self.coresys
@ -69,7 +71,7 @@ class RestAPI(CoreSysAttributes):
]) ])
def _register_hassos(self): def _register_hassos(self):
"""Register hassos function.""" """Register HassOS functions."""
api_hassos = APIHassOS() api_hassos = APIHassOS()
api_hassos.coresys = self.coresys api_hassos.coresys = self.coresys
@ -81,7 +83,7 @@ class RestAPI(CoreSysAttributes):
]) ])
def _register_hardware(self): def _register_hardware(self):
"""Register hardware function.""" """Register hardware functions."""
api_hardware = APIHardware() api_hardware = APIHardware()
api_hardware.coresys = self.coresys api_hardware.coresys = self.coresys
@ -90,8 +92,17 @@ class RestAPI(CoreSysAttributes):
web.get('/hardware/audio', api_hardware.audio), web.get('/hardware/audio', api_hardware.audio),
]) ])
def _register_version(self):
"""Register version functions."""
api_version = APIVersion()
api_version.coresys = self.coresys
self.webapp.add_routes([
web.get('/version', api_version.info),
])
def _register_supervisor(self): def _register_supervisor(self):
"""Register supervisor function.""" """Register Supervisor functions."""
api_supervisor = APISupervisor() api_supervisor = APISupervisor()
api_supervisor.coresys = self.coresys api_supervisor.coresys = self.coresys
@ -106,7 +117,7 @@ class RestAPI(CoreSysAttributes):
]) ])
def _register_homeassistant(self): def _register_homeassistant(self):
"""Register homeassistant function.""" """Register Home Assistant functions."""
api_hass = APIHomeAssistant() api_hass = APIHomeAssistant()
api_hass.coresys = self.coresys api_hass.coresys = self.coresys
@ -123,7 +134,7 @@ class RestAPI(CoreSysAttributes):
]) ])
def _register_proxy(self): def _register_proxy(self):
"""Register HomeAssistant API Proxy.""" """Register Home Assistant API Proxy."""
api_proxy = APIProxy() api_proxy = APIProxy()
api_proxy.coresys = self.coresys api_proxy.coresys = self.coresys
@ -137,7 +148,7 @@ class RestAPI(CoreSysAttributes):
]) ])
def _register_addons(self): def _register_addons(self):
"""Register homeassistant function.""" """Register Add-on functions."""
api_addons = APIAddons() api_addons = APIAddons()
api_addons.coresys = self.coresys api_addons.coresys = self.coresys
@ -163,7 +174,7 @@ class RestAPI(CoreSysAttributes):
]) ])
def _register_snapshots(self): def _register_snapshots(self):
"""Register snapshots function.""" """Register snapshots functions."""
api_snapshots = APISnapshots() api_snapshots = APISnapshots()
api_snapshots.coresys = self.coresys api_snapshots.coresys = self.coresys
@ -183,6 +194,7 @@ class RestAPI(CoreSysAttributes):
]) ])
def _register_services(self): def _register_services(self):
"""Register services functions."""
api_services = APIServices() api_services = APIServices()
api_services.coresys = self.coresys api_services.coresys = self.coresys
@ -194,19 +206,20 @@ class RestAPI(CoreSysAttributes):
]) ])
def _register_discovery(self): def _register_discovery(self):
"""Register discovery functions."""
api_discovery = APIDiscovery() api_discovery = APIDiscovery()
api_discovery.coresys = self.coresys api_discovery.coresys = self.coresys
self.webapp.add_routes([ self.webapp.add_routes([
web.get('/services/discovery', api_discovery.list), web.get('/discovery', api_discovery.list),
web.get('/services/discovery/{uuid}', api_discovery.get_discovery), web.get('/discovery/{uuid}', api_discovery.get_discovery),
web.delete('/services/discovery/{uuid}', web.delete('/discovery/{uuid}',
api_discovery.del_discovery), api_discovery.del_discovery),
web.post('/services/discovery', api_discovery.set_discovery), web.post('/discovery', api_discovery.set_discovery),
]) ])
def _register_panel(self): def _register_panel(self):
"""Register panel for homeassistant.""" """Register panel for Home Assistant."""
panel_dir = Path(__file__).parent.joinpath("panel") panel_dir = Path(__file__).parent.joinpath("panel")
def create_response(panel_file): def create_response(panel_file):
@ -234,7 +247,7 @@ class RestAPI(CoreSysAttributes):
self.webapp.add_routes([web.static('/app', panel_dir)]) self.webapp.add_routes([web.static('/app', panel_dir)])
async def start(self): async def start(self):
"""Run rest api webserver.""" """Run RESTful API webserver."""
await self._runner.setup() await self._runner.setup()
self._site = web.TCPSite( self._site = web.TCPSite(
self._runner, host="0.0.0.0", port=80, shutdown_timeout=5) self._runner, host="0.0.0.0", port=80, shutdown_timeout=5)
@ -248,7 +261,7 @@ class RestAPI(CoreSysAttributes):
_LOGGER.info("Start API on %s", self.sys_docker.network.supervisor) _LOGGER.info("Start API on %s", self.sys_docker.network.supervisor)
async def stop(self): async def stop(self):
"""Stop rest api webserver.""" """Stop RESTful API webserver."""
if not self._site: if not self._site:
return return

View File

@ -1,4 +1,4 @@
"""Init file for HassIO homeassistant rest api.""" """Init file for Hass.io Home Assistant RESTful API."""
import asyncio import asyncio
import logging import logging
@ -20,11 +20,10 @@ from ..const import (
ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES, ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES,
ATTR_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API,
ATTR_FULL_ACCESS, ATTR_PROTECTED, ATTR_RATING, ATTR_HOST_PID, ATTR_FULL_ACCESS, ATTR_PROTECTED, ATTR_RATING, ATTR_HOST_PID,
ATTR_HASSIO_ROLE, ATTR_HASSIO_ROLE, ATTR_MACHINE, ATTR_AVAILABLE,
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT, REQUEST_FROM) CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT, REQUEST_FROM)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..validate import DOCKER_PORTS, ALSA_DEVICE from ..validate import DOCKER_PORTS, ALSA_DEVICE
from ..exceptions import APINotSupportedError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -48,7 +47,7 @@ SCHEMA_SECURITY = vol.Schema({
class APIAddons(CoreSysAttributes): class APIAddons(CoreSysAttributes):
"""Handle rest api for addons functions.""" """Handle RESTful API for add-on functions."""
def _extract_addon(self, request, check_installed=True): def _extract_addon(self, request, check_installed=True):
"""Return addon, throw an exception it it doesn't exist.""" """Return addon, throw an exception it it doesn't exist."""
@ -56,7 +55,7 @@ class APIAddons(CoreSysAttributes):
# Lookup itself # Lookup itself
if addon_slug == 'self': if addon_slug == 'self':
addon_slug = request.get(REQUEST_FROM) return request.get(REQUEST_FROM)
addon = self.sys_addons.get(addon_slug) addon = self.sys_addons.get(addon_slug)
if not addon: if not addon:
@ -67,17 +66,9 @@ class APIAddons(CoreSysAttributes):
return addon return addon
@staticmethod
def _pretty_devices(addon):
"""Return a simplified device list."""
dev_list = addon.devices
if not dev_list:
return None
return [row.split(':')[0] for row in dev_list]
@api_process @api_process
async def list(self, request): async def list(self, request):
"""Return all addons / repositories .""" """Return all add-ons or repositories."""
data_addons = [] data_addons = []
for addon in self.sys_addons.list_addons: for addon in self.sys_addons.list_addons:
data_addons.append({ data_addons.append({
@ -86,7 +77,7 @@ class APIAddons(CoreSysAttributes):
ATTR_DESCRIPTON: addon.description, ATTR_DESCRIPTON: addon.description,
ATTR_VERSION: addon.last_version, ATTR_VERSION: addon.last_version,
ATTR_INSTALLED: addon.version_installed, ATTR_INSTALLED: addon.version_installed,
ATTR_ARCH: addon.supported_arch, ATTR_AVAILABLE: addon.available,
ATTR_DETACHED: addon.is_detached, ATTR_DETACHED: addon.is_detached,
ATTR_REPOSITORY: addon.repository, ATTR_REPOSITORY: addon.repository,
ATTR_BUILD: addon.need_build, ATTR_BUILD: addon.need_build,
@ -112,13 +103,13 @@ class APIAddons(CoreSysAttributes):
@api_process @api_process
async def reload(self, request): async def reload(self, request):
"""Reload all addons data.""" """Reload all add-on data."""
await asyncio.shield(self.sys_addons.reload()) await asyncio.shield(self.sys_addons.reload())
return True return True
@api_process @api_process
async def info(self, request): async def info(self, request):
"""Return addon information.""" """Return add-on information."""
addon = self._extract_addon(request, check_installed=False) addon = self._extract_addon(request, check_installed=False)
return { return {
@ -135,8 +126,11 @@ class APIAddons(CoreSysAttributes):
ATTR_RATING: rating_security(addon), ATTR_RATING: rating_security(addon),
ATTR_BOOT: addon.boot, ATTR_BOOT: addon.boot,
ATTR_OPTIONS: addon.options, ATTR_OPTIONS: addon.options,
ATTR_ARCH: addon.supported_arch,
ATTR_MACHINE: addon.supported_machine,
ATTR_URL: addon.url, ATTR_URL: addon.url,
ATTR_DETACHED: addon.is_detached, ATTR_DETACHED: addon.is_detached,
ATTR_AVAILABLE: addon.available,
ATTR_BUILD: addon.need_build, ATTR_BUILD: addon.need_build,
ATTR_NETWORK: addon.ports, ATTR_NETWORK: addon.ports,
ATTR_HOST_NETWORK: addon.host_network, ATTR_HOST_NETWORK: addon.host_network,
@ -146,7 +140,7 @@ class APIAddons(CoreSysAttributes):
ATTR_PRIVILEGED: addon.privileged, ATTR_PRIVILEGED: addon.privileged,
ATTR_FULL_ACCESS: addon.with_full_access, ATTR_FULL_ACCESS: addon.with_full_access,
ATTR_APPARMOR: addon.apparmor, ATTR_APPARMOR: addon.apparmor,
ATTR_DEVICES: self._pretty_devices(addon), ATTR_DEVICES: _pretty_devices(addon),
ATTR_ICON: addon.with_icon, ATTR_ICON: addon.with_icon,
ATTR_LOGO: addon.with_logo, ATTR_LOGO: addon.with_logo,
ATTR_CHANGELOG: addon.with_changelog, ATTR_CHANGELOG: addon.with_changelog,
@ -161,13 +155,13 @@ class APIAddons(CoreSysAttributes):
ATTR_AUDIO: addon.with_audio, ATTR_AUDIO: addon.with_audio,
ATTR_AUDIO_INPUT: addon.audio_input, ATTR_AUDIO_INPUT: addon.audio_input,
ATTR_AUDIO_OUTPUT: addon.audio_output, ATTR_AUDIO_OUTPUT: addon.audio_output,
ATTR_SERVICES: addon.services, ATTR_SERVICES: _pretty_services(addon),
ATTR_DISCOVERY: addon.discovery, ATTR_DISCOVERY: addon.discovery,
} }
@api_process @api_process
async def options(self, request): async def options(self, request):
"""Store user options for addon.""" """Store user options for add-on."""
addon = self._extract_addon(request) addon = self._extract_addon(request)
addon_schema = SCHEMA_OPTIONS.extend({ addon_schema = SCHEMA_OPTIONS.extend({
@ -194,15 +188,8 @@ class APIAddons(CoreSysAttributes):
@api_process @api_process
async def security(self, request): async def security(self, request):
"""Store security options for addon.""" """Store security options for add-on."""
addon = self._extract_addon(request) addon = self._extract_addon(request)
# Have Access
# REMOVE: don't needed anymore
if addon.slug == request[REQUEST_FROM]:
_LOGGER.error("Can't self modify his security!")
raise APINotSupportedError()
body = await api_validate(SCHEMA_SECURITY, request) body = await api_validate(SCHEMA_SECURITY, request)
if ATTR_PROTECTED in body: if ATTR_PROTECTED in body:
@ -233,19 +220,19 @@ class APIAddons(CoreSysAttributes):
@api_process @api_process
def install(self, request): def install(self, request):
"""Install addon.""" """Install add-on."""
addon = self._extract_addon(request, check_installed=False) addon = self._extract_addon(request, check_installed=False)
return asyncio.shield(addon.install()) return asyncio.shield(addon.install())
@api_process @api_process
def uninstall(self, request): def uninstall(self, request):
"""Uninstall addon.""" """Uninstall add-on."""
addon = self._extract_addon(request) addon = self._extract_addon(request)
return asyncio.shield(addon.uninstall()) return asyncio.shield(addon.uninstall())
@api_process @api_process
def start(self, request): def start(self, request):
"""Start addon.""" """Start add-on."""
addon = self._extract_addon(request) addon = self._extract_addon(request)
# check options # check options
@ -259,13 +246,13 @@ class APIAddons(CoreSysAttributes):
@api_process @api_process
def stop(self, request): def stop(self, request):
"""Stop addon.""" """Stop add-on."""
addon = self._extract_addon(request) addon = self._extract_addon(request)
return asyncio.shield(addon.stop()) return asyncio.shield(addon.stop())
@api_process @api_process
def update(self, request): def update(self, request):
"""Update addon.""" """Update add-on."""
addon = self._extract_addon(request) addon = self._extract_addon(request)
if addon.last_version == addon.version_installed: if addon.last_version == addon.version_installed:
@ -275,13 +262,13 @@ class APIAddons(CoreSysAttributes):
@api_process @api_process
def restart(self, request): def restart(self, request):
"""Restart addon.""" """Restart add-on."""
addon = self._extract_addon(request) addon = self._extract_addon(request)
return asyncio.shield(addon.restart()) return asyncio.shield(addon.restart())
@api_process @api_process
def rebuild(self, request): def rebuild(self, request):
"""Rebuild local build addon.""" """Rebuild local build add-on."""
addon = self._extract_addon(request) addon = self._extract_addon(request)
if not addon.need_build: if not addon.need_build:
raise RuntimeError("Only local build addons are supported") raise RuntimeError("Only local build addons are supported")
@ -290,13 +277,13 @@ class APIAddons(CoreSysAttributes):
@api_process_raw(CONTENT_TYPE_BINARY) @api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request): def logs(self, request):
"""Return logs from addon.""" """Return logs from add-on."""
addon = self._extract_addon(request) addon = self._extract_addon(request)
return addon.logs() return addon.logs()
@api_process_raw(CONTENT_TYPE_PNG) @api_process_raw(CONTENT_TYPE_PNG)
async def icon(self, request): async def icon(self, request):
"""Return icon from addon.""" """Return icon from add-on."""
addon = self._extract_addon(request, check_installed=False) addon = self._extract_addon(request, check_installed=False)
if not addon.with_icon: if not addon.with_icon:
raise RuntimeError("No icon found!") raise RuntimeError("No icon found!")
@ -306,7 +293,7 @@ class APIAddons(CoreSysAttributes):
@api_process_raw(CONTENT_TYPE_PNG) @api_process_raw(CONTENT_TYPE_PNG)
async def logo(self, request): async def logo(self, request):
"""Return logo from addon.""" """Return logo from add-on."""
addon = self._extract_addon(request, check_installed=False) addon = self._extract_addon(request, check_installed=False)
if not addon.with_logo: if not addon.with_logo:
raise RuntimeError("No logo found!") raise RuntimeError("No logo found!")
@ -316,7 +303,7 @@ class APIAddons(CoreSysAttributes):
@api_process_raw(CONTENT_TYPE_TEXT) @api_process_raw(CONTENT_TYPE_TEXT)
async def changelog(self, request): async def changelog(self, request):
"""Return changelog from addon.""" """Return changelog from add-on."""
addon = self._extract_addon(request, check_installed=False) addon = self._extract_addon(request, check_installed=False)
if not addon.with_changelog: if not addon.with_changelog:
raise RuntimeError("No changelog found!") raise RuntimeError("No changelog found!")
@ -326,10 +313,26 @@ class APIAddons(CoreSysAttributes):
@api_process @api_process
async def stdin(self, request): async def stdin(self, request):
"""Write to stdin of addon.""" """Write to stdin of add-on."""
addon = self._extract_addon(request) addon = self._extract_addon(request)
if not addon.with_stdin: if not addon.with_stdin:
raise RuntimeError("STDIN not supported by addon") raise RuntimeError("STDIN not supported by add-on")
data = await request.read() data = await request.read()
return await asyncio.shield(addon.write_stdin(data)) return await asyncio.shield(addon.write_stdin(data))
def _pretty_devices(addon):
"""Return a simplified device list."""
dev_list = addon.devices
if not dev_list:
return None
return [row.split(':')[0] for row in dev_list]
def _pretty_services(addon):
"""Return a simplified services role list."""
services = []
for name, access in addon.services_role.items():
services.append(f"{name}:{access}")
return services

View File

@ -1,38 +1,48 @@
"""Init file for HassIO network rest api.""" """Init file for Hass.io network RESTful API."""
import voluptuous as vol import voluptuous as vol
from .utils import api_process, api_validate from .utils import api_process, api_validate
from ..const import ( from ..const import (
ATTR_PROVIDER, ATTR_UUID, ATTR_COMPONENT, ATTR_PLATFORM, ATTR_CONFIG, ATTR_ADDON, ATTR_UUID, ATTR_COMPONENT, ATTR_PLATFORM, ATTR_CONFIG,
ATTR_DISCOVERY, REQUEST_FROM) ATTR_DISCOVERY, ATTR_SERVICE, REQUEST_FROM)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIError, APIForbidden
from ..validate import SERVICE_ALL
SCHEMA_DISCOVERY = vol.Schema({ SCHEMA_DISCOVERY = vol.Schema({
vol.Required(ATTR_SERVICE): SERVICE_ALL,
vol.Required(ATTR_COMPONENT): vol.Coerce(str), vol.Required(ATTR_COMPONENT): vol.Coerce(str),
vol.Optional(ATTR_PLATFORM): vol.Any(None, vol.Coerce(str)), vol.Optional(ATTR_PLATFORM): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_CONFIG): vol.Any(None, dict), vol.Optional(ATTR_CONFIG): vol.Maybe(dict),
}) })
class APIDiscovery(CoreSysAttributes): class APIDiscovery(CoreSysAttributes):
"""Handle rest api for discovery functions.""" """Handle RESTful API for discovery functions."""
def _extract_message(self, request): def _extract_message(self, request):
"""Extract discovery message from URL.""" """Extract discovery message from URL."""
message = self.sys_discovery.get(request.match_info.get('uuid')) message = self.sys_discovery.get(request.match_info.get('uuid'))
if not message: if not message:
raise RuntimeError("Discovery message not found") raise APIError("Discovery message not found")
return message return message
def _check_permission_ha(self, request):
"""Check permission for API call / Home Assistant."""
if request[REQUEST_FROM] != self.sys_homeassistant:
raise APIForbidden("Only HomeAssistant can use this API!")
@api_process @api_process
async def list(self, request): async def list(self, request):
"""Show register services.""" """Show register services."""
self._check_permission_ha(request)
discovery = [] discovery = []
for message in self.sys_discovery.list_messages: for message in self.sys_discovery.list_messages:
discovery.append({ discovery.append({
ATTR_PROVIDER: message.provider, ATTR_ADDON: message.addon,
ATTR_SERVICE: message.service,
ATTR_UUID: message.uuid, ATTR_UUID: message.uuid,
ATTR_COMPONENT: message.component, ATTR_COMPONENT: message.component,
ATTR_PLATFORM: message.platform, ATTR_PLATFORM: message.platform,
@ -45,8 +55,14 @@ class APIDiscovery(CoreSysAttributes):
async def set_discovery(self, request): async def set_discovery(self, request):
"""Write data into a discovery pipeline.""" """Write data into a discovery pipeline."""
body = await api_validate(SCHEMA_DISCOVERY, request) body = await api_validate(SCHEMA_DISCOVERY, request)
message = self.sys_discovery.send( addon = request[REQUEST_FROM]
provider=request[REQUEST_FROM], **body)
# Access?
if body[ATTR_SERVICE] not in addon.discovery:
raise APIForbidden(f"Can't use discovery!")
# Process discovery message
message = self.sys_discovery.send(addon, **body)
return {ATTR_UUID: message.uuid} return {ATTR_UUID: message.uuid}
@ -55,8 +71,12 @@ class APIDiscovery(CoreSysAttributes):
"""Read data into a discovery message.""" """Read data into a discovery message."""
message = self._extract_message(request) message = self._extract_message(request)
# HomeAssistant?
self._check_permission_ha(request)
return { return {
ATTR_PROVIDER: message.provider, ATTR_ADDON: message.addon,
ATTR_SERVICE: message.service,
ATTR_UUID: message.uuid, ATTR_UUID: message.uuid,
ATTR_COMPONENT: message.component, ATTR_COMPONENT: message.component,
ATTR_PLATFORM: message.platform, ATTR_PLATFORM: message.platform,
@ -67,6 +87,11 @@ class APIDiscovery(CoreSysAttributes):
async def del_discovery(self, request): async def del_discovery(self, request):
"""Delete data into a discovery message.""" """Delete data into a discovery message."""
message = self._extract_message(request) message = self._extract_message(request)
addon = request[REQUEST_FROM]
# Permission
if message.addon != addon.slug:
raise APIForbidden(f"Can't remove discovery message")
self.sys_discovery.remove(message) self.sys_discovery.remove(message)
return True return True

View File

@ -1,4 +1,4 @@
"""Init file for HassIO hardware rest api.""" """Init file for Hass.io hardware RESTful API."""
import logging import logging
from .utils import api_process from .utils import api_process
@ -10,7 +10,7 @@ _LOGGER = logging.getLogger(__name__)
class APIHardware(CoreSysAttributes): class APIHardware(CoreSysAttributes):
"""Handle rest api for hardware functions.""" """Handle RESTful API for hardware functions."""
@api_process @api_process
async def info(self, request): async def info(self, request):

View File

@ -1,4 +1,4 @@
"""Init file for Hass.io hassos rest api.""" """Init file for Hass.io HassOS RESTful API."""
import asyncio import asyncio
import logging import logging
@ -18,11 +18,11 @@ SCHEMA_VERSION = vol.Schema({
class APIHassOS(CoreSysAttributes): class APIHassOS(CoreSysAttributes):
"""Handle rest api for hassos functions.""" """Handle RESTful API for HassOS functions."""
@api_process @api_process
async def info(self, request): async def info(self, request):
"""Return hassos information.""" """Return HassOS information."""
return { return {
ATTR_VERSION: self.sys_hassos.version, ATTR_VERSION: self.sys_hassos.version,
ATTR_VERSION_CLI: self.sys_hassos.version_cli, ATTR_VERSION_CLI: self.sys_hassos.version_cli,

View File

@ -1,4 +1,4 @@
"""Init file for HassIO homeassistant rest api.""" """Init file for Hass.io Home Assistant RESTful API."""
import asyncio import asyncio
import logging import logging
@ -13,6 +13,7 @@ from ..const import (
ATTR_REFRESH_TOKEN, CONTENT_TYPE_BINARY) ATTR_REFRESH_TOKEN, CONTENT_TYPE_BINARY)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..validate import NETWORK_PORT, DOCKER_IMAGE from ..validate import NETWORK_PORT, DOCKER_IMAGE
from ..exceptions import APIError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -39,7 +40,7 @@ SCHEMA_VERSION = vol.Schema({
class APIHomeAssistant(CoreSysAttributes): class APIHomeAssistant(CoreSysAttributes):
"""Handle rest api for homeassistant functions.""" """Handle RESTful API for Home Assistant functions."""
@api_process @api_process
async def info(self, request): async def info(self, request):
@ -59,7 +60,7 @@ class APIHomeAssistant(CoreSysAttributes):
@api_process @api_process
async def options(self, request): async def options(self, request):
"""Set homeassistant options.""" """Set Home Assistant options."""
body = await api_validate(SCHEMA_OPTIONS, request) body = await api_validate(SCHEMA_OPTIONS, request)
if ATTR_IMAGE in body and ATTR_LAST_VERSION in body: if ATTR_IMAGE in body and ATTR_LAST_VERSION in body:
@ -94,7 +95,7 @@ class APIHomeAssistant(CoreSysAttributes):
"""Return resource information.""" """Return resource information."""
stats = await self.sys_homeassistant.stats() stats = await self.sys_homeassistant.stats()
if not stats: if not stats:
raise RuntimeError("No stats available") raise APIError("No stats available")
return { return {
ATTR_CPU_PERCENT: stats.cpu_percent, ATTR_CPU_PERCENT: stats.cpu_percent,
@ -108,7 +109,7 @@ class APIHomeAssistant(CoreSysAttributes):
@api_process @api_process
async def update(self, request): async def update(self, request):
"""Update homeassistant.""" """Update Home Assistant."""
body = await api_validate(SCHEMA_VERSION, request) body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.sys_homeassistant.last_version) version = body.get(ATTR_VERSION, self.sys_homeassistant.last_version)
@ -116,29 +117,29 @@ class APIHomeAssistant(CoreSysAttributes):
@api_process @api_process
def stop(self, request): def stop(self, request):
"""Stop homeassistant.""" """Stop Home Assistant."""
return asyncio.shield(self.sys_homeassistant.stop()) return asyncio.shield(self.sys_homeassistant.stop())
@api_process @api_process
def start(self, request): def start(self, request):
"""Start homeassistant.""" """Start Home Assistant."""
return asyncio.shield(self.sys_homeassistant.start()) return asyncio.shield(self.sys_homeassistant.start())
@api_process @api_process
def restart(self, request): def restart(self, request):
"""Restart homeassistant.""" """Restart Home Assistant."""
return asyncio.shield(self.sys_homeassistant.restart()) return asyncio.shield(self.sys_homeassistant.restart())
@api_process_raw(CONTENT_TYPE_BINARY) @api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request): def logs(self, request):
"""Return homeassistant docker logs.""" """Return Home Assistant Docker logs."""
return self.sys_homeassistant.logs() return self.sys_homeassistant.logs()
@api_process @api_process
async def check(self, request): async def check(self, request):
"""Check config of homeassistant.""" """Check configuration of Home Assistant."""
result = await self.sys_homeassistant.check_config() result = await self.sys_homeassistant.check_config()
if not result.valid: if not result.valid:
raise RuntimeError(result.log) raise APIError(result.log)
return True return True

View File

@ -1,4 +1,4 @@
"""Init file for HassIO host rest api.""" """Init file for Hass.io host RESTful API."""
import asyncio import asyncio
import logging import logging
@ -21,7 +21,7 @@ SCHEMA_OPTIONS = vol.Schema({
class APIHost(CoreSysAttributes): class APIHost(CoreSysAttributes):
"""Handle rest api for host functions.""" """Handle RESTful API for host functions."""
@api_process @api_process
async def info(self, request): async def info(self, request):

File diff suppressed because one or more lines are too long

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

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

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,652 @@
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[4],{102:function(n,r,t){"use strict";t.r(r),t.d(r,"marked",function(){return a}),t.d(r,"filterXSS",function(){return c});var e=t(91),i=t.n(e),o=t(93),u=t.n(o),a=i.a,c=u.a}}]);
//# sourceMappingURL=chunk.7ee37c2565bcf2d88182.js.map

Binary file not shown.

View File

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///../src/resources/load_markdown.js"],"names":["__webpack_require__","r","__webpack_exports__","d","marked","filterXSS","marked__WEBPACK_IMPORTED_MODULE_0__","marked__WEBPACK_IMPORTED_MODULE_0___default","n","xss__WEBPACK_IMPORTED_MODULE_1__","xss__WEBPACK_IMPORTED_MODULE_1___default","a"],"mappings":"0FAAAA,EAAAC,EAAAC,GAAAF,EAAAG,EAAAD,EAAA,2BAAAE,IAAAJ,EAAAG,EAAAD,EAAA,8BAAAG,IAAA,IAAAC,EAAAN,EAAA,IAAAO,EAAAP,EAAAQ,EAAAF,GAAAG,EAAAT,EAAA,IAAAU,EAAAV,EAAAQ,EAAAC,GAGaL,EAASG,EAAAI,EACTN,EAAYK,EAAAC","file":"chunk.7ee37c2565bcf2d88182.js","sourcesContent":["import marked_ from 'marked';\nimport filterXSS_ from 'xss';\n\nexport const marked = marked_;\nexport const filterXSS = filterXSS_;\n"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

View File

@ -1,419 +0,0 @@
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/

File diff suppressed because one or more lines are too long

View File

@ -138,89 +138,101 @@ Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/ */
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/** /**
@license @license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved. Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt

Binary file not shown.

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[5],{104:function(n,r,t){"use strict";t.r(r),t.d(r,"marked",function(){return a}),t.d(r,"filterXSS",function(){return c});var e=t(99),i=t.n(e),o=t(97),u=t.n(o),a=i.a,c=u.a}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1,2 @@
!function(e){function n(n){for(var t,o,i=n[0],u=n[1],a=0,l=[];a<i.length;a++)o=i[a],r[o]&&l.push(r[o][0]),r[o]=0;for(t in u)Object.prototype.hasOwnProperty.call(u,t)&&(e[t]=u[t]);for(f&&f(n);l.length;)l.shift()()}var t={},r={6:0};function o(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,o),r.l=!0,r.exports}o.e=function(e){var n=[],t=r[e];if(0!==t)if(t)n.push(t[2]);else{var i=new Promise(function(n,o){t=r[e]=[n,o]});n.push(t[2]=i);var u,a=document.getElementsByTagName("head")[0],f=document.createElement("script");f.charset="utf-8",f.timeout=120,o.nc&&f.setAttribute("nonce",o.nc),f.src=function(e){return o.p+"chunk."+{0:"f3880aa331d3ef2ddf32",1:"a8e86d80be46b3b6e16d",2:"0ef4ef1053fe3d5107b5",3:"ff92199b0d422767d108",4:"c77b56beea1d4547ff5f",5:"c93f37c558ff32991708"}[e]+".js"}(e),u=function(n){f.onerror=f.onload=null,clearTimeout(l);var t=r[e];if(0!==t){if(t){var o=n&&("load"===n.type?"missing":n.type),i=n&&n.target&&n.target.src,u=new Error("Loading chunk "+e+" failed.\n("+o+": "+i+")");u.type=o,u.request=i,t[1](u)}r[e]=void 0}};var l=setTimeout(function(){u({type:"timeout",target:f})},12e4);f.onerror=f.onload=u,a.appendChild(f)}return Promise.all(n)},o.m=e,o.c=t,o.d=function(e,n,t){o.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:t})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(e,n){if(1&n&&(e=o(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(o.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var r in e)o.d(t,r,function(n){return e[n]}.bind(null,r));return t},o.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(n,"a",n),n},o.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},o.p="/api/hassio/app/",o.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],u=i.push.bind(i);i.push=n,i=i.slice();for(var a=0;a<i.length;a++)n(i[a]);var f=u;o(o.s=0)}([function(e,n,t){window.loadES5Adapter().then(function(){Promise.all([t.e(0),t.e(3)]).then(t.bind(null,1)),Promise.all([t.e(0),t.e(1),t.e(2)]).then(t.bind(null,2))})}]); !function(e){function n(n){for(var t,o,a=n[0],i=n[1],u=0,l=[];u<a.length;u++)o=a[u],r[o]&&l.push(r[o][0]),r[o]=0;for(t in i)Object.prototype.hasOwnProperty.call(i,t)&&(e[t]=i[t]);for(c&&c(n);l.length;)l.shift()()}var t={},r={1:0};function o(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,o),r.l=!0,r.exports}o.e=function(e){var n=[],t=r[e];if(0!==t)if(t)n.push(t[2]);else{var a=new Promise(function(n,o){t=r[e]=[n,o]});n.push(t[2]=a);var i,u=document.getElementsByTagName("head")[0],c=document.createElement("script");c.charset="utf-8",c.timeout=120,o.nc&&c.setAttribute("nonce",o.nc),c.src=function(e){return o.p+"chunk."+{0:"a8fa5591357cce978816",2:"457ac71b0904d7243237",3:"57f5b43a82b988080555",4:"7ee37c2565bcf2d88182",5:"72a6da063fe4cb6308e8",6:"ad9001ac29bd3acbb520"}[e]+".js"}(e),i=function(n){c.onerror=c.onload=null,clearTimeout(l);var t=r[e];if(0!==t){if(t){var o=n&&("load"===n.type?"missing":n.type),a=n&&n.target&&n.target.src,i=new Error("Loading chunk "+e+" failed.\n("+o+": "+a+")");i.type=o,i.request=a,t[1](i)}r[e]=void 0}};var l=setTimeout(function(){i({type:"timeout",target:c})},12e4);c.onerror=c.onload=i,u.appendChild(c)}return Promise.all(n)},o.m=e,o.c=t,o.d=function(e,n,t){o.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:t})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(e,n){if(1&n&&(e=o(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(o.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var r in e)o.d(t,r,function(n){return e[n]}.bind(null,r));return t},o.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(n,"a",n),n},o.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},o.p="/api/hassio/app/",o.oe=function(e){throw console.error(e),e};var a=window.webpackJsonp=window.webpackJsonp||[],i=a.push.bind(a);a.push=n,a=a.slice();for(var u=0;u<a.length;u++)n(a[u]);var c=i;o(o.s=0)}([function(e,n,t){window.loadES5Adapter().then(function(){Promise.all([t.e(0),t.e(2)]).then(t.bind(null,2)),Promise.all([t.e(0),t.e(5),t.e(3)]).then(t.bind(null,1))})}]);
//# sourceMappingURL=entrypoint.js.map

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
"""Utils for HomeAssistant Proxy.""" """Utils for Home Assistant Proxy."""
import asyncio import asyncio
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import logging import logging
@ -7,7 +7,7 @@ import aiohttp
from aiohttp import web from aiohttp import web
from aiohttp.web_exceptions import ( from aiohttp.web_exceptions import (
HTTPBadGateway, HTTPInternalServerError, HTTPUnauthorized) HTTPBadGateway, HTTPInternalServerError, HTTPUnauthorized)
from aiohttp.hdrs import CONTENT_TYPE from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION
import async_timeout import async_timeout
from ..const import HEADER_HA_ACCESS from ..const import HEADER_HA_ACCESS
@ -18,19 +18,19 @@ _LOGGER = logging.getLogger(__name__)
class APIProxy(CoreSysAttributes): class APIProxy(CoreSysAttributes):
"""API Proxy for Home-Assistant.""" """API Proxy for Home Assistant."""
def _check_access(self, request): def _check_access(self, request):
"""Check the Hass.io token.""" """Check the Hass.io token."""
hassio_token = request.headers.get(HEADER_HA_ACCESS) if AUTHORIZATION in request.headers:
bearer = request.headers[AUTHORIZATION]
hassio_token = bearer.split(' ')[-1]
else:
hassio_token = request.headers.get(HEADER_HA_ACCESS)
addon = self.sys_addons.from_token(hassio_token) addon = self.sys_addons.from_token(hassio_token)
# REMOVE 132
if not addon: if not addon:
addon = self.sys_addons.from_uuid(hassio_token) _LOGGER.warning("Unknown Home Assistant API access!")
if not addon:
_LOGGER.warning("Unknown HomeAssistant API access!")
elif not addon.access_homeassistant_api: elif not addon.access_homeassistant_api:
_LOGGER.warning("Not permitted API access: %s", addon.slug) _LOGGER.warning("Not permitted API access: %s", addon.slug)
else: else:
@ -41,7 +41,7 @@ class APIProxy(CoreSysAttributes):
@asynccontextmanager @asynccontextmanager
async def _api_client(self, request, path, timeout=300): async def _api_client(self, request, path, timeout=300):
"""Return a client request with proxy origin for Home-Assistant.""" """Return a client request with proxy origin for Home Assistant."""
try: try:
# read data # read data
with async_timeout.timeout(30): with async_timeout.timeout(30):
@ -76,7 +76,7 @@ class APIProxy(CoreSysAttributes):
"""Proxy HomeAssistant EventStream Requests.""" """Proxy HomeAssistant EventStream Requests."""
self._check_access(request) self._check_access(request)
_LOGGER.info("Home-Assistant EventStream start") _LOGGER.info("Home Assistant EventStream start")
async with self._api_client(request, 'stream', timeout=None) as client: async with self._api_client(request, 'stream', timeout=None) as client:
response = web.StreamResponse() response = web.StreamResponse()
response.content_type = request.headers.get(CONTENT_TYPE) response.content_type = request.headers.get(CONTENT_TYPE)
@ -93,12 +93,12 @@ class APIProxy(CoreSysAttributes):
finally: finally:
client.close() client.close()
_LOGGER.info("Home-Assistant EventStream close") _LOGGER.info("Home Assistant EventStream close")
return response return response
async def api(self, request): async def api(self, request):
"""Proxy HomeAssistant API Requests.""" """Proxy Home Assistant API Requests."""
self._check_access(request) self._check_access(request)
# Normal request # Normal request
@ -112,14 +112,14 @@ class APIProxy(CoreSysAttributes):
) )
async def _websocket_client(self): async def _websocket_client(self):
"""Initialize a websocket api connection.""" """Initialize a WebSocket API connection."""
url = f"{self.sys_homeassistant.api_url}/api/websocket" url = f"{self.sys_homeassistant.api_url}/api/websocket"
try: try:
client = await self.sys_websession_ssl.ws_connect( client = await self.sys_websession_ssl.ws_connect(
url, heartbeat=60, verify_ssl=False) url, heartbeat=60, verify_ssl=False)
# handle authentication # Handle authentication
data = await client.receive_json() data = await client.receive_json()
if data.get('type') == 'auth_ok': if data.get('type') == 'auth_ok':
@ -128,7 +128,7 @@ class APIProxy(CoreSysAttributes):
if data.get('type') != 'auth_required': if data.get('type') != 'auth_required':
# Invalid protocol # Invalid protocol
_LOGGER.error( _LOGGER.error(
'Got unexpected response from HA websocket: %s', data) "Got unexpected response from HA WebSocket: %s", data)
raise HTTPBadGateway() raise HTTPBadGateway()
if self.sys_homeassistant.refresh_token: if self.sys_homeassistant.refresh_token:
@ -157,15 +157,15 @@ class APIProxy(CoreSysAttributes):
raise HomeAssistantAuthError() raise HomeAssistantAuthError()
except (RuntimeError, ValueError) as err: except (RuntimeError, ValueError) as err:
_LOGGER.error("Client error on websocket API %s.", err) _LOGGER.error("Client error on WebSocket API %s.", err)
except HomeAssistantAuthError as err: except HomeAssistantAuthError as err:
_LOGGER.error("Failed authentication to HomeAssistant websocket") _LOGGER.error("Failed authentication to Home Assistant WebSocket")
raise HTTPBadGateway() raise HTTPBadGateway()
async def websocket(self, request): async def websocket(self, request):
"""Initialize a websocket api connection.""" """Initialize a WebSocket API connection."""
_LOGGER.info("Home-Assistant Websocket API request initialze") _LOGGER.info("Home Assistant WebSocket API request initialize")
# init server # init server
server = web.WebSocketResponse(heartbeat=60) server = web.WebSocketResponse(heartbeat=60)
@ -184,19 +184,15 @@ class APIProxy(CoreSysAttributes):
response.get('access_token')) response.get('access_token'))
addon = self.sys_addons.from_token(hassio_token) addon = self.sys_addons.from_token(hassio_token)
# REMOVE 132
if not addon:
addon = self.sys_addons.from_uuid(hassio_token)
if not addon or not addon.access_homeassistant_api: if not addon or not addon.access_homeassistant_api:
_LOGGER.warning("Unauthorized websocket access!") _LOGGER.warning("Unauthorized WebSocket access!")
await server.send_json({ await server.send_json({
'type': 'auth_invalid', 'type': 'auth_invalid',
'message': 'Invalid access', 'message': 'Invalid access',
}) })
return server return server
_LOGGER.info("Websocket access from %s", addon.slug) _LOGGER.info("WebSocket access from %s", addon.slug)
await server.send_json({ await server.send_json({
'type': 'auth_ok', 'type': 'auth_ok',
@ -209,7 +205,7 @@ class APIProxy(CoreSysAttributes):
# init connection to hass # init connection to hass
client = await self._websocket_client() client = await self._websocket_client()
_LOGGER.info("Home-Assistant Websocket API request running") _LOGGER.info("Home Assistant WebSocket API request running")
try: try:
client_read = None client_read = None
server_read = None server_read = None
@ -243,7 +239,7 @@ class APIProxy(CoreSysAttributes):
pass pass
except RuntimeError as err: except RuntimeError as err:
_LOGGER.info("Home-Assistant Websocket API error: %s", err) _LOGGER.info("Home Assistant WebSocket API error: %s", err)
finally: finally:
if client_read: if client_read:
@ -255,5 +251,5 @@ class APIProxy(CoreSysAttributes):
await client.close() await client.close()
await server.close() await server.close()
_LOGGER.info("Home-Assistant Websocket API connection is closed") _LOGGER.info("Home Assistant WebSocket API connection is closed")
return server return server

View File

@ -12,22 +12,30 @@ from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Block Anytime
BLACKLIST = re.compile(
r"^(?:"
r"|/homeassistant/api/hassio/.*"
r")$"
)
# Free to call or have own security concepts # Free to call or have own security concepts
NO_SECURITY_CHECK = re.compile( NO_SECURITY_CHECK = re.compile(
r"^(?:" r"^(?:"
r"|/homeassistant/api/.*" r"|/homeassistant/api/.*"
r"|/homeassistant/websocket" r"|/homeassistant/websocket"
r"|/supervisor/ping" r"|/supervisor/ping"
r"|/services.*"
r")$" r")$"
) )
# Can called by every add-on # Can called by every add-on
ADDONS_API_BYPASS = re.compile( ADDONS_API_BYPASS = re.compile(
r"^(?:" r"^(?:"
r"|/homeassistant/info" r"|/addons/self/(?!security)[^/]+"
r"|/supervisor/info" r"|/version"
r"|/addons(?:/self/(?!security)[^/]+)?" r"|/services.*"
r"|/discovery.*"
r")$" r")$"
) )
@ -36,6 +44,7 @@ ADDONS_ROLE_ACCESS = {
ROLE_DEFAULT: re.compile( ROLE_DEFAULT: re.compile(
r"^(?:" r"^(?:"
r"|/[^/]+/info" r"|/[^/]+/info"
r"|/addons"
r")$" r")$"
), ),
ROLE_HOMEASSISTANT: re.compile( ROLE_HOMEASSISTANT: re.compile(
@ -50,13 +59,12 @@ ADDONS_ROLE_ACCESS = {
r"|/hardware/.+" r"|/hardware/.+"
r"|/hassos/.+" r"|/hassos/.+"
r"|/supervisor/.+" r"|/supervisor/.+"
r"|/addons/.+/(?!security|options).+" r"|/addons/[^/]+/(?!security).+"
r"|/addons(?:/self/(?!security).+)"
r"|/snapshots.*" r"|/snapshots.*"
r")$" r")$"
), ),
ROLE_ADMIN: re.compile( ROLE_ADMIN: re.compile(
r".+" r".*"
), ),
} }
@ -74,6 +82,11 @@ class SecurityMiddleware(CoreSysAttributes):
request_from = None request_from = None
hassio_token = request.headers.get(HEADER_TOKEN) hassio_token = request.headers.get(HEADER_TOKEN)
# Blacklist
if BLACKLIST.match(request.path):
_LOGGER.warning("%s is blacklisted!", request.path)
raise HTTPForbidden()
# Ignore security check # Ignore security check
if NO_SECURITY_CHECK.match(request.path): if NO_SECURITY_CHECK.match(request.path):
_LOGGER.debug("Passthrough %s", request.path) _LOGGER.debug("Passthrough %s", request.path)
@ -88,38 +101,34 @@ class SecurityMiddleware(CoreSysAttributes):
# UUID check need removed with 131 # UUID check need removed with 131
if hassio_token in (self.sys_homeassistant.uuid, if hassio_token in (self.sys_homeassistant.uuid,
self.sys_homeassistant.hassio_token): self.sys_homeassistant.hassio_token):
_LOGGER.debug("%s access from Home-Assistant", request.path) _LOGGER.debug("%s access from Home Assistant", request.path)
request_from = 'homeassistant' request_from = self.sys_homeassistant
# Host # Host
if hassio_token == self.sys_machine_id: if hassio_token == self.sys_machine_id:
_LOGGER.debug("%s access from Host", request.path) _LOGGER.debug("%s access from Host", request.path)
request_from = 'host' request_from = self.sys_host
# Add-on # Add-on
addon = None addon = None
if hassio_token and not request_from: if hassio_token and not request_from:
addon = self.sys_addons.from_token(hassio_token) addon = self.sys_addons.from_token(hassio_token)
# REMOVE 132
if not addon:
addon = self.sys_addons.from_uuid(hassio_token)
# Check Add-on API access # Check Add-on API access
if addon and ADDONS_API_BYPASS.match(request.path): if addon and ADDONS_API_BYPASS.match(request.path):
_LOGGER.debug("Passthrough %s from %s", request.path, addon.slug) _LOGGER.debug("Passthrough %s from %s", request.path, addon.slug)
request_from = addon.slug request_from = addon
elif addon and addon.access_hassio_api: elif addon and addon.access_hassio_api:
# Check Role # Check Role
if ADDONS_ROLE_ACCESS[addon.hassio_role].match(request.path): if ADDONS_ROLE_ACCESS[addon.hassio_role].match(request.path):
_LOGGER.info("%s access from %s", request.path, addon.slug) _LOGGER.info("%s access from %s", request.path, addon.slug)
request_from = addon.slug request_from = addon
else: else:
_LOGGER.warning("%s no role for %s", request.path, addon.slug) _LOGGER.warning("%s no role for %s", request.path, addon.slug)
request_from = addon.slug # REMOVE: 132
if request_from: if request_from:
request[REQUEST_FROM] = request_from request[REQUEST_FROM] = request_from
return await handler(request) return await handler(request)
_LOGGER.warning("Invalid token for access %s", request.path) _LOGGER.error("Invalid token for access %s", request.path)
raise HTTPForbidden() raise HTTPForbidden()

View File

@ -1,19 +1,21 @@
"""Init file for HassIO network rest api.""" """Init file for Hass.io network RESTful API."""
from .utils import api_process, api_validate from .utils import api_process, api_validate
from ..const import ( from ..const import (
ATTR_AVAILABLE, ATTR_PROVIDER, ATTR_SLUG, ATTR_SERVICES, REQUEST_FROM) ATTR_AVAILABLE, ATTR_PROVIDERS, ATTR_SLUG, ATTR_SERVICES, REQUEST_FROM,
PROVIDE_SERVICE)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIError, APIForbidden
class APIServices(CoreSysAttributes): class APIServices(CoreSysAttributes):
"""Handle rest api for services functions.""" """Handle RESTful API for services functions."""
def _extract_service(self, request): def _extract_service(self, request):
"""Return service, throw an exception if it doesn't exist.""" """Return service, throw an exception if it doesn't exist."""
service = self.sys_services.get(request.match_info.get('service')) service = self.sys_services.get(request.match_info.get('service'))
if not service: if not service:
raise RuntimeError("Service does not exist") raise APIError("Service does not exist")
return service return service
@ -25,7 +27,7 @@ class APIServices(CoreSysAttributes):
services.append({ services.append({
ATTR_SLUG: service.slug, ATTR_SLUG: service.slug,
ATTR_AVAILABLE: service.enabled, ATTR_AVAILABLE: service.enabled,
ATTR_PROVIDER: service.provider, ATTR_PROVIDERS: service.providers,
}) })
return {ATTR_SERVICES: services} return {ATTR_SERVICES: services}
@ -35,21 +37,39 @@ class APIServices(CoreSysAttributes):
"""Write data into a service.""" """Write data into a service."""
service = self._extract_service(request) service = self._extract_service(request)
body = await api_validate(service.schema, request) body = await api_validate(service.schema, request)
addon = request[REQUEST_FROM]
return service.set_service_data(request[REQUEST_FROM], body) _check_access(request, service.slug)
service.set_service_data(addon, body)
@api_process @api_process
async def get_service(self, request): async def get_service(self, request):
"""Read data into a service.""" """Read data into a service."""
service = self._extract_service(request) service = self._extract_service(request)
return { # Access
ATTR_AVAILABLE: service.enabled, _check_access(request, service.slug)
service.slug: service.get_service_data(),
} if not service.enabled:
raise APIError("Service not enabled")
return service.get_service_data()
@api_process @api_process
async def del_service(self, request): async def del_service(self, request):
"""Delete data into a service.""" """Delete data into a service."""
service = self._extract_service(request) service = self._extract_service(request)
return service.del_service_data(request[REQUEST_FROM]) addon = request[REQUEST_FROM]
# Access
_check_access(request, service.slug, True)
service.del_service_data(addon)
def _check_access(request, service, provide=False):
"""Raise error if the rights are wrong."""
addon = request[REQUEST_FROM]
if not addon.services_role.get(service):
raise APIForbidden(f"No access to {service} service!")
if provide and addon.services_role.get(service) != PROVIDE_SERVICE:
raise APIForbidden(f"No access to write {service} service!")

View File

@ -1,4 +1,4 @@
"""Init file for HassIO snapshot rest api.""" """Init file for Hass.io snapshot RESTful API."""
import asyncio import asyncio
import logging import logging
from pathlib import Path from pathlib import Path
@ -14,6 +14,7 @@ from ..const import (
ATTR_HOMEASSISTANT, ATTR_VERSION, ATTR_SIZE, ATTR_FOLDERS, ATTR_TYPE, ATTR_HOMEASSISTANT, ATTR_VERSION, ATTR_SIZE, ATTR_FOLDERS, ATTR_TYPE,
ATTR_SNAPSHOTS, ATTR_PASSWORD, ATTR_PROTECTED, CONTENT_TYPE_TAR) ATTR_SNAPSHOTS, ATTR_PASSWORD, ATTR_PROTECTED, CONTENT_TYPE_TAR)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -46,13 +47,13 @@ SCHEMA_SNAPSHOT_PARTIAL = SCHEMA_SNAPSHOT_FULL.extend({
class APISnapshots(CoreSysAttributes): class APISnapshots(CoreSysAttributes):
"""Handle rest api for snapshot functions.""" """Handle RESTful API for snapshot functions."""
def _extract_snapshot(self, request): def _extract_snapshot(self, request):
"""Return snapshot, throw an exception if it doesn't exist.""" """Return snapshot, throw an exception if it doesn't exist."""
snapshot = self.sys_snapshots.get(request.match_info.get('snapshot')) snapshot = self.sys_snapshots.get(request.match_info.get('snapshot'))
if not snapshot: if not snapshot:
raise RuntimeError("Snapshot does not exist") raise APIError("Snapshot does not exist")
return snapshot return snapshot
@api_process @api_process

View File

@ -1,4 +1,4 @@
"""Init file for HassIO supervisor rest api.""" """Init file for Hass.io Supervisor RESTful API."""
import asyncio import asyncio
import logging import logging
@ -13,7 +13,9 @@ from ..const import (
ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_BLK_READ, ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_BLK_READ,
ATTR_BLK_WRITE, CONTENT_TYPE_BINARY, ATTR_ICON) ATTR_BLK_WRITE, CONTENT_TYPE_BINARY, ATTR_ICON)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..validate import validate_timezone, WAIT_BOOT, REPOSITORIES, CHANNELS from ..validate import WAIT_BOOT, REPOSITORIES, CHANNELS
from ..exceptions import APIError
from ..utils.validate import validate_timezone
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,11 +32,11 @@ SCHEMA_VERSION = vol.Schema({
class APISupervisor(CoreSysAttributes): class APISupervisor(CoreSysAttributes):
"""Handle rest api for supervisor functions.""" """Handle RESTful API for Supervisor functions."""
@api_process @api_process
async def ping(self, request): async def ping(self, request):
"""Return ok for signal that the api is ready.""" """Return ok for signal that the API is ready."""
return True return True
@api_process @api_process
@ -68,7 +70,7 @@ class APISupervisor(CoreSysAttributes):
@api_process @api_process
async def options(self, request): async def options(self, request):
"""Set supervisor options.""" """Set Supervisor options."""
body = await api_validate(SCHEMA_OPTIONS, request) body = await api_validate(SCHEMA_OPTIONS, request)
if ATTR_CHANNEL in body: if ATTR_CHANNEL in body:
@ -93,7 +95,7 @@ class APISupervisor(CoreSysAttributes):
"""Return resource information.""" """Return resource information."""
stats = await self.sys_supervisor.stats() stats = await self.sys_supervisor.stats()
if not stats: if not stats:
raise RuntimeError("No stats available") raise APIError("No stats available")
return { return {
ATTR_CPU_PERCENT: stats.cpu_percent, ATTR_CPU_PERCENT: stats.cpu_percent,
@ -107,19 +109,19 @@ class APISupervisor(CoreSysAttributes):
@api_process @api_process
async def update(self, request): async def update(self, request):
"""Update supervisor OS.""" """Update Supervisor OS."""
body = await api_validate(SCHEMA_VERSION, request) body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.sys_updater.version_hassio) version = body.get(ATTR_VERSION, self.sys_updater.version_hassio)
if version == self.sys_supervisor.version: if version == self.sys_supervisor.version:
raise RuntimeError("Version {} is already in use".format(version)) raise APIError("Version {} is already in use".format(version))
return await asyncio.shield( return await asyncio.shield(
self.sys_supervisor.update(version)) self.sys_supervisor.update(version))
@api_process @api_process
async def reload(self, request): async def reload(self, request):
"""Reload addons, config etc.""" """Reload add-ons, configuration, etc."""
tasks = [ tasks = [
self.sys_updater.reload(), self.sys_updater.reload(),
] ]
@ -128,11 +130,11 @@ class APISupervisor(CoreSysAttributes):
for result in results: for result in results:
if result.exception() is not None: if result.exception() is not None:
raise RuntimeError("Some reload task fails!") raise APIError("Some reload task fails!")
return True return True
@api_process_raw(CONTENT_TYPE_BINARY) @api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request): def logs(self, request):
"""Return supervisor docker logs.""" """Return supervisor Docker logs."""
return self.sys_supervisor.logs() return self.sys_supervisor.logs()

View File

@ -1,4 +1,4 @@
"""Init file for HassIO util for rest api.""" """Init file for Hass.io util for RESTful API."""
import json import json
import logging import logging
@ -9,7 +9,7 @@ from voluptuous.humanize import humanize_error
from ..const import ( from ..const import (
JSON_RESULT, JSON_DATA, JSON_MESSAGE, RESULT_OK, RESULT_ERROR, JSON_RESULT, JSON_DATA, JSON_MESSAGE, RESULT_OK, RESULT_ERROR,
CONTENT_TYPE_BINARY) CONTENT_TYPE_BINARY)
from ..exceptions import HassioError from ..exceptions import HassioError, APIError, APIForbidden
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -21,19 +21,19 @@ def json_loads(data):
try: try:
return json.loads(data) return json.loads(data)
except json.JSONDecodeError: except json.JSONDecodeError:
raise RuntimeError("Invalid json") raise APIError("Invalid json")
def api_process(method): def api_process(method):
"""Wrap function with true/false calls to rest api.""" """Wrap function with true/false calls to rest api."""
async def wrap_api(api, *args, **kwargs): async def wrap_api(api, *args, **kwargs):
"""Return api information.""" """Return API information."""
try: try:
answer = await method(api, *args, **kwargs) answer = await method(api, *args, **kwargs)
except HassioError: except (APIError, APIForbidden) as err:
return api_return_error()
except RuntimeError as err:
return api_return_error(message=str(err)) return api_return_error(message=str(err))
except HassioError:
return api_return_error(message="Unknown Error, see logs")
if isinstance(answer, dict): if isinstance(answer, dict):
return api_return_ok(data=answer) return api_return_ok(data=answer)
@ -55,7 +55,7 @@ def api_process_raw(content):
try: try:
msg_data = await method(api, *args, **kwargs) msg_data = await method(api, *args, **kwargs)
msg_type = content msg_type = content
except RuntimeError as err: except (APIError, APIForbidden) as err:
msg_data = str(err).encode() msg_data = str(err).encode()
msg_type = CONTENT_TYPE_BINARY msg_type = CONTENT_TYPE_BINARY
except HassioError: except HassioError:
@ -90,6 +90,6 @@ async def api_validate(schema, request):
try: try:
data = schema(data) data = schema(data)
except vol.Invalid as ex: except vol.Invalid as ex:
raise RuntimeError(humanize_error(data, ex)) from None raise APIError(humanize_error(data, ex)) from None
return data return data

26
hassio/api/version.py Normal file
View File

@ -0,0 +1,26 @@
"""Init file for Hass.io version RESTful API."""
import logging
from .utils import api_process
from ..const import (
ATTR_HOMEASSISTANT, ATTR_SUPERVISOR, ATTR_MACHINE, ATTR_ARCH, ATTR_HASSOS,
ATTR_CHANNEL)
from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__)
class APIVersion(CoreSysAttributes):
"""Handle RESTful API for version functions."""
@api_process
async def info(self, request):
"""Show version info."""
return {
ATTR_SUPERVISOR: self.sys_supervisor.version,
ATTR_HOMEASSISTANT: self.sys_homeassistant.version,
ATTR_HASSOS: self.sys_hassos.version,
ATTR_MACHINE: self.sys_machine,
ATTR_ARCH: self.sys_arch,
ATTR_CHANNEL: self.sys_updater.channel,
}

View File

@ -18,7 +18,7 @@ from .snapshots import SnapshotManager
from .tasks import Tasks from .tasks import Tasks
from .updater import Updater from .updater import Updater
from .services import ServiceManager from .services import ServiceManager
from .services import Discovery from .discovery import Discovery
from .host import HostManager from .host import HostManager
from .dbus import DBusManager from .dbus import DBusManager
from .hassos import HassOS from .hassos import HassOS

View File

@ -2,7 +2,7 @@
from pathlib import Path from pathlib import Path
from ipaddress import ip_network from ipaddress import ip_network
HASSIO_VERSION = '131' HASSIO_VERSION = '132'
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons" URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
URL_HASSIO_VERSION = \ URL_HASSIO_VERSION = \
@ -21,6 +21,7 @@ FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json")
FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json") FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json")
FILE_HASSIO_UPDATER = Path(HASSIO_DATA, "updater.json") FILE_HASSIO_UPDATER = Path(HASSIO_DATA, "updater.json")
FILE_HASSIO_SERVICES = Path(HASSIO_DATA, "services.json") FILE_HASSIO_SERVICES = Path(HASSIO_DATA, "services.json")
FILE_HASSIO_DISCOVERY = Path(HASSIO_DATA, "discovery.json")
SOCKET_DOCKER = Path("/var/run/docker.sock") SOCKET_DOCKER = Path("/var/run/docker.sock")
@ -74,6 +75,7 @@ ATTR_TYPE = 'type'
ATTR_SOURCE = 'source' ATTR_SOURCE = 'source'
ATTR_FEATURES = 'features' ATTR_FEATURES = 'features'
ATTR_ADDONS = 'addons' ATTR_ADDONS = 'addons'
ATTR_PROVIDERS = 'providers'
ATTR_VERSION = 'version' ATTR_VERSION = 'version'
ATTR_VERSION_LATEST = 'version_latest' ATTR_VERSION_LATEST = 'version_latest'
ATTR_AUTO_UART = 'auto_uart' ATTR_AUTO_UART = 'auto_uart'
@ -107,8 +109,6 @@ ATTR_MAINTAINER = 'maintainer'
ATTR_PASSWORD = 'password' ATTR_PASSWORD = 'password'
ATTR_TOTP = 'totp' ATTR_TOTP = 'totp'
ATTR_INITIALIZE = 'initialize' ATTR_INITIALIZE = 'initialize'
ATTR_SESSION = 'session'
ATTR_SESSIONS = 'sessions'
ATTR_LOCATON = 'location' ATTR_LOCATON = 'location'
ATTR_BUILD = 'build' ATTR_BUILD = 'build'
ATTR_DEVICES = 'devices' ATTR_DEVICES = 'devices'
@ -154,7 +154,7 @@ ATTR_MEMORY_LIMIT = 'memory_limit'
ATTR_MEMORY_USAGE = 'memory_usage' ATTR_MEMORY_USAGE = 'memory_usage'
ATTR_BLK_READ = 'blk_read' ATTR_BLK_READ = 'blk_read'
ATTR_BLK_WRITE = 'blk_write' ATTR_BLK_WRITE = 'blk_write'
ATTR_PROVIDER = 'provider' ATTR_ADDON = 'addon'
ATTR_AVAILABLE = 'available' ATTR_AVAILABLE = 'available'
ATTR_HOST = 'host' ATTR_HOST = 'host'
ATTR_USERNAME = 'username' ATTR_USERNAME = 'username'
@ -163,8 +163,8 @@ ATTR_DISCOVERY = 'discovery'
ATTR_PLATFORM = 'platform' ATTR_PLATFORM = 'platform'
ATTR_COMPONENT = 'component' ATTR_COMPONENT = 'component'
ATTR_CONFIG = 'config' ATTR_CONFIG = 'config'
ATTR_DISCOVERY_ID = 'discovery_id'
ATTR_SERVICES = 'services' ATTR_SERVICES = 'services'
ATTR_SERVICE = 'service'
ATTR_DISCOVERY = 'discovery' ATTR_DISCOVERY = 'discovery'
ATTR_PROTECTED = 'protected' ATTR_PROTECTED = 'protected'
ATTR_CRYPTO = 'crypto' ATTR_CRYPTO = 'crypto'
@ -185,8 +185,12 @@ ATTR_FULL_ACCESS = 'full_access'
ATTR_PROTECTED = 'protected' ATTR_PROTECTED = 'protected'
ATTR_RATING = 'rating' ATTR_RATING = 'rating'
ATTR_HASSIO_ROLE = 'hassio_role' ATTR_HASSIO_ROLE = 'hassio_role'
ATTR_SUPERVISOR = 'supervisor'
SERVICE_MQTT = 'mqtt' SERVICE_MQTT = 'mqtt'
PROVIDE_SERVICE = 'provide'
NEED_SERVICE = 'need'
WANT_SERVICE = 'want'
STARTUP_INITIALIZE = 'initialize' STARTUP_INITIALIZE = 'initialize'
STARTUP_SYSTEM = 'system' STARTUP_SYSTEM = 'system'

View File

@ -52,6 +52,9 @@ class HassIO(CoreSysAttributes):
# load services # load services
await self.sys_services.load() await self.sys_services.load()
# Load discovery
await self.sys_discovery.load()
# start dns forwarding # start dns forwarding
self.sys_create_task(self.sys_dns.start()) self.sys_create_task(self.sys_dns.start())

View File

@ -16,7 +16,7 @@ class Systemd(DBusInterface):
"""Systemd function handler.""" """Systemd function handler."""
async def connect(self): async def connect(self):
"""Connect do bus.""" """Connect to D-Bus."""
try: try:
self.dbus = await DBus.connect(DBUS_NAME, DBUS_OBJECT) self.dbus = await DBus.connect(DBUS_NAME, DBUS_OBJECT)
except DBusError: except DBusError:

123
hassio/discovery.py Normal file
View File

@ -0,0 +1,123 @@
"""Handle discover message for Home Assistant."""
import logging
from contextlib import suppress
from uuid import uuid4
import attr
import voluptuous as vol
from voluptuous.humanize import humanize_error
from .const import FILE_HASSIO_DISCOVERY, ATTR_CONFIG, ATTR_DISCOVERY
from .coresys import CoreSysAttributes
from .exceptions import DiscoveryError, HomeAssistantAPIError
from .validate import SCHEMA_DISCOVERY_CONFIG
from .utils.json import JsonConfig
from .services.validate import DISCOVERY_SERVICES
_LOGGER = logging.getLogger(__name__)
CMD_NEW = 'post'
CMD_DEL = 'delete'
class Discovery(CoreSysAttributes, JsonConfig):
"""Home Assistant Discovery handler."""
def __init__(self, coresys):
"""Initialize discovery handler."""
super().__init__(FILE_HASSIO_DISCOVERY, SCHEMA_DISCOVERY_CONFIG)
self.coresys = coresys
self.message_obj = {}
async def load(self):
"""Load exists discovery message into storage."""
messages = {}
for message in self._data[ATTR_DISCOVERY]:
discovery = Message(**message)
messages[discovery.uuid] = discovery
self.message_obj = messages
def save(self):
"""Write discovery message into data file."""
messages = []
for message in self.list_messages:
messages.append(attr.asdict(message))
self._data[ATTR_DISCOVERY].clear()
self._data[ATTR_DISCOVERY].extend(messages)
self.save_data()
def get(self, uuid):
"""Return discovery message."""
return self.message_obj.get(uuid)
@property
def list_messages(self):
"""Return list of available discovery messages."""
return list(self.message_obj.values())
def send(self, addon, service, component, platform, config):
"""Send a discovery message to Home Assistant."""
try:
DISCOVERY_SERVICES[service](config)
except vol.Invalid as err:
_LOGGER.error(
"Invalid discovery %s config", humanize_error(config, err))
raise DiscoveryError() from None
# Create message
message = Message(addon.slug, service, component, platform, config)
# Already exists?
for old_message in self.list_messages:
if old_message != message:
continue
_LOGGER.warning("Duplicate discovery message from %s", addon.slug)
return old_message
_LOGGER.info("Send discovery to Home Assistant %s/%s from %s",
component, platform, addon.slug)
self.message_obj[message.uuid] = message
self.save()
self.sys_create_task(self._push_discovery(message, CMD_NEW))
return message
def remove(self, message):
"""Remove a discovery message from Home Assistant."""
self.message_obj.pop(message.uuid, None)
self.save()
_LOGGER.info("Delete discovery to Home Assistant %s/%s from %s",
message.component, message.platform, message.addon)
self.sys_create_task(self._push_discovery(message, CMD_DEL))
async def _push_discovery(self, message, command):
"""Send a discovery request."""
if not await self.sys_homeassistant.check_api_state():
_LOGGER.info("Discovery %s mesage ignore", message.uuid)
return
data = attr.asdict(message)
data.pop(ATTR_CONFIG)
with suppress(HomeAssistantAPIError):
async with self.sys_homeassistant.make_request(
command, f"api/hassio_push/discovery/{message.uuid}",
json=data, timeout=10):
_LOGGER.info("Discovery %s message send", message.uuid)
return
_LOGGER.warning("Discovery %s message fail", message.uuid)
@attr.s
class Message:
"""Represent a single Discovery message."""
addon = attr.ib()
service = attr.ib()
component = attr.ib()
platform = attr.ib()
config = attr.ib(cmp=False)
uuid = attr.ib(factory=lambda: uuid4().hex, cmp=False)

View File

@ -1,4 +1,4 @@
"""Init file for HassIO docker object.""" """Init file for Hass.io Docker object."""
from contextlib import suppress from contextlib import suppress
import logging import logging
@ -15,13 +15,13 @@ CommandReturn = attr.make_class('CommandReturn', ['exit_code', 'output'])
class DockerAPI: class DockerAPI:
"""Docker hassio wrapper. """Docker Hass.io wrapper.
This class is not AsyncIO safe! This class is not AsyncIO safe!
""" """
def __init__(self): def __init__(self):
"""Initialize docker base wrapper.""" """Initialize Docker base wrapper."""
self.docker = docker.DockerClient( self.docker = docker.DockerClient(
base_url="unix:/{}".format(str(SOCKET_DOCKER)), base_url="unix:/{}".format(str(SOCKET_DOCKER)),
version='auto', timeout=900) version='auto', timeout=900)
@ -29,21 +29,21 @@ class DockerAPI:
@property @property
def images(self): def images(self):
"""Return api images.""" """Return API images."""
return self.docker.images return self.docker.images
@property @property
def containers(self): def containers(self):
"""Return api containers.""" """Return API containers."""
return self.docker.containers return self.docker.containers
@property @property
def api(self): def api(self):
"""Return api containers.""" """Return API containers."""
return self.docker.api return self.docker.api
def run(self, image, **kwargs): def run(self, image, **kwargs):
""""Create a docker and run it. """"Create a Docker container and run it.
Need run inside executor. Need run inside executor.
""" """
@ -51,7 +51,7 @@ class DockerAPI:
network_mode = kwargs.get('network_mode') network_mode = kwargs.get('network_mode')
hostname = kwargs.get('hostname') hostname = kwargs.get('hostname')
# setup network # Setup network
kwargs['dns_search'] = ["."] kwargs['dns_search'] = ["."]
if network_mode: if network_mode:
kwargs['dns'] = [str(self.network.supervisor)] kwargs['dns'] = [str(self.network.supervisor)]
@ -59,7 +59,7 @@ class DockerAPI:
else: else:
kwargs['network'] = None kwargs['network'] = None
# create container # Create container
try: try:
container = self.docker.containers.create(image, **kwargs) container = self.docker.containers.create(image, **kwargs)
except docker.errors.DockerException as err: except docker.errors.DockerException as err:

View File

@ -1,4 +1,4 @@
"""Init file for HassIO addon docker object.""" """Init file for Hass.io add-on Docker object."""
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
@ -19,45 +19,45 @@ AUDIO_DEVICE = "/dev/snd:/dev/snd:rwm"
class DockerAddon(DockerInterface): class DockerAddon(DockerInterface):
"""Docker hassio wrapper for HomeAssistant.""" """Docker Hass.io wrapper for Home Assistant."""
def __init__(self, coresys, slug): def __init__(self, coresys, slug):
"""Initialize docker homeassistant wrapper.""" """Initialize Docker Home Assistant wrapper."""
super().__init__(coresys) super().__init__(coresys)
self._id = slug self._id = slug
@property @property
def addon(self): def addon(self):
"""Return addon of docker image.""" """Return add-on of Docker image."""
return self.sys_addons.get(self._id) return self.sys_addons.get(self._id)
@property @property
def image(self): def image(self):
"""Return name of docker image.""" """Return name of Docker image."""
return self.addon.image return self.addon.image
@property @property
def timeout(self): def timeout(self):
"""Return timeout for docker actions.""" """Return timeout for Docker actions."""
return self.addon.timeout return self.addon.timeout
@property @property
def version(self): def version(self):
"""Return version of docker image.""" """Return version of Docker image."""
if not self.addon.legacy: if not self.addon.legacy:
return super().version return super().version
return self.addon.version_installed return self.addon.version_installed
@property @property
def arch(self): def arch(self):
"""Return arch of docker image.""" """Return arch of Docker image."""
if not self.addon.legacy: if not self.addon.legacy:
return super().arch return super().arch
return self.sys_arch return self.sys_arch
@property @property
def name(self): def name(self):
"""Return name of docker container.""" """Return name of Docker container."""
return "addon_{}".format(self.addon.slug) return "addon_{}".format(self.addon.slug)
@property @property
@ -74,12 +74,12 @@ class DockerAddon(DockerInterface):
@property @property
def hostname(self): def hostname(self):
"""Return slug/id of addon.""" """Return slug/id of add-on."""
return self.addon.slug.replace('_', '-') return self.addon.slug.replace('_', '-')
@property @property
def environment(self): def environment(self):
"""Return environment for docker add-on.""" """Return environment for Docker add-on."""
addon_env = self.addon.environment or {} addon_env = self.addon.environment or {}
# Need audio settings # Need audio settings
@ -114,7 +114,7 @@ class DockerAddon(DockerInterface):
@property @property
def ports(self): def ports(self):
"""Filter None from addon ports.""" """Filter None from add-on ports."""
if not self.addon.ports: if not self.addon.ports:
return None return None
@ -126,7 +126,7 @@ class DockerAddon(DockerInterface):
@property @property
def security_opt(self): def security_opt(self):
"""Controlling security opt.""" """Controlling security options."""
security = [] security = []
# AppArmor # AppArmor
@ -144,7 +144,7 @@ class DockerAddon(DockerInterface):
@property @property
def tmpfs(self): def tmpfs(self):
"""Return tmpfs for docker add-on.""" """Return tmpfs for Docker add-on."""
options = self.addon.tmpfs options = self.addon.tmpfs
if options: if options:
return {"/tmpfs": f"{options}"} return {"/tmpfs": f"{options}"}
@ -160,14 +160,14 @@ class DockerAddon(DockerInterface):
@property @property
def network_mode(self): def network_mode(self):
"""Return network mode for addon.""" """Return network mode for add-on."""
if self.addon.host_network: if self.addon.host_network:
return 'host' return 'host'
return None return None
@property @property
def pid_mode(self): def pid_mode(self):
"""Return PID mode for addon.""" """Return PID mode for add-on."""
if not self.addon.protected and self.addon.host_pid: if not self.addon.protected and self.addon.host_pid:
return 'host' return 'host'
return None return None
@ -242,7 +242,7 @@ class DockerAddon(DockerInterface):
}, },
}) })
# Host dbus system # Host D-Bus system
if self.addon.host_dbus: if self.addon.host_dbus:
volumes.update({ volumes.update({
"/var/run/dbus": { "/var/run/dbus": {
@ -259,7 +259,7 @@ class DockerAddon(DockerInterface):
return volumes return volumes
def _run(self): def _run(self):
"""Run docker image. """Run Docker image.
Need run inside executor. Need run inside executor.
""" """
@ -269,7 +269,7 @@ class DockerAddon(DockerInterface):
# Security check # Security check
if not self.addon.protected: if not self.addon.protected:
_LOGGER.warning( _LOGGER.warning(
"%s run with disabled proteced mode!", self.addon.name) "%s run with disabled protected mode!", self.addon.name)
# cleanup # cleanup
self._stop() self._stop()
@ -296,13 +296,13 @@ class DockerAddon(DockerInterface):
) )
if ret: if ret:
_LOGGER.info("Start docker addon %s with version %s", _LOGGER.info("Start Docker add-on %s with version %s",
self.image, self.version) self.image, self.version)
return ret return ret
def _install(self, tag): def _install(self, tag):
"""Pull docker image or build it. """Pull Docker image or build it.
Need run inside executor. Need run inside executor.
""" """
@ -312,7 +312,7 @@ class DockerAddon(DockerInterface):
return super()._install(tag) return super()._install(tag)
def _build(self, tag): def _build(self, tag):
"""Build a docker container. """Build a Docker container.
Need run inside executor. Need run inside executor.
""" """
@ -329,7 +329,7 @@ class DockerAddon(DockerInterface):
# Update meta data # Update meta data
self._meta = image.attrs self._meta = image.attrs
except (docker.errors.DockerException) as err: except docker.errors.DockerException as err:
_LOGGER.error("Can't build %s:%s: %s", self.image, tag, err) _LOGGER.error("Can't build %s:%s: %s", self.image, tag, err)
return False return False
@ -403,7 +403,7 @@ class DockerAddon(DockerInterface):
return False return False
try: try:
# load needed docker objects # Load needed docker objects
container = self.sys_docker.containers.get(self.name) container = self.sys_docker.containers.get(self.name)
socket = container.attach_socket(params={'stdin': 1, 'stream': 1}) socket = container.attach_socket(params={'stdin': 1, 'stream': 1})
except docker.errors.DockerException as err: except docker.errors.DockerException as err:
@ -411,7 +411,7 @@ class DockerAddon(DockerInterface):
return False return False
try: try:
# write to stdin # Write to stdin
data += b"\n" data += b"\n"
os.write(socket.fileno(), data) os.write(socket.fileno(), data)
socket.close() socket.close()

View File

@ -10,11 +10,11 @@ _LOGGER = logging.getLogger(__name__)
class DockerHassOSCli(DockerInterface, CoreSysAttributes): class DockerHassOSCli(DockerInterface, CoreSysAttributes):
"""Docker hassio wrapper for HassOS Cli.""" """Docker Hass.io wrapper for HassOS Cli."""
@property @property
def image(self): def image(self):
"""Return name of HassOS cli image.""" """Return name of HassOS CLI image."""
return f"homeassistant/{self.sys_arch}-hassio-cli" return f"homeassistant/{self.sys_arch}-hassio-cli"
def _stop(self): def _stop(self):
@ -22,16 +22,16 @@ class DockerHassOSCli(DockerInterface, CoreSysAttributes):
return True return True
def _attach(self): def _attach(self):
"""Attach to running docker container. """Attach to running Docker container.
Need run inside executor. Need run inside executor.
""" """
try: try:
image = self.sys_docker.images.get(self.image) image = self.sys_docker.images.get(self.image)
except docker.errors.DockerException: except docker.errors.DockerException:
_LOGGER.warning("Can't find a HassOS cli %s", self.image) _LOGGER.warning("Can't find a HassOS CLI %s", self.image)
else: else:
self._meta = image.attrs self._meta = image.attrs
_LOGGER.info("Found HassOS cli %s with version %s", _LOGGER.info("Found HassOS CLI %s with version %s",
self.image, self.version) self.image, self.version)

View File

@ -1,4 +1,4 @@
"""Init file for HassIO docker object.""" """Init file for Hass.io Docker object."""
import logging import logging
import docker import docker
@ -12,35 +12,35 @@ HASS_DOCKER_NAME = 'homeassistant'
class DockerHomeAssistant(DockerInterface): class DockerHomeAssistant(DockerInterface):
"""Docker hassio wrapper for HomeAssistant.""" """Docker Hass.io wrapper for Home Assistant."""
@property @property
def machine(self): def machine(self):
"""Return machine of Home-Assistant docker image.""" """Return machine of Home Assistant Docker image."""
if self._meta and LABEL_MACHINE in self._meta['Config']['Labels']: if self._meta and LABEL_MACHINE in self._meta['Config']['Labels']:
return self._meta['Config']['Labels'][LABEL_MACHINE] return self._meta['Config']['Labels'][LABEL_MACHINE]
return None return None
@property @property
def image(self): def image(self):
"""Return name of docker image.""" """Return name of Docker image."""
return self.sys_homeassistant.image return self.sys_homeassistant.image
@property @property
def name(self): def name(self):
"""Return name of docker container.""" """Return name of Docker container."""
return HASS_DOCKER_NAME return HASS_DOCKER_NAME
@property @property
def devices(self): def devices(self):
"""Create list of special device to map into docker.""" """Create list of special device to map into Docker."""
devices = [] devices = []
for device in self.sys_hardware.serial_devices: for device in self.sys_hardware.serial_devices:
devices.append(f"{device}:{device}:rwm") devices.append(f"{device}:{device}:rwm")
return devices or None return devices or None
def _run(self): def _run(self):
"""Run docker image. """Run Docker image.
Need run inside executor. Need run inside executor.
""" """
@ -108,7 +108,7 @@ class DockerHomeAssistant(DockerInterface):
) )
def is_initialize(self): def is_initialize(self):
"""Return True if docker container exists.""" """Return True if Docker container exists."""
return self.sys_run_in_executor(self._is_initialize) return self.sys_run_in_executor(self._is_initialize)
def _is_initialize(self): def _is_initialize(self):

View File

@ -1,4 +1,4 @@
"""Interface class for HassIO docker object.""" """Interface class for Hass.io Docker object."""
import asyncio import asyncio
from contextlib import suppress from contextlib import suppress
import logging import logging
@ -14,27 +14,27 @@ _LOGGER = logging.getLogger(__name__)
class DockerInterface(CoreSysAttributes): class DockerInterface(CoreSysAttributes):
"""Docker hassio interface.""" """Docker Hass.io interface."""
def __init__(self, coresys): def __init__(self, coresys):
"""Initialize docker base wrapper.""" """Initialize Docker base wrapper."""
self.coresys = coresys self.coresys = coresys
self._meta = None self._meta = None
self.lock = asyncio.Lock(loop=coresys.loop) self.lock = asyncio.Lock(loop=coresys.loop)
@property @property
def timeout(self): def timeout(self):
"""Return timeout for docker actions.""" """Return timeout for Docker actions."""
return 30 return 30
@property @property
def name(self): def name(self):
"""Return name of docker container.""" """Return name of Docker container."""
return None return None
@property @property
def meta_config(self): def meta_config(self):
"""Return meta data of config for container/image.""" """Return meta data of configuration for container/image."""
if not self._meta: if not self._meta:
return {} return {}
return self._meta.get('Config', {}) return self._meta.get('Config', {})
@ -42,21 +42,21 @@ class DockerInterface(CoreSysAttributes):
@property @property
def meta_labels(self): def meta_labels(self):
"""Return meta data of labels for container/image.""" """Return meta data of labels for container/image."""
return self.meta_config.get('Labels', {}) return self.meta_config.get('Labels') or {}
@property @property
def image(self): def image(self):
"""Return name of docker image.""" """Return name of Docker image."""
return self.meta_config.get('Image') return self.meta_config.get('Image')
@property @property
def version(self): def version(self):
"""Return version of docker image.""" """Return version of Docker image."""
return self.meta_labels.get(LABEL_VERSION) return self.meta_labels.get(LABEL_VERSION)
@property @property
def arch(self): def arch(self):
"""Return arch of docker image.""" """Return arch of Docker image."""
return self.meta_labels.get(LABEL_ARCH) return self.meta_labels.get(LABEL_ARCH)
@property @property
@ -70,7 +70,7 @@ class DockerInterface(CoreSysAttributes):
return self.sys_run_in_executor(self._install, tag) return self.sys_run_in_executor(self._install, tag)
def _install(self, tag): def _install(self, tag):
"""Pull docker image. """Pull Docker image.
Need run inside executor. Need run inside executor.
""" """
@ -88,11 +88,11 @@ class DockerInterface(CoreSysAttributes):
return True return True
def exists(self): def exists(self):
"""Return True if docker image exists in local repo.""" """Return True if Docker image exists in local repository."""
return self.sys_run_in_executor(self._exists) return self.sys_run_in_executor(self._exists)
def _exists(self): def _exists(self):
"""Return True if docker image exists in local repo. """Return True if Docker image exists in local repository.
Need run inside executor. Need run inside executor.
""" """
@ -105,14 +105,14 @@ class DockerInterface(CoreSysAttributes):
return True return True
def is_running(self): def is_running(self):
"""Return True if docker is Running. """Return True if Docker is running.
Return a Future. Return a Future.
""" """
return self.sys_run_in_executor(self._is_running) return self.sys_run_in_executor(self._is_running)
def _is_running(self): def _is_running(self):
"""Return True if docker is Running. """Return True if Docker is running.
Need run inside executor. Need run inside executor.
""" """
@ -134,7 +134,7 @@ class DockerInterface(CoreSysAttributes):
@process_lock @process_lock
def attach(self): def attach(self):
"""Attach to running docker container.""" """Attach to running Docker container."""
return self.sys_run_in_executor(self._attach) return self.sys_run_in_executor(self._attach)
def _attach(self): def _attach(self):
@ -157,11 +157,11 @@ class DockerInterface(CoreSysAttributes):
@process_lock @process_lock
def run(self): def run(self):
"""Run docker image.""" """Run Docker image."""
return self.sys_run_in_executor(self._run) return self.sys_run_in_executor(self._run)
def _run(self): def _run(self):
"""Run docker image. """Run Docker image.
Need run inside executor. Need run inside executor.
""" """
@ -169,7 +169,7 @@ class DockerInterface(CoreSysAttributes):
@process_lock @process_lock
def stop(self): def stop(self):
"""Stop/remove docker container.""" """Stop/remove Docker container."""
return self.sys_run_in_executor(self._stop) return self.sys_run_in_executor(self._stop)
def _stop(self): def _stop(self):
@ -183,19 +183,19 @@ class DockerInterface(CoreSysAttributes):
return False return False
if container.status == 'running': if container.status == 'running':
_LOGGER.info("Stop %s docker application", self.image) _LOGGER.info("Stop %s Docker application", self.image)
with suppress(docker.errors.DockerException): with suppress(docker.errors.DockerException):
container.stop(timeout=self.timeout) container.stop(timeout=self.timeout)
with suppress(docker.errors.DockerException): with suppress(docker.errors.DockerException):
_LOGGER.info("Clean %s docker application", self.image) _LOGGER.info("Clean %s Docker application", self.image)
container.remove(force=True) container.remove(force=True)
return True return True
@process_lock @process_lock
def remove(self): def remove(self):
"""Remove docker images.""" """Remove Docker images."""
return self.sys_run_in_executor(self._remove) return self.sys_run_in_executor(self._remove)
def _remove(self): def _remove(self):
@ -203,11 +203,11 @@ class DockerInterface(CoreSysAttributes):
Need run inside executor. Need run inside executor.
""" """
# cleanup container # Cleanup container
self._stop() self._stop()
_LOGGER.info( _LOGGER.info(
"Remove docker %s with latest and %s", self.image, self.version) "Remove Docker %s with latest and %s", self.image, self.version)
try: try:
with suppress(docker.errors.ImageNotFound): with suppress(docker.errors.ImageNotFound):
@ -227,7 +227,7 @@ class DockerInterface(CoreSysAttributes):
@process_lock @process_lock
def update(self, tag): def update(self, tag):
"""Update a docker image.""" """Update a Docker image."""
return self.sys_run_in_executor(self._update, tag) return self.sys_run_in_executor(self._update, tag)
def _update(self, tag): def _update(self, tag):
@ -236,27 +236,27 @@ class DockerInterface(CoreSysAttributes):
Need run inside executor. Need run inside executor.
""" """
_LOGGER.info( _LOGGER.info(
"Update docker %s with %s:%s", self.version, self.image, tag) "Update Docker %s with %s:%s", self.version, self.image, tag)
# update docker image # Update docker image
if not self._install(tag): if not self._install(tag):
return False return False
# stop container & cleanup # Stop container & cleanup
self._stop() self._stop()
self._cleanup() self._cleanup()
return True return True
def logs(self): def logs(self):
"""Return docker logs of container. """Return Docker logs of container.
Return a Future. Return a Future.
""" """
return self.sys_run_in_executor(self._logs) return self.sys_run_in_executor(self._logs)
def _logs(self): def _logs(self):
"""Return docker logs of container. """Return Docker logs of container.
Need run inside executor. Need run inside executor.
""" """
@ -268,7 +268,7 @@ class DockerInterface(CoreSysAttributes):
try: try:
return container.logs(tail=100, stdout=True, stderr=True) return container.logs(tail=100, stdout=True, stderr=True)
except docker.errors.DockerException as err: except docker.errors.DockerException as err:
_LOGGER.warning("Can't grap logs from %s: %s", self.image, err) _LOGGER.warning("Can't grep logs from %s: %s", self.image, err)
@process_lock @process_lock
def cleanup(self): def cleanup(self):
@ -291,7 +291,7 @@ class DockerInterface(CoreSysAttributes):
continue continue
with suppress(docker.errors.DockerException): with suppress(docker.errors.DockerException):
_LOGGER.info("Cleanup docker images: %s", image.tags) _LOGGER.info("Cleanup Docker images: %s", image.tags)
self.sys_docker.images.remove(image.id, force=True) self.sys_docker.images.remove(image.id, force=True)
return True return True

View File

@ -1,4 +1,4 @@
"""Internal network manager for HassIO.""" """Internal network manager for Hass.io."""
import logging import logging
import docker import docker
@ -9,13 +9,13 @@ _LOGGER = logging.getLogger(__name__)
class DockerNetwork: class DockerNetwork:
"""Internal HassIO Network. """Internal Hass.io Network.
This class is not AsyncIO safe! This class is not AsyncIO safe!
""" """
def __init__(self, dock): def __init__(self, dock):
"""Initialize internal hassio network.""" """Initialize internal Hass.io network."""
self.docker = dock self.docker = dock
self.network = self._get_network() self.network = self._get_network()
@ -44,7 +44,7 @@ class DockerNetwork:
try: try:
return self.docker.networks.get(DOCKER_NETWORK) return self.docker.networks.get(DOCKER_NETWORK)
except docker.errors.NotFound: except docker.errors.NotFound:
_LOGGER.info("Can't find HassIO network, create new network") _LOGGER.info("Can't find Hass.io network, create new network")
ipam_pool = docker.types.IPAMPool( ipam_pool = docker.types.IPAMPool(
subnet=str(DOCKER_NETWORK_MASK), subnet=str(DOCKER_NETWORK_MASK),
@ -61,7 +61,7 @@ class DockerNetwork:
}) })
def attach_container(self, container, alias=None, ipv4=None): def attach_container(self, container, alias=None, ipv4=None):
"""Attach container to hassio network. """Attach container to Hass.io network.
Need run inside executor. Need run inside executor.
""" """
@ -77,7 +77,7 @@ class DockerNetwork:
return True return True
def detach_default_bridge(self, container): def detach_default_bridge(self, container):
"""Detach default docker bridge. """Detach default Docker bridge.
Need run inside executor. Need run inside executor.
""" """

View File

@ -1,4 +1,4 @@
"""Calc & represent docker stats data.""" """Calc and represent docker stats data."""
from contextlib import suppress from contextlib import suppress
@ -6,7 +6,7 @@ class DockerStats:
"""Hold stats data from container inside.""" """Hold stats data from container inside."""
def __init__(self, stats): def __init__(self, stats):
"""Initialize docker stats.""" """Initialize Docker stats."""
self._cpu = 0.0 self._cpu = 0.0
self._network_rx = 0 self._network_rx = 0
self._network_tx = 0 self._network_tx = 0

View File

@ -1,4 +1,4 @@
"""Init file for HassIO docker object.""" """Init file for Hass.io Docker object."""
import logging import logging
import os import os
@ -11,11 +11,11 @@ _LOGGER = logging.getLogger(__name__)
class DockerSupervisor(DockerInterface, CoreSysAttributes): class DockerSupervisor(DockerInterface, CoreSysAttributes):
"""Docker hassio wrapper for Supervisor.""" """Docker Hass.io wrapper for Supervisor."""
@property @property
def name(self): def name(self):
"""Return name of docker container.""" """Return name of Docker container."""
return os.environ['SUPERVISOR_NAME'] return os.environ['SUPERVISOR_NAME']
def _attach(self): def _attach(self):
@ -29,14 +29,14 @@ class DockerSupervisor(DockerInterface, CoreSysAttributes):
return False return False
self._meta = container.attrs self._meta = container.attrs
_LOGGER.info("Attach to supervisor %s with version %s", _LOGGER.info("Attach to Supervisor %s with version %s",
self.image, self.version) self.image, self.version)
# if already attach # If already attach
if container in self.sys_docker.network.containers: if container in self.sys_docker.network.containers:
return True return True
# attach to network # Attach to network
return self.sys_docker.network.attach_container( return self.sys_docker.network.attach_container(
container, alias=['hassio'], container, alias=['hassio'],
ipv4=self.sys_docker.network.supervisor) ipv4=self.sys_docker.network.supervisor)

View File

@ -81,13 +81,25 @@ class HostAppArmorError(HostError):
# API # API
class APIError(HassioError): class APIError(HassioError, RuntimeError):
"""API errors.""" """API errors."""
pass pass
class APINotSupportedError(HassioNotSupportedError): class APIForbidden(APIError):
"""API not supported error.""" """API forbidden error."""
pass
# Service / Discovery
class DiscoveryError(HassioError):
"""Discovery Errors."""
pass
class ServicesError(HassioError):
"""Services Errors."""
pass pass

View File

@ -439,19 +439,6 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
_LOGGER.warning("Home Assistant API config mismatch: %d", err) _LOGGER.warning("Home Assistant API config mismatch: %d", err)
return False return False
async def send_event(self, event_type, event_data=None):
"""Send event to Home-Assistant."""
with suppress(HomeAssistantAPIError):
async with self.make_request(
'get', f'api/events/{event_type}'
) as resp:
if resp.status in (200, 201):
return
err = resp.status
_LOGGER.warning("Home Assistant event %s fails: %s", event_type, err)
return HomeAssistantError()
async def _block_till_run(self): async def _block_till_run(self):
"""Block until Home-Assistant is booting up or startup timeout.""" """Block until Home-Assistant is booting up or startup timeout."""
start_time = time.monotonic() start_time = time.monotonic()

View File

@ -1,4 +1,4 @@
"""Host function like audio/dbus/systemd.""" """Host function like audio, D-Bus or systemd."""
from contextlib import suppress from contextlib import suppress
import logging import logging
@ -35,7 +35,7 @@ class HostManager(CoreSysAttributes):
@property @property
def apparmor(self): def apparmor(self):
"""Return host apparmor handler.""" """Return host AppArmor handler."""
return self._apparmor return self._apparmor
@property @property

View File

@ -1,4 +1,4 @@
"""Host Audio-support.""" """Host Audio support."""
import logging import logging
import json import json
from pathlib import Path from pathlib import Path
@ -19,7 +19,7 @@ class AlsaAudio(CoreSysAttributes):
"""Handle Audio ALSA host data.""" """Handle Audio ALSA host data."""
def __init__(self, coresys): def __init__(self, coresys):
"""Initialize Alsa audio system.""" """Initialize ALSA audio system."""
self.coresys = coresys self.coresys = coresys
self._data = { self._data = {
ATTR_INPUT: {}, ATTR_INPUT: {},

View File

@ -13,7 +13,7 @@ SYSTEMD_SERVICES = {'hassos-apparmor.service', 'hassio-apparmor.service'}
class AppArmorControl(CoreSysAttributes): class AppArmorControl(CoreSysAttributes):
"""Handle host apparmor controls.""" """Handle host AppArmor controls."""
def __init__(self, coresys): def __init__(self, coresys):
"""Initialize host power handling.""" """Initialize host power handling."""
@ -23,7 +23,7 @@ class AppArmorControl(CoreSysAttributes):
@property @property
def available(self): def available(self):
"""Return True if AppArmor is availabe on host.""" """Return True if AppArmor is available on host."""
return self._service is not None return self._service is not None
def exists(self, profile): def exists(self, profile):
@ -62,12 +62,12 @@ class AppArmorControl(CoreSysAttributes):
if self.available: if self.available:
await self._reload_service() await self._reload_service()
else: else:
_LOGGER.info("AppArmor is not enabled on Host") _LOGGER.info("AppArmor is not enabled on host")
async def load_profile(self, profile_name, profile_file): async def load_profile(self, profile_name, profile_file):
"""Load/Update a new/exists profile into AppArmor.""" """Load/Update a new/exists profile into AppArmor."""
if not validate_profile(profile_name, profile_file): if not validate_profile(profile_name, profile_file):
_LOGGER.error("profile is not valid with name %s", profile_name) _LOGGER.error("Profile is not valid with name %s", profile_name)
raise HostAppArmorError() raise HostAppArmorError()
# Copy to AppArmor folder # Copy to AppArmor folder

View File

@ -24,7 +24,7 @@ class SystemControl(CoreSysAttributes):
if flag == HOSTNAME and self.sys_dbus.hostname.is_connected: if flag == HOSTNAME and self.sys_dbus.hostname.is_connected:
return return
_LOGGER.error("No %s dbus connection available", flag) _LOGGER.error("No %s D-Bus connection available", flag)
raise HostNotSupportedError() raise HostNotSupportedError()
async def reboot(self): async def reboot(self):
@ -51,6 +51,6 @@ class SystemControl(CoreSysAttributes):
"""Set local a new Hostname.""" """Set local a new Hostname."""
self._check_dbus(HOSTNAME) self._check_dbus(HOSTNAME)
_LOGGER.info("Set Hostname %s", hostname) _LOGGER.info("Set hostname %s", hostname)
await self.sys_dbus.hostname.set_static_hostname(hostname) await self.sys_dbus.hostname.set_static_hostname(hostname)
await self.sys_host.info.update() await self.sys_host.info.update()

View File

@ -48,7 +48,7 @@ class InfoCenter(CoreSysAttributes):
async def update(self): async def update(self):
"""Update properties over dbus.""" """Update properties over dbus."""
if not self.sys_dbus.hostname.is_connected: if not self.sys_dbus.hostname.is_connected:
_LOGGER.error("No hostname dbus connection available") _LOGGER.error("No hostname D-Bus connection available")
raise HostNotSupportedError() raise HostNotSupportedError()
_LOGGER.info("Update local host information") _LOGGER.info("Update local host information")

View File

@ -95,5 +95,5 @@ class ServiceInfo:
@staticmethod @staticmethod
def read_from(unit): def read_from(unit):
"""Parse data from dbus into this object.""" """Parse data from D-Bus into this object."""
return ServiceInfo(unit[0], unit[1], unit[3]) return ServiceInfo(unit[0], unit[1], unit[3])

View File

@ -24,7 +24,7 @@ RE_TTY = re.compile(r"tty[A-Z]+")
class Hardware: class Hardware:
"""Represent an interface to procfs, sysfs and udev.""" """Representation of an interface to procfs, sysfs and udev."""
def __init__(self): def __init__(self):
"""Init hardware object.""" """Init hardware object."""

View File

@ -1,4 +1,4 @@
"""Schedule for HassIO.""" """Schedule for Hass.io."""
import logging import logging
from datetime import date, datetime, time, timedelta from datetime import date, datetime, time, timedelta
@ -11,7 +11,7 @@ TASK = 'task'
class Scheduler: class Scheduler:
"""Schedule task inside HassIO.""" """Schedule task inside Hass.io."""
def __init__(self, loop): def __init__(self, loop):
"""Initialize task schedule.""" """Initialize task schedule."""
@ -22,18 +22,18 @@ class Scheduler:
def register_task(self, coro_callback, interval, repeat=True): def register_task(self, coro_callback, interval, repeat=True):
"""Schedule a coroutine. """Schedule a coroutine.
The coroutien need to be a callback without arguments. The coroutine need to be a callback without arguments.
""" """
task_id = hash(coro_callback) task_id = hash(coro_callback)
# generate data # Generate data
opts = { opts = {
CALL: coro_callback, CALL: coro_callback,
INTERVAL: interval, INTERVAL: interval,
REPEAT: repeat, REPEAT: repeat,
} }
# schedule task # Schedule task
self._data[task_id] = opts self._data[task_id] = opts
self._schedule_task(interval, task_id) self._schedule_task(interval, task_id)
@ -60,7 +60,7 @@ class Scheduler:
tomorrow = datetime.combine( tomorrow = datetime.combine(
date.today() + timedelta(days=1), interval) date.today() + timedelta(days=1), interval)
# check if we run it today or next day # Check if we run it today or next day
if today > datetime.today(): if today > datetime.today():
calc = today calc = today
else: else:
@ -68,7 +68,7 @@ class Scheduler:
job = self.loop.call_at(calc.timestamp(), self._run_task, task_id) job = self.loop.call_at(calc.timestamp(), self._run_task, task_id)
else: else:
_LOGGER.fatal("Unknow interval %s (type: %s) for scheduler %s", _LOGGER.fatal("Unknown interval %s (type: %s) for scheduler %s",
interval, type(interval), task_id) interval, type(interval), task_id)
# Store job # Store job

View File

@ -1,6 +1,4 @@
"""Handle internal services discovery.""" """Handle internal services discovery."""
from .discovery import Discovery # noqa
from .mqtt import MQTTService from .mqtt import MQTTService
from .data import ServicesData from .data import ServicesData
from ..const import SERVICE_MQTT from ..const import SERVICE_MQTT
@ -35,10 +33,6 @@ class ServiceManager(CoreSysAttributes):
for slug, service in AVAILABLE_SERVICES.items(): for slug, service in AVAILABLE_SERVICES.items():
self.services_obj[slug] = service(self.coresys) self.services_obj[slug] = service(self.coresys)
# Read exists discovery messages
self.sys_discovery.load()
def reset(self): def reset(self):
"""Reset available data.""" """Reset available data."""
self.data.reset_data() self.data.reset_data()
self.sys_discovery.load()

View File

@ -1,7 +1,7 @@
"""Handle service data for persistent supervisor reboot.""" """Handle service data for persistent supervisor reboot."""
from .validate import SCHEMA_SERVICES_FILE from .validate import SCHEMA_SERVICES_CONFIG
from ..const import FILE_HASSIO_SERVICES, ATTR_DISCOVERY, SERVICE_MQTT from ..const import FILE_HASSIO_SERVICES, SERVICE_MQTT
from ..utils.json import JsonConfig from ..utils.json import JsonConfig
@ -10,14 +10,9 @@ class ServicesData(JsonConfig):
def __init__(self): def __init__(self):
"""Initialize services data.""" """Initialize services data."""
super().__init__(FILE_HASSIO_SERVICES, SCHEMA_SERVICES_FILE) super().__init__(FILE_HASSIO_SERVICES, SCHEMA_SERVICES_CONFIG)
@property
def discovery(self):
"""Return discovery data for home-assistant."""
return self._data[ATTR_DISCOVERY]
@property @property
def mqtt(self): def mqtt(self):
"""Return settings for mqtt service.""" """Return settings for MQTT service."""
return self._data[SERVICE_MQTT] return self._data[SERVICE_MQTT]

View File

@ -1,107 +0,0 @@
"""Handle discover message for Home-Assistant."""
import logging
from uuid import uuid4
from ..const import ATTR_UUID
from ..coresys import CoreSysAttributes
_LOGGER = logging.getLogger(__name__)
EVENT_DISCOVERY_ADD = 'hassio_discovery_add'
EVENT_DISCOVERY_DEL = 'hassio_discovery_del'
class Discovery(CoreSysAttributes):
"""Home-Assistant Discovery handler."""
def __init__(self, coresys):
"""Initialize discovery handler."""
self.coresys = coresys
self.message_obj = {}
def load(self):
"""Load exists discovery message into storage."""
messages = {}
for message in self._data:
discovery = Message(**message)
messages[discovery.uuid] = discovery
self.message_obj = messages
def save(self):
"""Write discovery message into data file."""
messages = []
for message in self.message_obj.values():
messages.append(message.raw())
self._data.clear()
self._data.extend(messages)
self.sys_services.data.save_data()
def get(self, uuid):
"""Return discovery message."""
return self.message_obj.get(uuid)
@property
def _data(self):
"""Return discovery data."""
return self.sys_services.data.discovery
@property
def list_messages(self):
"""Return list of available discovery messages."""
return self.message_obj.values()
def send(self, provider, component, platform=None, config=None):
"""Send a discovery message to Home-Assistant."""
message = Message(provider, component, platform, config)
# Already exists?
for exists_message in self.message_obj:
if exists_message == message:
_LOGGER.warning("Found douplicate discovery message from %s",
provider)
return exists_message
_LOGGER.info("Send discovery to Home-Assistant %s/%s from %s",
component, platform, provider)
self.message_obj[message.uuid] = message
self.save()
# send event to Home-Assistant
self.sys_create_task(self.sys_homeassistant.send_event(
EVENT_DISCOVERY_ADD, {ATTR_UUID: message.uuid}))
return message
def remove(self, message):
"""Remove a discovery message from Home-Assistant."""
self.message_obj.pop(message.uuid, None)
self.save()
# send event to Home-Assistant
self.sys_create_task(self.sys_homeassistant.send_event(
EVENT_DISCOVERY_DEL, {ATTR_UUID: message.uuid}))
class Message:
"""Represent a single Discovery message."""
def __init__(self, provider, component, platform, config, uuid=None):
"""Initialize discovery message."""
self.provider = provider
self.component = component
self.platform = platform
self.config = config
self.uuid = uuid or uuid4().hex
def raw(self):
"""Return raw discovery message."""
return self.__dict__
def __eq__(self, other):
"""Compare with other message."""
for attribute in ('provider', 'component', 'platform', 'config'):
if getattr(self, attribute) != getattr(other, attribute):
return False
return True

View File

@ -1,6 +1,7 @@
"""Interface for single service.""" """Interface for single service."""
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..const import PROVIDE_SERVICE
class ServiceInterface(CoreSysAttributes): class ServiceInterface(CoreSysAttributes):
@ -26,9 +27,13 @@ class ServiceInterface(CoreSysAttributes):
return None return None
@property @property
def provider(self): def providers(self):
"""Return name of service provider.""" """Return name of service providers addon."""
return None addons = []
for addon in self.sys_addons.list_installed:
if addon.services_role.get(self.slug) == PROVIDE_SERVICE:
addons.append(addon.slug)
return addons
@property @property
def enabled(self): def enabled(self):
@ -45,10 +50,10 @@ class ServiceInterface(CoreSysAttributes):
return self._data return self._data
return None return None
def set_service_data(self, provider, data): def set_service_data(self, addon, data):
"""Write the data into service object.""" """Write the data into service object."""
raise NotImplementedError() raise NotImplementedError()
def del_service_data(self, provider): def del_service_data(self, addon):
"""Remove the data from service object.""" """Remove the data from service object."""
raise NotImplementedError() raise NotImplementedError()

View File

@ -1,17 +1,16 @@
"""Provide MQTT Service.""" """Provide the MQTT Service."""
import logging import logging
from .interface import ServiceInterface from .interface import ServiceInterface
from .validate import SCHEMA_SERVICE_MQTT from .validate import SCHEMA_SERVICE_MQTT
from ..const import ( from ..const import ATTR_ADDON, SERVICE_MQTT
ATTR_PROVIDER, SERVICE_MQTT, ATTR_HOST, ATTR_PORT, ATTR_USERNAME, from ..exceptions import ServicesError
ATTR_PASSWORD, ATTR_PROTOCOL, ATTR_DISCOVERY_ID)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class MQTTService(ServiceInterface): class MQTTService(ServiceInterface):
"""Provide mqtt services.""" """Provide MQTT services."""
@property @property
def slug(self): def slug(self):
@ -28,62 +27,24 @@ class MQTTService(ServiceInterface):
"""Return data schema of this service.""" """Return data schema of this service."""
return SCHEMA_SERVICE_MQTT return SCHEMA_SERVICE_MQTT
@property def set_service_data(self, addon, data):
def provider(self):
"""Return name of service provider."""
return self._data.get(ATTR_PROVIDER)
@property
def hass_config(self):
"""Return Home-Assistant mqtt config."""
if not self.enabled:
return None
hass_config = {
'host': self._data[ATTR_HOST],
'port': self._data[ATTR_PORT],
'protocol': self._data[ATTR_PROTOCOL]
}
if ATTR_USERNAME in self._data:
hass_config['user']: self._data[ATTR_USERNAME]
if ATTR_PASSWORD in self._data:
hass_config['password']: self._data[ATTR_PASSWORD]
return hass_config
def set_service_data(self, provider, data):
"""Write the data into service object.""" """Write the data into service object."""
if self.enabled: if self.enabled:
_LOGGER.error("It is already a mqtt in use from %s", self.provider) _LOGGER.error(
return False "It is already a MQTT in use from %s", self._data[ATTR_ADDON])
raise ServicesError()
self._data.update(data) self._data.update(data)
self._data[ATTR_PROVIDER] = provider self._data[ATTR_ADDON] = addon.slug
if provider == 'homeassistant': _LOGGER.info("Set %s as service provider for mqtt", addon.slug)
_LOGGER.info("Use mqtt settings from Home-Assistant")
self.save()
return True
# discover mqtt to homeassistant
message = self.sys_discovery.send(
provider, SERVICE_MQTT, None, self.hass_config)
self._data[ATTR_DISCOVERY_ID] = message.uuid
self.save() self.save()
return True
def del_service_data(self, provider): def del_service_data(self, addon):
"""Remove the data from service object.""" """Remove the data from service object."""
if not self.enabled: if not self.enabled:
_LOGGER.warning("Can't remove not exists services.") _LOGGER.warning("Can't remove not exists services")
return False raise ServicesError()
discovery_id = self._data.get(ATTR_DISCOVERY_ID)
if discovery_id:
self.sys_discovery.remove(
self.sys_discovery.get(discovery_id))
self._data.clear() self._data.clear()
self.save() self.save()
return True

View File

@ -1,23 +1,11 @@
"""Validate services schema.""" """Validate services schema."""
import voluptuous as vol import voluptuous as vol
from ..const import ( from ..const import (
SERVICE_MQTT, ATTR_HOST, ATTR_PORT, ATTR_PASSWORD, ATTR_USERNAME, ATTR_SSL, SERVICE_MQTT, ATTR_HOST, ATTR_PORT, ATTR_PASSWORD, ATTR_USERNAME, ATTR_SSL,
ATTR_PROVIDER, ATTR_PROTOCOL, ATTR_DISCOVERY, ATTR_COMPONENT, ATTR_UUID, ATTR_ADDON, ATTR_PROTOCOL)
ATTR_PLATFORM, ATTR_CONFIG, ATTR_DISCOVERY_ID)
from ..validate import NETWORK_PORT from ..validate import NETWORK_PORT
from ..utils.validate import schema_or
SCHEMA_DISCOVERY = vol.Schema([
vol.Schema({
vol.Required(ATTR_UUID): vol.Match(r"^[0-9a-f]{32}$"),
vol.Required(ATTR_PROVIDER): vol.Coerce(str),
vol.Required(ATTR_COMPONENT): vol.Coerce(str),
vol.Required(ATTR_PLATFORM): vol.Any(None, vol.Coerce(str)),
vol.Required(ATTR_CONFIG): vol.Any(None, dict),
}, extra=vol.REMOVE_EXTRA)
])
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
@ -33,12 +21,15 @@ SCHEMA_SERVICE_MQTT = vol.Schema({
SCHEMA_CONFIG_MQTT = SCHEMA_SERVICE_MQTT.extend({ SCHEMA_CONFIG_MQTT = SCHEMA_SERVICE_MQTT.extend({
vol.Required(ATTR_PROVIDER): vol.Coerce(str), vol.Required(ATTR_ADDON): vol.Coerce(str),
vol.Optional(ATTR_DISCOVERY_ID): vol.Match(r"^[0-9a-f]{32}$"),
}) })
SCHEMA_SERVICES_FILE = vol.Schema({ SCHEMA_SERVICES_CONFIG = vol.Schema({
vol.Optional(SERVICE_MQTT, default=dict): vol.Any({}, SCHEMA_CONFIG_MQTT), vol.Optional(SERVICE_MQTT, default=dict): schema_or(SCHEMA_CONFIG_MQTT),
vol.Optional(ATTR_DISCOVERY, default=list): vol.Any([], SCHEMA_DISCOVERY),
}, extra=vol.REMOVE_EXTRA) }, extra=vol.REMOVE_EXTRA)
DISCOVERY_SERVICES = {
SERVICE_MQTT: SCHEMA_SERVICE_MQTT,
}

View File

@ -1,4 +1,4 @@
"""Represent a snapshot file.""" """Representation of a snapshot file."""
import asyncio import asyncio
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
import json import json
@ -29,7 +29,7 @@ _LOGGER = logging.getLogger(__name__)
class Snapshot(CoreSysAttributes): class Snapshot(CoreSysAttributes):
"""A signle hassio snapshot.""" """A single Hass.io snapshot."""
def __init__(self, coresys, tar_file): def __init__(self, coresys, tar_file):
"""Initialize a snapshot.""" """Initialize a snapshot."""
@ -72,7 +72,7 @@ class Snapshot(CoreSysAttributes):
@property @property
def addon_list(self): def addon_list(self):
"""Return a list of addons slugs.""" """Return a list of add-ons slugs."""
return [addon_data[ATTR_SLUG] for addon_data in self.addons] return [addon_data[ATTR_SLUG] for addon_data in self.addons]
@property @property
@ -92,12 +92,12 @@ class Snapshot(CoreSysAttributes):
@property @property
def homeassistant_version(self): def homeassistant_version(self):
"""Return snapshot homeassistant version.""" """Return snapshot Home Assistant version."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_VERSION) return self._data[ATTR_HOMEASSISTANT].get(ATTR_VERSION)
@property @property
def homeassistant(self): def homeassistant(self):
"""Return snapshot homeassistant data.""" """Return snapshot Home Assistant data."""
return self._data[ATTR_HOMEASSISTANT] return self._data[ATTR_HOMEASSISTANT]
@property @property
@ -119,7 +119,7 @@ class Snapshot(CoreSysAttributes):
def new(self, slug, name, date, sys_type, password=None): def new(self, slug, name, date, sys_type, password=None):
"""Initialize a new snapshot.""" """Initialize a new snapshot."""
# init metadata # Init metadata
self._data[ATTR_SLUG] = slug self._data[ATTR_SLUG] = slug
self._data[ATTR_NAME] = name self._data[ATTR_NAME] = name
self._data[ATTR_DATE] = date self._data[ATTR_DATE] = date
@ -306,16 +306,16 @@ class Snapshot(CoreSysAttributes):
await asyncio.wait(tasks) await asyncio.wait(tasks)
async def store_folders(self, folder_list=None): async def store_folders(self, folder_list=None):
"""Backup hassio data into snapshot.""" """Backup Hass.io data into snapshot."""
folder_list = set(folder_list or ALL_FOLDERS) folder_list = set(folder_list or ALL_FOLDERS)
def _folder_save(name): def _folder_save(name):
"""Intenal function to snapshot a folder.""" """Internal function to snapshot a folder."""
slug_name = name.replace("/", "_") slug_name = name.replace("/", "_")
tar_name = Path(self._tmp.name, f"{slug_name}.tar.gz") tar_name = Path(self._tmp.name, f"{slug_name}.tar.gz")
origin_dir = Path(self.sys_config.path_hassio, name) origin_dir = Path(self.sys_config.path_hassio, name)
# Check if exsits # Check if exists
if not origin_dir.is_dir(): if not origin_dir.is_dir():
_LOGGER.warning("Can't find snapshot folder %s", name) _LOGGER.warning("Can't find snapshot folder %s", name)
return return
@ -338,7 +338,7 @@ class Snapshot(CoreSysAttributes):
await asyncio.wait(tasks) await asyncio.wait(tasks)
async def restore_folders(self, folder_list=None): async def restore_folders(self, folder_list=None):
"""Backup hassio data into snapshot.""" """Backup Hass.io data into snapshot."""
folder_list = set(folder_list or self.folders) folder_list = set(folder_list or self.folders)
def _folder_restore(name): def _folder_restore(name):
@ -356,7 +356,7 @@ class Snapshot(CoreSysAttributes):
if origin_dir.is_dir(): if origin_dir.is_dir():
remove_folder(origin_dir) remove_folder(origin_dir)
# Performe a restore # Perform a restore
try: try:
_LOGGER.info("Restore folder %s", name) _LOGGER.info("Restore folder %s", name)
with SecureTarFile(tar_name, 'r', key=self._key) as tar_file: with SecureTarFile(tar_name, 'r', key=self._key) as tar_file:
@ -372,7 +372,7 @@ class Snapshot(CoreSysAttributes):
await asyncio.wait(tasks) await asyncio.wait(tasks)
def store_homeassistant(self): def store_homeassistant(self):
"""Read all data from homeassistant object.""" """Read all data from Home Assistant object."""
self.homeassistant[ATTR_VERSION] = self.sys_homeassistant.version self.homeassistant[ATTR_VERSION] = self.sys_homeassistant.version
self.homeassistant[ATTR_WATCHDOG] = self.sys_homeassistant.watchdog self.homeassistant[ATTR_WATCHDOG] = self.sys_homeassistant.watchdog
self.homeassistant[ATTR_BOOT] = self.sys_homeassistant.boot self.homeassistant[ATTR_BOOT] = self.sys_homeassistant.boot
@ -393,7 +393,7 @@ class Snapshot(CoreSysAttributes):
self._encrypt_data(self.sys_homeassistant.api_password) self._encrypt_data(self.sys_homeassistant.api_password)
def restore_homeassistant(self): def restore_homeassistant(self):
"""Write all data to homeassistant object.""" """Write all data to the Home Assistant object."""
self.sys_homeassistant.watchdog = self.homeassistant[ATTR_WATCHDOG] self.sys_homeassistant.watchdog = self.homeassistant[ATTR_WATCHDOG]
self.sys_homeassistant.boot = self.homeassistant[ATTR_BOOT] self.sys_homeassistant.boot = self.homeassistant[ATTR_BOOT]
self.sys_homeassistant.wait_boot = self.homeassistant[ATTR_WAIT_BOOT] self.sys_homeassistant.wait_boot = self.homeassistant[ATTR_WAIT_BOOT]

View File

@ -1,4 +1,4 @@
"""Util addons functions.""" """Util add-on functions."""
import hashlib import hashlib
import shutil import shutil
import re import re

View File

@ -1,5 +1,4 @@
"""Validate some things around restore.""" """Validate some things around restore."""
import voluptuous as vol import voluptuous as vol
from ..const import ( from ..const import (

28
hassio/utils/validate.py Normal file
View File

@ -0,0 +1,28 @@
"""Validate utils."""
import pytz
import voluptuous as vol
def schema_or(schema):
"""Allow schema or empty."""
def _wrapper(value):
"""Wrapper for validator."""
if not value:
return value
return schema(value)
return _wrapper
def validate_timezone(timezone):
"""Validate voluptuous timezone."""
try:
pytz.timezone(timezone)
except pytz.exceptions.UnknownTimeZoneError:
raise vol.Invalid(
"Invalid time zone passed in. Valid options can be found here: "
"http://en.wikipedia.org/wiki/List_of_tz_database_time_zones") \
from None
return timezone

View File

@ -3,15 +3,17 @@ import uuid
import re import re
import voluptuous as vol import voluptuous as vol
import pytz
from .const import ( from .const import (
ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_CHANNEL, ATTR_TIMEZONE, ATTR_HASSOS, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_CHANNEL, ATTR_TIMEZONE, ATTR_HASSOS,
ATTR_ADDONS_CUSTOM_LIST, ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO, ATTR_ADDONS_CUSTOM_LIST, ATTR_PASSWORD, ATTR_HOMEASSISTANT, ATTR_HASSIO,
ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, ATTR_BOOT, ATTR_LAST_BOOT, ATTR_SSL, ATTR_PORT, ATTR_WATCHDOG, ATTR_CONFIG,
ATTR_WAIT_BOOT, ATTR_UUID, ATTR_REFRESH_TOKEN, ATTR_HASSOS_CLI, ATTR_WAIT_BOOT, ATTR_UUID, ATTR_REFRESH_TOKEN, ATTR_HASSOS_CLI,
ATTR_ACCESS_TOKEN, ATTR_ACCESS_TOKEN, ATTR_DISCOVERY, ATTR_ADDON, ATTR_COMPONENT,
ATTR_PLATFORM, ATTR_SERVICE,
SERVICE_MQTT,
CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV) CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV)
from .utils.validate import schema_or, validate_timezone
RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$") RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$")
@ -21,6 +23,8 @@ WAIT_BOOT = vol.All(vol.Coerce(int), vol.Range(min=1, max=60))
DOCKER_IMAGE = vol.Match(r"^[\w{}]+/[\-\w{}]+$") DOCKER_IMAGE = vol.Match(r"^[\w{}]+/[\-\w{}]+$")
ALSA_DEVICE = vol.Maybe(vol.Match(r"\d+,\d+")) ALSA_DEVICE = vol.Maybe(vol.Match(r"\d+,\d+"))
CHANNELS = vol.In([CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV]) CHANNELS = vol.In([CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV])
UUID_MATCH = vol.Match(r"^[0-9a-f]{32}$")
SERVICE_ALL = vol.In([SERVICE_MQTT])
def validate_repository(repository): def validate_repository(repository):
@ -40,19 +44,6 @@ def validate_repository(repository):
REPOSITORIES = vol.All([validate_repository], vol.Unique()) REPOSITORIES = vol.All([validate_repository], vol.Unique())
def validate_timezone(timezone):
"""Validate voluptuous timezone."""
try:
pytz.timezone(timezone)
except pytz.exceptions.UnknownTimeZoneError:
raise vol.Invalid(
"Invalid time zone passed in. Valid options can be found here: "
"http://en.wikipedia.org/wiki/List_of_tz_database_time_zones") \
from None
return timezone
# pylint: disable=inconsistent-return-statements # pylint: disable=inconsistent-return-statements
def convert_to_docker_ports(data): def convert_to_docker_ports(data):
"""Convert data into Docker port list.""" """Convert data into Docker port list."""
@ -83,8 +74,7 @@ DOCKER_PORTS = vol.Schema({
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
SCHEMA_HASS_CONFIG = vol.Schema({ SCHEMA_HASS_CONFIG = vol.Schema({
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH,
vol.Match(r"^[0-9a-f]{32}$"),
vol.Optional(ATTR_ACCESS_TOKEN): vol.Match(r"^[0-9a-f]{64}$"), vol.Optional(ATTR_ACCESS_TOKEN): vol.Match(r"^[0-9a-f]{64}$"),
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(), vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE, vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE,
@ -117,3 +107,19 @@ SCHEMA_HASSIO_CONFIG = vol.Schema({
]): REPOSITORIES, ]): REPOSITORIES,
vol.Optional(ATTR_WAIT_BOOT, default=5): WAIT_BOOT, vol.Optional(ATTR_WAIT_BOOT, default=5): WAIT_BOOT,
}, extra=vol.REMOVE_EXTRA) }, extra=vol.REMOVE_EXTRA)
SCHEMA_DISCOVERY = vol.Schema([
vol.Schema({
vol.Required(ATTR_UUID): UUID_MATCH,
vol.Required(ATTR_ADDON): vol.Coerce(str),
vol.Required(ATTR_SERVICE): SERVICE_ALL,
vol.Required(ATTR_COMPONENT): vol.Coerce(str),
vol.Required(ATTR_PLATFORM): vol.Maybe(vol.Coerce(str)),
vol.Required(ATTR_CONFIG): vol.Maybe(dict),
}, extra=vol.REMOVE_EXTRA)
])
SCHEMA_DISCOVERY_CONFIG = vol.Schema({
vol.Optional(ATTR_DISCOVERY, default=list): schema_or(SCHEMA_DISCOVERY),
}, extra=vol.REMOVE_EXTRA)

@ -1 +1 @@
Subproject commit d71a80c4f8b50bfe43f62c991e41b2f97f1ae9b0 Subproject commit f11ca53282541326279cec0a4d3a824db45bcecf