Compare commits

...

23 Commits
182 ... 186

Author SHA1 Message Date
Pascal Vizeli
e3ede66943 Merge pull request #1287 from home-assistant/dev
Release 186
2019-09-11 18:26:22 +02:00
Pascal Vizeli
2672b800d4 DNS fallback to docker internal one (#1286)
* DNS fallback to docker internal one

* Fix log

* Fix style

* Fix startup handling
2019-09-11 17:54:16 +02:00
Pascal Vizeli
c60d4bda92 Check supervisor docker permission (#1285)
* Check supervisor docker permission

* Update log message
2019-09-11 17:47:49 +02:00
Pascal Vizeli
db9d0f2639 Fix lint (#1284) 2019-09-11 16:37:49 +02:00
Pascal Vizeli
02d4045ec3 Add secrets support for options (#1283)
* Add secrets API

* Don't expose secrets
2019-09-11 16:29:34 +02:00
Pascal Vizeli
a308ea6927 Update Dockerfile 2019-09-05 14:20:35 +02:00
Pascal Vizeli
edc5e5e812 Update Dockerfile 2019-09-05 12:41:42 +02:00
dependabot-preview[bot]
23b65cb479 Bump pytest from 5.1.1 to 5.1.2 (#1278)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.1.1 to 5.1.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/5.1.1...5.1.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-02 23:07:11 +02:00
Pascal Vizeli
e5eabd2143 Fix typing warning / hardware (#1276) 2019-09-02 15:54:37 +02:00
Pascal Vizeli
b0dd043975 Fix typing warning / hardware (#1277) 2019-09-02 15:45:31 +02:00
Pascal Vizeli
435a1096ed Cleanup debug gdbus output (#1275) 2019-09-02 15:08:26 +02:00
Pascal Vizeli
21a9084ca0 Bump version 186 2019-09-02 14:39:56 +02:00
Pascal Vizeli
10d9135d86 Merge pull request #1274 from home-assistant/dev
Release 185
2019-09-02 14:39:17 +02:00
Pascal Vizeli
272d8b29f3 Fix version handling with nightly (#1273)
* Fix version handling with nightly

* fix lint
2019-09-02 14:37:59 +02:00
Pascal Vizeli
3d665b9eec Support for udev device trigger (#1272) 2019-09-02 14:07:09 +02:00
Pascal Vizeli
c563f484c9 Add support for udev trigger 2019-09-02 11:28:49 +00:00
Pascal Vizeli
38268ea4ea Bump version to 185 2019-08-26 10:04:36 +02:00
Pascal Vizeli
c1ad64cddf Merge pull request #1264 from home-assistant/dev
Release 184
2019-08-26 10:03:53 +02:00
Pascal Vizeli
b898cd2a3a Preserve ordering of locals (#1263)
* Preserve ordering of locals

* fix lint
2019-08-26 09:45:10 +02:00
Pascal Vizeli
937b31d845 Bump version 184 2019-08-23 14:22:47 +02:00
Pascal Vizeli
e4e655493b Merge pull request #1259 from home-assistant/dev
Release 183
2019-08-23 14:21:58 +02:00
Pascal Vizeli
387d2dcc2e Add support for gvariant annotations (#1258) 2019-08-23 13:41:17 +02:00
Pascal Vizeli
8abe33d48a Bump version 183 2019-08-22 18:58:02 +02:00
31 changed files with 258 additions and 59 deletions

View File

@@ -34,10 +34,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies from requirements.txt if it exists
COPY requirements.txt requirements_tests.txt /workspaces/
RUN pip install -r requirements.txt \
&& pip3 install -r requirements_tests.txt \
&& pip install black tox
COPY requirements.txt requirements_tests.txt ./
RUN pip3 install -r requirements.txt -r requirements_tests.txt \
&& pip3 install black tox \
&& rm -f requirements.txt requirements_tests.txt
# Set the default shell to bash instead of sh
ENV SHELL /bin/bash

4
API.md
View File

@@ -350,6 +350,10 @@ Load host configs from a USB stick.
}
```
- POST `/hardware/trigger`
Trigger an udev reload
### Home Assistant
- GET `/homeassistant/info`

View File

@@ -351,7 +351,7 @@ class Addon(AddonModel):
options = self.options
try:
schema(options)
options = schema(options)
write_json_file(self.path_options, options)
except vol.Invalid as ex:
_LOGGER.error(
@@ -438,7 +438,9 @@ class Addon(AddonModel):
options = {**self.persist[ATTR_OPTIONS], **default_options}
# create voluptuous
new_schema = vol.Schema(vol.All(dict, validate_options(new_raw_schema)))
new_schema = vol.Schema(
vol.All(dict, validate_options(self.coresys, new_raw_schema))
)
# validate
try:
@@ -471,6 +473,7 @@ class Addon(AddonModel):
if self.with_audio:
self.write_asound()
# Start Add-on
try:
await self.instance.run()
except DockerAPIError:

View File

@@ -1,8 +1,8 @@
"""Init file for Hass.io add-ons."""
from distutils.version import StrictVersion
from pathlib import Path
from typing import Any, Awaitable, Dict, List, Optional
from packaging import version as pkg_version
import voluptuous as vol
from ..const import (
@@ -461,7 +461,7 @@ class AddonModel(CoreSysAttributes):
if isinstance(raw_schema, bool):
return vol.Schema(dict)
return vol.Schema(vol.All(dict, validate_options(raw_schema)))
return vol.Schema(vol.All(dict, validate_options(self.coresys, raw_schema)))
def __eq__(self, other):
"""Compaired add-on objects."""
@@ -482,7 +482,9 @@ class AddonModel(CoreSysAttributes):
# Home Assistant
version = config.get(ATTR_HOMEASSISTANT) or self.sys_homeassistant.version
if StrictVersion(self.sys_homeassistant.version) < StrictVersion(version):
if pkg_version.parse(self.sys_homeassistant.version) < pkg_version.parse(
version
):
return False
return True

View File

@@ -109,16 +109,21 @@ V_EMAIL = "email"
V_URL = "url"
V_PORT = "port"
V_MATCH = "match"
V_LIST = "list"
RE_SCHEMA_ELEMENT = re.compile(
r"^(?:"
r"|str|bool|email|url|port"
r"|bool|email|url|port"
r"|str(?:\((?P<s_min>\d+)?,(?P<s_max>\d+)?\))?"
r"|int(?:\((?P<i_min>\d+)?,(?P<i_max>\d+)?\))?"
r"|float(?:\((?P<f_min>[\d\.]+)?,(?P<f_max>[\d\.]+)?\))?"
r"|match\((?P<match>.*)\)"
r"|list\((?P<list>.+)\)"
r")\??$"
)
_SCHEMA_LENGTH_PARTS = ("i_min", "i_max", "f_min", "f_max", "s_min", "s_max")
RE_DOCKER_IMAGE = re.compile(r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)$")
RE_DOCKER_IMAGE_BUILD = re.compile(
r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)(:[\.\-\w{}]+)?$"
@@ -305,7 +310,7 @@ SCHEMA_ADDON_SNAPSHOT = vol.Schema(
)
def validate_options(raw_schema):
def validate_options(coresys, raw_schema):
"""Validate schema."""
def validate(struct):
@@ -323,13 +328,13 @@ def validate_options(raw_schema):
try:
if isinstance(typ, list):
# nested value list
options[key] = _nested_validate_list(typ[0], value, key)
options[key] = _nested_validate_list(coresys, typ[0], value, key)
elif isinstance(typ, dict):
# nested value dict
options[key] = _nested_validate_dict(typ, value, key)
options[key] = _nested_validate_dict(coresys, typ, value, key)
else:
# normal value
options[key] = _single_validate(typ, value, key)
options[key] = _single_validate(coresys, typ, value, key)
except (IndexError, KeyError):
raise vol.Invalid(f"Type error for {key}") from None
@@ -341,24 +346,31 @@ def validate_options(raw_schema):
# pylint: disable=no-value-for-parameter
# pylint: disable=inconsistent-return-statements
def _single_validate(typ, value, key):
def _single_validate(coresys, typ, value, key):
"""Validate a single element."""
# if required argument
if value is None:
raise vol.Invalid(f"Missing required option '{key}'")
# Lookup secret
if str(value).startswith("!secret "):
secret: str = value.partition(" ")[2]
value = coresys.secrets.get(secret)
if value is None:
raise vol.Invalid(f"Unknown secret {secret}")
# parse extend data from type
match = RE_SCHEMA_ELEMENT.match(typ)
# prepare range
range_args = {}
for group_name in ("i_min", "i_max", "f_min", "f_max"):
for group_name in _SCHEMA_LENGTH_PARTS:
group_value = match.group(group_name)
if group_value:
range_args[group_name[2:]] = float(group_value)
if typ.startswith(V_STR):
return str(value)
return vol.All(str(value), vol.Range(**range_args))(value)
elif typ.startswith(V_INT):
return vol.All(vol.Coerce(int), vol.Range(**range_args))(value)
elif typ.startswith(V_FLOAT):
@@ -373,26 +385,28 @@ def _single_validate(typ, value, key):
return NETWORK_PORT(value)
elif typ.startswith(V_MATCH):
return vol.Match(match.group("match"))(str(value))
elif typ.strartswith(V_LIST):
return vol.In(match.group("list").split("|"))(str(value))
raise vol.Invalid(f"Fatal error for {key} type {typ}")
def _nested_validate_list(typ, data_list, key):
def _nested_validate_list(coresys, typ, data_list, key):
"""Validate nested items."""
options = []
for element in data_list:
# Nested?
if isinstance(typ, dict):
c_options = _nested_validate_dict(typ, element, key)
c_options = _nested_validate_dict(coresys, typ, element, key)
options.append(c_options)
else:
options.append(_single_validate(typ, element, key))
options.append(_single_validate(coresys, typ, element, key))
return options
def _nested_validate_dict(typ, data_dict, key):
def _nested_validate_dict(coresys, typ, data_dict, key):
"""Validate nested items."""
options = {}
@@ -404,9 +418,11 @@ def _nested_validate_dict(typ, data_dict, key):
# Nested?
if isinstance(typ[c_key], list):
options[c_key] = _nested_validate_list(typ[c_key][0], c_value, c_key)
options[c_key] = _nested_validate_list(
coresys, typ[c_key][0], c_value, c_key
)
else:
options[c_key] = _single_validate(typ[c_key], c_value, c_key)
options[c_key] = _single_validate(coresys, typ[c_key], c_value, c_key)
_check_missing_options(typ, options, key)
return options

View File

@@ -101,6 +101,7 @@ class RestAPI(CoreSysAttributes):
[
web.get("/hardware/info", api_hardware.info),
web.get("/hardware/audio", api_hardware.audio),
web.post("/hardware/trigger", api_hardware.trigger),
]
)

View File

@@ -269,7 +269,9 @@ class APIAddons(CoreSysAttributes):
addon_schema = SCHEMA_OPTIONS.extend(
{vol.Optional(ATTR_OPTIONS): vol.Any(None, addon.schema)}
)
body: Dict[str, Any] = await api_validate(addon_schema, request)
body: Dict[str, Any] = await api_validate(
addon_schema, request, origin=[ATTR_OPTIONS]
)
if ATTR_OPTIONS in body:
addon.options = body[ATTR_OPTIONS]

View File

@@ -1,5 +1,9 @@
"""Init file for Hass.io hardware RESTful API."""
import asyncio
import logging
from typing import Any, Dict
from aiohttp import web
from .utils import api_process
from ..const import (
@@ -19,7 +23,7 @@ class APIHardware(CoreSysAttributes):
"""Handle RESTful API for hardware functions."""
@api_process
async def info(self, request):
async def info(self, request: web.Request) -> Dict[str, Any]:
"""Show hardware info."""
return {
ATTR_SERIAL: list(
@@ -32,7 +36,7 @@ class APIHardware(CoreSysAttributes):
}
@api_process
async def audio(self, request):
async def audio(self, request: web.Request) -> Dict[str, Any]:
"""Show ALSA audio devices."""
return {
ATTR_AUDIO: {
@@ -40,3 +44,8 @@ class APIHardware(CoreSysAttributes):
ATTR_OUTPUT: self.sys_host.alsa.output_devices,
}
}
@api_process
def trigger(self, request: web.Request) -> None:
"""Trigger a udev device reload."""
return asyncio.shield(self.sys_hardware.udev_trigger())

View File

@@ -40,7 +40,9 @@ NO_SECURITY_CHECK = re.compile(
ADDONS_API_BYPASS = re.compile(
r"^(?:"
r"|/addons/self/(?!security|update)[^/]+"
r"|/secrets/.+"
r"|/info"
r"|/hardware/trigger"
r"|/services.*"
r"|/discovery.*"
r"|/auth"

View File

@@ -161,7 +161,9 @@ class APISupervisor(CoreSysAttributes):
@api_process
def reload(self, request: web.Request) -> Awaitable[None]:
"""Reload add-ons, configuration, etc."""
return asyncio.shield(self.sys_updater.reload())
return asyncio.shield(
asyncio.wait([self.sys_updater.reload(), self.sys_secrets.reload()])
)
@api_process
def repair(self, request: web.Request) -> Awaitable[None]:

View File

@@ -1,6 +1,7 @@
"""Init file for Hass.io util for RESTful API."""
import json
import logging
from typing import Optional, List
from aiohttp import web
import voluptuous as vol
@@ -89,12 +90,22 @@ def api_return_ok(data=None):
return web.json_response({JSON_RESULT: RESULT_OK, JSON_DATA: data or {}})
async def api_validate(schema, request):
async def api_validate(
schema: vol.Schema, request: web.Request, origin: Optional[List[str]] = None
):
"""Validate request data with schema."""
data = await request.json(loads=json_loads)
try:
data = schema(data)
data_validated = schema(data)
except vol.Invalid as ex:
raise APIError(humanize_error(data, ex)) from None
return data
if not origin:
return data_validated
for origin_value in origin:
if origin_value not in data_validated:
continue
data_validated[origin_value] = data[origin_value]
return data_validated

View File

@@ -27,6 +27,7 @@ from .store import StoreManager
from .supervisor import Supervisor
from .tasks import Tasks
from .updater import Updater
from .secrets import SecretsManager
from .utils.dt import fetch_timezone
_LOGGER: logging.Logger = logging.getLogger(__name__)
@@ -61,6 +62,7 @@ async def initialize_coresys():
coresys.discovery = Discovery(coresys)
coresys.dbus = DBusManager(coresys)
coresys.hassos = HassOS(coresys)
coresys.secrets = SecretsManager(coresys)
# bootstrap config
initialize_system_data(coresys)

View File

@@ -2,8 +2,8 @@
from pathlib import Path
from ipaddress import ip_network
HASSIO_VERSION = "186"
HASSIO_VERSION = "182"
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
URL_HASSIO_VERSION = "https://version.home-assistant.io/{channel}.json"
@@ -220,6 +220,7 @@ ATTR_DNS = "dns"
ATTR_SERVERS = "servers"
ATTR_LOCALS = "locals"
ATTR_UDEV = "udev"
ATTR_VALUE = "value"
PROVIDE_SERVICE = "provide"
NEED_SERVICE = "need"

View File

@@ -72,6 +72,9 @@ class HassIO(CoreSysAttributes):
# Load ingress
await self.sys_ingress.load()
# Load secrets
await self.sys_secrets.load()
async def start(self):
"""Start Hass.io orchestration."""
await self.sys_api.start()

View File

@@ -24,6 +24,7 @@ if TYPE_CHECKING:
from .homeassistant import HomeAssistant
from .host import HostManager
from .ingress import Ingress
from .secrets import SecretsManager
from .services import ServiceManager
from .snapshots import SnapshotManager
from .supervisor import Supervisor
@@ -70,6 +71,7 @@ class CoreSys:
self._dbus: Optional[DBusManager] = None
self._hassos: Optional[HassOS] = None
self._services: Optional[ServiceManager] = None
self._secrets: Optional[SecretsManager] = None
self._store: Optional[StoreManager] = None
self._discovery: Optional[Discovery] = None
@@ -209,6 +211,18 @@ class CoreSys:
raise RuntimeError("Updater already set!")
self._updater = value
@property
def secrets(self) -> SecretsManager:
"""Return SecretsManager object."""
return self._secrets
@secrets.setter
def secrets(self, value: SecretsManager):
"""Set a Updater object."""
if self._secrets:
raise RuntimeError("SecretsManager already set!")
self._secrets = value
@property
def addons(self) -> AddonManager:
"""Return AddonManager object."""
@@ -437,6 +451,11 @@ class CoreSysAttributes:
"""Return Updater object."""
return self.coresys.updater
@property
def sys_secrets(self) -> SecretsManager:
"""Return SecretsManager object."""
return self.coresys.secrets
@property
def sys_addons(self) -> AddonManager:
"""Return AddonManager object."""

View File

@@ -115,14 +115,15 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
# Start DNS forwarder
self.sys_create_task(self.forwarder.start(self.sys_docker.network.dns))
self._update_local_resolv()
with suppress(CoreDNSError):
self._update_local_resolv()
# Start is not Running
# Reset container configuration
if await self.instance.is_running():
await self.restart()
else:
with suppress(DockerAPIError):
await self.instance.stop()
# Run CoreDNS
with suppress(CoreDNSError):
await self.start()
async def unload(self) -> None:
@@ -148,9 +149,8 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
self.version = self.instance.version
self.save_data()
# Init Hosts / Run server
# Init Hosts
self.write_hosts()
await self.start()
async def update(self, version: Optional[str] = None) -> None:
"""Update CoreDNS plugin."""
@@ -207,6 +207,9 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
def _write_corefile(self) -> None:
"""Write CoreDNS config."""
dns_servers: List[str] = []
# Load Template
try:
corefile_template: Template = Template(COREDNS_TMPL.read_text())
except OSError as err:
@@ -214,8 +217,8 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
raise CoreDNSError() from None
# Prepare DNS serverlist: Prio 1 Local, Prio 2 Manual, Prio 3 Fallback
dns_servers = []
for server in self.sys_host.network.dns_servers + self.servers + DNS_SERVERS:
local_dns: List[str] = self.sys_host.network.dns_servers or ["dns://127.0.0.11"]
for server in local_dns + self.servers + DNS_SERVERS:
try:
DNS_URL(server)
if server not in dns_servers:
@@ -358,7 +361,7 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
resolv_lines.append(line.strip())
except OSError as err:
_LOGGER.warning("Can't read local resolv: %s", err)
raise CoreDNSError() from None
return
if nameserver in resolv_lines:
return
@@ -372,4 +375,4 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
resolv.write(f"{line}\n")
except OSError as err:
_LOGGER.warning("Can't write local resolv: %s", err)
raise CoreDNSError() from None
return

View File

@@ -54,6 +54,7 @@ class DockerAPI:
self,
image: str,
version: str = "latest",
dns: bool = True,
ipv4: Optional[IPv4Address] = None,
**kwargs: Dict[str, Any],
) -> docker.models.containers.Container:
@@ -61,14 +62,15 @@ class DockerAPI:
Need run inside executor.
"""
name: str = kwargs.get("name", image)
name: str = kwargs.get("name")
network_mode: str = kwargs.get("network_mode")
hostname: str = kwargs.get("hostname")
# Setup DNS
kwargs["dns"] = [str(self.network.dns)]
kwargs["dns_search"] = [DNS_SUFFIX]
kwargs["domainname"] = DNS_SUFFIX
if dns:
kwargs["dns"] = [str(self.network.dns)]
kwargs["dns_search"] = [DNS_SUFFIX]
kwargs["domainname"] = DNS_SUFFIX
# Setup network
if not network_mode:

View File

@@ -41,6 +41,7 @@ class DockerDNS(DockerInterface, CoreSysAttributes):
docker_container = self.sys_docker.run(
self.image,
version=self.sys_dns.version,
dns=False,
ipv4=self.sys_docker.network.dns,
name=self.name,
hostname=self.name.replace("_", "-"),

View File

@@ -42,6 +42,13 @@ class DockerInterface(CoreSysAttributes):
return {}
return self._meta.get("Config", {})
@property
def meta_host(self) -> Dict[str, Any]:
"""Return meta data of configuration for host."""
if not self._meta:
return {}
return self._meta.get("HostConfig", {})
@property
def meta_labels(self) -> Dict[str, str]:
"""Return meta data of labels for container/image."""

View File

@@ -26,6 +26,11 @@ class DockerSupervisor(DockerInterface, CoreSysAttributes):
"""Return IP address of this container."""
return self.sys_docker.network.supervisor
@property
def privileged(self) -> bool:
"""Return True if the container run with Privileged."""
return self.meta_host.get("Privileged", False)
def _attach(self, tag: str) -> None:
"""Attach to running docker container.

View File

@@ -188,3 +188,10 @@ class JsonFileError(HassioError):
class DockerAPIError(HassioError):
"""Docker API error."""
# Hardware
class HardwareNotSupportedError(HassioNotSupportedError):
"""Raise if hardware function is not supported."""

View File

@@ -2,7 +2,6 @@
import asyncio
from contextlib import asynccontextmanager, suppress
from datetime import datetime, timedelta
from distutils.version import StrictVersion
from ipaddress import IPv4Address
import logging
import os
@@ -16,6 +15,7 @@ from uuid import UUID
import aiohttp
from aiohttp import hdrs
import attr
from packaging import version as pkg_version
from .const import (
ATTR_ACCESS_TOKEN,
@@ -80,7 +80,9 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
try:
# Evaluate Version if we lost this information
if not self.version:
self.version = await self.instance.get_latest_version(key=StrictVersion)
self.version = await self.instance.get_latest_version(
key=pkg_version.parse
)
await self.instance.attach(tag=self.version)
except DockerAPIError:

View File

@@ -1,6 +1,6 @@
"""Info control for host."""
import logging
from typing import List, Set
from typing import List
from ..coresys import CoreSysAttributes, CoreSys
from ..exceptions import HostNotSupportedError, DBusNotConnectedError, DBusError
@@ -19,13 +19,13 @@ class NetworkManager(CoreSysAttributes):
def dns_servers(self) -> List[str]:
"""Return a list of local DNS servers."""
# Read all local dns servers
servers: Set[str] = set()
servers: List[str] = []
for config in self.sys_dbus.nmi_dns.configuration:
if config.vpn or not config.nameservers:
continue
servers |= set(config.nameservers)
servers.extend(config.nameservers)
return [f"dns://{server}" for server in servers]
return [f"dns://{server}" for server in list(dict.fromkeys(servers))]
async def update(self):
"""Update properties over dbus."""

View File

@@ -1,4 +1,5 @@
"""Read hardware info from system."""
import asyncio
from datetime import datetime
import logging
from pathlib import Path
@@ -8,6 +9,7 @@ from typing import Any, Dict, Optional, Set
import pyudev
from ..const import ATTR_DEVICES, ATTR_NAME, ATTR_TYPE, CHAN_ID, CHAN_TYPE
from ..exceptions import HardwareNotSupportedError
_LOGGER: logging.Logger = logging.getLogger(__name__)
@@ -148,3 +150,14 @@ class Hardware:
return None
return datetime.utcfromtimestamp(int(found.group(1)))
async def udev_trigger(self) -> None:
"""Trigger a udev reload."""
proc = await asyncio.create_subprocess_exec("udevadm", "trigger")
await proc.wait()
if proc.returncode == 0:
return
_LOGGER.warning("udevadm device triggering fails!")
raise HardwareNotSupportedError()

54
hassio/secrets.py Normal file
View File

@@ -0,0 +1,54 @@
"""Handle Home Assistant secrets to add-ons."""
from typing import Dict
from pathlib import Path
import logging
from ruamel.yaml import YAML, YAMLError
from .coresys import CoreSys, CoreSysAttributes
_LOGGER: logging.Logger = logging.getLogger(__name__)
class SecretsManager(CoreSysAttributes):
"""Manage Home Assistant secrets."""
def __init__(self, coresys: CoreSys):
"""Initialize secret manager."""
self.coresys: CoreSys = coresys
self.secrets: Dict[str, str] = {}
@property
def path_secrets(self) -> Path:
"""Return path to secret file."""
return Path(self.sys_config.path_homeassistant, "secrets.yaml")
def get(self, secret: str) -> str:
"""Get secret from store."""
_LOGGER.info("Request secret %s", secret)
return self.secrets.get(secret)
async def load(self) -> None:
"""Load secrets on start."""
await self._read_secrets()
_LOGGER.info("Load Home Assistant secrets: %s", len(self.secrets))
async def reload(self) -> None:
"""Reload secrets."""
await self._read_secrets()
async def _read_secrets(self):
"""Read secrets.yaml into memory."""
if not self.path_secrets.exists():
_LOGGER.debug("Home Assistant secrets not exists")
return
# Read secrets
try:
yaml = YAML()
self.secrets = await self.sys_run_in_executor(yaml.load, self.path_secrets)
except YAMLError as err:
_LOGGER.error("Can't process Home Assistant secrets: %s", err)
else:
_LOGGER.debug("Reload Home Assistant secrets: %s", len(self.secrets))

View File

@@ -41,6 +41,12 @@ class Supervisor(CoreSysAttributes):
with suppress(DockerAPIError):
await self.instance.cleanup()
# Check privileged mode
if not self.instance.privileged:
_LOGGER.error(
"Supervisor does not run in Privileged mode. Hassio runs with limited functionality!"
)
@property
def ip_address(self) -> IPv4Address:
"""Return IP of Supervisor instance."""

View File

@@ -19,6 +19,7 @@ RUN_RELOAD_SNAPSHOTS = 72000
RUN_RELOAD_HOST = 72000
RUN_RELOAD_UPDATER = 7200
RUN_RELOAD_INGRESS = 930
RUN_RELOAD_SECRETS = 940
RUN_WATCHDOG_HOMEASSISTANT_DOCKER = 15
RUN_WATCHDOG_HOMEASSISTANT_API = 300
@@ -77,6 +78,11 @@ class Tasks(CoreSysAttributes):
self.sys_ingress.reload, RUN_RELOAD_INGRESS
)
)
self.jobs.add(
self.sys_scheduler.register_task(
self.sys_secrets.reload, RUN_RELOAD_SECRETS
)
)
# Watchdog
self.jobs.add(

View File

@@ -21,8 +21,8 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
# Use to convert GVariant into json
RE_GVARIANT_TYPE: re.Match = re.compile(
r"(?:boolean|byte|int16|uint16|int32|uint32|handle|int64|uint64|double|"
r"string|objectpath|signature) "
r"\"[^\"\\]*(?:\\.[^\"\\]*)*\"|(boolean|byte|int16|uint16|int32|uint32|handle|int64|uint64|double|"
r"string|objectpath|signature|@[asviumodf\{\}]+) "
)
RE_GVARIANT_VARIANT: re.Match = re.compile(r"\"[^\"\\]*(?:\\.[^\"\\]*)*\"|(<|>)")
RE_GVARIANT_STRING_ESC: re.Match = re.compile(
@@ -90,7 +90,6 @@ class DBus:
raise DBusParseError() from None
# Read available methods
_LOGGER.debug("Introspect XML: %s", data)
for interface in xml.findall("./interface"):
interface_name = interface.get("name")
@@ -107,11 +106,16 @@ class DBus:
@staticmethod
def parse_gvariant(raw: str) -> Any:
"""Parse GVariant input to python."""
json_raw: str = RE_GVARIANT_TYPE.sub("", raw)
# Process first string
json_raw = RE_GVARIANT_STRING_ESC.sub(
lambda x: x.group(0).replace('"', '\\"'), json_raw
lambda x: x.group(0).replace('"', '\\"'), raw
)
json_raw = RE_GVARIANT_STRING.sub(r'"\1"', json_raw)
# Remove complex type handling
json_raw: str = RE_GVARIANT_TYPE.sub(
lambda x: x.group(0) if not x.group(1) else "", json_raw
)
json_raw = RE_GVARIANT_VARIANT.sub(
lambda x: x.group(0) if not x.group(1) else "", json_raw
)

View File

@@ -7,8 +7,10 @@ cpe==1.2.1
cryptography==2.7
docker==4.0.2
gitpython==3.0.2
packaging==19.1
pytz==2019.2
pyudev==0.21.0
ruamel.yaml==0.15.100
uvloop==0.12.2
voluptuous==0.11.7
ptvsd==4.3.2

View File

@@ -1,5 +1,5 @@
flake8==3.7.8
pylint==2.3.1
pytest==5.1.1
pytest==5.1.2
pytest-timeout==1.3.3
pytest-aiohttp==0.3.0

View File

@@ -300,3 +300,13 @@ def test_networkmanager_dns_properties():
],
}
]
def test_networkmanager_dns_properties_empty():
"""Test NetworkManager DNS properties."""
raw = "({'Mode': <'default'>, 'RcManager': <'resolvconf'>, 'Configuration': <@aa{sv} []>},)"
# parse data
data = DBus.parse_gvariant(raw)
assert data == [{"Mode": "default", "RcManager": "resolvconf", "Configuration": []}]