mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-28 03:26:32 +00:00
commit
e3ede66943
@ -34,10 +34,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install Python dependencies from requirements.txt if it exists
|
# Install Python dependencies from requirements.txt if it exists
|
||||||
COPY requirements.txt requirements_tests.txt /workspaces/
|
COPY requirements.txt requirements_tests.txt ./
|
||||||
RUN pip install -r requirements.txt \
|
RUN pip3 install -r requirements.txt -r requirements_tests.txt \
|
||||||
&& pip3 install -r requirements_tests.txt \
|
&& pip3 install black tox \
|
||||||
&& pip install black tox
|
&& rm -f requirements.txt requirements_tests.txt
|
||||||
|
|
||||||
# Set the default shell to bash instead of sh
|
# Set the default shell to bash instead of sh
|
||||||
ENV SHELL /bin/bash
|
ENV SHELL /bin/bash
|
||||||
|
@ -351,7 +351,7 @@ class Addon(AddonModel):
|
|||||||
options = self.options
|
options = self.options
|
||||||
|
|
||||||
try:
|
try:
|
||||||
schema(options)
|
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(
|
_LOGGER.error(
|
||||||
@ -438,7 +438,9 @@ class Addon(AddonModel):
|
|||||||
options = {**self.persist[ATTR_OPTIONS], **default_options}
|
options = {**self.persist[ATTR_OPTIONS], **default_options}
|
||||||
|
|
||||||
# create voluptuous
|
# 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
|
# validate
|
||||||
try:
|
try:
|
||||||
|
@ -461,7 +461,7 @@ class AddonModel(CoreSysAttributes):
|
|||||||
|
|
||||||
if isinstance(raw_schema, bool):
|
if isinstance(raw_schema, bool):
|
||||||
return vol.Schema(dict)
|
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):
|
def __eq__(self, other):
|
||||||
"""Compaired add-on objects."""
|
"""Compaired add-on objects."""
|
||||||
|
@ -109,16 +109,21 @@ V_EMAIL = "email"
|
|||||||
V_URL = "url"
|
V_URL = "url"
|
||||||
V_PORT = "port"
|
V_PORT = "port"
|
||||||
V_MATCH = "match"
|
V_MATCH = "match"
|
||||||
|
V_LIST = "list"
|
||||||
|
|
||||||
RE_SCHEMA_ELEMENT = re.compile(
|
RE_SCHEMA_ELEMENT = re.compile(
|
||||||
r"^(?:"
|
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"|int(?:\((?P<i_min>\d+)?,(?P<i_max>\d+)?\))?"
|
||||||
r"|float(?:\((?P<f_min>[\d\.]+)?,(?P<f_max>[\d\.]+)?\))?"
|
r"|float(?:\((?P<f_min>[\d\.]+)?,(?P<f_max>[\d\.]+)?\))?"
|
||||||
r"|match\((?P<match>.*)\)"
|
r"|match\((?P<match>.*)\)"
|
||||||
|
r"|list\((?P<list>.+)\)"
|
||||||
r")\??$"
|
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 = re.compile(r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)$")
|
||||||
RE_DOCKER_IMAGE_BUILD = re.compile(
|
RE_DOCKER_IMAGE_BUILD = re.compile(
|
||||||
r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)(:[\.\-\w{}]+)?$"
|
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."""
|
"""Validate schema."""
|
||||||
|
|
||||||
def validate(struct):
|
def validate(struct):
|
||||||
@ -323,13 +328,13 @@ def validate_options(raw_schema):
|
|||||||
try:
|
try:
|
||||||
if isinstance(typ, list):
|
if isinstance(typ, list):
|
||||||
# nested value 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):
|
elif isinstance(typ, dict):
|
||||||
# nested value dict
|
# nested value dict
|
||||||
options[key] = _nested_validate_dict(typ, value, key)
|
options[key] = _nested_validate_dict(coresys, typ, value, key)
|
||||||
else:
|
else:
|
||||||
# normal value
|
# normal value
|
||||||
options[key] = _single_validate(typ, value, key)
|
options[key] = _single_validate(coresys, typ, value, key)
|
||||||
except (IndexError, KeyError):
|
except (IndexError, KeyError):
|
||||||
raise vol.Invalid(f"Type error for {key}") from None
|
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=no-value-for-parameter
|
||||||
# pylint: disable=inconsistent-return-statements
|
# pylint: disable=inconsistent-return-statements
|
||||||
def _single_validate(typ, value, key):
|
def _single_validate(coresys, typ, value, key):
|
||||||
"""Validate a single element."""
|
"""Validate a single element."""
|
||||||
# if required argument
|
# if required argument
|
||||||
if value is None:
|
if value is None:
|
||||||
raise vol.Invalid(f"Missing required option '{key}'")
|
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
|
# parse extend data from type
|
||||||
match = RE_SCHEMA_ELEMENT.match(typ)
|
match = RE_SCHEMA_ELEMENT.match(typ)
|
||||||
|
|
||||||
# prepare range
|
# prepare range
|
||||||
range_args = {}
|
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)
|
group_value = match.group(group_name)
|
||||||
if group_value:
|
if group_value:
|
||||||
range_args[group_name[2:]] = float(group_value)
|
range_args[group_name[2:]] = float(group_value)
|
||||||
|
|
||||||
if typ.startswith(V_STR):
|
if typ.startswith(V_STR):
|
||||||
return str(value)
|
return vol.All(str(value), vol.Range(**range_args))(value)
|
||||||
elif typ.startswith(V_INT):
|
elif typ.startswith(V_INT):
|
||||||
return vol.All(vol.Coerce(int), vol.Range(**range_args))(value)
|
return vol.All(vol.Coerce(int), vol.Range(**range_args))(value)
|
||||||
elif typ.startswith(V_FLOAT):
|
elif typ.startswith(V_FLOAT):
|
||||||
@ -373,26 +385,28 @@ def _single_validate(typ, value, key):
|
|||||||
return NETWORK_PORT(value)
|
return NETWORK_PORT(value)
|
||||||
elif typ.startswith(V_MATCH):
|
elif typ.startswith(V_MATCH):
|
||||||
return vol.Match(match.group("match"))(str(value))
|
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}")
|
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."""
|
"""Validate nested items."""
|
||||||
options = []
|
options = []
|
||||||
|
|
||||||
for element in data_list:
|
for element in data_list:
|
||||||
# Nested?
|
# Nested?
|
||||||
if isinstance(typ, dict):
|
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)
|
options.append(c_options)
|
||||||
else:
|
else:
|
||||||
options.append(_single_validate(typ, element, key))
|
options.append(_single_validate(coresys, typ, element, key))
|
||||||
|
|
||||||
return options
|
return options
|
||||||
|
|
||||||
|
|
||||||
def _nested_validate_dict(typ, data_dict, key):
|
def _nested_validate_dict(coresys, typ, data_dict, key):
|
||||||
"""Validate nested items."""
|
"""Validate nested items."""
|
||||||
options = {}
|
options = {}
|
||||||
|
|
||||||
@ -404,9 +418,11 @@ def _nested_validate_dict(typ, data_dict, key):
|
|||||||
|
|
||||||
# Nested?
|
# Nested?
|
||||||
if isinstance(typ[c_key], list):
|
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:
|
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)
|
_check_missing_options(typ, options, key)
|
||||||
return options
|
return options
|
||||||
|
@ -269,7 +269,9 @@ class APIAddons(CoreSysAttributes):
|
|||||||
addon_schema = SCHEMA_OPTIONS.extend(
|
addon_schema = SCHEMA_OPTIONS.extend(
|
||||||
{vol.Optional(ATTR_OPTIONS): vol.Any(None, addon.schema)}
|
{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:
|
if ATTR_OPTIONS in body:
|
||||||
addon.options = body[ATTR_OPTIONS]
|
addon.options = body[ATTR_OPTIONS]
|
||||||
|
@ -40,6 +40,7 @@ NO_SECURITY_CHECK = re.compile(
|
|||||||
ADDONS_API_BYPASS = re.compile(
|
ADDONS_API_BYPASS = re.compile(
|
||||||
r"^(?:"
|
r"^(?:"
|
||||||
r"|/addons/self/(?!security|update)[^/]+"
|
r"|/addons/self/(?!security|update)[^/]+"
|
||||||
|
r"|/secrets/.+"
|
||||||
r"|/info"
|
r"|/info"
|
||||||
r"|/hardware/trigger"
|
r"|/hardware/trigger"
|
||||||
r"|/services.*"
|
r"|/services.*"
|
||||||
|
@ -161,7 +161,9 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
def reload(self, request: web.Request) -> Awaitable[None]:
|
def reload(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Reload add-ons, configuration, etc."""
|
"""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
|
@api_process
|
||||||
def repair(self, request: web.Request) -> Awaitable[None]:
|
def repair(self, request: web.Request) -> Awaitable[None]:
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Init file for Hass.io util for RESTful API."""
|
"""Init file for Hass.io util for RESTful API."""
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
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 {}})
|
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."""
|
"""Validate request data with schema."""
|
||||||
data = await request.json(loads=json_loads)
|
data = await request.json(loads=json_loads)
|
||||||
try:
|
try:
|
||||||
data = schema(data)
|
data_validated = schema(data)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
raise APIError(humanize_error(data, ex)) from None
|
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
|
||||||
|
@ -27,6 +27,7 @@ from .store import StoreManager
|
|||||||
from .supervisor import Supervisor
|
from .supervisor import Supervisor
|
||||||
from .tasks import Tasks
|
from .tasks import Tasks
|
||||||
from .updater import Updater
|
from .updater import Updater
|
||||||
|
from .secrets import SecretsManager
|
||||||
from .utils.dt import fetch_timezone
|
from .utils.dt import fetch_timezone
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@ -61,6 +62,7 @@ async def initialize_coresys():
|
|||||||
coresys.discovery = Discovery(coresys)
|
coresys.discovery = Discovery(coresys)
|
||||||
coresys.dbus = DBusManager(coresys)
|
coresys.dbus = DBusManager(coresys)
|
||||||
coresys.hassos = HassOS(coresys)
|
coresys.hassos = HassOS(coresys)
|
||||||
|
coresys.secrets = SecretsManager(coresys)
|
||||||
|
|
||||||
# bootstrap config
|
# bootstrap config
|
||||||
initialize_system_data(coresys)
|
initialize_system_data(coresys)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from ipaddress import ip_network
|
from ipaddress import ip_network
|
||||||
|
|
||||||
HASSIO_VERSION = "185"
|
HASSIO_VERSION = "186"
|
||||||
|
|
||||||
|
|
||||||
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
||||||
@ -220,6 +220,7 @@ ATTR_DNS = "dns"
|
|||||||
ATTR_SERVERS = "servers"
|
ATTR_SERVERS = "servers"
|
||||||
ATTR_LOCALS = "locals"
|
ATTR_LOCALS = "locals"
|
||||||
ATTR_UDEV = "udev"
|
ATTR_UDEV = "udev"
|
||||||
|
ATTR_VALUE = "value"
|
||||||
|
|
||||||
PROVIDE_SERVICE = "provide"
|
PROVIDE_SERVICE = "provide"
|
||||||
NEED_SERVICE = "need"
|
NEED_SERVICE = "need"
|
||||||
|
@ -72,6 +72,9 @@ class HassIO(CoreSysAttributes):
|
|||||||
# Load ingress
|
# Load ingress
|
||||||
await self.sys_ingress.load()
|
await self.sys_ingress.load()
|
||||||
|
|
||||||
|
# Load secrets
|
||||||
|
await self.sys_secrets.load()
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""Start Hass.io orchestration."""
|
"""Start Hass.io orchestration."""
|
||||||
await self.sys_api.start()
|
await self.sys_api.start()
|
||||||
|
@ -24,6 +24,7 @@ if TYPE_CHECKING:
|
|||||||
from .homeassistant import HomeAssistant
|
from .homeassistant import HomeAssistant
|
||||||
from .host import HostManager
|
from .host import HostManager
|
||||||
from .ingress import Ingress
|
from .ingress import Ingress
|
||||||
|
from .secrets import SecretsManager
|
||||||
from .services import ServiceManager
|
from .services import ServiceManager
|
||||||
from .snapshots import SnapshotManager
|
from .snapshots import SnapshotManager
|
||||||
from .supervisor import Supervisor
|
from .supervisor import Supervisor
|
||||||
@ -70,6 +71,7 @@ class CoreSys:
|
|||||||
self._dbus: Optional[DBusManager] = None
|
self._dbus: Optional[DBusManager] = None
|
||||||
self._hassos: Optional[HassOS] = None
|
self._hassos: Optional[HassOS] = None
|
||||||
self._services: Optional[ServiceManager] = None
|
self._services: Optional[ServiceManager] = None
|
||||||
|
self._secrets: Optional[SecretsManager] = None
|
||||||
self._store: Optional[StoreManager] = None
|
self._store: Optional[StoreManager] = None
|
||||||
self._discovery: Optional[Discovery] = None
|
self._discovery: Optional[Discovery] = None
|
||||||
|
|
||||||
@ -209,6 +211,18 @@ class CoreSys:
|
|||||||
raise RuntimeError("Updater already set!")
|
raise RuntimeError("Updater already set!")
|
||||||
self._updater = value
|
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
|
@property
|
||||||
def addons(self) -> AddonManager:
|
def addons(self) -> AddonManager:
|
||||||
"""Return AddonManager object."""
|
"""Return AddonManager object."""
|
||||||
@ -437,6 +451,11 @@ class CoreSysAttributes:
|
|||||||
"""Return Updater object."""
|
"""Return Updater object."""
|
||||||
return self.coresys.updater
|
return self.coresys.updater
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_secrets(self) -> SecretsManager:
|
||||||
|
"""Return SecretsManager object."""
|
||||||
|
return self.coresys.secrets
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sys_addons(self) -> AddonManager:
|
def sys_addons(self) -> AddonManager:
|
||||||
"""Return AddonManager object."""
|
"""Return AddonManager object."""
|
||||||
|
@ -115,14 +115,15 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
|
|||||||
|
|
||||||
# Start DNS forwarder
|
# Start DNS forwarder
|
||||||
self.sys_create_task(self.forwarder.start(self.sys_docker.network.dns))
|
self.sys_create_task(self.forwarder.start(self.sys_docker.network.dns))
|
||||||
|
self._update_local_resolv()
|
||||||
|
|
||||||
with suppress(CoreDNSError):
|
# Reset container configuration
|
||||||
self._update_local_resolv()
|
|
||||||
|
|
||||||
# Start is not Running
|
|
||||||
if await self.instance.is_running():
|
if await self.instance.is_running():
|
||||||
await self.restart()
|
with suppress(DockerAPIError):
|
||||||
else:
|
await self.instance.stop()
|
||||||
|
|
||||||
|
# Run CoreDNS
|
||||||
|
with suppress(CoreDNSError):
|
||||||
await self.start()
|
await self.start()
|
||||||
|
|
||||||
async def unload(self) -> None:
|
async def unload(self) -> None:
|
||||||
@ -148,9 +149,8 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
|
|||||||
self.version = self.instance.version
|
self.version = self.instance.version
|
||||||
self.save_data()
|
self.save_data()
|
||||||
|
|
||||||
# Init Hosts / Run server
|
# Init Hosts
|
||||||
self.write_hosts()
|
self.write_hosts()
|
||||||
await self.start()
|
|
||||||
|
|
||||||
async def update(self, version: Optional[str] = None) -> None:
|
async def update(self, version: Optional[str] = None) -> None:
|
||||||
"""Update CoreDNS plugin."""
|
"""Update CoreDNS plugin."""
|
||||||
@ -207,6 +207,9 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
|
|||||||
|
|
||||||
def _write_corefile(self) -> None:
|
def _write_corefile(self) -> None:
|
||||||
"""Write CoreDNS config."""
|
"""Write CoreDNS config."""
|
||||||
|
dns_servers: List[str] = []
|
||||||
|
|
||||||
|
# Load Template
|
||||||
try:
|
try:
|
||||||
corefile_template: Template = Template(COREDNS_TMPL.read_text())
|
corefile_template: Template = Template(COREDNS_TMPL.read_text())
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
@ -214,8 +217,8 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
|
|||||||
raise CoreDNSError() from None
|
raise CoreDNSError() from None
|
||||||
|
|
||||||
# Prepare DNS serverlist: Prio 1 Local, Prio 2 Manual, Prio 3 Fallback
|
# Prepare DNS serverlist: Prio 1 Local, Prio 2 Manual, Prio 3 Fallback
|
||||||
dns_servers = []
|
local_dns: List[str] = self.sys_host.network.dns_servers or ["dns://127.0.0.11"]
|
||||||
for server in self.sys_host.network.dns_servers + self.servers + DNS_SERVERS:
|
for server in local_dns + self.servers + DNS_SERVERS:
|
||||||
try:
|
try:
|
||||||
DNS_URL(server)
|
DNS_URL(server)
|
||||||
if server not in dns_servers:
|
if server not in dns_servers:
|
||||||
@ -358,7 +361,7 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
|
|||||||
resolv_lines.append(line.strip())
|
resolv_lines.append(line.strip())
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
_LOGGER.warning("Can't read local resolv: %s", err)
|
_LOGGER.warning("Can't read local resolv: %s", err)
|
||||||
raise CoreDNSError() from None
|
return
|
||||||
|
|
||||||
if nameserver in resolv_lines:
|
if nameserver in resolv_lines:
|
||||||
return
|
return
|
||||||
@ -372,4 +375,4 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
|
|||||||
resolv.write(f"{line}\n")
|
resolv.write(f"{line}\n")
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
_LOGGER.warning("Can't write local resolv: %s", err)
|
_LOGGER.warning("Can't write local resolv: %s", err)
|
||||||
raise CoreDNSError() from None
|
return
|
||||||
|
@ -54,6 +54,7 @@ class DockerAPI:
|
|||||||
self,
|
self,
|
||||||
image: str,
|
image: str,
|
||||||
version: str = "latest",
|
version: str = "latest",
|
||||||
|
dns: bool = True,
|
||||||
ipv4: Optional[IPv4Address] = None,
|
ipv4: Optional[IPv4Address] = None,
|
||||||
**kwargs: Dict[str, Any],
|
**kwargs: Dict[str, Any],
|
||||||
) -> docker.models.containers.Container:
|
) -> docker.models.containers.Container:
|
||||||
@ -61,14 +62,15 @@ class DockerAPI:
|
|||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
name: str = kwargs.get("name", image)
|
name: str = kwargs.get("name")
|
||||||
network_mode: str = kwargs.get("network_mode")
|
network_mode: str = kwargs.get("network_mode")
|
||||||
hostname: str = kwargs.get("hostname")
|
hostname: str = kwargs.get("hostname")
|
||||||
|
|
||||||
# Setup DNS
|
# Setup DNS
|
||||||
kwargs["dns"] = [str(self.network.dns)]
|
if dns:
|
||||||
kwargs["dns_search"] = [DNS_SUFFIX]
|
kwargs["dns"] = [str(self.network.dns)]
|
||||||
kwargs["domainname"] = DNS_SUFFIX
|
kwargs["dns_search"] = [DNS_SUFFIX]
|
||||||
|
kwargs["domainname"] = DNS_SUFFIX
|
||||||
|
|
||||||
# Setup network
|
# Setup network
|
||||||
if not network_mode:
|
if not network_mode:
|
||||||
|
@ -41,6 +41,7 @@ class DockerDNS(DockerInterface, CoreSysAttributes):
|
|||||||
docker_container = self.sys_docker.run(
|
docker_container = self.sys_docker.run(
|
||||||
self.image,
|
self.image,
|
||||||
version=self.sys_dns.version,
|
version=self.sys_dns.version,
|
||||||
|
dns=False,
|
||||||
ipv4=self.sys_docker.network.dns,
|
ipv4=self.sys_docker.network.dns,
|
||||||
name=self.name,
|
name=self.name,
|
||||||
hostname=self.name.replace("_", "-"),
|
hostname=self.name.replace("_", "-"),
|
||||||
|
@ -42,6 +42,13 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
return {}
|
return {}
|
||||||
return self._meta.get("Config", {})
|
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
|
@property
|
||||||
def meta_labels(self) -> Dict[str, str]:
|
def meta_labels(self) -> Dict[str, str]:
|
||||||
"""Return meta data of labels for container/image."""
|
"""Return meta data of labels for container/image."""
|
||||||
|
@ -26,6 +26,11 @@ class DockerSupervisor(DockerInterface, CoreSysAttributes):
|
|||||||
"""Return IP address of this container."""
|
"""Return IP address of this container."""
|
||||||
return self.sys_docker.network.supervisor
|
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:
|
def _attach(self, tag: str) -> None:
|
||||||
"""Attach to running docker container.
|
"""Attach to running docker container.
|
||||||
|
|
||||||
|
@ -159,5 +159,5 @@ class Hardware:
|
|||||||
if proc.returncode == 0:
|
if proc.returncode == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.waring("udevadm device triggering fails!")
|
_LOGGER.warning("udevadm device triggering fails!")
|
||||||
raise HardwareNotSupportedError()
|
raise HardwareNotSupportedError()
|
||||||
|
54
hassio/secrets.py
Normal file
54
hassio/secrets.py
Normal 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))
|
@ -41,6 +41,12 @@ class Supervisor(CoreSysAttributes):
|
|||||||
with suppress(DockerAPIError):
|
with suppress(DockerAPIError):
|
||||||
await self.instance.cleanup()
|
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
|
@property
|
||||||
def ip_address(self) -> IPv4Address:
|
def ip_address(self) -> IPv4Address:
|
||||||
"""Return IP of Supervisor instance."""
|
"""Return IP of Supervisor instance."""
|
||||||
|
@ -19,6 +19,7 @@ RUN_RELOAD_SNAPSHOTS = 72000
|
|||||||
RUN_RELOAD_HOST = 72000
|
RUN_RELOAD_HOST = 72000
|
||||||
RUN_RELOAD_UPDATER = 7200
|
RUN_RELOAD_UPDATER = 7200
|
||||||
RUN_RELOAD_INGRESS = 930
|
RUN_RELOAD_INGRESS = 930
|
||||||
|
RUN_RELOAD_SECRETS = 940
|
||||||
|
|
||||||
RUN_WATCHDOG_HOMEASSISTANT_DOCKER = 15
|
RUN_WATCHDOG_HOMEASSISTANT_DOCKER = 15
|
||||||
RUN_WATCHDOG_HOMEASSISTANT_API = 300
|
RUN_WATCHDOG_HOMEASSISTANT_API = 300
|
||||||
@ -77,6 +78,11 @@ class Tasks(CoreSysAttributes):
|
|||||||
self.sys_ingress.reload, RUN_RELOAD_INGRESS
|
self.sys_ingress.reload, RUN_RELOAD_INGRESS
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.jobs.add(
|
||||||
|
self.sys_scheduler.register_task(
|
||||||
|
self.sys_secrets.reload, RUN_RELOAD_SECRETS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Watchdog
|
# Watchdog
|
||||||
self.jobs.add(
|
self.jobs.add(
|
||||||
|
@ -90,7 +90,6 @@ class DBus:
|
|||||||
raise DBusParseError() from None
|
raise DBusParseError() from None
|
||||||
|
|
||||||
# Read available methods
|
# Read available methods
|
||||||
_LOGGER.debug("Introspect XML: %s", data)
|
|
||||||
for interface in xml.findall("./interface"):
|
for interface in xml.findall("./interface"):
|
||||||
interface_name = interface.get("name")
|
interface_name = interface.get("name")
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ gitpython==3.0.2
|
|||||||
packaging==19.1
|
packaging==19.1
|
||||||
pytz==2019.2
|
pytz==2019.2
|
||||||
pyudev==0.21.0
|
pyudev==0.21.0
|
||||||
|
ruamel.yaml==0.15.100
|
||||||
uvloop==0.12.2
|
uvloop==0.12.2
|
||||||
voluptuous==0.11.7
|
voluptuous==0.11.7
|
||||||
ptvsd==4.3.2
|
ptvsd==4.3.2
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
flake8==3.7.8
|
flake8==3.7.8
|
||||||
pylint==2.3.1
|
pylint==2.3.1
|
||||||
pytest==5.1.1
|
pytest==5.1.2
|
||||||
pytest-timeout==1.3.3
|
pytest-timeout==1.3.3
|
||||||
pytest-aiohttp==0.3.0
|
pytest-aiohttp==0.3.0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user