mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-19 07:06:30 +00:00
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:
parent
9f3f47eb80
commit
1196788856
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -18,3 +18,6 @@ venv/
|
|||||||
home-assistant-polymer/*
|
home-assistant-polymer/*
|
||||||
misc/*
|
misc/*
|
||||||
script/*
|
script/*
|
||||||
|
|
||||||
|
# Test ENV
|
||||||
|
data/
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
73
.vscode/tasks.json
vendored
@ -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
42
API.md
@ -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
|
||||||
|
@ -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."""
|
||||||
|
@ -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")
|
||||||
|
@ -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
87
hassio/api/dns.py
Normal 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()
|
@ -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
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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"
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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
9
hassio/data/coredns.tmpl
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.:53 {
|
||||||
|
log
|
||||||
|
hosts /config/hosts {
|
||||||
|
fallthrough
|
||||||
|
}
|
||||||
|
forward . $servers {
|
||||||
|
health_check 10s
|
||||||
|
}
|
||||||
|
}
|
2
hassio/data/hosts.tmpl
Normal file
2
hassio/data/hosts.tmpl
Normal 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
305
hassio/dns.py
Normal 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")
|
@ -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:
|
||||||
|
@ -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
56
hassio/docker/dns.py
Normal 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)
|
@ -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]
|
|
||||||
|
@ -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]
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
@ -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:
|
||||||
|
@ -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 ""
|
||||||
|
@ -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!")
|
@ -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!")
|
||||||
|
@ -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]
|
||||||
|
@ -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
102
script/test_env.sh
Executable 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
0
script/update-frontend.sh
Normal file → Executable file
Loading…
x
Reference in New Issue
Block a user