mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-25 18:16:32 +00:00
commit
fa783a0d2c
180
.vscode/tasks.json
vendored
180
.vscode/tasks.json
vendored
@ -1,92 +1,90 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"label": "Run Testenv",
|
"label": "Run Testenv",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "./scripts/test_env.sh",
|
"command": "./scripts/test_env.sh",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
"isDefault": true,
|
"isDefault": true
|
||||||
},
|
},
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"panel": "new"
|
"panel": "new"
|
||||||
},
|
},
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Run Testenv CLI",
|
"label": "Run Testenv CLI",
|
||||||
"type": "shell",
|
"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",
|
"command": "docker exec -ti hassio_cli /usr/bin/cli.sh",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
"isDefault": true,
|
"isDefault": true
|
||||||
},
|
},
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"panel": "new"
|
"panel": "new"
|
||||||
},
|
},
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Update UI",
|
"label": "Update UI",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "./scripts/update-frontend.sh",
|
"command": "./scripts/update-frontend.sh",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
},
|
},
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"panel": "new"
|
"panel": "new"
|
||||||
},
|
},
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Pytest",
|
"label": "Pytest",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "pytest --timeout=10 tests",
|
"command": "pytest --timeout=10 tests",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
"isDefault": true,
|
"isDefault": true
|
||||||
},
|
},
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"panel": "new"
|
"panel": "new"
|
||||||
},
|
},
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Flake8",
|
"label": "Flake8",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "flake8 hassio tests",
|
"command": "flake8 hassio tests",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
"isDefault": true,
|
"isDefault": true
|
||||||
},
|
},
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"panel": "new"
|
"panel": "new"
|
||||||
},
|
},
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Pylint",
|
"label": "Pylint",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "pylint hassio",
|
"command": "pylint hassio",
|
||||||
"dependsOn": [
|
"dependsOn": ["Install all Requirements"],
|
||||||
"Install all Requirements"
|
"group": {
|
||||||
],
|
"kind": "test",
|
||||||
"group": {
|
"isDefault": true
|
||||||
"kind": "test",
|
},
|
||||||
"isDefault": true,
|
"presentation": {
|
||||||
},
|
"reveal": "always",
|
||||||
"presentation": {
|
"panel": "new"
|
||||||
"reveal": "always",
|
},
|
||||||
"panel": "new"
|
"problemMatcher": []
|
||||||
},
|
}
|
||||||
"problemMatcher": []
|
]
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
|
182
API.md
182
API.md
@ -291,9 +291,7 @@ return:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "2.3",
|
"version": "2.3",
|
||||||
"version_cli": "7",
|
|
||||||
"version_latest": "2.4",
|
"version_latest": "2.4",
|
||||||
"version_cli_latest": "8",
|
|
||||||
"board": "ova|rpi",
|
"board": "ova|rpi",
|
||||||
"boot": "rauc boot slot"
|
"boot": "rauc boot slot"
|
||||||
}
|
}
|
||||||
@ -307,14 +305,6 @@ return:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- POST `/os/update/cli`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "optional"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- POST `/os/config/sync`
|
- POST `/os/config/sync`
|
||||||
|
|
||||||
Load host configs from a USB stick.
|
Load host configs from a USB stick.
|
||||||
@ -857,90 +847,29 @@ return:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Audio
|
### DNS
|
||||||
|
|
||||||
- GET `/audio/info`
|
- GET `/dns/info`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"host": "ip-address",
|
"host": "ip-address",
|
||||||
"version": "1",
|
"version": "1",
|
||||||
"latest_version": "2",
|
"latest_version": "2",
|
||||||
"audio": {
|
"servers": ["dns://8.8.8.8"],
|
||||||
"card": [
|
"locals": ["dns://xy"]
|
||||||
{
|
|
||||||
"name": "...",
|
|
||||||
"index": 1,
|
|
||||||
"driver": "...",
|
|
||||||
"profiles": [
|
|
||||||
{
|
|
||||||
"name": "...",
|
|
||||||
"description": "...",
|
|
||||||
"active": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"input": [
|
|
||||||
{
|
|
||||||
"name": "...",
|
|
||||||
"index": 0,
|
|
||||||
"description": "...",
|
|
||||||
"volume": 0.3,
|
|
||||||
"mute": false,
|
|
||||||
"default": false,
|
|
||||||
"card": "null|int",
|
|
||||||
"applications": [
|
|
||||||
{
|
|
||||||
"name": "...",
|
|
||||||
"index": 0,
|
|
||||||
"stream_index": 0,
|
|
||||||
"stream_type": "INPUT",
|
|
||||||
"volume": 0.3,
|
|
||||||
"mute": false,
|
|
||||||
"addon": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"output": [
|
|
||||||
{
|
|
||||||
"name": "...",
|
|
||||||
"index": 0,
|
|
||||||
"description": "...",
|
|
||||||
"volume": 0.3,
|
|
||||||
"mute": false,
|
|
||||||
"default": false,
|
|
||||||
"card": "null|int",
|
|
||||||
"applications": [
|
|
||||||
{
|
|
||||||
"name": "...",
|
|
||||||
"index": 0,
|
|
||||||
"stream_index": 0,
|
|
||||||
"stream_type": "OUTPUT",
|
|
||||||
"volume": 0.3,
|
|
||||||
"mute": false,
|
|
||||||
"addon": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"application": [
|
|
||||||
{
|
|
||||||
"name": "...",
|
|
||||||
"index": 0,
|
|
||||||
"stream_index": 0,
|
|
||||||
"stream_type": "OUTPUT",
|
|
||||||
"volume": 0.3,
|
|
||||||
"mute": false,
|
|
||||||
"addon": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- POST `/audio/update`
|
- POST `/dns/options`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"servers": ["dns://8.8.8.8"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/dns/update`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -948,92 +877,47 @@ return:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- POST `/audio/restart`
|
- POST `/dns/restart`
|
||||||
|
|
||||||
- POST `/audio/reload`
|
- POST `/dns/reset`
|
||||||
|
|
||||||
- GET `/audio/logs`
|
- GET `/dns/logs`
|
||||||
|
|
||||||
- POST `/audio/volume/input`
|
- GET `/dns/stats`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"index": "...",
|
"cpu_percent": 0.0,
|
||||||
"volume": 0.5
|
"memory_usage": 283123,
|
||||||
|
"memory_limit": 329392,
|
||||||
|
"memory_percent": 1.4,
|
||||||
|
"network_tx": 0,
|
||||||
|
"network_rx": 0,
|
||||||
|
"blk_read": 0,
|
||||||
|
"blk_write": 0
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- POST `/audio/volume/output`
|
### CLI
|
||||||
|
|
||||||
|
- GET `/cli/info`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"index": "...",
|
"version": "1",
|
||||||
"volume": 0.5
|
"version_latest": "2"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- POST `/audio/volume/{output|input}/application`
|
- POST `/cli/update`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"index": "...",
|
"version": "VERSION"
|
||||||
"volume": 0.5
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- POST `/audio/mute/input`
|
- GET `/cli/stats`
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"index": "...",
|
|
||||||
"active": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- POST `/audio/mute/output`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"index": "...",
|
|
||||||
"active": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- POST `/audio/mute/{output|input}/application`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"index": "...",
|
|
||||||
"active": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- POST `/audio/default/input`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- POST `/audio/default/output`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- POST `/audio/profile`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"card": "...",
|
|
||||||
"name": "..."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- GET `/audio/stats`
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
@ -8,53 +8,19 @@ trigger:
|
|||||||
pr: none
|
pr: none
|
||||||
variables:
|
variables:
|
||||||
- name: versionWheels
|
- name: versionWheels
|
||||||
value: "1.6-3.7-alpine3.11"
|
value: '1.6.1-3.7-alpine3.11'
|
||||||
- group: wheels
|
resources:
|
||||||
|
repositories:
|
||||||
|
- repository: azure
|
||||||
|
type: github
|
||||||
|
name: 'home-assistant/ci-azure'
|
||||||
|
endpoint: 'home-assistant'
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: "Wheels"
|
- template: templates/azp-job-wheels.yaml@azure
|
||||||
timeoutInMinutes: 360
|
parameters:
|
||||||
pool:
|
builderVersion: '$(versionWheels)'
|
||||||
vmImage: "ubuntu-latest"
|
builderApk: 'build-base;libffi-dev;openssl-dev'
|
||||||
strategy:
|
builderPip: 'Cython'
|
||||||
maxParallel: 5
|
wheelsRequirement: 'requirements.txt'
|
||||||
matrix:
|
|
||||||
amd64:
|
|
||||||
buildArch: "amd64"
|
|
||||||
i386:
|
|
||||||
buildArch: "i386"
|
|
||||||
armhf:
|
|
||||||
buildArch: "armhf"
|
|
||||||
armv7:
|
|
||||||
buildArch: "armv7"
|
|
||||||
aarch64:
|
|
||||||
buildArch: "aarch64"
|
|
||||||
steps:
|
|
||||||
- script: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y --no-install-recommends \
|
|
||||||
qemu-user-static \
|
|
||||||
binfmt-support \
|
|
||||||
curl
|
|
||||||
|
|
||||||
sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
|
|
||||||
sudo update-binfmts --enable qemu-arm
|
|
||||||
sudo update-binfmts --enable qemu-aarch64
|
|
||||||
displayName: "Initial cross build"
|
|
||||||
- script: |
|
|
||||||
mkdir -p .ssh
|
|
||||||
echo -e "-----BEGIN RSA PRIVATE KEY-----\n$(wheelsSSH)\n-----END RSA PRIVATE KEY-----" >> .ssh/id_rsa
|
|
||||||
ssh-keyscan -H $(wheelsHost) >> .ssh/known_hosts
|
|
||||||
chmod 600 .ssh/*
|
|
||||||
displayName: "Install ssh key"
|
|
||||||
- script: sudo docker pull homeassistant/$(buildArch)-wheels:$(versionWheels)
|
|
||||||
displayName: "Install wheels builder"
|
|
||||||
- script: |
|
|
||||||
sudo docker run --rm -v $(pwd):/data:ro -v $(pwd)/.ssh:/root/.ssh:rw \
|
|
||||||
homeassistant/$(buildArch)-wheels:$(versionWheels) \
|
|
||||||
--apk "build-base;libffi-dev;openssl-dev" \
|
|
||||||
--index $(wheelsIndex) \
|
|
||||||
--requirement requirements.txt \
|
|
||||||
--upload rsync \
|
|
||||||
--remote wheels@$(wheelsHost):/opt/wheels
|
|
||||||
displayName: "Run wheels build"
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
aiohttp==3.6.1
|
aiohttp==3.6.1
|
||||||
async_timeout==3.0.1
|
async_timeout==3.0.1
|
||||||
attrs==19.3.0
|
attrs==19.3.0
|
||||||
cchardet==2.1.5
|
cchardet==2.1.6
|
||||||
colorlog==4.1.0
|
colorlog==4.1.0
|
||||||
cpe==1.2.1
|
cpe==1.2.1
|
||||||
cryptography==2.8
|
cryptography==2.8
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
flake8==3.7.9
|
flake8==3.7.9
|
||||||
pylint==2.4.4
|
pylint==2.4.4
|
||||||
pytest==5.3.5
|
pytest==5.4.1
|
||||||
pytest-timeout==1.3.4
|
pytest-timeout==1.3.4
|
||||||
pytest-aiohttp==0.3.0
|
pytest-aiohttp==0.3.0
|
||||||
black==19.10b0
|
black==19.10b0
|
||||||
|
@ -2,4 +2,6 @@
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Start Service service
|
# Start Service service
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
export LD_PRELOAD="/usr/local/lib/libjemalloc.so.2"
|
||||||
|
|
||||||
exec python3 -m supervisor
|
exec python3 -m supervisor
|
@ -81,11 +81,6 @@ function cleanup_docker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function install_cli() {
|
|
||||||
docker pull homeassistant/amd64-hassio-cli:dev
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function setup_test_env() {
|
function setup_test_env() {
|
||||||
mkdir -p /workspaces/test_supervisor
|
mkdir -p /workspaces/test_supervisor
|
||||||
|
|
||||||
@ -131,7 +126,6 @@ start_docker
|
|||||||
trap "stop_docker" ERR
|
trap "stop_docker" ERR
|
||||||
|
|
||||||
build_supervisor
|
build_supervisor
|
||||||
install_cli
|
|
||||||
cleanup_lastboot
|
cleanup_lastboot
|
||||||
cleanup_docker
|
cleanup_docker
|
||||||
init_dbus
|
init_dbus
|
||||||
|
@ -59,7 +59,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
def from_token(self, token: str) -> Optional[Addon]:
|
def from_token(self, token: str) -> Optional[Addon]:
|
||||||
"""Return an add-on from Supervisor token."""
|
"""Return an add-on from Supervisor token."""
|
||||||
for addon in self.installed:
|
for addon in self.installed:
|
||||||
if token == addon.hassio_token:
|
if token == addon.supervisor_token:
|
||||||
return addon
|
return addon
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -164,7 +164,7 @@ class Addon(AddonModel):
|
|||||||
return self.persist[ATTR_UUID]
|
return self.persist[ATTR_UUID]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hassio_token(self) -> Optional[str]:
|
def supervisor_token(self) -> Optional[str]:
|
||||||
"""Return access token for Supervisor API."""
|
"""Return access token for Supervisor API."""
|
||||||
return self.persist.get(ATTR_ACCESS_TOKEN)
|
return self.persist.get(ATTR_ACCESS_TOKEN)
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ class AddonModel(CoreSysAttributes):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hassio_token(self) -> Optional[str]:
|
def supervisor_token(self) -> Optional[str]:
|
||||||
"""Return access token for Supervisor API."""
|
"""Return access token for Supervisor API."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -261,7 +261,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
),
|
),
|
||||||
vol.Optional(ATTR_IMAGE): vol.Match(RE_DOCKER_IMAGE),
|
vol.Optional(ATTR_IMAGE): vol.Match(RE_DOCKER_IMAGE),
|
||||||
vol.Optional(ATTR_TIMEOUT, default=10): vol.All(
|
vol.Optional(ATTR_TIMEOUT, default=10): vol.All(
|
||||||
vol.Coerce(int), vol.Range(min=10, max=120)
|
vol.Coerce(int), vol.Range(min=10, max=300)
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
|
@ -7,11 +7,13 @@ from aiohttp import web
|
|||||||
|
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from .addons import APIAddons
|
from .addons import APIAddons
|
||||||
|
from .audio import APIAudio
|
||||||
from .auth import APIAuth
|
from .auth import APIAuth
|
||||||
|
from .cli import APICli
|
||||||
from .discovery import APIDiscovery
|
from .discovery import APIDiscovery
|
||||||
from .dns import APICoreDNS
|
from .dns import APICoreDNS
|
||||||
from .hardware import APIHardware
|
from .hardware import APIHardware
|
||||||
from .hassos import APIHassOS
|
from .os import APIOS
|
||||||
from .homeassistant import APIHomeAssistant
|
from .homeassistant import APIHomeAssistant
|
||||||
from .host import APIHost
|
from .host import APIHost
|
||||||
from .info import APIInfo
|
from .info import APIInfo
|
||||||
@ -21,7 +23,6 @@ from .security import SecurityMiddleware
|
|||||||
from .services import APIServices
|
from .services import APIServices
|
||||||
from .snapshots import APISnapshots
|
from .snapshots import APISnapshots
|
||||||
from .supervisor import APISupervisor
|
from .supervisor import APISupervisor
|
||||||
from .audio import APIAudio
|
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -49,7 +50,8 @@ class RestAPI(CoreSysAttributes):
|
|||||||
"""Register REST API Calls."""
|
"""Register REST API Calls."""
|
||||||
self._register_supervisor()
|
self._register_supervisor()
|
||||||
self._register_host()
|
self._register_host()
|
||||||
self._register_hassos()
|
self._register_os()
|
||||||
|
self._register_cli()
|
||||||
self._register_hardware()
|
self._register_hardware()
|
||||||
self._register_homeassistant()
|
self._register_homeassistant()
|
||||||
self._register_proxy()
|
self._register_proxy()
|
||||||
@ -84,22 +86,29 @@ class RestAPI(CoreSysAttributes):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def _register_hassos(self) -> None:
|
def _register_os(self) -> None:
|
||||||
"""Register HassOS functions."""
|
"""Register OS functions."""
|
||||||
api_hassos = APIHassOS()
|
api_os = APIOS()
|
||||||
api_hassos.coresys = self.coresys
|
api_os.coresys = self.coresys
|
||||||
|
|
||||||
self.webapp.add_routes(
|
self.webapp.add_routes(
|
||||||
[
|
[
|
||||||
web.get("/os/info", api_hassos.info),
|
web.get("/os/info", api_os.info),
|
||||||
web.post("/os/update", api_hassos.update),
|
web.post("/os/update", api_os.update),
|
||||||
web.post("/os/update/cli", api_hassos.update_cli),
|
web.post("/os/config/sync", api_os.config_sync),
|
||||||
web.post("/os/config/sync", api_hassos.config_sync),
|
]
|
||||||
# Remove with old Supervisor fallback
|
)
|
||||||
web.get("/hassos/info", api_hassos.info),
|
|
||||||
web.post("/hassos/update", api_hassos.update),
|
def _register_cli(self) -> None:
|
||||||
web.post("/hassos/update/cli", api_hassos.update_cli),
|
"""Register HA cli functions."""
|
||||||
web.post("/hassos/config/sync", api_hassos.config_sync),
|
api_cli = APICli()
|
||||||
|
api_cli.coresys = self.coresys
|
||||||
|
|
||||||
|
self.webapp.add_routes(
|
||||||
|
[
|
||||||
|
web.get("/cli/info", api_cli.info),
|
||||||
|
web.get("/cli/stats", api_cli.stats),
|
||||||
|
web.post("/cli/update", api_cli.update),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -166,5 +166,5 @@ class APIAudio(CoreSysAttributes):
|
|||||||
body = await api_validate(SCHEMA_PROFILE, request)
|
body = await api_validate(SCHEMA_PROFILE, request)
|
||||||
|
|
||||||
await asyncio.shield(
|
await asyncio.shield(
|
||||||
self.sys_host.sound.set_profile(body[ATTR_CARD], body[ATTR_NAME])
|
self.sys_host.sound.ativate_profile(body[ATTR_CARD], body[ATTR_NAME])
|
||||||
)
|
)
|
||||||
|
62
supervisor/api/cli.py
Normal file
62
supervisor/api/cli.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
"""Init file for Supervisor HA cli RESTful API."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from ..const import (
|
||||||
|
ATTR_VERSION,
|
||||||
|
ATTR_VERSION_LATEST,
|
||||||
|
ATTR_BLK_READ,
|
||||||
|
ATTR_BLK_WRITE,
|
||||||
|
ATTR_CPU_PERCENT,
|
||||||
|
ATTR_MEMORY_LIMIT,
|
||||||
|
ATTR_MEMORY_PERCENT,
|
||||||
|
ATTR_MEMORY_USAGE,
|
||||||
|
ATTR_NETWORK_RX,
|
||||||
|
ATTR_NETWORK_TX,
|
||||||
|
)
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
|
||||||
|
|
||||||
|
|
||||||
|
class APICli(CoreSysAttributes):
|
||||||
|
"""Handle RESTful API for HA Cli functions."""
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
|
"""Return HA cli information."""
|
||||||
|
return {
|
||||||
|
ATTR_VERSION: self.sys_cli.version,
|
||||||
|
ATTR_VERSION_LATEST: self.sys_cli.latest_version,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
||||||
|
"""Return resource information."""
|
||||||
|
stats = await self.sys_cli.stats()
|
||||||
|
|
||||||
|
return {
|
||||||
|
ATTR_CPU_PERCENT: stats.cpu_percent,
|
||||||
|
ATTR_MEMORY_USAGE: stats.memory_usage,
|
||||||
|
ATTR_MEMORY_LIMIT: stats.memory_limit,
|
||||||
|
ATTR_MEMORY_PERCENT: stats.memory_percent,
|
||||||
|
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 HA CLI."""
|
||||||
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
|
version = body.get(ATTR_VERSION, self.sys_cli.latest_version)
|
||||||
|
|
||||||
|
await asyncio.shield(self.sys_cli.update(version))
|
@ -10,8 +10,6 @@ from ..const import (
|
|||||||
ATTR_BOARD,
|
ATTR_BOARD,
|
||||||
ATTR_BOOT,
|
ATTR_BOOT,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
ATTR_VERSION_CLI,
|
|
||||||
ATTR_VERSION_CLI_LATEST,
|
|
||||||
ATTR_VERSION_LATEST,
|
ATTR_VERSION_LATEST,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
@ -22,38 +20,28 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
|
|||||||
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
|
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
|
||||||
|
|
||||||
|
|
||||||
class APIHassOS(CoreSysAttributes):
|
class APIOS(CoreSysAttributes):
|
||||||
"""Handle RESTful API for HassOS functions."""
|
"""Handle RESTful API for OS functions."""
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Return HassOS information."""
|
"""Return OS information."""
|
||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_hassos.version,
|
ATTR_VERSION: self.sys_hassos.version,
|
||||||
ATTR_VERSION_CLI: self.sys_hassos.version_cli,
|
ATTR_VERSION_LATEST: self.sys_hassos.latest_version,
|
||||||
ATTR_VERSION_LATEST: self.sys_hassos.version_latest,
|
|
||||||
ATTR_VERSION_CLI_LATEST: self.sys_hassos.version_cli_latest,
|
|
||||||
ATTR_BOARD: self.sys_hassos.board,
|
ATTR_BOARD: self.sys_hassos.board,
|
||||||
ATTR_BOOT: self.sys_dbus.rauc.boot_slot,
|
ATTR_BOOT: self.sys_dbus.rauc.boot_slot,
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def update(self, request: web.Request) -> None:
|
async def update(self, request: web.Request) -> None:
|
||||||
"""Update HassOS."""
|
"""Update OS."""
|
||||||
body = await api_validate(SCHEMA_VERSION, request)
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
version = body.get(ATTR_VERSION, self.sys_hassos.version_latest)
|
version = body.get(ATTR_VERSION, self.sys_hassos.latest_version)
|
||||||
|
|
||||||
await asyncio.shield(self.sys_hassos.update(version))
|
await asyncio.shield(self.sys_hassos.update(version))
|
||||||
|
|
||||||
@api_process
|
|
||||||
async def update_cli(self, request: web.Request) -> None:
|
|
||||||
"""Update HassOS CLI."""
|
|
||||||
body = await api_validate(SCHEMA_VERSION, request)
|
|
||||||
version = body.get(ATTR_VERSION, self.sys_hassos.version_cli_latest)
|
|
||||||
|
|
||||||
await asyncio.shield(self.sys_hassos.update_cli(version))
|
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def config_sync(self, request: web.Request) -> Awaitable[None]:
|
def config_sync(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Trigger config reload on HassOS."""
|
"""Trigger config reload on OS."""
|
||||||
return asyncio.shield(self.sys_hassos.config_sync())
|
return asyncio.shield(self.sys_hassos.config_sync())
|
@ -26,11 +26,11 @@ class APIProxy(CoreSysAttributes):
|
|||||||
"""Check the Supervisor token."""
|
"""Check the Supervisor token."""
|
||||||
if AUTHORIZATION in request.headers:
|
if AUTHORIZATION in request.headers:
|
||||||
bearer = request.headers[AUTHORIZATION]
|
bearer = request.headers[AUTHORIZATION]
|
||||||
hassio_token = bearer.split(" ")[-1]
|
supervisor_token = bearer.split(" ")[-1]
|
||||||
else:
|
else:
|
||||||
hassio_token = request.headers.get(HEADER_HA_ACCESS)
|
supervisor_token = request.headers.get(HEADER_HA_ACCESS)
|
||||||
|
|
||||||
addon = self.sys_addons.from_token(hassio_token)
|
addon = self.sys_addons.from_token(supervisor_token)
|
||||||
if not addon:
|
if not addon:
|
||||||
_LOGGER.warning("Unknown Home Assistant API access!")
|
_LOGGER.warning("Unknown Home Assistant API access!")
|
||||||
elif not addon.access_homeassistant_api:
|
elif not addon.access_homeassistant_api:
|
||||||
@ -177,8 +177,10 @@ class APIProxy(CoreSysAttributes):
|
|||||||
|
|
||||||
# Check API access
|
# Check API access
|
||||||
response = await server.receive_json()
|
response = await server.receive_json()
|
||||||
hassio_token = response.get("api_password") or response.get("access_token")
|
supervisor_token = response.get("api_password") or response.get(
|
||||||
addon = self.sys_addons.from_token(hassio_token)
|
"access_token"
|
||||||
|
)
|
||||||
|
addon = self.sys_addons.from_token(supervisor_token)
|
||||||
|
|
||||||
if not addon or not addon.access_homeassistant_api:
|
if not addon or not addon.access_homeassistant_api:
|
||||||
_LOGGER.warning("Unauthorized WebSocket access!")
|
_LOGGER.warning("Unauthorized WebSocket access!")
|
||||||
|
@ -75,6 +75,7 @@ ADDONS_ROLE_ACCESS = {
|
|||||||
r"^(?:"
|
r"^(?:"
|
||||||
r"|/audio/.*"
|
r"|/audio/.*"
|
||||||
r"|/dns/.*"
|
r"|/dns/.*"
|
||||||
|
r"|/cli/.*"
|
||||||
r"|/core/.+"
|
r"|/core/.+"
|
||||||
r"|/homeassistant/.+"
|
r"|/homeassistant/.+"
|
||||||
r"|/host/.+"
|
r"|/host/.+"
|
||||||
@ -123,12 +124,13 @@ class SecurityMiddleware(CoreSysAttributes):
|
|||||||
raise HTTPUnauthorized()
|
raise HTTPUnauthorized()
|
||||||
|
|
||||||
# Home-Assistant
|
# Home-Assistant
|
||||||
if supervisor_token == self.sys_homeassistant.hassio_token:
|
if supervisor_token == self.sys_homeassistant.supervisor_token:
|
||||||
_LOGGER.debug("%s access from Home Assistant", request.path)
|
_LOGGER.debug("%s access from Home Assistant", request.path)
|
||||||
request_from = self.sys_homeassistant
|
request_from = self.sys_homeassistant
|
||||||
|
|
||||||
# Host
|
# Host
|
||||||
if supervisor_token == self.sys_machine_id:
|
# Remove machine_id handling later if all use new CLI
|
||||||
|
if supervisor_token in (self.sys_machine_id, self.sys_cli.supervisor_token):
|
||||||
_LOGGER.debug("%s access from Host", request.path)
|
_LOGGER.debug("%s access from Host", request.path)
|
||||||
request_from = self.sys_host
|
request_from = self.sys_host
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ class Audio(JsonConfig, CoreSysAttributes):
|
|||||||
|
|
||||||
@version.setter
|
@version.setter
|
||||||
def version(self, value: str) -> None:
|
def version(self, value: str) -> None:
|
||||||
"""Return current version of Audio."""
|
"""Set current version of Audio."""
|
||||||
self._data[ATTR_VERSION] = value
|
self._data[ATTR_VERSION] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -14,6 +14,7 @@ from .auth import Auth
|
|||||||
from .audio import Audio
|
from .audio import Audio
|
||||||
from .const import SOCKET_DOCKER, UpdateChannels
|
from .const import SOCKET_DOCKER, UpdateChannels
|
||||||
from .core import Core
|
from .core import Core
|
||||||
|
from .cli import HaCli
|
||||||
from .coresys import CoreSys
|
from .coresys import CoreSys
|
||||||
from .dbus import DBusManager
|
from .dbus import DBusManager
|
||||||
from .discovery import Discovery
|
from .discovery import Discovery
|
||||||
@ -67,6 +68,7 @@ async def initialize_coresys():
|
|||||||
coresys.dbus = DBusManager(coresys)
|
coresys.dbus = DBusManager(coresys)
|
||||||
coresys.hassos = HassOS(coresys)
|
coresys.hassos = HassOS(coresys)
|
||||||
coresys.secrets = SecretsManager(coresys)
|
coresys.secrets = SecretsManager(coresys)
|
||||||
|
coresys.cli = HaCli(coresys)
|
||||||
|
|
||||||
# bootstrap config
|
# bootstrap config
|
||||||
initialize_system_data(coresys)
|
initialize_system_data(coresys)
|
||||||
|
156
supervisor/cli.py
Normal file
156
supervisor/cli.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
"""CLI support on supervisor."""
|
||||||
|
import asyncio
|
||||||
|
from contextlib import suppress
|
||||||
|
import logging
|
||||||
|
import secrets
|
||||||
|
from typing import Awaitable, Optional
|
||||||
|
|
||||||
|
from .const import ATTR_ACCESS_TOKEN, ATTR_VERSION, FILE_HASSIO_CLI
|
||||||
|
from .coresys import CoreSys, CoreSysAttributes
|
||||||
|
from .docker.cli import DockerCli
|
||||||
|
from .docker.stats import DockerStats
|
||||||
|
from .exceptions import CliError, CliUpdateError, DockerAPIError
|
||||||
|
from .utils.json import JsonConfig
|
||||||
|
from .validate import SCHEMA_CLI_CONFIG
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HaCli(CoreSysAttributes, JsonConfig):
|
||||||
|
"""HA cli interface inside supervisor."""
|
||||||
|
|
||||||
|
def __init__(self, coresys: CoreSys):
|
||||||
|
"""Initialize cli handler."""
|
||||||
|
super().__init__(FILE_HASSIO_CLI, SCHEMA_CLI_CONFIG)
|
||||||
|
self.coresys: CoreSys = coresys
|
||||||
|
self.instance: DockerCli = DockerCli(coresys)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self) -> Optional[str]:
|
||||||
|
"""Return version of cli."""
|
||||||
|
return self._data.get(ATTR_VERSION)
|
||||||
|
|
||||||
|
@version.setter
|
||||||
|
def version(self, value: str) -> None:
|
||||||
|
"""Set current version of cli."""
|
||||||
|
self._data[ATTR_VERSION] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_version(self) -> str:
|
||||||
|
"""Return version of latest cli."""
|
||||||
|
return self.sys_updater.version_cli
|
||||||
|
|
||||||
|
@property
|
||||||
|
def need_update(self) -> bool:
|
||||||
|
"""Return true if a cli update is available."""
|
||||||
|
return self.version != self.latest_version
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supervisor_token(self) -> str:
|
||||||
|
"""Return an access token for the Supervisor API."""
|
||||||
|
return self._data.get(ATTR_ACCESS_TOKEN)
|
||||||
|
|
||||||
|
@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 cli setup."""
|
||||||
|
# Check cli 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 Audio plugin Docker image %s found.", self.instance.image)
|
||||||
|
|
||||||
|
# Install cli
|
||||||
|
with suppress(CliError):
|
||||||
|
await self.install()
|
||||||
|
else:
|
||||||
|
self.version = self.instance.version
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
|
# Run PulseAudio
|
||||||
|
with suppress(CliError):
|
||||||
|
if not await self.instance.is_running():
|
||||||
|
await self.start()
|
||||||
|
|
||||||
|
async def install(self) -> None:
|
||||||
|
"""Install cli."""
|
||||||
|
_LOGGER.info("Setup cli plugin")
|
||||||
|
while True:
|
||||||
|
# read audio 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 cli plugin. Retry in 30sec")
|
||||||
|
await asyncio.sleep(30)
|
||||||
|
|
||||||
|
_LOGGER.info("cli plugin now installed")
|
||||||
|
self.version = self.instance.version
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
|
async def update(self, version: Optional[str] = None) -> None:
|
||||||
|
"""Update local HA cli."""
|
||||||
|
version = version or self.latest_version
|
||||||
|
|
||||||
|
if version == self.version:
|
||||||
|
_LOGGER.warning("Version %s is already installed for cli", version)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.instance.update(version, latest=True)
|
||||||
|
except DockerAPIError:
|
||||||
|
_LOGGER.error("HA cli update fails")
|
||||||
|
raise CliUpdateError() from None
|
||||||
|
else:
|
||||||
|
# Cleanup
|
||||||
|
with suppress(DockerAPIError):
|
||||||
|
await self.instance.cleanup()
|
||||||
|
|
||||||
|
async def start(self) -> None:
|
||||||
|
"""Run cli."""
|
||||||
|
# Create new API token
|
||||||
|
self._data[ATTR_ACCESS_TOKEN] = secrets.token_hex(56)
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
|
# Start Instance
|
||||||
|
_LOGGER.info("Start cli plugin")
|
||||||
|
try:
|
||||||
|
await self.instance.run()
|
||||||
|
except DockerAPIError:
|
||||||
|
_LOGGER.error("Can't start cli plugin")
|
||||||
|
raise CliError() from None
|
||||||
|
|
||||||
|
async def stats(self) -> DockerStats:
|
||||||
|
"""Return stats of cli."""
|
||||||
|
try:
|
||||||
|
return await self.instance.stats()
|
||||||
|
except DockerAPIError:
|
||||||
|
raise CliError() from None
|
||||||
|
|
||||||
|
def is_running(self) -> Awaitable[bool]:
|
||||||
|
"""Return True if Docker container is running.
|
||||||
|
|
||||||
|
Return a coroutine.
|
||||||
|
"""
|
||||||
|
return self.instance.is_running()
|
||||||
|
|
||||||
|
async def repair(self) -> None:
|
||||||
|
"""Repair cli container."""
|
||||||
|
if await self.instance.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.info("Repair HA cli %s", self.version)
|
||||||
|
try:
|
||||||
|
await self.instance.install(self.version, latest=True)
|
||||||
|
except DockerAPIError:
|
||||||
|
_LOGGER.error("Repairing of HA cli fails")
|
@ -3,7 +3,7 @@ from enum import Enum
|
|||||||
from ipaddress import ip_network
|
from ipaddress import ip_network
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
SUPERVISOR_VERSION = "209"
|
SUPERVISOR_VERSION = "210"
|
||||||
|
|
||||||
|
|
||||||
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
||||||
@ -27,6 +27,7 @@ FILE_HASSIO_DISCOVERY = Path(SUPERVISOR_DATA, "discovery.json")
|
|||||||
FILE_HASSIO_INGRESS = Path(SUPERVISOR_DATA, "ingress.json")
|
FILE_HASSIO_INGRESS = Path(SUPERVISOR_DATA, "ingress.json")
|
||||||
FILE_HASSIO_DNS = Path(SUPERVISOR_DATA, "dns.json")
|
FILE_HASSIO_DNS = Path(SUPERVISOR_DATA, "dns.json")
|
||||||
FILE_HASSIO_AUDIO = Path(SUPERVISOR_DATA, "audio.json")
|
FILE_HASSIO_AUDIO = Path(SUPERVISOR_DATA, "audio.json")
|
||||||
|
FILE_HASSIO_CLI = Path(SUPERVISOR_DATA, "cli.json")
|
||||||
|
|
||||||
SOCKET_DOCKER = Path("/run/docker.sock")
|
SOCKET_DOCKER = Path("/run/docker.sock")
|
||||||
|
|
||||||
@ -34,7 +35,6 @@ 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://1.1.1.1", "dns://9.9.9.9"]
|
|
||||||
DNS_SUFFIX = "local.hass.io"
|
DNS_SUFFIX = "local.hass.io"
|
||||||
|
|
||||||
LABEL_VERSION = "io.hass.version"
|
LABEL_VERSION = "io.hass.version"
|
||||||
@ -190,9 +190,6 @@ ATTR_DEVICETREE = "devicetree"
|
|||||||
ATTR_CPE = "cpe"
|
ATTR_CPE = "cpe"
|
||||||
ATTR_BOARD = "board"
|
ATTR_BOARD = "board"
|
||||||
ATTR_HASSOS = "hassos"
|
ATTR_HASSOS = "hassos"
|
||||||
ATTR_HASSOS_CLI = "hassos_cli"
|
|
||||||
ATTR_VERSION_CLI = "version_cli"
|
|
||||||
ATTR_VERSION_CLI_LATEST = "version_cli_latest"
|
|
||||||
ATTR_REFRESH_TOKEN = "refresh_token"
|
ATTR_REFRESH_TOKEN = "refresh_token"
|
||||||
ATTR_ACCESS_TOKEN = "access_token"
|
ATTR_ACCESS_TOKEN = "access_token"
|
||||||
ATTR_DOCKER_API = "docker_api"
|
ATTR_DOCKER_API = "docker_api"
|
||||||
|
@ -41,7 +41,9 @@ class Core(CoreSysAttributes):
|
|||||||
await self.sys_host.load()
|
await self.sys_host.load()
|
||||||
|
|
||||||
# Load Plugins container
|
# Load Plugins container
|
||||||
await asyncio.wait([self.sys_dns.load(), self.sys_audio.load()])
|
await asyncio.wait(
|
||||||
|
[self.sys_dns.load(), self.sys_audio.load(), self.sys_cli.load()]
|
||||||
|
)
|
||||||
|
|
||||||
# Load Home Assistant
|
# Load Home Assistant
|
||||||
await self.sys_homeassistant.load()
|
await self.sys_homeassistant.load()
|
||||||
@ -203,13 +205,11 @@ class Core(CoreSysAttributes):
|
|||||||
|
|
||||||
# Restore core functionality
|
# Restore core functionality
|
||||||
await self.sys_dns.repair()
|
await self.sys_dns.repair()
|
||||||
|
await self.sys_audio.repair()
|
||||||
|
await self.sys_cli.repair()
|
||||||
await self.sys_addons.repair()
|
await self.sys_addons.repair()
|
||||||
await self.sys_homeassistant.repair()
|
await self.sys_homeassistant.repair()
|
||||||
|
|
||||||
# Fix HassOS specific
|
|
||||||
if self.sys_hassos.available:
|
|
||||||
await self.sys_hassos.repair_cli()
|
|
||||||
|
|
||||||
# Tag version for latest
|
# Tag version for latest
|
||||||
await self.sys_supervisor.repair()
|
await self.sys_supervisor.repair()
|
||||||
_LOGGER.info("Finished repairing of Supervisor Environment")
|
_LOGGER.info("Finished repairing of Supervisor Environment")
|
||||||
|
@ -18,6 +18,7 @@ if TYPE_CHECKING:
|
|||||||
from .audio import Audio
|
from .audio import Audio
|
||||||
from .auth import Auth
|
from .auth import Auth
|
||||||
from .core import Core
|
from .core import Core
|
||||||
|
from .cli import HaCli
|
||||||
from .dbus import DBusManager
|
from .dbus import DBusManager
|
||||||
from .discovery import Discovery
|
from .discovery import Discovery
|
||||||
from .dns import CoreDNS
|
from .dns import CoreDNS
|
||||||
@ -62,6 +63,7 @@ class CoreSys:
|
|||||||
self._audio: Optional[Audio] = None
|
self._audio: Optional[Audio] = None
|
||||||
self._auth: Optional[Auth] = None
|
self._auth: Optional[Auth] = None
|
||||||
self._dns: Optional[CoreDNS] = None
|
self._dns: Optional[CoreDNS] = None
|
||||||
|
self._cli: Optional[HaCli] = None
|
||||||
self._homeassistant: Optional[HomeAssistant] = None
|
self._homeassistant: Optional[HomeAssistant] = None
|
||||||
self._supervisor: Optional[Supervisor] = None
|
self._supervisor: Optional[Supervisor] = None
|
||||||
self._addons: Optional[AddonManager] = None
|
self._addons: Optional[AddonManager] = None
|
||||||
@ -143,6 +145,18 @@ class CoreSys:
|
|||||||
raise RuntimeError("Core already set!")
|
raise RuntimeError("Core already set!")
|
||||||
self._core = value
|
self._core = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cli(self) -> HaCli:
|
||||||
|
"""Return HaCli object."""
|
||||||
|
return self._cli
|
||||||
|
|
||||||
|
@cli.setter
|
||||||
|
def cli(self, value: HaCli):
|
||||||
|
"""Set a HaCli object."""
|
||||||
|
if self._cli:
|
||||||
|
raise RuntimeError("HaCli already set!")
|
||||||
|
self._cli = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def arch(self) -> CpuArch:
|
def arch(self) -> CpuArch:
|
||||||
"""Return CpuArch object."""
|
"""Return CpuArch object."""
|
||||||
@ -449,6 +463,11 @@ class CoreSysAttributes:
|
|||||||
"""Return core object."""
|
"""Return core object."""
|
||||||
return self.coresys.core
|
return self.coresys.core
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_cli(self) -> HaCli:
|
||||||
|
"""Return HaCli object."""
|
||||||
|
return self.coresys.cli
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sys_arch(self) -> CpuArch:
|
def sys_arch(self) -> CpuArch:
|
||||||
"""Return CpuArch object."""
|
"""Return CpuArch object."""
|
||||||
|
@ -1,15 +1,31 @@
|
|||||||
.:53 {
|
.:53 {
|
||||||
log
|
log
|
||||||
errors
|
errors
|
||||||
|
loop
|
||||||
hosts /config/hosts {
|
hosts /config/hosts {
|
||||||
fallthrough
|
fallthrough
|
||||||
}
|
}
|
||||||
template ANY AAAA local.hass.io hassio {
|
template ANY AAAA local.hass.io hassio {
|
||||||
rcode NOERROR
|
rcode NOERROR
|
||||||
}
|
}
|
||||||
forward . $servers {
|
forward . {{ locals | join(" ") }} dns://127.0.0.1:5353 {
|
||||||
except local.hass.io
|
except local.hass.io
|
||||||
policy sequential
|
policy sequential
|
||||||
|
health_check 5s
|
||||||
|
}
|
||||||
|
fallback REFUSED . dns://127.0.0.1:5353
|
||||||
|
fallback SERVFAIL . dns://127.0.0.1:5353
|
||||||
|
fallback NXDOMAIN . dns://127.0.0.1:5353
|
||||||
|
cache 10
|
||||||
|
}
|
||||||
|
|
||||||
|
.:5353 {
|
||||||
|
log
|
||||||
|
errors
|
||||||
|
forward . tls://1.1.1.1 tls://1.0.0.1 {
|
||||||
|
tls_servername cloudflare-dns.com
|
||||||
|
except local.hass.io
|
||||||
health_check 10s
|
health_check 10s
|
||||||
}
|
}
|
||||||
|
cache 30
|
||||||
}
|
}
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
"""Discovery service for Home Panel."""
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from supervisor.validate import network_port
|
|
||||||
|
|
||||||
from ..const import ATTR_HOST, ATTR_PORT
|
|
||||||
|
|
||||||
|
|
||||||
SCHEMA = vol.Schema(
|
|
||||||
{vol.Required(ATTR_HOST): vol.Coerce(str), vol.Required(ATTR_PORT): network_port}
|
|
||||||
)
|
|
@ -4,13 +4,13 @@ from contextlib import suppress
|
|||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from string import Template
|
|
||||||
from typing import Awaitable, List, Optional
|
from typing import Awaitable, List, Optional
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
import jinja2
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from .const import ATTR_SERVERS, ATTR_VERSION, DNS_SERVERS, DNS_SUFFIX, FILE_HASSIO_DNS
|
from .const import ATTR_SERVERS, ATTR_VERSION, DNS_SUFFIX, FILE_HASSIO_DNS
|
||||||
from .coresys import CoreSys, CoreSysAttributes
|
from .coresys import CoreSys, CoreSysAttributes
|
||||||
from .docker.dns import DockerDNS
|
from .docker.dns import DockerDNS
|
||||||
from .docker.stats import DockerStats
|
from .docker.stats import DockerStats
|
||||||
@ -42,8 +42,10 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
|
|||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self.instance: DockerDNS = DockerDNS(coresys)
|
self.instance: DockerDNS = DockerDNS(coresys)
|
||||||
self.forwarder: DNSForward = DNSForward()
|
self.forwarder: DNSForward = DNSForward()
|
||||||
|
self.coredns_template: Optional[jinja2.Template] = None
|
||||||
|
|
||||||
self._hosts: List[HostEntry] = []
|
self._hosts: List[HostEntry] = []
|
||||||
|
self._loop: bool = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def corefile(self) -> Path:
|
def corefile(self) -> Path:
|
||||||
@ -116,6 +118,12 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
|
|||||||
# Start DNS forwarder
|
# Start DNS forwarder
|
||||||
self.sys_create_task(self.forwarder.start(self.sys_docker.network.dns))
|
self.sys_create_task(self.forwarder.start(self.sys_docker.network.dns))
|
||||||
|
|
||||||
|
# Initialize CoreDNS Template
|
||||||
|
try:
|
||||||
|
self.coredns_template = jinja2.Template(COREDNS_TMPL.read_text())
|
||||||
|
except OSError as err:
|
||||||
|
_LOGGER.error("Can't read coredns.tmpl: %s", err)
|
||||||
|
|
||||||
# Run CoreDNS
|
# Run CoreDNS
|
||||||
with suppress(CoreDNSError):
|
with suppress(CoreDNSError):
|
||||||
if await self.instance.is_running():
|
if await self.instance.is_running():
|
||||||
@ -202,30 +210,43 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
|
|||||||
self.hosts.unlink()
|
self.hosts.unlink()
|
||||||
self._init_hosts()
|
self._init_hosts()
|
||||||
|
|
||||||
|
# Reset loop protection
|
||||||
|
self._loop = False
|
||||||
|
|
||||||
await self.sys_addons.sync_dns()
|
await self.sys_addons.sync_dns()
|
||||||
|
|
||||||
|
async def loop_detection(self) -> None:
|
||||||
|
"""Check if there was a loop found."""
|
||||||
|
log = await self.instance.logs()
|
||||||
|
|
||||||
|
# Check the log for loop plugin output
|
||||||
|
if b"plugin/loop: Loop" in log:
|
||||||
|
_LOGGER.error("Detect a DNS loop in local Network!")
|
||||||
|
self._loop = True
|
||||||
|
else:
|
||||||
|
self._loop = False
|
||||||
|
|
||||||
def _write_corefile(self) -> None:
|
def _write_corefile(self) -> None:
|
||||||
"""Write CoreDNS config."""
|
"""Write CoreDNS config."""
|
||||||
dns_servers: List[str] = []
|
dns_servers: List[str] = []
|
||||||
|
local_dns: List[str] = []
|
||||||
# Load Template
|
servers: List[str] = []
|
||||||
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
|
|
||||||
|
|
||||||
# Prepare DNS serverlist: Prio 1 Manual, Prio 2 Local, Prio 3 Fallback
|
# Prepare DNS serverlist: Prio 1 Manual, Prio 2 Local, Prio 3 Fallback
|
||||||
local_dns: List[str] = self.sys_host.network.dns_servers or ["dns://127.0.0.11"]
|
if not self._loop:
|
||||||
servers: List[str] = self.servers + local_dns + DNS_SERVERS
|
local_dns = self.sys_host.network.dns_servers or ["dns://127.0.0.11"]
|
||||||
|
servers = self.servers + local_dns
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("Ignore user DNS settings because of loop")
|
||||||
|
|
||||||
|
# Print some usefully debug data
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"config-dns = %s, local-dns = %s , backup-dns = %s",
|
"config-dns = %s, local-dns = %s , backup-dns = CloudFlare DoT",
|
||||||
self.servers,
|
self.servers,
|
||||||
local_dns,
|
local_dns,
|
||||||
DNS_SERVERS,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Make sure, they are valid
|
||||||
for server in servers:
|
for server in servers:
|
||||||
try:
|
try:
|
||||||
dns_url(server)
|
dns_url(server)
|
||||||
@ -235,7 +256,7 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
|
|||||||
_LOGGER.warning("Ignore invalid DNS Server: %s", server)
|
_LOGGER.warning("Ignore invalid DNS Server: %s", server)
|
||||||
|
|
||||||
# Generate config file
|
# Generate config file
|
||||||
data = corefile_template.safe_substitute(servers=" ".join(dns_servers))
|
data = self.coredns_template.render(locals=dns_servers)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.corefile.write_text(data)
|
self.corefile.write_text(data)
|
||||||
@ -339,7 +360,6 @@ class CoreDNS(JsonConfig, CoreSysAttributes):
|
|||||||
|
|
||||||
def is_fails(self) -> Awaitable[bool]:
|
def is_fails(self) -> Awaitable[bool]:
|
||||||
"""Return True if a Docker container is fails state.
|
"""Return True if a Docker container is fails state.
|
||||||
|
|
||||||
Return a coroutine.
|
Return a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.instance.is_fails()
|
return self.instance.is_fails()
|
||||||
|
@ -117,8 +117,8 @@ class DockerAddon(DockerInterface):
|
|||||||
return {
|
return {
|
||||||
**addon_env,
|
**addon_env,
|
||||||
ENV_TIME: self.sys_timezone,
|
ENV_TIME: self.sys_timezone,
|
||||||
ENV_TOKEN: self.addon.hassio_token,
|
ENV_TOKEN: self.addon.supervisor_token,
|
||||||
ENV_TOKEN_OLD: self.addon.hassio_token,
|
ENV_TOKEN_OLD: self.addon.supervisor_token,
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
64
supervisor/docker/cli.py
Normal file
64
supervisor/docker/cli.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
"""HA Cli docker object."""
|
||||||
|
from contextlib import suppress
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import DockerAPIError
|
||||||
|
from .interface import DockerInterface
|
||||||
|
from ..const import ENV_TIME, ENV_TOKEN
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CLI_DOCKER_NAME: str = "hassio_cli"
|
||||||
|
|
||||||
|
|
||||||
|
class DockerCli(DockerInterface, CoreSysAttributes):
|
||||||
|
"""Docker Supervisor wrapper for HA cli."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def image(self):
|
||||||
|
"""Return name of HA cli image."""
|
||||||
|
return f"homeassistant/{self.sys_arch.supervisor}-hassio-cli"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return name of Docker container."""
|
||||||
|
return CLI_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,
|
||||||
|
entrypoint=["/init"],
|
||||||
|
command=["/bin/bash", "-c", "sleep infinity"],
|
||||||
|
version=self.sys_cli.version,
|
||||||
|
init=False,
|
||||||
|
ipv4=self.sys_docker.network.cli,
|
||||||
|
name=self.name,
|
||||||
|
hostname=self.name.replace("_", "-"),
|
||||||
|
detach=True,
|
||||||
|
extra_hosts={"supervisor": self.sys_docker.network.supervisor},
|
||||||
|
environment={
|
||||||
|
ENV_TIME: self.sys_timezone,
|
||||||
|
ENV_TOKEN: self.sys_cli.supervisor_token,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self._meta = docker_container.attrs
|
||||||
|
_LOGGER.info(
|
||||||
|
"Start CLI %s with version %s - %s",
|
||||||
|
self.image,
|
||||||
|
self.version,
|
||||||
|
self.sys_docker.network.audio,
|
||||||
|
)
|
@ -1,38 +0,0 @@
|
|||||||
"""HassOS Cli docker object."""
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import docker
|
|
||||||
|
|
||||||
from ..coresys import CoreSysAttributes
|
|
||||||
from .interface import DockerInterface
|
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DockerHassOSCli(DockerInterface, CoreSysAttributes):
|
|
||||||
"""Docker Supervisor wrapper for HassOS Cli."""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def image(self):
|
|
||||||
"""Return name of HassOS CLI image."""
|
|
||||||
return f"homeassistant/{self.sys_arch.supervisor}-hassio-cli"
|
|
||||||
|
|
||||||
def _stop(self, remove_container=True):
|
|
||||||
"""Don't need stop."""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _attach(self, tag: str):
|
|
||||||
"""Attach to running Docker container.
|
|
||||||
Need run inside executor.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
image = self.sys_docker.images.get(f"{self.image}:{tag}")
|
|
||||||
|
|
||||||
except docker.errors.DockerException:
|
|
||||||
_LOGGER.warning("Can't find a HassOS CLI %s", self.image)
|
|
||||||
|
|
||||||
else:
|
|
||||||
self._meta = image.attrs
|
|
||||||
_LOGGER.info(
|
|
||||||
"Found HassOS CLI %s with version %s", self.image, self.version
|
|
||||||
)
|
|
@ -112,8 +112,8 @@ class DockerHomeAssistant(DockerInterface):
|
|||||||
"HASSIO": self.sys_docker.network.supervisor,
|
"HASSIO": self.sys_docker.network.supervisor,
|
||||||
"SUPERVISOR": self.sys_docker.network.supervisor,
|
"SUPERVISOR": self.sys_docker.network.supervisor,
|
||||||
ENV_TIME: self.sys_timezone,
|
ENV_TIME: self.sys_timezone,
|
||||||
ENV_TOKEN: self.sys_homeassistant.hassio_token,
|
ENV_TOKEN: self.sys_homeassistant.supervisor_token,
|
||||||
ENV_TOKEN_OLD: self.sys_homeassistant.hassio_token,
|
ENV_TOKEN_OLD: self.sys_homeassistant.supervisor_token,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -53,6 +53,11 @@ class DockerNetwork:
|
|||||||
"""Return audio of the network."""
|
"""Return audio of the network."""
|
||||||
return DOCKER_NETWORK_MASK[4]
|
return DOCKER_NETWORK_MASK[4]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cli(self) -> IPv4Address:
|
||||||
|
"""Return cli of the network."""
|
||||||
|
return DOCKER_NETWORK_MASK[5]
|
||||||
|
|
||||||
def _get_network(self) -> docker.models.networks.Network:
|
def _get_network(self) -> docker.models.networks.Network:
|
||||||
"""Get supervisor network."""
|
"""Get supervisor network."""
|
||||||
try:
|
try:
|
||||||
|
@ -54,6 +54,17 @@ class HassOSNotSupportedError(HassioNotSupportedError):
|
|||||||
"""Function not supported by HassOS."""
|
"""Function not supported by HassOS."""
|
||||||
|
|
||||||
|
|
||||||
|
# HaCli
|
||||||
|
|
||||||
|
|
||||||
|
class CliError(HassioError):
|
||||||
|
"""HA cli exception."""
|
||||||
|
|
||||||
|
|
||||||
|
class CliUpdateError(HassOSError):
|
||||||
|
"""Error on update of a HA cli."""
|
||||||
|
|
||||||
|
|
||||||
# DNS
|
# DNS
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
"""HassOS support on supervisor."""
|
"""HassOS support on supervisor."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from contextlib import suppress
|
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Awaitable, Optional
|
from typing import Awaitable, Optional
|
||||||
@ -10,12 +9,10 @@ from cpe import CPE
|
|||||||
|
|
||||||
from .const import URL_HASSOS_OTA
|
from .const import URL_HASSOS_OTA
|
||||||
from .coresys import CoreSysAttributes, CoreSys
|
from .coresys import CoreSysAttributes, CoreSys
|
||||||
from .docker.hassos_cli import DockerHassOSCli
|
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
DBusError,
|
DBusError,
|
||||||
HassOSNotSupportedError,
|
HassOSNotSupportedError,
|
||||||
HassOSUpdateError,
|
HassOSUpdateError,
|
||||||
DockerAPIError,
|
|
||||||
)
|
)
|
||||||
from .dbus.rauc import RaucState
|
from .dbus.rauc import RaucState
|
||||||
|
|
||||||
@ -28,7 +25,6 @@ class HassOS(CoreSysAttributes):
|
|||||||
def __init__(self, coresys: CoreSys):
|
def __init__(self, coresys: CoreSys):
|
||||||
"""Initialize HassOS handler."""
|
"""Initialize HassOS handler."""
|
||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self.instance: DockerHassOSCli = DockerHassOSCli(coresys)
|
|
||||||
self._available: bool = False
|
self._available: bool = False
|
||||||
self._version: Optional[str] = None
|
self._version: Optional[str] = None
|
||||||
self._board: Optional[str] = None
|
self._board: Optional[str] = None
|
||||||
@ -44,29 +40,14 @@ class HassOS(CoreSysAttributes):
|
|||||||
return self._version
|
return self._version
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version_cli(self) -> Optional[str]:
|
def latest_version(self) -> str:
|
||||||
"""Return version of HassOS cli."""
|
|
||||||
return self.instance.version
|
|
||||||
|
|
||||||
@property
|
|
||||||
def version_latest(self) -> str:
|
|
||||||
"""Return version of HassOS."""
|
"""Return version of HassOS."""
|
||||||
return self.sys_updater.version_hassos
|
return self.sys_updater.version_hassos
|
||||||
|
|
||||||
@property
|
|
||||||
def version_cli_latest(self) -> str:
|
|
||||||
"""Return version of HassOS."""
|
|
||||||
return self.sys_updater.version_cli
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def need_update(self) -> bool:
|
def need_update(self) -> bool:
|
||||||
"""Return true if a HassOS update is available."""
|
"""Return true if a HassOS update is available."""
|
||||||
return self.version != self.version_latest
|
return self.version != self.latest_version
|
||||||
|
|
||||||
@property
|
|
||||||
def need_cli_update(self) -> bool:
|
|
||||||
"""Return true if a HassOS cli update is available."""
|
|
||||||
return self.version_cli != self.version_cli_latest
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def board(self) -> Optional[str]:
|
def board(self) -> Optional[str]:
|
||||||
@ -134,8 +115,6 @@ class HassOS(CoreSysAttributes):
|
|||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Detect HassOS %s / BootSlot %s", self.version, self.sys_dbus.rauc.boot_slot
|
"Detect HassOS %s / BootSlot %s", self.version, self.sys_dbus.rauc.boot_slot
|
||||||
)
|
)
|
||||||
with suppress(DockerAPIError):
|
|
||||||
await self.instance.attach(tag="latest")
|
|
||||||
|
|
||||||
def config_sync(self) -> Awaitable[None]:
|
def config_sync(self) -> Awaitable[None]:
|
||||||
"""Trigger a host config reload from usb.
|
"""Trigger a host config reload from usb.
|
||||||
@ -149,7 +128,7 @@ class HassOS(CoreSysAttributes):
|
|||||||
|
|
||||||
async def update(self, version: Optional[str] = None) -> None:
|
async def update(self, version: Optional[str] = None) -> None:
|
||||||
"""Update HassOS system."""
|
"""Update HassOS system."""
|
||||||
version = version or self.version_latest
|
version = version or self.latest_version
|
||||||
|
|
||||||
# Check installed version
|
# Check installed version
|
||||||
self._check_host()
|
self._check_host()
|
||||||
@ -183,35 +162,6 @@ class HassOS(CoreSysAttributes):
|
|||||||
_LOGGER.error("HassOS update fails with: %s", self.sys_dbus.rauc.last_error)
|
_LOGGER.error("HassOS update fails with: %s", self.sys_dbus.rauc.last_error)
|
||||||
raise HassOSUpdateError()
|
raise HassOSUpdateError()
|
||||||
|
|
||||||
async def update_cli(self, version: Optional[str] = None) -> None:
|
|
||||||
"""Update local HassOS cli."""
|
|
||||||
version = version or self.version_cli_latest
|
|
||||||
|
|
||||||
if version == self.version_cli:
|
|
||||||
_LOGGER.warning("Version %s is already installed for CLI", version)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self.instance.update(version, latest=True)
|
|
||||||
except DockerAPIError:
|
|
||||||
_LOGGER.error("HassOS CLI update fails")
|
|
||||||
raise HassOSUpdateError() from None
|
|
||||||
else:
|
|
||||||
# Cleanup
|
|
||||||
with suppress(DockerAPIError):
|
|
||||||
await self.instance.cleanup()
|
|
||||||
|
|
||||||
async def repair_cli(self) -> None:
|
|
||||||
"""Repair CLI container."""
|
|
||||||
if await self.instance.exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
_LOGGER.info("Repair HassOS CLI %s", self.version_cli)
|
|
||||||
try:
|
|
||||||
await self.instance.install(self.version_cli, latest=True)
|
|
||||||
except DockerAPIError:
|
|
||||||
_LOGGER.error("Repairing of HassOS CLI fails")
|
|
||||||
|
|
||||||
async def mark_healthy(self):
|
async def mark_healthy(self):
|
||||||
"""Set booted partition as good for rauc."""
|
"""Set booted partition as good for rauc."""
|
||||||
try:
|
try:
|
||||||
|
@ -221,7 +221,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
return self._data[ATTR_UUID]
|
return self._data[ATTR_UUID]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hassio_token(self) -> str:
|
def supervisor_token(self) -> str:
|
||||||
"""Return an access token for the Supervisor API."""
|
"""Return an access token for the Supervisor API."""
|
||||||
return self._data.get(ATTR_ACCESS_TOKEN)
|
return self._data.get(ATTR_ACCESS_TOKEN)
|
||||||
|
|
||||||
@ -465,13 +465,16 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
"python3 -m homeassistant -c /config --script check_config"
|
"python3 -m homeassistant -c /config --script check_config"
|
||||||
)
|
)
|
||||||
|
|
||||||
# if not valid
|
# If not valid
|
||||||
if result.exit_code is None:
|
if result.exit_code is None:
|
||||||
_LOGGER.error("Fatal error on config check!")
|
_LOGGER.error("Fatal error on config check!")
|
||||||
raise HomeAssistantError()
|
raise HomeAssistantError()
|
||||||
|
|
||||||
# parse output
|
# Convert output
|
||||||
log = convert_to_ascii(result.output)
|
log = convert_to_ascii(result.output)
|
||||||
|
_LOGGER.debug("Result config check: %s", log.strip())
|
||||||
|
|
||||||
|
# Parse output
|
||||||
if result.exit_code != 0 or RE_YAML_ERROR.search(log):
|
if result.exit_code != 0 or RE_YAML_ERROR.search(log):
|
||||||
_LOGGER.error("Invalid Home Assistant config found!")
|
_LOGGER.error("Invalid Home Assistant config found!")
|
||||||
return ConfigResult(False, log)
|
return ConfigResult(False, log)
|
||||||
|
@ -28,7 +28,7 @@ class HwMonitor(CoreSysAttributes):
|
|||||||
self.monitor = pyudev.Monitor.from_netlink(self.context)
|
self.monitor = pyudev.Monitor.from_netlink(self.context)
|
||||||
self.observer = pyudev.MonitorObserver(self.monitor, self._udev_events)
|
self.observer = pyudev.MonitorObserver(self.monitor, self._udev_events)
|
||||||
except OSError:
|
except OSError:
|
||||||
_LOGGER.fatal("Not privileged to run udev. Update your installation!")
|
_LOGGER.fatal("Not privileged to run udev monitor!")
|
||||||
else:
|
else:
|
||||||
self.observer.start()
|
self.observer.start()
|
||||||
_LOGGER.info("Started Supervisor hardware monitor")
|
_LOGGER.info("Started Supervisor hardware monitor")
|
||||||
|
@ -25,7 +25,8 @@ RUN_WATCHDOG_HOMEASSISTANT_DOCKER = 15
|
|||||||
RUN_WATCHDOG_HOMEASSISTANT_API = 300
|
RUN_WATCHDOG_HOMEASSISTANT_API = 300
|
||||||
|
|
||||||
RUN_WATCHDOG_DNS_DOCKER = 20
|
RUN_WATCHDOG_DNS_DOCKER = 20
|
||||||
RUN_WATCHDOG_AUDIO_DOCKER = 20
|
RUN_WATCHDOG_AUDIO_DOCKER = 30
|
||||||
|
RUN_WATCHDOG_CLI_DOCKER = 40
|
||||||
|
|
||||||
|
|
||||||
class Tasks(CoreSysAttributes):
|
class Tasks(CoreSysAttributes):
|
||||||
@ -102,6 +103,11 @@ class Tasks(CoreSysAttributes):
|
|||||||
self._watchdog_audio_docker, RUN_WATCHDOG_AUDIO_DOCKER
|
self._watchdog_audio_docker, RUN_WATCHDOG_AUDIO_DOCKER
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.jobs.add(
|
||||||
|
self.sys_scheduler.register_task(
|
||||||
|
self._watchdog_cli_docker, RUN_WATCHDOG_CLI_DOCKER
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER.info("All core tasks are scheduled")
|
_LOGGER.info("All core tasks are scheduled")
|
||||||
|
|
||||||
@ -202,12 +208,12 @@ class Tasks(CoreSysAttributes):
|
|||||||
self._cache[HASS_WATCHDOG_API] = 0
|
self._cache[HASS_WATCHDOG_API] = 0
|
||||||
|
|
||||||
async def _update_cli(self):
|
async def _update_cli(self):
|
||||||
"""Check and run update of CLI."""
|
"""Check and run update of cli."""
|
||||||
if not self.sys_hassos.need_cli_update:
|
if not self.sys_cli.need_update:
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.info("Found new CLI version")
|
_LOGGER.info("Found new cli version")
|
||||||
await self.sys_hassos.update_cli()
|
await self.sys_cli.update()
|
||||||
|
|
||||||
async def _update_dns(self):
|
async def _update_dns(self):
|
||||||
"""Check and run update of CoreDNS plugin."""
|
"""Check and run update of CoreDNS plugin."""
|
||||||
@ -232,9 +238,11 @@ class Tasks(CoreSysAttributes):
|
|||||||
return
|
return
|
||||||
_LOGGER.warning("Watchdog found a problem with CoreDNS plugin!")
|
_LOGGER.warning("Watchdog found a problem with CoreDNS plugin!")
|
||||||
|
|
||||||
|
# Reset of fails
|
||||||
if await self.sys_dns.is_fails():
|
if await self.sys_dns.is_fails():
|
||||||
_LOGGER.warning("CoreDNS plugin is in fails state / Reset config")
|
_LOGGER.error("CoreDNS plugin is in fails state / Reset config")
|
||||||
await self.sys_dns.reset()
|
await self.sys_dns.reset()
|
||||||
|
await self.sys_dns.loop_detection()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.sys_dns.start()
|
await self.sys_dns.start()
|
||||||
@ -252,3 +260,15 @@ class Tasks(CoreSysAttributes):
|
|||||||
await self.sys_audio.start()
|
await self.sys_audio.start()
|
||||||
except CoreDNSError:
|
except CoreDNSError:
|
||||||
_LOGGER.error("Watchdog PulseAudio reanimation fails!")
|
_LOGGER.error("Watchdog PulseAudio reanimation fails!")
|
||||||
|
|
||||||
|
async def _watchdog_cli_docker(self):
|
||||||
|
"""Check running state of Docker and start if they is close."""
|
||||||
|
# if cli plugin is active
|
||||||
|
if await self.sys_cli.is_running() or self.sys_cli.in_progress:
|
||||||
|
return
|
||||||
|
_LOGGER.warning("Watchdog found a problem with cli plugin!")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.sys_cli.start()
|
||||||
|
except CoreDNSError:
|
||||||
|
_LOGGER.error("Watchdog cli reanimation fails!")
|
||||||
|
@ -208,7 +208,7 @@ class DBus:
|
|||||||
raise exception()
|
raise exception()
|
||||||
|
|
||||||
# General
|
# General
|
||||||
_LOGGER.error("DBus return error: %s", error)
|
_LOGGER.error("DBus return error: %s", error.strip())
|
||||||
raise DBusFatalError()
|
raise DBusFatalError()
|
||||||
|
|
||||||
def attach_signals(self, filters=None):
|
def attach_signals(self, filters=None):
|
||||||
|
@ -182,3 +182,12 @@ SCHEMA_DNS_CONFIG = vol.Schema(
|
|||||||
SCHEMA_AUDIO_CONFIG = vol.Schema(
|
SCHEMA_AUDIO_CONFIG = vol.Schema(
|
||||||
{vol.Optional(ATTR_VERSION): vol.Maybe(vol.Coerce(str))}, extra=vol.REMOVE_EXTRA,
|
{vol.Optional(ATTR_VERSION): vol.Maybe(vol.Coerce(str))}, extra=vol.REMOVE_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SCHEMA_CLI_CONFIG = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_VERSION): vol.Maybe(vol.Coerce(str)),
|
||||||
|
vol.Optional(ATTR_ACCESS_TOKEN): token,
|
||||||
|
},
|
||||||
|
extra=vol.REMOVE_EXTRA,
|
||||||
|
)
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
"""Test adguard discovery."""
|
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from supervisor.discovery.validate import valid_discovery_config
|
|
||||||
|
|
||||||
|
|
||||||
def test_good_config():
|
|
||||||
"""Test good deconz config."""
|
|
||||||
|
|
||||||
valid_discovery_config("home_panel", {"host": "test", "port": 3812})
|
|
||||||
|
|
||||||
|
|
||||||
def test_bad_config():
|
|
||||||
"""Test good adguard config."""
|
|
||||||
|
|
||||||
with pytest.raises(vol.Invalid):
|
|
||||||
valid_discovery_config("home_panel", {"host": "test"})
|
|
Loading…
x
Reference in New Issue
Block a user