Add CoreDNS as DNS backend (#1195)

* Add CoreDNS / DNS configuration

* Support get version

* add version

* add coresys

* Add more logic

* move forwareder into dns

* Setup docker inside

* add docker to env

* Add more function

* more interface

* Update hosts template

* Add DNS folder

* Fix issues

* Add more logic

* Add handling for hosts

* Fix setting

* fix lint

* Fix some issues

* Fix issue

* Run with no cache

* Fix issue on validate

* Fix bug

* Allow to jump into dev mode

* Fix permission

* Fix issue

* Add dns search

* Add watchdog

* Fix set issues

* add API description

* Add API endpoint

* Add CLI support

* Fix logs + add hostname

* Add/remove DNS entry

* Fix attribute

* Fix style

* Better shutdown

* Remove ha from network mapping

* Add more options

* Fix env shutdown

* Add support for new repair function

* Start coreDNS faster after restart

* remove options

* Fix ha fix
This commit is contained in:
Pascal Vizeli 2019-08-13 14:20:42 +02:00 committed by GitHub
parent 9f3f47eb80
commit 1196788856
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 978 additions and 138 deletions

View File

@ -1,8 +1,8 @@
FROM python:3.7 FROM python:3.7
WORKDIR /workspace WORKDIR /workspaces
# install Node/Yarn for Frontent # Install Node/Yarn for Frontent
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
curl \ curl \
git \ git \
@ -17,8 +17,24 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
ENV NVM_DIR /root/.nvm ENV NVM_DIR /root/.nvm
# Install docker
# https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/
RUN apt-get update && apt-get install -y --no-install-recommends \
apt-transport-https \
ca-certificates \
curl \
software-properties-common \
gpg-agent \
&& curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add - \
&& add-apt-repository "deb https://download.docker.com/linux/debian $(lsb_release -cs) stable" \
&& apt-get update && apt-get install -y --no-install-recommends \
docker-ce \
docker-ce-cli \
containerd.io \
&& 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 /workspace/ COPY requirements.txt requirements_tests.txt /workspaces/
RUN pip install -r requirements.txt \ RUN pip install -r requirements.txt \
&& pip3 install -r requirements_tests.txt \ && pip3 install -r requirements_tests.txt \
&& pip install black tox && pip install black tox

View File

@ -3,9 +3,11 @@
"name": "Hass.io dev", "name": "Hass.io dev",
"context": "..", "context": "..",
"dockerFile": "Dockerfile", "dockerFile": "Dockerfile",
"appPort": "9123:8123",
"runArgs": [ "runArgs": [
"-e", "-e",
"GIT_EDTIOR='code --wait'" "GIT_EDITOR='code --wait'",
"--privileged"
], ],
"extensions": [ "extensions": [
"ms-python.python" "ms-python.python"
@ -24,4 +26,4 @@
"editor.formatOnType": true, "editor.formatOnType": true,
"files.trimTrailingWhitespace": true "files.trimTrailingWhitespace": true
} }
} }

View File

@ -18,3 +18,6 @@ venv/
home-assistant-polymer/* home-assistant-polymer/*
misc/* misc/*
script/* script/*
# Test ENV
data/

3
.gitignore vendored
View File

@ -95,3 +95,6 @@ ENV/
.vscode/* .vscode/*
!.vscode/cSpell.json !.vscode/cSpell.json
!.vscode/tasks.json !.vscode/tasks.json
# Test Env
test_data/

73
.vscode/tasks.json vendored
View File

@ -1,6 +1,34 @@
{ {
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{
"label": "Run Testenv",
"type": "shell",
"command": "./script/test_env.sh",
"group": {
"kind": "test",
"isDefault": true,
},
"presentation": {
"reveal": "always",
"panel": "new"
},
"problemMatcher": []
},
{
"label": "Run Testenv CLI",
"type": "shell",
"command": "docker run --rm -ti -v /etc/machine-id:/etc/machine-id --network=hassio --add-host hassio:172.30.32.2 homeassistant/amd64-hassio-cli:dev",
"group": {
"kind": "test",
"isDefault": true,
},
"presentation": {
"reveal": "always",
"panel": "new"
},
"problemMatcher": []
},
{ {
"label": "Update UI", "label": "Update UI",
"type": "shell", "type": "shell",
@ -14,6 +42,51 @@
"panel": "new" "panel": "new"
}, },
"problemMatcher": [] "problemMatcher": []
},
{
"label": "Pytest",
"type": "shell",
"command": "pytest --timeout=10 tests",
"group": {
"kind": "test",
"isDefault": true,
},
"presentation": {
"reveal": "always",
"panel": "new"
},
"problemMatcher": []
},
{
"label": "Flake8",
"type": "shell",
"command": "flake8 homeassistant tests",
"group": {
"kind": "test",
"isDefault": true,
},
"presentation": {
"reveal": "always",
"panel": "new"
},
"problemMatcher": []
},
{
"label": "Pylint",
"type": "shell",
"command": "pylint homeassistant",
"dependsOn": [
"Install all Requirements"
],
"group": {
"kind": "test",
"isDefault": true,
},
"presentation": {
"reveal": "always",
"panel": "new"
},
"problemMatcher": []
} }
] ]
} }

42
API.md
View File

@ -473,6 +473,7 @@ Get all available addons.
{ {
"name": "xy bla", "name": "xy bla",
"slug": "xdssd_xybla", "slug": "xdssd_xybla",
"hostname": "xdssd-xybla",
"description": "description", "description": "description",
"long_description": "null|markdown", "long_description": "null|markdown",
"auto_update": "bool", "auto_update": "bool",
@ -739,6 +740,47 @@ return:
} }
``` ```
### DNS
- GET `/dns/info`
```json
{
"host": "ip-address",
"version": "1",
"latest_version": "2",
"servers": ["dns://8.8.8.8"]
}
```
- POST `/dns/options`
```json
{
"servers": ["dns://8.8.8.8"]
}
```
- POST `/dns/update`
```json
{
"version": "VERSION"
}
```
- GET `/dns/logs`
- GET `/dns/stats`
```json
{
"cpu_percent": 0.0,
"memory_usage": 283123,
"memory_limit": 329392,
"network_tx": 0,
"network_rx": 0,
"blk_read": 0,
"blk_write": 0
}
```
### Auth / SSO API ### Auth / SSO API
You can use the user system on homeassistant. We handle this auth system on You can use the user system on homeassistant. We handle this auth system on

View File

@ -109,6 +109,11 @@ class AddonModel(CoreSysAttributes):
"""Return name of add-on.""" """Return name of add-on."""
return self.data[ATTR_NAME] return self.data[ATTR_NAME]
@property
def hostname(self) -> str:
"""Return slug/id of add-on."""
return self.slug.replace("_", "-")
@property @property
def timeout(self) -> int: def timeout(self) -> int:
"""Return timeout of addon for docker stop.""" """Return timeout of addon for docker stop."""

View File

@ -9,6 +9,7 @@ from ..coresys import CoreSys, CoreSysAttributes
from .addons import APIAddons from .addons import APIAddons
from .auth import APIAuth from .auth import APIAuth
from .discovery import APIDiscovery from .discovery import APIDiscovery
from .dns import APICoreDNS
from .hardware import APIHardware from .hardware import APIHardware
from .hassos import APIHassOS from .hassos import APIHassOS
from .homeassistant import APIHomeAssistant from .homeassistant import APIHomeAssistant
@ -55,6 +56,7 @@ class RestAPI(CoreSysAttributes):
self._register_services() self._register_services()
self._register_info() self._register_info()
self._register_auth() self._register_auth()
self._register_dns()
def _register_host(self) -> None: def _register_host(self) -> None:
"""Register hostcontrol functions.""" """Register hostcontrol functions."""
@ -264,6 +266,21 @@ class RestAPI(CoreSysAttributes):
] ]
) )
def _register_dns(self) -> None:
"""Register DNS functions."""
api_dns = APICoreDNS()
api_dns.coresys = self.coresys
self.webapp.add_routes(
[
web.get("/dns/info", api_dns.info),
web.get("/dns/stats", api_dns.stats),
web.get("/dns/logs", api_dns.logs),
web.post("/dns/update", api_dns.update),
web.post("/dns/options", api_dns.options),
]
)
def _register_panel(self) -> None: def _register_panel(self) -> None:
"""Register panel for Home Assistant.""" """Register panel for Home Assistant."""
panel_dir = Path(__file__).parent.joinpath("panel") panel_dir = Path(__file__).parent.joinpath("panel")

View File

@ -41,6 +41,7 @@ from ..const import (
ATTR_HOST_IPC, ATTR_HOST_IPC,
ATTR_HOST_NETWORK, ATTR_HOST_NETWORK,
ATTR_HOST_PID, ATTR_HOST_PID,
ATTR_HOSTNAME,
ATTR_ICON, ATTR_ICON,
ATTR_INGRESS, ATTR_INGRESS,
ATTR_INGRESS_ENTRY, ATTR_INGRESS_ENTRY,
@ -180,6 +181,7 @@ class APIAddons(CoreSysAttributes):
data = { data = {
ATTR_NAME: addon.name, ATTR_NAME: addon.name,
ATTR_SLUG: addon.slug, ATTR_SLUG: addon.slug,
ATTR_HOSTNAME: addon.hostname,
ATTR_DESCRIPTON: addon.description, ATTR_DESCRIPTON: addon.description,
ATTR_LONG_DESCRIPTION: addon.long_description, ATTR_LONG_DESCRIPTION: addon.long_description,
ATTR_AUTO_UPDATE: None, ATTR_AUTO_UPDATE: None,

87
hassio/api/dns.py Normal file
View File

@ -0,0 +1,87 @@
"""Init file for Hass.io DNS RESTful API."""
import asyncio
import logging
from typing import Any, Awaitable, Dict
from aiohttp import web
import voluptuous as vol
from ..const import (
ATTR_BLK_READ,
ATTR_BLK_WRITE,
ATTR_CPU_PERCENT,
ATTR_HOST,
ATTR_LATEST_VERSION,
ATTR_MEMORY_LIMIT,
ATTR_MEMORY_USAGE,
ATTR_NETWORK_RX,
ATTR_NETWORK_TX,
ATTR_SERVERS,
ATTR_VERSION,
CONTENT_TYPE_BINARY,
)
from ..coresys import CoreSysAttributes
from ..exceptions import APIError
from ..validate import DNS_SERVER_LIST
from .utils import api_process, api_process_raw, api_validate
_LOGGER = logging.getLogger(__name__)
# pylint: disable=no-value-for-parameter
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_SERVERS): DNS_SERVER_LIST})
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
class APICoreDNS(CoreSysAttributes):
"""Handle RESTful API for DNS functions."""
@api_process
async def info(self, request: web.Request) -> Dict[str, Any]:
"""Return DNS information."""
return {
ATTR_VERSION: self.sys_dns.version,
ATTR_LATEST_VERSION: self.sys_dns.latest_version,
ATTR_HOST: str(self.sys_docker.network.dns),
ATTR_SERVERS: self.sys_dns.servers,
}
@api_process
async def options(self, request: web.Request) -> None:
"""Set DNS options."""
body = await api_validate(SCHEMA_OPTIONS, request)
if ATTR_SERVERS in body:
self.sys_dns.servers = body[ATTR_SERVERS]
self.sys_dns.save_data()
@api_process
async def stats(self, request: web.Request) -> Dict[str, Any]:
"""Return resource information."""
stats = await self.sys_dns.stats()
return {
ATTR_CPU_PERCENT: stats.cpu_percent,
ATTR_MEMORY_USAGE: stats.memory_usage,
ATTR_MEMORY_LIMIT: stats.memory_limit,
ATTR_NETWORK_RX: stats.network_rx,
ATTR_NETWORK_TX: stats.network_tx,
ATTR_BLK_READ: stats.blk_read,
ATTR_BLK_WRITE: stats.blk_write,
}
@api_process
async def update(self, request: web.Request) -> None:
"""Update DNS plugin."""
body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.sys_dns.latest_version)
if version == self.sys_dns.version:
raise APIError("Version {} is already in use".format(version))
await asyncio.shield(self.sys_dns.update(version))
@api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request: web.Request) -> Awaitable[bytes]:
"""Return DNS Docker logs."""
return self.sys_dns.logs()

View File

@ -10,6 +10,8 @@ from .utils.json import read_json_file
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ARCH_JSON: Path = Path(__file__).parent.joinpath("data/arch.json")
MAP_CPU = { MAP_CPU = {
"armv7": "armv7", "armv7": "armv7",
"armv6": "armhf", "armv6": "armhf",
@ -47,7 +49,7 @@ class CpuArch(CoreSysAttributes):
async def load(self) -> None: async def load(self) -> None:
"""Load data and initialize default arch.""" """Load data and initialize default arch."""
try: try:
arch_data = read_json_file(Path(__file__).parent.joinpath("arch.json")) arch_data = read_json_file(ARCH_JSON)
except JsonFileError: except JsonFileError:
_LOGGER.warning("Can't read arch json") _LOGGER.warning("Can't read arch json")
return return

View File

@ -11,11 +11,12 @@ from .addons import AddonManager
from .api import RestAPI from .api import RestAPI
from .arch import CpuArch from .arch import CpuArch
from .auth import Auth from .auth import Auth
from .const import SOCKET_DOCKER from .const import CHANNEL_DEV, SOCKET_DOCKER
from .core import HassIO from .core import HassIO
from .coresys import CoreSys from .coresys import CoreSys
from .dbus import DBusManager from .dbus import DBusManager
from .discovery import Discovery from .discovery import Discovery
from .dns import CoreDNS
from .hassos import HassOS from .hassos import HassOS
from .homeassistant import HomeAssistant from .homeassistant import HomeAssistant
from .host import HostManager from .host import HostManager
@ -43,6 +44,7 @@ async def initialize_coresys():
# Initialize core objects # Initialize core objects
coresys.core = HassIO(coresys) coresys.core = HassIO(coresys)
coresys.dns = CoreDNS(coresys)
coresys.arch = CpuArch(coresys) coresys.arch = CpuArch(coresys)
coresys.auth = Auth(coresys) coresys.auth = Auth(coresys)
coresys.updater = Updater(coresys) coresys.updater = Updater(coresys)
@ -127,9 +129,19 @@ def initialize_system_data(coresys: CoreSys):
_LOGGER.info("Create Hass.io Apparmor folder %s", config.path_apparmor) _LOGGER.info("Create Hass.io Apparmor folder %s", config.path_apparmor)
config.path_apparmor.mkdir() config.path_apparmor.mkdir()
# dns folder
if not config.path_dns.is_dir():
_LOGGER.info("Create Hass.io DNS folder %s", config.path_dns)
config.path_dns.mkdir()
# Update log level # Update log level
coresys.config.modify_log_level() coresys.config.modify_log_level()
# Check if ENV is in development mode
if bool(os.environ.get("SUPERVISOR_DEV", 0)):
_LOGGER.warning("SUPERVISOR_DEV is set")
coresys.updater.channel = CHANNEL_DEV
def migrate_system_env(coresys: CoreSys): def migrate_system_env(coresys: CoreSys):
"""Cleanup some stuff after update.""" """Cleanup some stuff after update."""

View File

@ -34,6 +34,7 @@ BACKUP_DATA = PurePath("backup")
SHARE_DATA = PurePath("share") SHARE_DATA = PurePath("share")
TMP_DATA = PurePath("tmp") TMP_DATA = PurePath("tmp")
APPARMOR_DATA = PurePath("apparmor") APPARMOR_DATA = PurePath("apparmor")
DNS_DATA = PurePath("dns")
DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat() DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat()
@ -211,6 +212,16 @@ class CoreConfig(JsonConfig):
"""Return root share data folder external for Docker.""" """Return root share data folder external for Docker."""
return PurePath(self.path_extern_hassio, SHARE_DATA) return PurePath(self.path_extern_hassio, SHARE_DATA)
@property
def path_extern_dns(self):
"""Return dns path external for Docker."""
return str(PurePath(self.path_extern_hassio, DNS_DATA))
@property
def path_dns(self):
"""Return dns path inside supervisor."""
return Path(HASSIO_DATA, DNS_DATA)
@property @property
def addons_repositories(self): def addons_repositories(self):
"""Return list of custom Add-on repositories.""" """Return list of custom Add-on repositories."""

View File

@ -24,6 +24,7 @@ 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") FILE_HASSIO_DISCOVERY = Path(HASSIO_DATA, "discovery.json")
FILE_HASSIO_INGRESS = Path(HASSIO_DATA, "ingress.json") FILE_HASSIO_INGRESS = Path(HASSIO_DATA, "ingress.json")
FILE_HASSIO_DNS = Path(HASSIO_DATA, "dns.json")
SOCKET_DOCKER = Path("/var/run/docker.sock") SOCKET_DOCKER = Path("/var/run/docker.sock")
@ -31,6 +32,9 @@ DOCKER_NETWORK = "hassio"
DOCKER_NETWORK_MASK = ip_network("172.30.32.0/23") DOCKER_NETWORK_MASK = ip_network("172.30.32.0/23")
DOCKER_NETWORK_RANGE = ip_network("172.30.33.0/24") DOCKER_NETWORK_RANGE = ip_network("172.30.33.0/24")
DNS_SERVERS = ["dns://8.8.8.8", "dns://1.1.1.1"]
DNS_SUFFIX = "local.hass.io"
LABEL_VERSION = "io.hass.version" LABEL_VERSION = "io.hass.version"
LABEL_ARCH = "io.hass.arch" LABEL_ARCH = "io.hass.arch"
LABEL_TYPE = "io.hass.type" LABEL_TYPE = "io.hass.type"
@ -86,6 +90,7 @@ ATTR_VERSION_LATEST = "version_latest"
ATTR_AUTO_UART = "auto_uart" ATTR_AUTO_UART = "auto_uart"
ATTR_LAST_BOOT = "last_boot" ATTR_LAST_BOOT = "last_boot"
ATTR_LAST_VERSION = "last_version" ATTR_LAST_VERSION = "last_version"
ATTR_LATEST_VERSION = "latest_version"
ATTR_CHANNEL = "channel" ATTR_CHANNEL = "channel"
ATTR_NAME = "name" ATTR_NAME = "name"
ATTR_SLUG = "slug" ATTR_SLUG = "slug"
@ -210,6 +215,8 @@ ATTR_ADMIN = "admin"
ATTR_PANELS = "panels" ATTR_PANELS = "panels"
ATTR_DEBUG = "debug" ATTR_DEBUG = "debug"
ATTR_DEBUG_BLOCK = "debug_block" ATTR_DEBUG_BLOCK = "debug_block"
ATTR_DNS = "dns"
ATTR_SERVERS = "servers"
PROVIDE_SERVICE = "provide" PROVIDE_SERVICE = "provide"
NEED_SERVICE = "need" NEED_SERVICE = "need"

View File

@ -30,6 +30,9 @@ class HassIO(CoreSysAttributes):
async def setup(self): async def setup(self):
"""Setup HassIO orchestration.""" """Setup HassIO orchestration."""
# Load CoreDNS
await self.sys_dns.load()
# Load DBus # Load DBus
await self.sys_dbus.load() await self.sys_dbus.load()
@ -69,9 +72,6 @@ class HassIO(CoreSysAttributes):
# Load ingress # Load ingress
await self.sys_ingress.load() await self.sys_ingress.load()
# start dns forwarding
self.sys_create_task(self.sys_dns.start())
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()
@ -142,10 +142,10 @@ class HassIO(CoreSysAttributes):
await asyncio.wait( await asyncio.wait(
[ [
self.sys_api.stop(), self.sys_api.stop(),
self.sys_dns.stop(),
self.sys_websession.close(), self.sys_websession.close(),
self.sys_websession_ssl.close(), self.sys_websession_ssl.close(),
self.sys_ingress.unload(), self.sys_ingress.unload(),
self.sys_dns.unload(),
] ]
) )
except asyncio.TimeoutError: except asyncio.TimeoutError:
@ -176,6 +176,7 @@ class HassIO(CoreSysAttributes):
await self.sys_run_in_executor(self.sys_docker.repair) await self.sys_run_in_executor(self.sys_docker.repair)
# Restore core functionality # Restore core functionality
await self.sys_dns.repair()
await self.sys_addons.repair() await self.sys_addons.repair()
await self.sys_homeassistant.repair() await self.sys_homeassistant.repair()

View File

@ -1,14 +1,13 @@
"""Handle core shared data.""" """Handle core shared data."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
import aiohttp import aiohttp
from .config import CoreConfig from .config import CoreConfig
from .const import CHANNEL_DEV from .const import CHANNEL_DEV
from .docker import DockerAPI from .docker import DockerAPI
from .misc.dns import DNSForward
from .misc.hardware import Hardware from .misc.hardware import Hardware
from .misc.scheduler import Scheduler from .misc.scheduler import Scheduler
@ -20,6 +19,7 @@ if TYPE_CHECKING:
from .core import HassIO from .core import HassIO
from .dbus import DBusManager from .dbus import DBusManager
from .discovery import Discovery from .discovery import Discovery
from .dns import CoreDNS
from .hassos import HassOS from .hassos import HassOS
from .homeassistant import HomeAssistant from .homeassistant import HomeAssistant
from .host import HostManager from .host import HostManager
@ -52,26 +52,26 @@ class CoreSys:
self._hardware: Hardware = Hardware() self._hardware: Hardware = Hardware()
self._docker: DockerAPI = DockerAPI() self._docker: DockerAPI = DockerAPI()
self._scheduler: Scheduler = Scheduler() self._scheduler: Scheduler = Scheduler()
self._dns: DNSForward = DNSForward()
# Internal objects pointers # Internal objects pointers
self._core: HassIO = None self._core: Optional[HassIO] = None
self._arch: CpuArch = None self._arch: Optional[CpuArch] = None
self._auth: Auth = None self._auth: Optional[Auth] = None
self._homeassistant: HomeAssistant = None self._dns: Optional[CoreDNS] = None
self._supervisor: Supervisor = None self._homeassistant: Optional[HomeAssistant] = None
self._addons: AddonManager = None self._supervisor: Optional[Supervisor] = None
self._api: RestAPI = None self._addons: Optional[AddonManager] = None
self._updater: Updater = None self._api: Optional[RestAPI] = None
self._snapshots: SnapshotManager = None self._updater: Optional[Updater] = None
self._tasks: Tasks = None self._snapshots: Optional[SnapshotManager] = None
self._host: HostManager = None self._tasks: Optional[Tasks] = None
self._ingress: Ingress = None self._host: Optional[HostManager] = None
self._dbus: DBusManager = None self._ingress: Optional[Ingress] = None
self._hassos: HassOS = None self._dbus: Optional[DBusManager] = None
self._services: ServiceManager = None self._hassos: Optional[HassOS] = None
self._store: StoreManager = None self._services: Optional[ServiceManager] = None
self._discovery: Discovery = None self._store: Optional[StoreManager] = None
self._discovery: Optional[Discovery] = None
@property @property
def machine(self) -> str: def machine(self) -> str:
@ -125,11 +125,6 @@ class CoreSys:
"""Return Scheduler object.""" """Return Scheduler object."""
return self._scheduler return self._scheduler
@property
def dns(self) -> DNSForward:
"""Return DNSForward object."""
return self._dns
@property @property
def core(self) -> HassIO: def core(self) -> HassIO:
"""Return HassIO object.""" """Return HassIO object."""
@ -298,6 +293,18 @@ class CoreSys:
raise RuntimeError("DBusManager already set!") raise RuntimeError("DBusManager already set!")
self._dbus = value self._dbus = value
@property
def dns(self) -> CoreDNS:
"""Return CoreDNS object."""
return self._dns
@dns.setter
def dns(self, value: CoreDNS):
"""Set a CoreDNS object."""
if self._dns:
raise RuntimeError("CoreDNS already set!")
self._dns = value
@property @property
def host(self) -> HostManager: def host(self) -> HostManager:
"""Return HostManager object.""" """Return HostManager object."""
@ -395,11 +402,6 @@ class CoreSysAttributes:
"""Return Scheduler object.""" """Return Scheduler object."""
return self.coresys.scheduler return self.coresys.scheduler
@property
def sys_dns(self) -> DNSForward:
"""Return DNSForward object."""
return self.coresys.dns
@property @property
def sys_core(self) -> HassIO: def sys_core(self) -> HassIO:
"""Return HassIO object.""" """Return HassIO object."""
@ -470,6 +472,11 @@ class CoreSysAttributes:
"""Return DBusManager object.""" """Return DBusManager object."""
return self.coresys.dbus return self.coresys.dbus
@property
def sys_dns(self) -> CoreDNS:
"""Return CoreDNS object."""
return self.coresys.dns
@property @property
def sys_host(self) -> HostManager: def sys_host(self) -> HostManager:
"""Return HostManager object.""" """Return HostManager object."""

9
hassio/data/coredns.tmpl Normal file
View File

@ -0,0 +1,9 @@
.:53 {
log
hosts /config/hosts {
fallthrough
}
forward . $servers {
health_check 10s
}
}

2
hassio/data/hosts.tmpl Normal file
View File

@ -0,0 +1,2 @@
$supervisor hassio supervisor.local.hass.io hassio.local.hass.io
$homeassistant homeassistant homeassistant.local.hass.io home-assistant.local.hass.io

305
hassio/dns.py Normal file
View File

@ -0,0 +1,305 @@
"""Home Assistant control object."""
import asyncio
import logging
from contextlib import suppress
from ipaddress import IPv4Address, AddressValueError
from pathlib import Path
from string import Template
from typing import Awaitable, Dict, List, Optional
from .const import ATTR_SERVERS, ATTR_VERSION, DNS_SERVERS, FILE_HASSIO_DNS, DNS_SUFFIX
from .coresys import CoreSys, CoreSysAttributes
from .docker.dns import DockerDNS
from .docker.stats import DockerStats
from .exceptions import CoreDNSError, CoreDNSUpdateError, DockerAPIError
from .misc.forwarder import DNSForward
from .utils.json import JsonConfig
from .validate import SCHEMA_DNS_CONFIG
_LOGGER = logging.getLogger(__name__)
COREDNS_TMPL: Path = Path(__file__).parents[0].joinpath("data/coredns.tmpl")
class CoreDNS(JsonConfig, CoreSysAttributes):
"""Home Assistant core object for handle it."""
def __init__(self, coresys: CoreSys):
"""Initialize hass object."""
super().__init__(FILE_HASSIO_DNS, SCHEMA_DNS_CONFIG)
self.coresys: CoreSys = coresys
self.instance: DockerDNS = DockerDNS(coresys)
self.forwarder: DNSForward = DNSForward()
self._hosts: Dict[IPv4Address, List[str]] = {}
@property
def corefile(self) -> Path:
"""Return Path to corefile."""
return Path(self.sys_config.path_dns, "corefile")
@property
def hosts(self) -> Path:
"""Return Path to corefile."""
return Path(self.sys_config.path_dns, "hosts")
@property
def servers(self) -> List[str]:
"""Return list of DNS servers."""
return self._data[ATTR_SERVERS]
@servers.setter
def servers(self, value: List[str]) -> None:
"""Return list of DNS servers."""
self._data[ATTR_SERVERS] = value
@property
def version(self) -> Optional[str]:
"""Return current version of DNS."""
return self._data.get(ATTR_VERSION)
@version.setter
def version(self, value: str) -> None:
"""Return current version of DNS."""
self._data[ATTR_VERSION] = value
@property
def latest_version(self) -> Optional[str]:
"""Return latest version of CoreDNS."""
return self.sys_updater.version_dns
@property
def in_progress(self) -> bool:
"""Return True if a task is in progress."""
return self.instance.in_progress
async def load(self) -> None:
"""Load DNS setup."""
with suppress(CoreDNSError):
self._import_hosts()
# Check CoreDNS state
try:
# Evaluate Version if we lost this information
if not self.version:
self.version = await self.instance.get_latest_version(key=int)
await self.instance.attach(tag=self.version)
except DockerAPIError:
_LOGGER.info(
"No CoreDNS plugin Docker image %s found.", self.instance.image
)
# Install CoreDNS
with suppress(CoreDNSError):
await self.install()
else:
self.version = self.instance.version
self.save_data()
# Start DNS forwarder
self.sys_create_task(self.forwarder.start(self.sys_docker.network.dns))
# Start is not Running
if await self.instance.is_running():
return
await self.start()
async def unload(self) -> None:
"""Unload DNS forwarder."""
await self.forwarder.stop()
async def install(self) -> None:
"""Install CoreDNS."""
_LOGGER.info("Setup CoreDNS plugin")
while True:
# read homeassistant tag and install it
if not self.latest_version:
await self.sys_updater.reload()
if self.latest_version:
with suppress(DockerAPIError):
await self.instance.install(self.latest_version)
break
_LOGGER.warning("Error on install CoreDNS plugin. Retry in 30sec")
await asyncio.sleep(30)
_LOGGER.info("CoreDNS plugin now installed")
self.version = self.instance.version
self.save_data()
await self.start()
async def update(self, version: Optional[str] = None) -> None:
"""Update CoreDNS plugin."""
version = version or self.latest_version
if version == self.version:
_LOGGER.warning("Version %s is already installed for CoreDNS", version)
return
try:
await self.instance.update(version)
except DockerAPIError:
_LOGGER.error("CoreDNS update fails")
raise CoreDNSUpdateError() from None
else:
# Cleanup
with suppress(DockerAPIError):
await self.instance.cleanup()
self.version = version
self.save_data()
# Start CoreDNS
await self.start()
async def restart(self) -> None:
"""Restart CoreDNS plugin."""
with suppress(DockerAPIError):
await self.instance.stop()
await self.start()
async def start(self) -> None:
"""Run CoreDNS."""
self._write_corefile()
# Start Instance
_LOGGER.info("Start CoreDNS plugin")
try:
await self.instance.run()
except DockerAPIError:
_LOGGER.error("Can't start CoreDNS plugin")
raise CoreDNSError() from None
def reset(self) -> None:
"""Reset Config / Hosts."""
self.servers = DNS_SERVERS
with suppress(OSError):
self.hosts.unlink()
self._import_hosts()
def _write_corefile(self) -> None:
"""Write CoreDNS config."""
try:
corefile_template: Template = Template(COREDNS_TMPL.read_text())
except OSError as err:
_LOGGER.error("Can't read coredns template file: %s", err)
raise CoreDNSError() from None
# Generate config file
dns_servers = self.servers + list(set(DNS_SERVERS) - set(self.servers))
data = corefile_template.safe_substitute(servers=" ".join(dns_servers))
try:
self.corefile.write_text(data)
except OSError as err:
_LOGGER.error("Can't update corefile: %s", err)
raise CoreDNSError() from None
def _import_hosts(self) -> None:
"""Import hosts entry."""
# Generate Default
if not self.hosts.exists():
self.add_host(self.sys_docker.network.supervisor, ["hassio", "supervisor"])
self.add_host(
self.sys_docker.network.gateway, ["homeassistant", "home-assistant"]
)
return
# Import Exists host table
try:
with self.hosts.open("r") as hosts:
for line in hosts.readlines():
try:
data = line.split(" ")
self._hosts[IPv4Address(data[0])] = data[1:]
except AddressValueError:
_LOGGER.warning("Fails to read %s", line)
except OSError as err:
_LOGGER.error("Can't read hosts file: %s", err)
raise CoreDNSError() from None
def _write_hosts(self) -> None:
"""Write hosts from memory to file."""
try:
with self.hosts.open("w") as hosts:
for address, hostnames in self._hosts.items():
host = " ".join(hostnames)
hosts.write(f"{address!s} {host}")
except OSError as err:
_LOGGER.error("Can't write hosts file: %s", err)
raise CoreDNSError() from None
def add_host(self, ipv4: IPv4Address, names: List[str]) -> None:
"""Add a new host entry."""
hostnames: List[str] = []
for name in names:
hostnames.append(name)
hostnames.append(f"{name}.{DNS_SUFFIX}")
self._hosts[ipv4] = hostnames
_LOGGER.debug("Add Host entry %s -> %s", ipv4, hostnames)
self._write_hosts()
def delete_host(
self, ipv4: Optional[IPv4Address] = None, host: Optional[str] = None
) -> None:
"""Remove a entry from hosts."""
if host:
for address, hostnames in self._hosts.items():
if host not in hostnames:
continue
ipv4 = address
break
# Remove entry
if ipv4:
_LOGGER.debug("Remove Host entry %s", ipv4)
self._hosts.pop(ipv4, None)
self._write_hosts()
else:
_LOGGER.warning("Can't remove Host entry: %s/%s", ipv4, host)
def logs(self) -> Awaitable[bytes]:
"""Get CoreDNS docker logs.
Return Coroutine.
"""
return self.instance.logs()
async def stats(self) -> DockerStats:
"""Return stats of CoreDNS."""
try:
return await self.instance.stats()
except DockerAPIError:
raise CoreDNSError() from None
def is_running(self) -> Awaitable[bool]:
"""Return True if Docker container is running.
Return a coroutine.
"""
return self.instance.is_running()
def is_fails(self) -> Awaitable[bool]:
"""Return True if a Docker container is fails state.
Return a coroutine.
"""
return self.instance.is_fails()
async def repair(self):
"""Repair CoreDNS plugin."""
if await self.instance.exists():
return
_LOGGER.info("Repair CoreDNS %s", self.version)
try:
await self.instance.install(self.version)
except DockerAPIError:
_LOGGER.error("Repairing of CoreDNS fails")

View File

@ -1,12 +1,13 @@
"""Init file for Hass.io Docker object.""" """Init file for Hass.io Docker object."""
import logging
from contextlib import suppress from contextlib import suppress
from ipaddress import IPv4Address
import logging
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
import attr import attr
import docker import docker
from ..const import SOCKET_DOCKER from ..const import SOCKET_DOCKER, DNS_SUFFIX
from ..exceptions import DockerAPIError from ..exceptions import DockerAPIError
from .network import DockerNetwork from .network import DockerNetwork
@ -50,7 +51,11 @@ class DockerAPI:
return self.docker.api return self.docker.api
def run( def run(
self, image: str, version: str = "latest", **kwargs: Dict[str, Any] self,
image: str,
version: str = "latest",
ipv4: Optional[IPv4Address] = None,
**kwargs: Dict[str, Any],
) -> docker.models.containers.Container: ) -> docker.models.containers.Container:
""""Create a Docker container and run it. """"Create a Docker container and run it.
@ -60,12 +65,13 @@ class DockerAPI:
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
kwargs["dns"] = [str(self.network.dns)]
kwargs["dns_search"] = [DNS_SUFFIX]
kwargs["domainname"] = DNS_SUFFIX
# Setup network # Setup network
kwargs["dns_search"] = ["."] if not network_mode:
if network_mode:
kwargs["dns"] = [str(self.network.supervisor)]
kwargs["dns_opt"] = ["ndots:0"]
else:
kwargs["network"] = None kwargs["network"] = None
# Create container # Create container
@ -81,7 +87,7 @@ class DockerAPI:
if not network_mode: if not network_mode:
alias = [hostname] if hostname else None alias = [hostname] if hostname else None
try: try:
self.network.attach_container(container, alias=alias) self.network.attach_container(container, alias=alias, ipv4=ipv4)
except DockerAPIError: except DockerAPIError:
_LOGGER.warning("Can't attach %s to hassio-net!", name) _LOGGER.warning("Can't attach %s to hassio-net!", name)
else: else:

View File

@ -35,6 +35,7 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
AUDIO_DEVICE = "/dev/snd:/dev/snd:rwm" AUDIO_DEVICE = "/dev/snd:/dev/snd:rwm"
NO_ADDDRESS = ip_address("0.0.0.0")
class DockerAddon(DockerInterface): class DockerAddon(DockerInterface):
@ -62,7 +63,7 @@ class DockerAddon(DockerInterface):
self._meta["NetworkSettings"]["Networks"]["hassio"]["IPAddress"] self._meta["NetworkSettings"]["Networks"]["hassio"]["IPAddress"]
) )
except (KeyError, TypeError, ValueError): except (KeyError, TypeError, ValueError):
return ip_address("0.0.0.0") return NO_ADDDRESS
@property @property
def timeout(self) -> int: def timeout(self) -> int:
@ -100,11 +101,6 @@ class DockerAddon(DockerInterface):
"""Return True if full access is enabled.""" """Return True if full access is enabled."""
return not self.addon.protected and self.addon.with_full_access return not self.addon.protected and self.addon.with_full_access
@property
def hostname(self) -> str:
"""Return slug/id of add-on."""
return self.addon.slug.replace("_", "-")
@property @property
def environment(self) -> Dict[str, str]: def environment(self) -> Dict[str, str]:
"""Return environment for Docker add-on.""" """Return environment for Docker add-on."""
@ -186,10 +182,7 @@ class DockerAddon(DockerInterface):
@property @property
def network_mapping(self) -> Dict[str, str]: def network_mapping(self) -> Dict[str, str]:
"""Return hosts mapping.""" """Return hosts mapping."""
return { return {"hassio": self.sys_docker.network.supervisor}
"homeassistant": self.sys_docker.network.gateway,
"hassio": self.sys_docker.network.supervisor,
}
@property @property
def network_mode(self) -> Optional[str]: def network_mode(self) -> Optional[str]:
@ -329,7 +322,7 @@ class DockerAddon(DockerInterface):
self.image, self.image,
version=self.addon.version, version=self.addon.version,
name=self.name, name=self.name,
hostname=self.hostname, hostname=self.addon.hostname,
detach=True, detach=True,
init=True, init=True,
privileged=self.full_access, privileged=self.full_access,
@ -350,6 +343,9 @@ class DockerAddon(DockerInterface):
self._meta = docker_container.attrs self._meta = docker_container.attrs
_LOGGER.info("Start Docker add-on %s with version %s", self.image, self.version) _LOGGER.info("Start Docker add-on %s with version %s", self.image, self.version)
# Write data to DNS server
self.sys_dns.add_host(ipv4=self.ip_address, names=[self.addon.hostname])
def _install( def _install(
self, tag: str, image: Optional[str] = None, latest: bool = False self, tag: str, image: Optional[str] = None, latest: bool = False
) -> None: ) -> None:
@ -467,3 +463,12 @@ class DockerAddon(DockerInterface):
except OSError as err: except OSError as err:
_LOGGER.error("Can't write to %s stdin: %s", self.name, err) _LOGGER.error("Can't write to %s stdin: %s", self.name, err)
raise DockerAPIError() from None raise DockerAPIError() from None
def _stop(self, remove_container=True) -> None:
"""Stop/remove Docker container.
Need run inside executor.
"""
if self.ip_address != NO_ADDDRESS:
self.sys_dns.delete_host(ipv4=self.ip_address)
super()._stop(remove_container)

56
hassio/docker/dns.py Normal file
View File

@ -0,0 +1,56 @@
"""HassOS Cli docker object."""
from contextlib import suppress
import logging
from ..const import ENV_TIME
from ..coresys import CoreSysAttributes
from ..exceptions import DockerAPIError
from .interface import DockerInterface
_LOGGER = logging.getLogger(__name__)
DNS_DOCKER_NAME: str = "hassio_dns"
class DockerDNS(DockerInterface, CoreSysAttributes):
"""Docker Hass.io wrapper for Hass.io DNS."""
@property
def image(self) -> str:
"""Return name of Hass.io DNS image."""
return f"homeassistant/{self.sys_arch.supervisor}-hassio-dns"
@property
def name(self) -> str:
"""Return name of Docker container."""
return DNS_DOCKER_NAME
def _run(self) -> None:
"""Run Docker image.
Need run inside executor.
"""
if self._is_running():
return
# Cleanup
with suppress(DockerAPIError):
self._stop()
# Create & Run container
docker_container = self.sys_docker.run(
self.image,
version=self.sys_dns.version,
ipv4=self.sys_docker.network.dns,
name=self.name,
hostname=self.name.replace("_", "-"),
detach=True,
init=True,
environment={ENV_TIME: self.sys_timezone},
volumes={
str(self.sys_config.path_extern_dns): {"bind": "/config", "mode": "ro"}
},
)
self._meta = docker_container.attrs
_LOGGER.info("Start DNS %s with version %s", self.image, self.version)

View File

@ -1,10 +1,8 @@
"""Init file for Hass.io Docker object.""" """Init file for Hass.io Docker object."""
from distutils.version import StrictVersion
from contextlib import suppress from contextlib import suppress
from ipaddress import IPv4Address from ipaddress import IPv4Address
import logging import logging
import re from typing import Awaitable, Optional
from typing import Awaitable, List, Optional
import docker import docker
@ -15,7 +13,6 @@ from .interface import CommandReturn, DockerInterface
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
HASS_DOCKER_NAME = "homeassistant" HASS_DOCKER_NAME = "homeassistant"
RE_VERSION = re.compile(r"(?P<version>\d+\.\d+\.\d+(?:b\d+|d\d+)?)")
class DockerHomeAssistant(DockerInterface): class DockerHomeAssistant(DockerInterface):
@ -139,33 +136,3 @@ class DockerHomeAssistant(DockerInterface):
return False return False
return True return True
def get_latest_version(self) -> Awaitable[str]:
"""Return latest version of local Home Asssistant image."""
return self.sys_run_in_executor(self._get_latest_version)
def _get_latest_version(self) -> str:
"""Return latest version of local Home Asssistant image.
Need run inside executor.
"""
available_version: List[str] = []
try:
for image in self.sys_docker.images.list(self.image):
for tag in image.tags:
match = RE_VERSION.search(tag)
if not match:
continue
available_version.append(match.group("version"))
assert available_version
except (docker.errors.DockerException, AssertionError):
_LOGGER.warning("No local HA version found")
raise DockerAPIError()
else:
_LOGGER.debug("Found HA versions: %s", available_version)
# Sort version and return latest version
available_version.sort(key=StrictVersion, reverse=True)
return available_version[0]

View File

@ -2,16 +2,16 @@
import asyncio import asyncio
from contextlib import suppress from contextlib import suppress
import logging import logging
from typing import Any, Dict, Optional, Awaitable from typing import Any, Awaitable, Dict, List, Optional
import docker import docker
from . import CommandReturn
from ..const import LABEL_ARCH, LABEL_VERSION from ..const import LABEL_ARCH, LABEL_VERSION
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import DockerAPIError from ..exceptions import DockerAPIError
from ..utils import process_lock from ..utils import process_lock
from .stats import DockerStats from .stats import DockerStats
from . import CommandReturn
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -50,7 +50,10 @@ class DockerInterface(CoreSysAttributes):
@property @property
def image(self) -> Optional[str]: def image(self) -> Optional[str]:
"""Return name of Docker image.""" """Return name of Docker image."""
return self.meta_config.get("Image") try:
return self.meta_config["Image"].partition(":")[0]
except KeyError:
return None
@property @property
def version(self) -> Optional[str]: def version(self) -> Optional[str]:
@ -80,7 +83,6 @@ class DockerInterface(CoreSysAttributes):
Need run inside executor. Need run inside executor.
""" """
image = image or self.image image = image or self.image
image = image.partition(":")[0] # remove potential tag
_LOGGER.info("Pull image %s tag %s.", image, tag) _LOGGER.info("Pull image %s tag %s.", image, tag)
try: try:
@ -397,3 +399,35 @@ class DockerInterface(CoreSysAttributes):
return True return True
return False return False
def get_latest_version(self, key: Any = int) -> Awaitable[str]:
"""Return latest version of local Home Asssistant image."""
return self.sys_run_in_executor(self._get_latest_version, key)
def _get_latest_version(self, key: Any = int) -> str:
"""Return latest version of local Home Asssistant image.
Need run inside executor.
"""
available_version: List[str] = []
try:
for image in self.sys_docker.images.list(self.image):
for tag in image.tags:
version = tag.partition(":")[2]
try:
key(version)
except (AttributeError, ValueError):
continue
available_version.append(version)
assert available_version
except (docker.errors.DockerException, AssertionError):
_LOGGER.debug("No version found for %s", self.image)
raise DockerAPIError()
else:
_LOGGER.debug("Found HA versions: %s", available_version)
# Sort version and return latest version
available_version.sort(key=key, reverse=True)
return available_version[0]

View File

@ -42,6 +42,11 @@ class DockerNetwork:
"""Return supervisor of the network.""" """Return supervisor of the network."""
return DOCKER_NETWORK_MASK[2] return DOCKER_NETWORK_MASK[2]
@property
def dns(self) -> IPv4Address:
"""Return dns of the network."""
return DOCKER_NETWORK_MASK[3]
def _get_network(self) -> docker.models.networks.Network: def _get_network(self) -> docker.models.networks.Network:
"""Get HassIO network.""" """Get HassIO network."""
try: try:

View File

@ -38,7 +38,9 @@ class DockerSupervisor(DockerInterface, CoreSysAttributes):
self._meta = docker_container.attrs self._meta = docker_container.attrs
_LOGGER.info( _LOGGER.info(
"Attach to Supervisor %s with version %s", self.image, self.version "Attach to Supervisor %s with version %s",
self.image,
self.sys_supervisor.version,
) )
# If already attach # If already attach

View File

@ -54,6 +54,17 @@ class HassOSNotSupportedError(HassioNotSupportedError):
"""Function not supported by HassOS.""" """Function not supported by HassOS."""
# DNS
class CoreDNSError(HassioError):
"""CoreDNS exception."""
class CoreDNSUpdateError(CoreDNSError):
"""Error on update of a CoreDNS."""
# Addons # Addons

View File

@ -188,13 +188,13 @@ class HassOS(CoreSysAttributes):
try: try:
await self.instance.update(version, latest=True) await self.instance.update(version, latest=True)
# Cleanup
with suppress(DockerAPIError):
await self.instance.cleanup()
except DockerAPIError: except DockerAPIError:
_LOGGER.error("HassOS CLI update fails") _LOGGER.error("HassOS CLI update fails")
raise HassOSUpdateError() from None raise HassOSUpdateError() from None
else:
# Cleanup
with suppress(DockerAPIError):
await self.instance.cleanup()
async def repair_cli(self) -> None: async def repair_cli(self) -> None:
"""Repair CLI container.""" """Repair CLI container."""

View File

@ -2,6 +2,7 @@
import asyncio import asyncio
from contextlib import asynccontextmanager, suppress from contextlib import asynccontextmanager, suppress
from datetime import datetime, timedelta from datetime import datetime, timedelta
from distutils.version import StrictVersion
from ipaddress import IPv4Address from ipaddress import IPv4Address
import logging import logging
import os import os
@ -79,7 +80,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
try: try:
# Evaluate Version if we lost this information # Evaluate Version if we lost this information
if not self.version: if not self.version:
self.version = await self.instance.get_latest_version() self.version = await self.instance.get_latest_version(key=StrictVersion)
await self.instance.attach(tag=self.version) await self.instance.attach(tag=self.version)
except DockerAPIError: except DockerAPIError:

View File

@ -15,6 +15,10 @@ _LOGGER = logging.getLogger(__name__)
DefaultConfig = attr.make_class("DefaultConfig", ["input", "output"]) DefaultConfig = attr.make_class("DefaultConfig", ["input", "output"])
AUDIODB_JSON: Path = Path(__file__).parents[1].joinpath("data/audiodb.json")
ASOUND_TMPL: Path = Path(__file__).parents[1].joinpath("data/asound.tmpl")
class AlsaAudio(CoreSysAttributes): class AlsaAudio(CoreSysAttributes):
"""Handle Audio ALSA host data.""" """Handle Audio ALSA host data."""
@ -82,12 +86,8 @@ class AlsaAudio(CoreSysAttributes):
@staticmethod @staticmethod
def _audio_database(): def _audio_database():
"""Read local json audio data into dict.""" """Read local json audio data into dict."""
json_file = Path(__file__).parent.joinpath("data/audiodb.json")
try: try:
# pylint: disable=no-member return json.loads(AUDIODB_JSON.read_text())
with json_file.open("r") as database:
return json.loads(database.read())
except (ValueError, OSError) as err: except (ValueError, OSError) as err:
_LOGGER.warning("Can't read audio DB: %s", err) _LOGGER.warning("Can't read audio DB: %s", err)
@ -122,11 +122,8 @@ class AlsaAudio(CoreSysAttributes):
alsa_output = alsa_output or self.default.output alsa_output = alsa_output or self.default.output
# Read Template # Read Template
asound_file = Path(__file__).parent.joinpath("data/asound.tmpl")
try: try:
# pylint: disable=no-member asound_data = ASOUND_TMPL.read_text()
with asound_file.open("r") as asound:
asound_data = asound.read()
except OSError as err: except OSError as err:
_LOGGER.error("Can't read asound.tmpl: %s", err) _LOGGER.error("Can't read asound.tmpl: %s", err)
return "" return ""

View File

@ -2,12 +2,14 @@
import asyncio import asyncio
import logging import logging
import shlex import shlex
from ipaddress import IPv4Address
from typing import Optional
import async_timeout import async_timeout
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
COMMAND = "socat UDP-RECVFROM:53,fork UDP-SENDTO:127.0.0.11:53" COMMAND = "socat UDP-RECVFROM:53,fork UDP-SENDTO:{!s}"
class DNSForward: class DNSForward:
@ -15,13 +17,13 @@ class DNSForward:
def __init__(self): def __init__(self):
"""Initialize DNS forwarding.""" """Initialize DNS forwarding."""
self.proc = None self.proc: Optional[asyncio.Process] = None
async def start(self): async def start(self, dns_server: IPv4Address) -> None:
"""Start DNS forwarding.""" """Start DNS forwarding."""
try: try:
self.proc = await asyncio.create_subprocess_exec( self.proc = await asyncio.create_subprocess_exec(
*shlex.split(COMMAND), *shlex.split(COMMAND.format(dns_server)),
stdin=asyncio.subprocess.DEVNULL, stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.DEVNULL, stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL,
@ -29,9 +31,9 @@ class DNSForward:
except OSError as err: except OSError as err:
_LOGGER.error("Can't start DNS forwarding: %s", err) _LOGGER.error("Can't start DNS forwarding: %s", err)
else: else:
_LOGGER.info("Start DNS port forwarding for host add-ons") _LOGGER.info("Start DNS port forwarding to %s", dns_server)
async def stop(self): async def stop(self) -> None:
"""Stop DNS forwarding.""" """Stop DNS forwarding."""
if not self.proc: if not self.proc:
_LOGGER.warning("DNS forwarding is not running!") _LOGGER.warning("DNS forwarding is not running!")

View File

@ -3,7 +3,7 @@ import asyncio
import logging import logging
from .coresys import CoreSysAttributes from .coresys import CoreSysAttributes
from .exceptions import HomeAssistantError from .exceptions import HomeAssistantError, CoreDNSError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -22,6 +22,8 @@ RUN_RELOAD_INGRESS = 930
RUN_WATCHDOG_HOMEASSISTANT_DOCKER = 15 RUN_WATCHDOG_HOMEASSISTANT_DOCKER = 15
RUN_WATCHDOG_HOMEASSISTANT_API = 300 RUN_WATCHDOG_HOMEASSISTANT_API = 300
RUN_WATCHDOG_DNS_DOCKER = 20
class Tasks(CoreSysAttributes): class Tasks(CoreSysAttributes):
"""Handle Tasks inside Hass.io.""" """Handle Tasks inside Hass.io."""
@ -83,6 +85,11 @@ class Tasks(CoreSysAttributes):
self._watchdog_homeassistant_api, RUN_WATCHDOG_HOMEASSISTANT_API self._watchdog_homeassistant_api, RUN_WATCHDOG_HOMEASSISTANT_API
) )
) )
self.jobs.add(
self.sys_scheduler.register_task(
self._watchdog_dns_docker, RUN_WATCHDOG_DNS_DOCKER
)
)
_LOGGER.info("All core tasks are scheduled") _LOGGER.info("All core tasks are scheduled")
@ -194,3 +201,19 @@ class Tasks(CoreSysAttributes):
_LOGGER.info("Found new HassOS CLI version") _LOGGER.info("Found new HassOS CLI version")
await self.sys_hassos.update_cli() await self.sys_hassos.update_cli()
async def _watchdog_dns_docker(self):
"""Check running state of Docker and start if they is close."""
# if Home Assistant is active
if await self.sys_dns.is_running():
return
_LOGGER.warning("Watchdog found a problem with CoreDNS plugin!")
if await self.sys_dns.is_fails():
_LOGGER.warning("CoreDNS plugin is in fails state / Reset config")
self.sys_dns.reset()
try:
await self.sys_dns.start()
except CoreDNSError:
_LOGGER.error("Watchdog CoreDNS reanimation fails!")

View File

@ -4,23 +4,25 @@ from contextlib import suppress
from datetime import timedelta from datetime import timedelta
import json import json
import logging import logging
from typing import Optional
import aiohttp import aiohttp
from .const import ( from .const import (
URL_HASSIO_VERSION,
FILE_HASSIO_UPDATER,
ATTR_HOMEASSISTANT,
ATTR_HASSIO,
ATTR_CHANNEL, ATTR_CHANNEL,
ATTR_DNS,
ATTR_HASSIO,
ATTR_HASSOS, ATTR_HASSOS,
ATTR_HASSOS_CLI, ATTR_HASSOS_CLI,
ATTR_HOMEASSISTANT,
FILE_HASSIO_UPDATER,
URL_HASSIO_VERSION,
) )
from .coresys import CoreSysAttributes from .coresys import CoreSysAttributes
from .exceptions import HassioUpdaterError
from .utils import AsyncThrottle from .utils import AsyncThrottle
from .utils.json import JsonConfig from .utils.json import JsonConfig
from .validate import SCHEMA_UPDATER_CONFIG from .validate import SCHEMA_UPDATER_CONFIG
from .exceptions import HassioUpdaterError
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -33,43 +35,48 @@ class Updater(JsonConfig, CoreSysAttributes):
super().__init__(FILE_HASSIO_UPDATER, SCHEMA_UPDATER_CONFIG) super().__init__(FILE_HASSIO_UPDATER, SCHEMA_UPDATER_CONFIG)
self.coresys = coresys self.coresys = coresys
async def load(self): async def load(self) -> None:
"""Update internal data.""" """Update internal data."""
with suppress(HassioUpdaterError): with suppress(HassioUpdaterError):
await self.fetch_data() await self.fetch_data()
async def reload(self): async def reload(self) -> None:
"""Update internal data.""" """Update internal data."""
with suppress(HassioUpdaterError): with suppress(HassioUpdaterError):
await self.fetch_data() await self.fetch_data()
@property @property
def version_homeassistant(self): def version_homeassistant(self) -> Optional[str]:
"""Return last version of Home Assistant.""" """Return latest version of Home Assistant."""
return self._data.get(ATTR_HOMEASSISTANT) return self._data.get(ATTR_HOMEASSISTANT)
@property @property
def version_hassio(self): def version_hassio(self) -> Optional[str]:
"""Return last version of Hass.io.""" """Return latest version of Hass.io."""
return self._data.get(ATTR_HASSIO) return self._data.get(ATTR_HASSIO)
@property @property
def version_hassos(self): def version_hassos(self) -> Optional[str]:
"""Return last version of HassOS.""" """Return latest version of HassOS."""
return self._data.get(ATTR_HASSOS) return self._data.get(ATTR_HASSOS)
@property @property
def version_hassos_cli(self): def version_hassos_cli(self) -> Optional[str]:
"""Return last version of HassOS cli.""" """Return latest version of HassOS cli."""
return self._data.get(ATTR_HASSOS_CLI) return self._data.get(ATTR_HASSOS_CLI)
@property @property
def channel(self): def version_dns(self) -> Optional[str]:
"""Return latest version of Hass.io DNS."""
return self._data.get(ATTR_DNS)
@property
def channel(self) -> str:
"""Return upstream channel of Hass.io instance.""" """Return upstream channel of Hass.io instance."""
return self._data[ATTR_CHANNEL] return self._data[ATTR_CHANNEL]
@channel.setter @channel.setter
def channel(self, value): def channel(self, value: str):
"""Set upstream mode.""" """Set upstream mode."""
self._data[ATTR_CHANNEL] = value self._data[ATTR_CHANNEL] = value
@ -104,6 +111,7 @@ class Updater(JsonConfig, CoreSysAttributes):
try: try:
# update supervisor version # update supervisor version
self._data[ATTR_HASSIO] = data["supervisor"] self._data[ATTR_HASSIO] = data["supervisor"]
self._data[ATTR_DNS] = data["dns"]
# update Home Assistant version # update Home Assistant version
self._data[ATTR_HOMEASSISTANT] = data["homeassistant"][machine] self._data[ATTR_HOMEASSISTANT] = data["homeassistant"][machine]

View File

@ -11,6 +11,7 @@ from .const import (
ATTR_CHANNEL, ATTR_CHANNEL,
ATTR_DEBUG, ATTR_DEBUG,
ATTR_DEBUG_BLOCK, ATTR_DEBUG_BLOCK,
ATTR_DNS,
ATTR_HASSIO, ATTR_HASSIO,
ATTR_HASSOS, ATTR_HASSOS,
ATTR_HASSOS_CLI, ATTR_HASSOS_CLI,
@ -23,6 +24,7 @@ from .const import (
ATTR_PORT, ATTR_PORT,
ATTR_PORTS, ATTR_PORTS,
ATTR_REFRESH_TOKEN, ATTR_REFRESH_TOKEN,
ATTR_SERVERS,
ATTR_SESSION, ATTR_SESSION,
ATTR_SSL, ATTR_SSL,
ATTR_TIMEZONE, ATTR_TIMEZONE,
@ -33,11 +35,13 @@ from .const import (
CHANNEL_BETA, CHANNEL_BETA,
CHANNEL_DEV, CHANNEL_DEV,
CHANNEL_STABLE, CHANNEL_STABLE,
DNS_SERVERS,
) )
from .utils.validate import validate_timezone from .utils.validate import validate_timezone
RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$") RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$")
# pylint: disable=no-value-for-parameter
NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)) NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
WAIT_BOOT = vol.All(vol.Coerce(int), vol.Range(min=1, max=60)) 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{}]+$")
@ -47,6 +51,7 @@ UUID_MATCH = vol.Match(r"^[0-9a-f]{32}$")
SHA256 = vol.Match(r"^[0-9a-f]{64}$") SHA256 = vol.Match(r"^[0-9a-f]{64}$")
TOKEN = vol.Match(r"^[0-9a-f]{32,256}$") TOKEN = vol.Match(r"^[0-9a-f]{32,256}$")
LOG_LEVEL = vol.In(["debug", "info", "warning", "error", "critical"]) LOG_LEVEL = vol.In(["debug", "info", "warning", "error", "critical"])
DNS_SERVER_LIST = vol.All([vol.Url()], vol.Length(max=8))
def validate_repository(repository): def validate_repository(repository):
@ -108,6 +113,7 @@ SCHEMA_UPDATER_CONFIG = vol.Schema(
vol.Optional(ATTR_HASSIO): vol.Coerce(str), vol.Optional(ATTR_HASSIO): vol.Coerce(str),
vol.Optional(ATTR_HASSOS): vol.Coerce(str), vol.Optional(ATTR_HASSOS): vol.Coerce(str),
vol.Optional(ATTR_HASSOS_CLI): vol.Coerce(str), vol.Optional(ATTR_HASSOS_CLI): vol.Coerce(str),
vol.Optional(ATTR_DNS): vol.Coerce(str),
}, },
extra=vol.REMOVE_EXTRA, extra=vol.REMOVE_EXTRA,
) )
@ -145,3 +151,12 @@ SCHEMA_INGRESS_CONFIG = vol.Schema(
}, },
extra=vol.REMOVE_EXTRA, extra=vol.REMOVE_EXTRA,
) )
SCHEMA_DNS_CONFIG = vol.Schema(
{
vol.Optional(ATTR_VERSION): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_SERVERS, default=DNS_SERVERS): DNS_SERVER_LIST,
},
extra=vol.REMOVE_EXTRA,
)

102
script/test_env.sh Executable file
View File

@ -0,0 +1,102 @@
#!/bin/bash
set -eE
DOCKER_TIMEOUT=30
DOCKER_PID=0
function start_docker() {
local starttime
local endtime
echo "Starting docker."
dockerd 2> /dev/null &
DOCKER_PID=$!
echo "Waiting for docker to initialize..."
starttime="$(date +%s)"
endtime="$(date +%s)"
until docker info >/dev/null 2>&1; do
if [ $((endtime - starttime)) -le $DOCKER_TIMEOUT ]; then
sleep 1
endtime=$(date +%s)
else
echo "Timeout while waiting for docker to come up"
exit 1
fi
done
echo "Docker was initialized"
}
function stop_docker() {
local starttime
local endtime
echo "Stopping in container docker..."
if [ "$DOCKER_PID" -gt 0 ] && kill -0 "$DOCKER_PID" 2> /dev/null; then
starttime="$(date +%s)"
endtime="$(date +%s)"
# Now wait for it to die
kill "$DOCKER_PID"
while kill -0 "$DOCKER_PID" 2> /dev/null; do
if [ $((endtime - starttime)) -le $DOCKER_TIMEOUT ]; then
sleep 1
endtime=$(date +%s)
else
echo "Timeout while waiting for container docker to die"
exit 1
fi
done
else
echo "Your host might have been left with unreleased resources"
fi
}
function build_supervisor() {
docker pull homeassistant/amd64-builder:dev
docker run --rm --privileged \
-v /run/docker.sock:/run/docker.sock -v "$(pwd):/data" \
homeassistant/amd64-builder:dev \
--supervisor 3.7-alpine3.10 --version dev \
-t /data --test --amd64 \
--no-cache --docker-hub homeassistant
}
function install_cli() {
docker pull homeassistant/amd64-hassio-cli:dev
}
function setup_test_env() {
mkdir -p test_data
docker run --rm --privileged \
--name hassio_supervisor \
--security-opt seccomp=unconfined \
--security-opt apparmor:unconfined \
-v /run/docker.sock:/run/docker.sock \
-v /run/dbus:/run/dbus \
-v "$(pwd)/test_data":/data \
-v /etc/machine-id:/etc/machine-id:ro \
-e SUPERVISOR_SHARE="$(pwd)/test_data" \
-e SUPERVISOR_NAME=hassio_supervisor \
-e SUPERVISOR_DEV=1 \
-e HOMEASSISTANT_REPOSITORY="homeassistant/qemux86-64-homeassistant" \
homeassistant/amd64-hassio-supervisor:latest
}
echo "Start Test-Env"
start_docker
trap "stop_docker" ERR
build_supervisor
install_cli
setup_test_env
stop_docker

0
script/update-frontend.sh Normal file → Executable file
View File