mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-08-21 06:59:20 +00:00
Compare commits
444 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
df9d62f874 | ||
![]() |
4a6aaa8559 | ||
![]() |
435f479984 | ||
![]() |
e2f39059c6 | ||
![]() |
531073d5ec | ||
![]() |
ef5b6a5f4c | ||
![]() |
03f0a136ab | ||
![]() |
7a6663ba80 | ||
![]() |
9dd5eee1ae | ||
![]() |
bb474a5c14 | ||
![]() |
6ab4dda5e8 | ||
![]() |
8a553dbb59 | ||
![]() |
1ee6c0491c | ||
![]() |
cc50a91a42 | ||
![]() |
637377f81d | ||
![]() |
a90f70e017 | ||
![]() |
949ecb255d | ||
![]() |
15f62837c8 | ||
![]() |
e5246a5b1d | ||
![]() |
394d66290d | ||
![]() |
79d541185f | ||
![]() |
b433d129ef | ||
![]() |
4b0278fee8 | ||
![]() |
8c59e6d05a | ||
![]() |
5c66278a1c | ||
![]() |
7abe9487a0 | ||
![]() |
73832dd6d6 | ||
![]() |
6cc3df54e9 | ||
![]() |
c07c7c5146 | ||
![]() |
a6d1078fe3 | ||
![]() |
eba6da485d | ||
![]() |
de880e24ed | ||
![]() |
f344df9e5c | ||
![]() |
5af62a8834 | ||
![]() |
800fb683f8 | ||
![]() |
ad2566d58a | ||
![]() |
6c679b07e1 | ||
![]() |
aa4f4c8d47 | ||
![]() |
b83da5d89f | ||
![]() |
0afff9a9e2 | ||
![]() |
0433d72ae6 | ||
![]() |
d33beb06cd | ||
![]() |
279d6ccd79 | ||
![]() |
af628293f3 | ||
![]() |
df6b815175 | ||
![]() |
d6127832a7 | ||
![]() |
8240623806 | ||
![]() |
2b4527fa64 | ||
![]() |
23143aede4 | ||
![]() |
8b93f0aee7 | ||
![]() |
5cc4a9a929 | ||
![]() |
288d2e5bdb | ||
![]() |
73d84113ea | ||
![]() |
4b15945ca1 | ||
![]() |
10720b2988 | ||
![]() |
bb991b69bb | ||
![]() |
7c9f6067c0 | ||
![]() |
e960a70217 | ||
![]() |
9b0a2e6da9 | ||
![]() |
cd0c151bd9 | ||
![]() |
b03c8c24dd | ||
![]() |
4416b6524e | ||
![]() |
c9d3f65cc8 | ||
![]() |
0407122fbe | ||
![]() |
5e871d9399 | ||
![]() |
6df7a88666 | ||
![]() |
5933b66b1c | ||
![]() |
a85e816cd7 | ||
![]() |
96f6c07912 | ||
![]() |
40bcee38f3 | ||
![]() |
6d2a38c96e | ||
![]() |
dafc2cfec2 | ||
![]() |
04f36e92e1 | ||
![]() |
4f97013df4 | ||
![]() |
53eae96a98 | ||
![]() |
74530baeb7 | ||
![]() |
271e4f0cc4 | ||
![]() |
f4c7f2cae1 | ||
![]() |
24cdb4787a | ||
![]() |
57b1c21af4 | ||
![]() |
f0eddb6926 | ||
![]() |
7c74c1bd8c | ||
![]() |
2d4a85ae43 | ||
![]() |
d48c439737 | ||
![]() |
874c50d3e8 | ||
![]() |
4beaf571c2 | ||
![]() |
58a948447e | ||
![]() |
32af7ef28b | ||
![]() |
208fb549b7 | ||
![]() |
ab704c11cf | ||
![]() |
966b962ccf | ||
![]() |
4ea3695982 | ||
![]() |
b2abe37d72 | ||
![]() |
9bf8d15b01 | ||
![]() |
70acbffc23 | ||
![]() |
c9b1eb751e | ||
![]() |
ad8d850ed7 | ||
![]() |
1b0eb9397d | ||
![]() |
8572f8c4e5 | ||
![]() |
66565dde87 | ||
![]() |
d54c23952f | ||
![]() |
62b364ea29 | ||
![]() |
e6f00144f2 | ||
![]() |
8894984c12 | ||
![]() |
49fbdedf6b | ||
![]() |
0899c16895 | ||
![]() |
0747a7e4b2 | ||
![]() |
0123d7935d | ||
![]() |
7a1009446b | ||
![]() |
034606cd0f | ||
![]() |
ddc30cfd7d | ||
![]() |
f10fccaff8 | ||
![]() |
31001280c8 | ||
![]() |
9638775944 | ||
![]() |
fbec0befde | ||
![]() |
81e7fac848 | ||
![]() |
97599b3e70 | ||
![]() |
c94b23a3fd | ||
![]() |
9758980ae0 | ||
![]() |
6ab3fbaab3 | ||
![]() |
4933ff83df | ||
![]() |
71e12ecb2b | ||
![]() |
36687530e0 | ||
![]() |
e7b5864c03 | ||
![]() |
9497f85db9 | ||
![]() |
419f603571 | ||
![]() |
f4f1fc524d | ||
![]() |
6d9f44a900 | ||
![]() |
aeb9b26d44 | ||
![]() |
631f78f468 | ||
![]() |
13cedb308e | ||
![]() |
82c183e1a8 | ||
![]() |
25cf1e7394 | ||
![]() |
91509a4205 | ||
![]() |
d93ebd15a2 | ||
![]() |
85e7f817e6 | ||
![]() |
772cadb435 | ||
![]() |
853aeef583 | ||
![]() |
cd07bde307 | ||
![]() |
3057df3181 | ||
![]() |
fe785622ec | ||
![]() |
2b6829a786 | ||
![]() |
7c6c982414 | ||
![]() |
07eeb2eaf2 | ||
![]() |
223f5b7bb1 | ||
![]() |
8a9657c452 | ||
![]() |
564e9811d0 | ||
![]() |
b944b52b21 | ||
![]() |
24aecdddf3 | ||
![]() |
7bbfb60039 | ||
![]() |
ece40008c7 | ||
![]() |
0177b38ded | ||
![]() |
16f2f63081 | ||
![]() |
5f376c2a27 | ||
![]() |
90a6f109ee | ||
![]() |
e6fd0ef5dc | ||
![]() |
d46ab56901 | ||
![]() |
3b1ad5c0cd | ||
![]() |
de8a241e72 | ||
![]() |
a4a0b43d91 | ||
![]() |
adf355e54f | ||
![]() |
4f9e646b4c | ||
![]() |
ce57d384ca | ||
![]() |
d53d526673 | ||
![]() |
cd8fc16bcb | ||
![]() |
6b58970354 | ||
![]() |
b70ed9a60d | ||
![]() |
22c8ff1314 | ||
![]() |
ba2cf8078e | ||
![]() |
ef138b619b | ||
![]() |
9252af5ddb | ||
![]() |
bcef34012d | ||
![]() |
2f18c177ae | ||
![]() |
12487fb69d | ||
![]() |
e629bab8ee | ||
![]() |
e85d7c3d2e | ||
![]() |
64c59d0fe9 | ||
![]() |
522cbe3295 | ||
![]() |
fd185fc326 | ||
![]() |
6ddc135266 | ||
![]() |
f8d5279d9c | ||
![]() |
2acae9af57 | ||
![]() |
b425d21d05 | ||
![]() |
4c7ba20a58 | ||
![]() |
a4f325dd2e | ||
![]() |
a99bfa2926 | ||
![]() |
bb127a614b | ||
![]() |
6f2f005897 | ||
![]() |
e22a20c165 | ||
![]() |
20c2121e5f | ||
![]() |
8a5831d6b2 | ||
![]() |
fb81946240 | ||
![]() |
4bec86c58c | ||
![]() |
7034b79991 | ||
![]() |
7b9a09dc4b | ||
![]() |
0746c4dec5 | ||
![]() |
6dadb933bd | ||
![]() |
07197e6a50 | ||
![]() |
6c79fb8325 | ||
![]() |
7488750ee4 | ||
![]() |
c9574254aa | ||
![]() |
f466721ffa | ||
![]() |
3834cead07 | ||
![]() |
75975de201 | ||
![]() |
cb9f998ef1 | ||
![]() |
eb9ce8ea1f | ||
![]() |
a5ed68b641 | ||
![]() |
1ef46424ea | ||
![]() |
53c99547d0 | ||
![]() |
a34e7622d2 | ||
![]() |
b234c18664 | ||
![]() |
d8d594c728 | ||
![]() |
1cd35841e8 | ||
![]() |
d05b7edd87 | ||
![]() |
95ef7d4508 | ||
![]() |
9812e5be6a | ||
![]() |
183182943d | ||
![]() |
a0189d65de | ||
![]() |
b59f741162 | ||
![]() |
efc2e826a1 | ||
![]() |
a3ad23e262 | ||
![]() |
5e3bcbfaac | ||
![]() |
7f3e4558b9 | ||
![]() |
567a01c2ed | ||
![]() |
2236cf146e | ||
![]() |
8e2f33ba1e | ||
![]() |
8190883a71 | ||
![]() |
c01218a97a | ||
![]() |
2437817a41 | ||
![]() |
682ee4529e | ||
![]() |
cee520f0b5 | ||
![]() |
0d915a3efc | ||
![]() |
f3a562006a | ||
![]() |
d78091cc60 | ||
![]() |
f785c4e909 | ||
![]() |
cda66ba737 | ||
![]() |
ea68ffc5a4 | ||
![]() |
31b0b721c8 | ||
![]() |
b97e33f5d5 | ||
![]() |
29e55d3664 | ||
![]() |
9112f27dc0 | ||
![]() |
9e67df26b3 | ||
![]() |
37d1a577ef | ||
![]() |
1eebb31004 | ||
![]() |
885764ea1c | ||
![]() |
b3d184b5c7 | ||
![]() |
96d04ec17e | ||
![]() |
e0bb3ad609 | ||
![]() |
1a8842cb81 | ||
![]() |
092d526749 | ||
![]() |
9db95c188a | ||
![]() |
0e45fc7d66 | ||
![]() |
4d1ddbfa2b | ||
![]() |
caa1c6f1bd | ||
![]() |
10d686b415 | ||
![]() |
29fae90da5 | ||
![]() |
e27337da85 | ||
![]() |
8f22316869 | ||
![]() |
dd10d3e037 | ||
![]() |
4a53c62af8 | ||
![]() |
1ebbf2b693 | ||
![]() |
62d198111c | ||
![]() |
1fc0ab71aa | ||
![]() |
f4402a1633 | ||
![]() |
13a17bcb34 | ||
![]() |
e1b49d90c2 | ||
![]() |
85ab25ea16 | ||
![]() |
80131ddfa8 | ||
![]() |
e9c123459f | ||
![]() |
d3e4bb7219 | ||
![]() |
fd98d38125 | ||
![]() |
3237611034 | ||
![]() |
ce2bffda15 | ||
![]() |
977e7b7adc | ||
![]() |
5082078527 | ||
![]() |
3615091c93 | ||
![]() |
fb1eb44d82 | ||
![]() |
13910d44bf | ||
![]() |
cda1d15070 | ||
![]() |
d0a1de23a6 | ||
![]() |
44fd75220f | ||
![]() |
ed594d653f | ||
![]() |
40bb3a7581 | ||
![]() |
df7f0345e8 | ||
![]() |
f7ab76bb9a | ||
![]() |
45e24bfa65 | ||
![]() |
8cd149783c | ||
![]() |
8e8e6e48a9 | ||
![]() |
816e0d503a | ||
![]() |
c43acd50f4 | ||
![]() |
16ce4296a2 | ||
![]() |
65386b753f | ||
![]() |
2be1529cb8 | ||
![]() |
98f8e032e3 | ||
![]() |
900b785789 | ||
![]() |
9194088947 | ||
![]() |
58c40cbef6 | ||
![]() |
e6c57dfc80 | ||
![]() |
82f76f60bd | ||
![]() |
b9af4aec6b | ||
![]() |
f71ce27248 | ||
![]() |
5b2b1765bc | ||
![]() |
2a892544c2 | ||
![]() |
bedb37ca6b | ||
![]() |
a456cd645f | ||
![]() |
9c68094cf6 | ||
![]() |
379cef9e35 | ||
![]() |
cb3e2dab71 | ||
![]() |
3e89f83e0b | ||
![]() |
af0bdd890a | ||
![]() |
f93f5d0e71 | ||
![]() |
667672a20b | ||
![]() |
9e1f899274 | ||
![]() |
75e0741665 | ||
![]() |
392d0e929b | ||
![]() |
b342073ba9 | ||
![]() |
ff4e550ba3 | ||
![]() |
17aa544be5 | ||
![]() |
390676dbc4 | ||
![]() |
d423252bc7 | ||
![]() |
790e887b70 | ||
![]() |
47e377683e | ||
![]() |
b1232c0d8d | ||
![]() |
059233c111 | ||
![]() |
55382d000b | ||
![]() |
75ab6eec43 | ||
![]() |
e30171746b | ||
![]() |
73849b7468 | ||
![]() |
a52713611c | ||
![]() |
85a66c663c | ||
![]() |
e478e68b70 | ||
![]() |
16095c319a | ||
![]() |
f4a6100fba | ||
![]() |
82060dd242 | ||
![]() |
a58cfb797c | ||
![]() |
c8256a50f4 | ||
![]() |
3ae974e9e2 | ||
![]() |
ac5e74a375 | ||
![]() |
05e3d3b779 | ||
![]() |
681a1ecff5 | ||
![]() |
2b411b0bf9 | ||
![]() |
fee16847d3 | ||
![]() |
501a52a3c6 | ||
![]() |
2bb014fda5 | ||
![]() |
09203f67b2 | ||
![]() |
169c7ec004 | ||
![]() |
202e94615e | ||
![]() |
5fe2a815ad | ||
![]() |
a13a0b4770 | ||
![]() |
455bbc457b | ||
![]() |
d50fd3b580 | ||
![]() |
455e80b07c | ||
![]() |
291becbdf9 | ||
![]() |
33385b46a7 | ||
![]() |
df17668369 | ||
![]() |
43449c85bb | ||
![]() |
9e86eda05a | ||
![]() |
b288554d9c | ||
![]() |
bee55d08fb | ||
![]() |
7a542aeb38 | ||
![]() |
8d42513ba8 | ||
![]() |
89b7247aa2 | ||
![]() |
29132e7f4c | ||
![]() |
3fd9baf78e | ||
![]() |
f3aa3757ce | ||
![]() |
3760967f59 | ||
![]() |
f7ab8e0f7f | ||
![]() |
0e46ea12b2 | ||
![]() |
be226b2b01 | ||
![]() |
9e1239e192 | ||
![]() |
2eba3d85b0 | ||
![]() |
9b569268ab | ||
![]() |
31f5033dca | ||
![]() |
78d9c60be5 | ||
![]() |
baa86f09e5 | ||
![]() |
a4c4b39ba8 | ||
![]() |
752068bb56 | ||
![]() |
739cfbb273 | ||
![]() |
115af4cadf | ||
![]() |
ae3274e559 | ||
![]() |
c61f096dbd | ||
![]() |
ee7b5c42fd | ||
![]() |
85d527bfbc | ||
![]() |
dd561da819 | ||
![]() |
cb5932cb8b | ||
![]() |
8630adc54a | ||
![]() |
90d8832cd2 | ||
![]() |
3802b97bb6 | ||
![]() |
2de175e181 | ||
![]() |
6b7d437b00 | ||
![]() |
e2faf906de | ||
![]() |
bb44ce5cd2 | ||
![]() |
15544ae589 | ||
![]() |
e421284471 | ||
![]() |
785dc64787 | ||
![]() |
7e7e3a7876 | ||
![]() |
2b45c059e0 | ||
![]() |
14ec61f9bd | ||
![]() |
5cc72756f8 | ||
![]() |
44785ef3e2 | ||
![]() |
e60d858feb | ||
![]() |
b31ecfefcd | ||
![]() |
c342231052 | ||
![]() |
673666837e | ||
![]() |
c8f74d6c0d | ||
![]() |
7ed9de8014 | ||
![]() |
8650947f04 | ||
![]() |
a0ac8ced31 | ||
![]() |
2145bbea81 | ||
![]() |
480000ee7f | ||
![]() |
9ec2ad022e | ||
![]() |
43e40816dc | ||
![]() |
941ea3ee68 | ||
![]() |
a6e4b5159e | ||
![]() |
6f542d58d5 | ||
![]() |
b2b5fcee7d | ||
![]() |
59a82345a9 | ||
![]() |
b61a747876 | ||
![]() |
72e5d800d5 | ||
![]() |
c7aa6d4804 | ||
![]() |
b31063449d | ||
![]() |
477672459d | ||
![]() |
9c33897296 | ||
![]() |
100cfb57c5 | ||
![]() |
40b34071e7 | ||
![]() |
341833fd8f | ||
![]() |
f647fd6fea | ||
![]() |
53642f2389 | ||
![]() |
b9bdd655ab | ||
![]() |
e9e1b5b54f | ||
![]() |
be2163d635 | ||
![]() |
7f6dde3a5f | ||
![]() |
334aafee23 | ||
![]() |
1a20c18b19 | ||
![]() |
6e655b165c | ||
![]() |
d768b2fa1e | ||
![]() |
85bce1cfba | ||
![]() |
a798a2466f | ||
![]() |
2a5d8a5c82 | ||
![]() |
ea62171d98 | ||
![]() |
196389d5ee | ||
![]() |
1776021620 | ||
![]() |
c42a9124d3 | ||
![]() |
a44647b4cd |
@@ -1,54 +0,0 @@
|
|||||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.8
|
|
||||||
|
|
||||||
WORKDIR /workspaces
|
|
||||||
|
|
||||||
# Set Docker daemon config
|
|
||||||
RUN \
|
|
||||||
mkdir -p /etc/docker \
|
|
||||||
&& echo '{"storage-driver": "vfs"}' > /etc/docker/daemon.json
|
|
||||||
|
|
||||||
# Install Node/Yarn for Frontent
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
curl \
|
|
||||||
git \
|
|
||||||
apt-utils \
|
|
||||||
apt-transport-https \
|
|
||||||
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
|
||||||
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
|
|
||||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
nodejs \
|
|
||||||
yarn \
|
|
||||||
&& curl -o - https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
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 tools
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
jq \
|
|
||||||
dbus \
|
|
||||||
network-manager \
|
|
||||||
libpulse0 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install Python dependencies from requirements.txt if it exists
|
|
||||||
COPY requirements.txt requirements_tests.txt ./
|
|
||||||
RUN pip3 install -U setuptools pip \
|
|
||||||
&& pip3 install -r requirements.txt -r requirements_tests.txt \
|
|
||||||
&& pip3 install tox \
|
|
||||||
&& rm -f requirements.txt requirements_tests.txt
|
|
@@ -1,11 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "Supervisor dev",
|
"name": "Supervisor dev",
|
||||||
"context": "..",
|
"image": "ghcr.io/home-assistant/devcontainer:supervisor",
|
||||||
"dockerFile": "Dockerfile",
|
"appPort": ["9123:8123", "7357:4357"],
|
||||||
"appPort": "9123:8123",
|
"postCreateCommand": "bash devcontainer_bootstrap",
|
||||||
"postCreateCommand": "pre-commit install",
|
|
||||||
"runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
|
"runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
|
||||||
"containerEnv": {"NVM_DIR":"/usr/local/share/nvm"},
|
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"ms-python.python",
|
"ms-python.python",
|
||||||
"ms-python.vscode-pylance",
|
"ms-python.vscode-pylance",
|
||||||
@@ -13,7 +11,12 @@
|
|||||||
"esbenp.prettier-vscode"
|
"esbenp.prettier-vscode"
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"terminal.integrated.shell.linux": "/bin/bash",
|
"terminal.integrated.profiles.linux": {
|
||||||
|
"zsh": {
|
||||||
|
"path": "/usr/bin/zsh"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"terminal.integrated.defaultProfile.linux": "zsh",
|
||||||
"editor.formatOnPaste": false,
|
"editor.formatOnPaste": false,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.formatOnType": true,
|
"editor.formatOnType": true,
|
||||||
@@ -22,7 +25,7 @@
|
|||||||
"python.linting.pylintEnabled": true,
|
"python.linting.pylintEnabled": true,
|
||||||
"python.linting.enabled": true,
|
"python.linting.enabled": true,
|
||||||
"python.formatting.provider": "black",
|
"python.formatting.provider": "black",
|
||||||
"python.formatting.blackArgs": ["--target-version", "py38"],
|
"python.formatting.blackArgs": ["--target-version", "py39"],
|
||||||
"python.formatting.blackPath": "/usr/local/bin/black",
|
"python.formatting.blackPath": "/usr/local/bin/black",
|
||||||
"python.linting.banditPath": "/usr/local/bin/bandit",
|
"python.linting.banditPath": "/usr/local/bin/bandit",
|
||||||
"python.linting.flake8Path": "/usr/local/bin/flake8",
|
"python.linting.flake8Path": "/usr/local/bin/flake8",
|
||||||
|
@@ -1,64 +1,78 @@
|
|||||||
name: Bug Report Form
|
name: Bug Report Form
|
||||||
about: Report an issue related to the Home Assistant Supervisor.
|
description: Report an issue related to the Home Assistant Supervisor.
|
||||||
labels: bug
|
labels: bug
|
||||||
title: ""
|
body:
|
||||||
issue_body: true
|
- type: markdown
|
||||||
inputs:
|
|
||||||
- type: description
|
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
This issue form is for reporting bugs with **supported** setups only!
|
This issue form is for reporting bugs with **supported** setups only!
|
||||||
|
|
||||||
If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr].
|
If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr].
|
||||||
|
|
||||||
[fr]: https://community.home-assistant.io/c/feature-requests
|
[fr]: https://community.home-assistant.io/c/feature-requests
|
||||||
- type: input
|
- type: textarea
|
||||||
attributes:
|
validations:
|
||||||
label: What is the version of the Supervisor used?
|
|
||||||
required: true
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Describe the issue you are experiencing
|
||||||
|
description: Provide a clear and concise description of what the bug is.
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Environment
|
||||||
|
- type: input
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: What is the used version of the Supervisor?
|
||||||
placeholder: supervisor-
|
placeholder: supervisor-
|
||||||
description: >
|
description: >
|
||||||
Can be found in the Supervisor panel -> System tab. Starts with
|
Can be found in the Supervisor panel -> System tab. Starts with
|
||||||
`supervisor-....`.
|
`supervisor-....`.
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
attributes:
|
attributes:
|
||||||
label: What type of installation are you running?
|
label: What type of installation are you running?
|
||||||
required: true
|
|
||||||
description: >
|
description: >
|
||||||
If you don't know, you can find it in: Configuration panel -> Info.
|
If you don't know, you can find it in: Configuration panel -> Info.
|
||||||
choices:
|
options:
|
||||||
- Home Assistant OS
|
- Home Assistant OS
|
||||||
- Home Assistant Supervised
|
- Home Assistant Supervised
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
attributes:
|
attributes:
|
||||||
label: Which operating system are you running on?
|
label: Which operating system are you running on?
|
||||||
required: true
|
options:
|
||||||
choices:
|
|
||||||
- Home Assistant Operating System
|
- Home Assistant Operating System
|
||||||
- Debian
|
- Debian
|
||||||
- Other (e.g., Raspbian/Raspberry Pi OS/Fedora)
|
- Other (e.g., Raspbian/Raspberry Pi OS/Fedora)
|
||||||
- type: input
|
- type: input
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
attributes:
|
attributes:
|
||||||
label: What is the version of your installed operating system?
|
label: What is the version of your installed operating system?
|
||||||
required: true
|
placeholder: "5.11"
|
||||||
placeholder: 5.10
|
|
||||||
description: Can be found in the Supervisor panel -> System tab.
|
description: Can be found in the Supervisor panel -> System tab.
|
||||||
- type: input
|
- type: input
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
attributes:
|
attributes:
|
||||||
label: What version of Home Assistant Core is installed?
|
label: What version of Home Assistant Core is installed?
|
||||||
required: true
|
|
||||||
placeholder: core-
|
placeholder: core-
|
||||||
description: >
|
description: >
|
||||||
Can be found in the Supervisor panel -> System tab. Starts with
|
Can be found in the Supervisor panel -> System tab. Starts with
|
||||||
`core-....`.
|
`core-....`.
|
||||||
- type: textarea
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
label: Describe the issue you are experiencing
|
value: |
|
||||||
required: true
|
# Details
|
||||||
description: Provide a clear and concise description of what the bug is.
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
attributes:
|
attributes:
|
||||||
label: Steps to reproduce the issue
|
label: Steps to reproduce the issue
|
||||||
required: true
|
|
||||||
description: |
|
description: |
|
||||||
Please tell us exactly how to reproduce your issue.
|
Please tell us exactly how to reproduce your issue.
|
||||||
Provide clear and concise step by step instructions and add code snippets if needed.
|
Provide clear and concise step by step instructions and add code snippets if needed.
|
||||||
@@ -68,13 +82,17 @@ inputs:
|
|||||||
3.
|
3.
|
||||||
...
|
...
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
attributes:
|
attributes:
|
||||||
label: Anything in the Supervisor logs that might be useful for us?
|
label: Anything in the Supervisor logs that might be useful for us?
|
||||||
required: false
|
|
||||||
description: >
|
description: >
|
||||||
The Supervisor logs can be found in the Supervisor panel -> System tab.
|
The Supervisor logs can be found in the Supervisor panel -> System tab.
|
||||||
- type: description
|
render: txt
|
||||||
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
label: Additional information
|
||||||
|
description: >
|
||||||
If you have any additional information for us, use the field below.
|
If you have any additional information for us, use the field below.
|
||||||
Please note, you can attach screenshots or screen recordings here.
|
Please note, you can attach screenshots or screen recordings here, by
|
||||||
|
dragging and dropping files in the field below.
|
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -37,6 +37,7 @@
|
|||||||
- This PR fixes or closes issue: fixes #
|
- This PR fixes or closes issue: fixes #
|
||||||
- This PR is related to issue:
|
- This PR is related to issue:
|
||||||
- Link to documentation pull request:
|
- Link to documentation pull request:
|
||||||
|
- Link to cli pull request:
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
|
117
.github/workflows/builder.yml
vendored
117
.github/workflows/builder.yml
vendored
@@ -35,7 +35,7 @@ on:
|
|||||||
env:
|
env:
|
||||||
BUILD_NAME: supervisor
|
BUILD_NAME: supervisor
|
||||||
BUILD_TYPE: supervisor
|
BUILD_TYPE: supervisor
|
||||||
WHEELS_TAG: 3.8-alpine3.12
|
WHEELS_TAG: 3.9-alpine3.14
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
init:
|
init:
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
requirements: ${{ steps.requirements.outputs.changed }}
|
requirements: ${{ steps.requirements.outputs.changed }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
- name: Check if requirements files changed
|
- name: Check if requirements files changed
|
||||||
id: requirements
|
id: requirements
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ steps.changed_files.outputs.all }}" =~ requirements.txt ]]; then
|
if [[ "${{ steps.changed_files.outputs.all }}" =~ (requirements.txt|build.json) ]]; then
|
||||||
echo "::set-output name=changed::true"
|
echo "::set-output name=changed::true"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -94,10 +94,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tag: ${{ env.WHEELS_TAG }}
|
tag: ${{ env.WHEELS_TAG }}
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
wheels-host: ${{ secrets.WHEELS_HOST }}
|
wheels-host: wheels.hass.io
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
wheels-user: wheels
|
wheels-user: wheels
|
||||||
apk: "build-base;libffi-dev;openssl-dev"
|
apk: "build-base;libffi-dev;openssl-dev;cargo"
|
||||||
skip-binary: aiohttp
|
skip-binary: aiohttp
|
||||||
requirements: "requirements.txt"
|
requirements: "requirements.txt"
|
||||||
|
|
||||||
@@ -109,24 +109,60 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.publish == 'true'
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1.10.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
if: needs.init.outputs.publish == 'true'
|
||||||
|
uses: docker/login-action@v1.10.0
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set build arguments
|
- name: Set build arguments
|
||||||
if: needs.init.outputs.publish == 'false'
|
if: needs.init.outputs.publish == 'false'
|
||||||
run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV
|
run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build supervisor
|
- name: Build supervisor
|
||||||
uses: home-assistant/builder@2021.01.1
|
uses: home-assistant/builder@2021.09.0
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
$BUILD_ARGS \
|
$BUILD_ARGS \
|
||||||
--${{ matrix.arch }} \
|
--${{ matrix.arch }} \
|
||||||
--target /data \
|
--target /data \
|
||||||
|
--with-codenotary "${{ secrets.VCN_USER }}" "${{ secrets.VCN_PASSWORD }}" "${{ secrets.VCN_ORG }}" \
|
||||||
|
--validate-from "${{ secrets.VCN_ORG }}" \
|
||||||
--generic ${{ needs.init.outputs.version }}
|
--generic ${{ needs.init.outputs.version }}
|
||||||
|
|
||||||
|
codenotary:
|
||||||
|
name: CodeNotary signature
|
||||||
|
needs: init
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
if: needs.init.outputs.publish == 'true'
|
||||||
|
uses: actions/checkout@v2.3.5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set version
|
||||||
|
if: needs.init.outputs.publish == 'true'
|
||||||
|
uses: home-assistant/actions/helpers/version@master
|
||||||
|
with:
|
||||||
|
type: ${{ env.BUILD_TYPE }}
|
||||||
|
|
||||||
|
- name: Signing image
|
||||||
|
if: needs.init.outputs.publish == 'true'
|
||||||
|
uses: home-assistant/actions/helpers/codenotary@master
|
||||||
|
with:
|
||||||
|
source: dir://${{ github.workspace }}
|
||||||
|
user: ${{ secrets.VCN_USER }}
|
||||||
|
password: ${{ secrets.VCN_PASSWORD }}
|
||||||
|
organisation: ${{ secrets.VCN_ORG }}
|
||||||
|
|
||||||
version:
|
version:
|
||||||
name: Update version
|
name: Update version
|
||||||
needs: ["init", "run_supervisor"]
|
needs: ["init", "run_supervisor"]
|
||||||
@@ -134,7 +170,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
if: needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.publish == 'true'
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
|
|
||||||
- name: Initialize git
|
- name: Initialize git
|
||||||
if: needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.publish == 'true'
|
||||||
@@ -155,13 +191,15 @@ jobs:
|
|||||||
run_supervisor:
|
run_supervisor:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Run the Supervisor
|
name: Run the Supervisor
|
||||||
needs: ["build"]
|
needs: ["build", "codenotary", "init"]
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
|
|
||||||
- name: Build the Supervisor
|
- name: Build the Supervisor
|
||||||
uses: home-assistant/builder@2021.01.1
|
if: needs.init.outputs.publish != 'true'
|
||||||
|
uses: home-assistant/builder@2021.09.0
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
--test \
|
--test \
|
||||||
@@ -169,13 +207,19 @@ jobs:
|
|||||||
--target /data \
|
--target /data \
|
||||||
--generic runner
|
--generic runner
|
||||||
|
|
||||||
|
- name: Pull Supervisor
|
||||||
|
if: needs.init.outputs.publish == 'true'
|
||||||
|
run: |
|
||||||
|
docker pull ghcr.io/home-assistant/amd64-hassio-supervisor:${{ needs.init.outputs.version }}
|
||||||
|
docker tag ghcr.io/home-assistant/amd64-hassio-supervisor:${{ needs.init.outputs.version }} homeassistant/amd64-hassio-supervisor:runner
|
||||||
|
|
||||||
- name: Create the Supervisor
|
- name: Create the Supervisor
|
||||||
run: |
|
run: |
|
||||||
mkdir -p /tmp/supervisor/data
|
mkdir -p /tmp/supervisor/data
|
||||||
docker create --name hassio_supervisor \
|
docker create --name hassio_supervisor \
|
||||||
--privileged \
|
--privileged \
|
||||||
--security-opt seccomp=unconfined \
|
--security-opt seccomp=unconfined \
|
||||||
--security-opt apparmor:unconfined \
|
--security-opt apparmor=unconfined \
|
||||||
-v /run/docker.sock:/run/docker.sock \
|
-v /run/docker.sock:/run/docker.sock \
|
||||||
-v /run/dbus:/run/dbus \
|
-v /run/dbus:/run/dbus \
|
||||||
-v /tmp/supervisor/data:/data \
|
-v /tmp/supervisor/data:/data \
|
||||||
@@ -194,22 +238,59 @@ jobs:
|
|||||||
SUPERVISOR=$(docker inspect --format='{{.NetworkSettings.IPAddress}}' hassio_supervisor)
|
SUPERVISOR=$(docker inspect --format='{{.NetworkSettings.IPAddress}}' hassio_supervisor)
|
||||||
ping="error"
|
ping="error"
|
||||||
while [ "$ping" != "ok" ]; do
|
while [ "$ping" != "ok" ]; do
|
||||||
ping=$(curl -sSL "http://$SUPERVISOR/supervisor/ping" | jq -r .result)
|
ping=$(curl -sSL "http://$SUPERVISOR/supervisor/ping" | jq -r '.result')
|
||||||
sleep 5
|
sleep 5
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Check the Supervisor
|
- name: Check the Supervisor
|
||||||
run: |
|
run: |
|
||||||
echo "Checking supervisor info"
|
echo "Checking supervisor info"
|
||||||
test=$(docker exec hassio_cli ha supervisor info --no-progress --raw-json | jq -r .result)
|
test=$(docker exec hassio_cli ha supervisor info --no-progress --raw-json | jq -r '.result')
|
||||||
if [ "$test" != "ok" ];then
|
if [ "$test" != "ok" ];then
|
||||||
docker logs hassio_supervisor
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Checking supervisor network info"
|
echo "Checking supervisor network info"
|
||||||
test=$(docker exec hassio_cli ha network info --no-progress --raw-json | jq -r .result)
|
test=$(docker exec hassio_cli ha network info --no-progress --raw-json | jq -r '.result')
|
||||||
if [ "$test" != "ok" ];then
|
if [ "$test" != "ok" ];then
|
||||||
docker logs hassio_supervisor
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Check the Store / Addon
|
||||||
|
run: |
|
||||||
|
echo "Install Core SSH Add-on"
|
||||||
|
test=$(docker exec hassio_cli ha addons install core_ssh --no-progress --raw-json | jq -r '.result')
|
||||||
|
if [ "$test" != "ok" ];then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Start Core SSH Add-on"
|
||||||
|
test=$(docker exec hassio_cli ha addons start core_ssh --no-progress --raw-json | jq -r '.result')
|
||||||
|
if [ "$test" != "ok" ];then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check the Supervisor code sign
|
||||||
|
if: needs.init.outputs.publish == 'true'
|
||||||
|
run: |
|
||||||
|
echo "Enable Content-Trust"
|
||||||
|
test=$(docker exec hassio_cli ha security options --content-trust=true --no-progress --raw-json | jq -r '.result')
|
||||||
|
if [ "$test" != "ok" ];then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Run supervisor health check"
|
||||||
|
test=$(docker exec hassio_cli ha resolution healthcheck --no-progress --raw-json | jq -r '.result')
|
||||||
|
if [ "$test" != "ok" ];then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Check supervisor unhealthy"
|
||||||
|
test=$(docker exec hassio_cli ha resolution info --no-progress --raw-json | jq -r '.data.unhealthy[]')
|
||||||
|
if [ "$test" != "" ];then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Get supervisor logs on failiure
|
||||||
|
if: ${{ cancelled() || failure() }}
|
||||||
|
run: docker logs hassio_supervisor
|
||||||
|
87
.github/workflows/ci.yaml
vendored
87
.github/workflows/ci.yaml
vendored
@@ -8,8 +8,9 @@ on:
|
|||||||
pull_request: ~
|
pull_request: ~
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEFAULT_PYTHON: 3.8
|
DEFAULT_PYTHON: 3.9
|
||||||
PRE_COMMIT_HOME: ~/.cache/pre-commit
|
PRE_COMMIT_HOME: ~/.cache/pre-commit
|
||||||
|
DEFAULT_VCN: v0.9.8
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Separate job to pre-populate the base dependency cache
|
# Separate job to pre-populate the base dependency cache
|
||||||
@@ -18,19 +19,19 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [3.8]
|
python-version: [3.9]
|
||||||
name: Prepare Python ${{ matrix.python-version }} dependencies
|
name: Prepare Python ${{ matrix.python-version }} dependencies
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v2.2.1
|
uses: actions/setup-python@v2.2.2
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.6
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -47,7 +48,7 @@ jobs:
|
|||||||
pip install -r requirements.txt -r requirements_tests.txt
|
pip install -r requirements.txt -r requirements_tests.txt
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.6
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_HOME }}
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
key: |
|
key: |
|
||||||
@@ -66,15 +67,15 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2.2.1
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.6
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -95,7 +96,7 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
- name: Register hadolint problem matcher
|
- name: Register hadolint problem matcher
|
||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
||||||
@@ -110,15 +111,15 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2.2.1
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.6
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -130,7 +131,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.6
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_HOME }}
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
key: |
|
key: |
|
||||||
@@ -154,15 +155,15 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2.2.1
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.6
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -186,15 +187,15 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2.2.1
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.6
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -206,7 +207,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.6
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_HOME }}
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
key: |
|
key: |
|
||||||
@@ -227,15 +228,15 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2.2.1
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.6
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -247,7 +248,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.6
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_HOME }}
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
key: |
|
key: |
|
||||||
@@ -271,15 +272,15 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2.2.1
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.6
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -303,15 +304,15 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2.2.1
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.6
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -323,7 +324,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.6
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_HOME }}
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
key: |
|
key: |
|
||||||
@@ -343,19 +344,23 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [3.8]
|
python-version: [3.9]
|
||||||
name: Run tests Python ${{ matrix.python-version }}
|
name: Run tests Python ${{ matrix.python-version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2.2.1
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install VCN tools
|
||||||
|
uses: home-assistant/actions/helpers/vcn@master
|
||||||
|
with:
|
||||||
|
vcn_version: ${{ env.DEFAULT_VCN }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.6
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -390,7 +395,7 @@ jobs:
|
|||||||
-o console_output_style=count \
|
-o console_output_style=count \
|
||||||
tests
|
tests
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
uses: actions/upload-artifact@v2.2.2
|
uses: actions/upload-artifact@v2.2.4
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}
|
name: coverage-${{ matrix.python-version }}
|
||||||
path: .coverage
|
path: .coverage
|
||||||
@@ -401,15 +406,15 @@ jobs:
|
|||||||
needs: pytest
|
needs: pytest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2.2.1
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.6
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -428,4 +433,4 @@ jobs:
|
|||||||
coverage report
|
coverage report
|
||||||
coverage xml
|
coverage xml
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v1.2.1
|
uses: codecov/codecov-action@v2.1.0
|
||||||
|
10
.github/workflows/lock.yml
vendored
10
.github/workflows/lock.yml
vendored
@@ -9,12 +9,12 @@ jobs:
|
|||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v2.0.3
|
- uses: dessant/lock-threads@v3
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-lock-inactive-days: "30"
|
issue-inactive-days: "30"
|
||||||
issue-exclude-created-before: "2020-10-01T00:00:00Z"
|
exclude-issue-created-before: "2020-10-01T00:00:00Z"
|
||||||
issue-lock-reason: ""
|
issue-lock-reason: ""
|
||||||
pr-lock-inactive-days: "1"
|
pr-inactive-days: "1"
|
||||||
pr-exclude-created-before: "2020-11-01T00:00:00Z"
|
exclude-pr-created-before: "2020-11-01T00:00:00Z"
|
||||||
pr-lock-reason: ""
|
pr-lock-reason: ""
|
||||||
|
2
.github/workflows/release-drafter.yml
vendored
2
.github/workflows/release-drafter.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
name: Release Drafter
|
name: Release Drafter
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
4
.github/workflows/sentry.yaml
vendored
4
.github/workflows/sentry.yaml
vendored
@@ -10,9 +10,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.5
|
||||||
- name: Sentry Release
|
- name: Sentry Release
|
||||||
uses: getsentry/action-release@v1.1
|
uses: getsentry/action-release@v1.1.6
|
||||||
env:
|
env:
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v3.0.15
|
- uses: actions/stale@v4
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
ignored:
|
ignored:
|
||||||
- DL3018
|
- DL3003
|
||||||
- DL3006
|
- DL3006
|
||||||
- DL3013
|
- DL3013
|
||||||
|
- DL3018
|
||||||
- SC2155
|
- SC2155
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 20.8b1
|
rev: 21.9b0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args:
|
args:
|
||||||
@@ -23,12 +23,12 @@ repos:
|
|||||||
- id: check-executables-have-shebangs
|
- id: check-executables-have-shebangs
|
||||||
stages: [manual]
|
stages: [manual]
|
||||||
- id: check-json
|
- id: check-json
|
||||||
- repo: https://github.com/pre-commit/mirrors-isort
|
- repo: https://github.com/PyCQA/isort
|
||||||
rev: v4.3.21
|
rev: 5.9.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.6.2
|
rev: v2.29.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py37-plus]
|
args: [--py39-plus]
|
||||||
|
21
.vcnignore
Normal file
21
.vcnignore
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# General files
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
.devcontainer
|
||||||
|
.vscode
|
||||||
|
.tox
|
||||||
|
|
||||||
|
# Data
|
||||||
|
home-assistant-polymer/
|
||||||
|
script/
|
||||||
|
tests/
|
||||||
|
data/
|
||||||
|
venv/
|
25
.vscode/tasks.json
vendored
25
.vscode/tasks.json
vendored
@@ -4,7 +4,7 @@
|
|||||||
{
|
{
|
||||||
"label": "Run Supervisor",
|
"label": "Run Supervisor",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "./scripts/run-supervisor.sh",
|
"command": "supervisor_run",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
@@ -15,20 +15,6 @@
|
|||||||
},
|
},
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"label": "Build Supervisor",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "./scripts/build-supervisor.sh",
|
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
"isDefault": true
|
|
||||||
},
|
|
||||||
"presentation": {
|
|
||||||
"reveal": "always",
|
|
||||||
"panel": "new"
|
|
||||||
},
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": "Run Supervisor CLI",
|
"label": "Run Supervisor CLI",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
@@ -46,7 +32,7 @@
|
|||||||
{
|
{
|
||||||
"label": "Update Supervisor Panel",
|
"label": "Update Supervisor Panel",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "./scripts/update-frontend.sh",
|
"command": "LOKALISE_TOKEN='${input:localiseToken}' ./scripts/update-frontend.sh",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
@@ -100,5 +86,12 @@
|
|||||||
},
|
},
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"id": "localiseToken",
|
||||||
|
"type": "promptString",
|
||||||
|
"description": "Paste your lokalise token to download frontend translations"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
12
Dockerfile
12
Dockerfile
@@ -1,25 +1,25 @@
|
|||||||
ARG BUILD_FROM
|
ARG BUILD_FROM
|
||||||
FROM $BUILD_FROM
|
FROM ${BUILD_FROM}
|
||||||
|
|
||||||
ENV \
|
ENV \
|
||||||
S6_SERVICES_GRACETIME=10000 \
|
S6_SERVICES_GRACETIME=10000 \
|
||||||
SUPERVISOR_API=http://localhost
|
SUPERVISOR_API=http://localhost
|
||||||
|
|
||||||
|
ARG BUILD_ARCH
|
||||||
|
WORKDIR /usr/src
|
||||||
|
|
||||||
# Install base
|
# Install base
|
||||||
RUN \
|
RUN \
|
||||||
apk add --no-cache \
|
set -x \
|
||||||
|
&& apk add --no-cache \
|
||||||
eudev \
|
eudev \
|
||||||
eudev-libs \
|
eudev-libs \
|
||||||
git \
|
git \
|
||||||
glib \
|
|
||||||
libffi \
|
libffi \
|
||||||
libpulse \
|
libpulse \
|
||||||
musl \
|
musl \
|
||||||
openssl
|
openssl
|
||||||
|
|
||||||
ARG BUILD_ARCH
|
|
||||||
WORKDIR /usr/src
|
|
||||||
|
|
||||||
# Install requirements
|
# Install requirements
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN \
|
RUN \
|
||||||
|
22
build.json
22
build.json
@@ -1,13 +1,21 @@
|
|||||||
{
|
{
|
||||||
"image": "homeassistant/{arch}-hassio-supervisor",
|
"image": "homeassistant/{arch}-hassio-supervisor",
|
||||||
|
"shadow_repository": "ghcr.io/home-assistant",
|
||||||
"build_from": {
|
"build_from": {
|
||||||
"aarch64": "homeassistant/aarch64-base-python:3.8-alpine3.12",
|
"aarch64": "ghcr.io/home-assistant/aarch64-base-python:3.9-alpine3.14",
|
||||||
"armhf": "homeassistant/armhf-base-python:3.8-alpine3.12",
|
"armhf": "ghcr.io/home-assistant/armhf-base-python:3.9-alpine3.14",
|
||||||
"armv7": "homeassistant/armv7-base-python:3.8-alpine3.12",
|
"armv7": "ghcr.io/home-assistant/armv7-base-python:3.9-alpine3.14",
|
||||||
"amd64": "homeassistant/amd64-base-python:3.8-alpine3.12",
|
"amd64": "ghcr.io/home-assistant/amd64-base-python:3.9-alpine3.14",
|
||||||
"i386": "homeassistant/i386-base-python:3.8-alpine3.12"
|
"i386": "ghcr.io/home-assistant/i386-base-python:3.9-alpine3.14"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"io.hass.type": "supervisor"
|
"io.hass.type": "supervisor",
|
||||||
|
"org.opencontainers.image.title": "Home Assistant Supervisor",
|
||||||
|
"org.opencontainers.image.description": "Container-based system for managing Home Assistant Core installation",
|
||||||
|
"org.opencontainers.image.source": "https://github.com/home-assistant/supervisor",
|
||||||
|
"org.opencontainers.image.authors": "The Home Assistant Authors",
|
||||||
|
"org.opencontainers.image.url": "https://www.home-assistant.io/",
|
||||||
|
"org.opencontainers.image.documentation": "https://www.home-assistant.io/docs/",
|
||||||
|
"org.opencontainers.image.licenses": "Apache License 2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
Submodule home-assistant-polymer updated: 4273b72d71...910cd98a38
6
pylintrc
6
pylintrc
@@ -2,7 +2,10 @@
|
|||||||
reports=no
|
reports=no
|
||||||
jobs=2
|
jobs=2
|
||||||
|
|
||||||
good-names=id,i,j,k,ex,Run,_,fp,T
|
good-names=id,i,j,k,ex,Run,_,fp,T,os
|
||||||
|
|
||||||
|
extension-pkg-whitelist=
|
||||||
|
ciso8601
|
||||||
|
|
||||||
# Reasons disabled:
|
# Reasons disabled:
|
||||||
# format - handled by black
|
# format - handled by black
|
||||||
@@ -37,6 +40,7 @@ disable=
|
|||||||
too-many-return-statements,
|
too-many-return-statements,
|
||||||
too-many-statements,
|
too-many-statements,
|
||||||
unused-argument,
|
unused-argument,
|
||||||
|
consider-using-with
|
||||||
|
|
||||||
[EXCEPTIONS]
|
[EXCEPTIONS]
|
||||||
overgeneral-exceptions=Exception
|
overgeneral-exceptions=Exception
|
||||||
|
@@ -1,20 +1,22 @@
|
|||||||
aiohttp==3.7.3
|
aiohttp==3.7.4.post0
|
||||||
async_timeout==3.0.1
|
async_timeout==3.0.1
|
||||||
atomicwrites==1.4.0
|
atomicwrites==1.4.0
|
||||||
attrs==20.3.0
|
attrs==21.2.0
|
||||||
awesomeversion==21.2.0
|
awesomeversion==21.8.1
|
||||||
brotli==1.0.9
|
brotlipy==0.7.0
|
||||||
cchardet==2.1.7
|
cchardet==2.1.7
|
||||||
colorlog==4.7.2
|
ciso8601==2.2.0
|
||||||
|
colorlog==6.5.0
|
||||||
cpe==1.2.1
|
cpe==1.2.1
|
||||||
cryptography==3.3.1
|
cryptography==35.0.0
|
||||||
debugpy==1.2.1
|
debugpy==1.5.1
|
||||||
docker==4.4.1
|
deepmerge==0.3.0
|
||||||
gitpython==3.1.12
|
docker==5.0.3
|
||||||
jinja2==2.11.3
|
gitpython==3.1.24
|
||||||
pulsectl==20.5.1
|
jinja2==3.0.2
|
||||||
pytz==2021.1
|
pulsectl==21.10.5
|
||||||
pyudev==0.22.0
|
pyudev==0.22.0
|
||||||
ruamel.yaml==0.15.100
|
ruamel.yaml==0.15.100
|
||||||
sentry-sdk==0.19.5
|
sentry-sdk==1.4.3
|
||||||
voluptuous==0.12.1
|
voluptuous==0.12.2
|
||||||
|
dbus-next==0.2.3
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
black==20.8b1
|
black==21.9b0
|
||||||
codecov==2.1.11
|
codecov==2.1.12
|
||||||
coverage==5.4
|
coverage==5.5
|
||||||
flake8-docstrings==1.5.0
|
flake8-docstrings==1.6.0
|
||||||
flake8==3.8.4
|
flake8==3.9.2
|
||||||
pre-commit==2.10.0
|
pre-commit==2.15.0
|
||||||
pydocstyle==5.1.1
|
pydocstyle==6.1.1
|
||||||
pylint==2.6.0
|
pylint==2.11.1
|
||||||
pytest-aiohttp==0.3.0
|
pytest-aiohttp==0.3.0
|
||||||
pytest-asyncio==0.12.0 # NB!: Versions over 0.12.0 breaks pytest-aiohttp (https://github.com/aio-libs/pytest-aiohttp/issues/16)
|
pytest-asyncio==0.12.0 # NB!: Versions over 0.12.0 breaks pytest-aiohttp (https://github.com/aio-libs/pytest-aiohttp/issues/16)
|
||||||
pytest-cov==2.11.1
|
pytest-cov==2.12.1
|
||||||
pytest-timeout==1.4.2
|
pytest-timeout==1.4.2
|
||||||
pytest==6.2.2
|
pytest==6.2.5
|
||||||
pyupgrade==2.9.0
|
pyupgrade==2.29.0
|
||||||
|
@@ -1,28 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
source "${BASH_SOURCE[0]%/*}/common.sh"
|
|
||||||
|
|
||||||
set -eE
|
|
||||||
|
|
||||||
DOCKER_TIMEOUT=30
|
|
||||||
DOCKER_PID=0
|
|
||||||
|
|
||||||
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 \
|
|
||||||
--generic latest \
|
|
||||||
--target /data \
|
|
||||||
--test \
|
|
||||||
--amd64 \
|
|
||||||
--no-cache
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Build Supervisor"
|
|
||||||
start_docker
|
|
||||||
trap "stop_docker" ERR
|
|
||||||
|
|
||||||
build_supervisor
|
|
@@ -1,58 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
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 cleanup_lastboot() {
|
|
||||||
if [[ -f /workspaces/test_supervisor/config.json ]]; then
|
|
||||||
echo "Cleaning up last boot"
|
|
||||||
cp /workspaces/test_supervisor/config.json /tmp/config.json
|
|
||||||
jq -rM 'del(.last_boot)' /tmp/config.json > /workspaces/test_supervisor/config.json
|
|
||||||
rm /tmp/config.json
|
|
||||||
fi
|
|
||||||
}
|
|
@@ -1,102 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
source "${BASH_SOURCE[0]%/*}/common.sh"
|
|
||||||
source "${BASH_SOURCE[0]%/*}/build-supervisor.sh"
|
|
||||||
|
|
||||||
set -eE
|
|
||||||
|
|
||||||
DOCKER_TIMEOUT=30
|
|
||||||
DOCKER_PID=0
|
|
||||||
|
|
||||||
|
|
||||||
function cleanup_docker() {
|
|
||||||
echo "Cleaning up stopped containers..."
|
|
||||||
docker rm $(docker ps -a -q) || true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function run_supervisor() {
|
|
||||||
mkdir -p /workspaces/test_supervisor
|
|
||||||
|
|
||||||
echo "Start Supervisor"
|
|
||||||
docker run --rm --privileged \
|
|
||||||
--name hassio_supervisor \
|
|
||||||
--privileged \
|
|
||||||
--security-opt seccomp=unconfined \
|
|
||||||
--security-opt apparmor:unconfined \
|
|
||||||
-v /run/docker.sock:/run/docker.sock:rw \
|
|
||||||
-v /run/dbus:/run/dbus:ro \
|
|
||||||
-v /run/udev:/run/udev:ro \
|
|
||||||
-v "/workspaces/test_supervisor":/data:rw \
|
|
||||||
-v /etc/machine-id:/etc/machine-id:ro \
|
|
||||||
-v /workspaces/supervisor:/usr/src/supervisor \
|
|
||||||
-e SUPERVISOR_SHARE="/workspaces/test_supervisor" \
|
|
||||||
-e SUPERVISOR_NAME=hassio_supervisor \
|
|
||||||
-e SUPERVISOR_DEV=1 \
|
|
||||||
-e SUPERVISOR_MACHINE="qemux86-64" \
|
|
||||||
homeassistant/amd64-hassio-supervisor:latest
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function init_dbus() {
|
|
||||||
if pgrep dbus-daemon; then
|
|
||||||
echo "Dbus is running"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Startup dbus"
|
|
||||||
mkdir -p /var/lib/dbus
|
|
||||||
cp -f /etc/machine-id /var/lib/dbus/machine-id
|
|
||||||
|
|
||||||
# cleanups
|
|
||||||
mkdir -p /run/dbus
|
|
||||||
rm -f /run/dbus/pid
|
|
||||||
|
|
||||||
# run
|
|
||||||
dbus-daemon --system --print-address
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function init_udev() {
|
|
||||||
if pgrep systemd-udevd; then
|
|
||||||
echo "udev is running"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Startup udev"
|
|
||||||
|
|
||||||
# cleanups
|
|
||||||
mkdir -p /run/udev
|
|
||||||
|
|
||||||
# run
|
|
||||||
/lib/systemd/systemd-udevd --daemon
|
|
||||||
sleep 3
|
|
||||||
udevadm trigger && udevadm settle
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Run Supervisor"
|
|
||||||
|
|
||||||
start_docker
|
|
||||||
trap "stop_docker" ERR
|
|
||||||
|
|
||||||
|
|
||||||
if [ "$( docker container inspect -f '{{.State.Status}}' hassio_supervisor )" == "running" ]; then
|
|
||||||
echo "Restarting Supervisor"
|
|
||||||
docker rm -f hassio_supervisor
|
|
||||||
init_dbus
|
|
||||||
init_udev
|
|
||||||
cleanup_lastboot
|
|
||||||
run_supervisor
|
|
||||||
stop_docker
|
|
||||||
|
|
||||||
else
|
|
||||||
echo "Starting Supervisor"
|
|
||||||
docker system prune -f
|
|
||||||
build_supervisor
|
|
||||||
cleanup_lastboot
|
|
||||||
cleanup_docker
|
|
||||||
init_dbus
|
|
||||||
init_udev
|
|
||||||
run_supervisor
|
|
||||||
stop_docker
|
|
||||||
fi
|
|
@@ -1,4 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
source "/etc/supervisor_scripts/common"
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Update frontend
|
# Update frontend
|
||||||
@@ -9,6 +11,10 @@ cd home-assistant-polymer
|
|||||||
nvm install
|
nvm install
|
||||||
script/bootstrap
|
script/bootstrap
|
||||||
|
|
||||||
|
# Download translations
|
||||||
|
start_docker
|
||||||
|
./script/translations_download
|
||||||
|
|
||||||
# build frontend
|
# build frontend
|
||||||
cd hassio
|
cd hassio
|
||||||
./script/build_hassio
|
./script/build_hassio
|
||||||
@@ -16,3 +22,9 @@ cd hassio
|
|||||||
# Copy frontend
|
# Copy frontend
|
||||||
rm -rf ../../supervisor/api/panel/*
|
rm -rf ../../supervisor/api/panel/*
|
||||||
cp -rf build/* ../../supervisor/api/panel/
|
cp -rf build/* ../../supervisor/api/panel/
|
||||||
|
|
||||||
|
# Reset frontend git
|
||||||
|
cd ..
|
||||||
|
git reset --hard HEAD
|
||||||
|
|
||||||
|
stop_docker
|
@@ -4,9 +4,8 @@ include_trailing_comma=True
|
|||||||
force_grid_wrap=0
|
force_grid_wrap=0
|
||||||
line_length=88
|
line_length=88
|
||||||
indent = " "
|
indent = " "
|
||||||
not_skip = __init__.py
|
|
||||||
force_sort_within_sections = true
|
force_sort_within_sections = true
|
||||||
sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
|
sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
|
||||||
default_section = THIRDPARTY
|
default_section = THIRDPARTY
|
||||||
forced_separate = tests
|
forced_separate = tests
|
||||||
combine_as_imports = true
|
combine_as_imports = true
|
||||||
|
6
setup.py
6
setup.py
@@ -33,8 +33,9 @@ setup(
|
|||||||
packages=[
|
packages=[
|
||||||
"supervisor.addons",
|
"supervisor.addons",
|
||||||
"supervisor.api",
|
"supervisor.api",
|
||||||
|
"supervisor.backups",
|
||||||
"supervisor.dbus.network",
|
"supervisor.dbus.network",
|
||||||
"supervisor.dbus.payloads",
|
"supervisor.dbus.network.setting",
|
||||||
"supervisor.dbus",
|
"supervisor.dbus",
|
||||||
"supervisor.discovery.services",
|
"supervisor.discovery.services",
|
||||||
"supervisor.discovery",
|
"supervisor.discovery",
|
||||||
@@ -44,11 +45,12 @@ setup(
|
|||||||
"supervisor.jobs",
|
"supervisor.jobs",
|
||||||
"supervisor.misc",
|
"supervisor.misc",
|
||||||
"supervisor.plugins",
|
"supervisor.plugins",
|
||||||
|
"supervisor.resolution.checks",
|
||||||
"supervisor.resolution.evaluations",
|
"supervisor.resolution.evaluations",
|
||||||
|
"supervisor.resolution.fixups",
|
||||||
"supervisor.resolution",
|
"supervisor.resolution",
|
||||||
"supervisor.services.modules",
|
"supervisor.services.modules",
|
||||||
"supervisor.services",
|
"supervisor.services",
|
||||||
"supervisor.snapshots",
|
|
||||||
"supervisor.store",
|
"supervisor.store",
|
||||||
"supervisor.utils",
|
"supervisor.utils",
|
||||||
"supervisor",
|
"supervisor",
|
||||||
|
@@ -3,7 +3,7 @@ import asyncio
|
|||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
import logging
|
import logging
|
||||||
import tarfile
|
import tarfile
|
||||||
from typing import Dict, List, Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
from ..const import AddonBoot, AddonStartup, AddonState
|
from ..const import AddonBoot, AddonStartup, AddonState
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
@@ -38,17 +38,17 @@ class AddonManager(CoreSysAttributes):
|
|||||||
"""Initialize Docker base wrapper."""
|
"""Initialize Docker base wrapper."""
|
||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self.data: AddonsData = AddonsData(coresys)
|
self.data: AddonsData = AddonsData(coresys)
|
||||||
self.local: Dict[str, Addon] = {}
|
self.local: dict[str, Addon] = {}
|
||||||
self.store: Dict[str, AddonStore] = {}
|
self.store: dict[str, AddonStore] = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all(self) -> List[AnyAddon]:
|
def all(self) -> list[AnyAddon]:
|
||||||
"""Return a list of all add-ons."""
|
"""Return a list of all add-ons."""
|
||||||
addons: Dict[str, AnyAddon] = {**self.store, **self.local}
|
addons: dict[str, AnyAddon] = {**self.store, **self.local}
|
||||||
return list(addons.values())
|
return list(addons.values())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def installed(self) -> List[Addon]:
|
def installed(self) -> list[Addon]:
|
||||||
"""Return a list of all installed add-ons."""
|
"""Return a list of all installed add-ons."""
|
||||||
return list(self.local.values())
|
return list(self.local.values())
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
|
|
||||||
async def boot(self, stage: AddonStartup) -> None:
|
async def boot(self, stage: AddonStartup) -> None:
|
||||||
"""Boot add-ons with mode auto."""
|
"""Boot add-ons with mode auto."""
|
||||||
tasks: List[Addon] = []
|
tasks: list[Addon] = []
|
||||||
for addon in self.installed:
|
for addon in self.installed:
|
||||||
if addon.boot != AddonBoot.AUTO or addon.startup != stage:
|
if addon.boot != AddonBoot.AUTO or addon.startup != stage:
|
||||||
continue
|
continue
|
||||||
@@ -123,7 +123,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
|
|
||||||
async def shutdown(self, stage: AddonStartup) -> None:
|
async def shutdown(self, stage: AddonStartup) -> None:
|
||||||
"""Shutdown addons."""
|
"""Shutdown addons."""
|
||||||
tasks: List[Addon] = []
|
tasks: list[Addon] = []
|
||||||
for addon in self.installed:
|
for addon in self.installed:
|
||||||
if addon.state != AddonState.STARTED or addon.startup != stage:
|
if addon.state != AddonState.STARTED or addon.startup != stage:
|
||||||
continue
|
continue
|
||||||
@@ -154,17 +154,16 @@ class AddonManager(CoreSysAttributes):
|
|||||||
async def install(self, slug: str) -> None:
|
async def install(self, slug: str) -> None:
|
||||||
"""Install an add-on."""
|
"""Install an add-on."""
|
||||||
if slug in self.local:
|
if slug in self.local:
|
||||||
_LOGGER.warning("Add-on %s is already installed", slug)
|
raise AddonsError(f"Add-on {slug} is already installed", _LOGGER.warning)
|
||||||
return
|
|
||||||
store = self.store.get(slug)
|
store = self.store.get(slug)
|
||||||
|
|
||||||
if not store:
|
if not store:
|
||||||
_LOGGER.error("Add-on %s not exists", slug)
|
raise AddonsError(f"Add-on {slug} not exists", _LOGGER.error)
|
||||||
raise AddonsError()
|
|
||||||
|
|
||||||
if not store.available:
|
if not store.available:
|
||||||
_LOGGER.error("Add-on %s not supported on that platform", slug)
|
raise AddonsNotSupportedError(
|
||||||
raise AddonsNotSupportedError()
|
f"Add-on {slug} not supported on that platform", _LOGGER.error
|
||||||
|
)
|
||||||
|
|
||||||
self.data.install(store)
|
self.data.install(store)
|
||||||
addon = Addon(self.coresys, slug)
|
addon = Addon(self.coresys, slug)
|
||||||
@@ -256,37 +255,38 @@ class AddonManager(CoreSysAttributes):
|
|||||||
async def update(self, slug: str) -> None:
|
async def update(self, slug: str) -> None:
|
||||||
"""Update add-on."""
|
"""Update add-on."""
|
||||||
if slug not in self.local:
|
if slug not in self.local:
|
||||||
_LOGGER.error("Add-on %s is not installed", slug)
|
raise AddonsError(f"Add-on {slug} is not installed", _LOGGER.error)
|
||||||
raise AddonsError()
|
|
||||||
addon = self.local[slug]
|
addon = self.local[slug]
|
||||||
|
|
||||||
if addon.is_detached:
|
if addon.is_detached:
|
||||||
_LOGGER.error("Add-on %s is not available inside store", slug)
|
raise AddonsError(
|
||||||
raise AddonsError()
|
f"Add-on {slug} is not available inside store", _LOGGER.error
|
||||||
|
)
|
||||||
store = self.store[slug]
|
store = self.store[slug]
|
||||||
|
|
||||||
if addon.version == store.version:
|
if addon.version == store.version:
|
||||||
_LOGGER.warning("No update available for add-on %s", slug)
|
raise AddonsError(f"No update available for add-on {slug}", _LOGGER.warning)
|
||||||
return
|
|
||||||
|
|
||||||
# Check if available, Maybe something have changed
|
# Check if available, Maybe something have changed
|
||||||
if not store.available:
|
if not store.available:
|
||||||
_LOGGER.error("Add-on %s not supported on that platform", slug)
|
raise AddonsNotSupportedError(
|
||||||
raise AddonsNotSupportedError()
|
f"Add-on {slug} not supported on that platform", _LOGGER.error
|
||||||
|
)
|
||||||
|
|
||||||
# Update instance
|
# Update instance
|
||||||
last_state: AddonState = addon.state
|
last_state: AddonState = addon.state
|
||||||
|
old_image = addon.image
|
||||||
try:
|
try:
|
||||||
await addon.instance.update(store.version, store.image)
|
await addon.instance.update(store.version, store.image)
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
with suppress(DockerError):
|
|
||||||
await addon.instance.cleanup()
|
|
||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
raise AddonsError() from err
|
raise AddonsError() from err
|
||||||
else:
|
|
||||||
self.data.update(store)
|
_LOGGER.info("Add-on '%s' successfully updated", slug)
|
||||||
_LOGGER.info("Add-on '%s' successfully updated", slug)
|
self.data.update(store)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
with suppress(DockerError):
|
||||||
|
await addon.instance.cleanup(old_image=old_image)
|
||||||
|
|
||||||
# Setup/Fix AppArmor profile
|
# Setup/Fix AppArmor profile
|
||||||
await addon.install_apparmor()
|
await addon.install_apparmor()
|
||||||
@@ -306,22 +306,24 @@ class AddonManager(CoreSysAttributes):
|
|||||||
async def rebuild(self, slug: str) -> None:
|
async def rebuild(self, slug: str) -> None:
|
||||||
"""Perform a rebuild of local build add-on."""
|
"""Perform a rebuild of local build add-on."""
|
||||||
if slug not in self.local:
|
if slug not in self.local:
|
||||||
_LOGGER.error("Add-on %s is not installed", slug)
|
raise AddonsError(f"Add-on {slug} is not installed", _LOGGER.error)
|
||||||
raise AddonsError()
|
|
||||||
addon = self.local[slug]
|
addon = self.local[slug]
|
||||||
|
|
||||||
if addon.is_detached:
|
if addon.is_detached:
|
||||||
_LOGGER.error("Add-on %s is not available inside store", slug)
|
raise AddonsError(
|
||||||
raise AddonsError()
|
f"Add-on {slug} is not available inside store", _LOGGER.error
|
||||||
|
)
|
||||||
store = self.store[slug]
|
store = self.store[slug]
|
||||||
|
|
||||||
# Check if a rebuild is possible now
|
# Check if a rebuild is possible now
|
||||||
if addon.version != store.version:
|
if addon.version != store.version:
|
||||||
_LOGGER.error("Version changed, use Update instead Rebuild")
|
raise AddonsError(
|
||||||
raise AddonsError()
|
"Version changed, use Update instead Rebuild", _LOGGER.error
|
||||||
|
)
|
||||||
if not addon.need_build:
|
if not addon.need_build:
|
||||||
_LOGGER.error("Can't rebuild a image based add-on")
|
raise AddonsNotSupportedError(
|
||||||
raise AddonsNotSupportedError()
|
"Can't rebuild a image based add-on", _LOGGER.error
|
||||||
|
)
|
||||||
|
|
||||||
# remove docker container but not addon config
|
# remove docker container but not addon config
|
||||||
last_state: AddonState = addon.state
|
last_state: AddonState = addon.state
|
||||||
@@ -371,7 +373,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
@Job(conditions=[JobCondition.FREE_SPACE, JobCondition.INTERNET_HOST])
|
@Job(conditions=[JobCondition.FREE_SPACE, JobCondition.INTERNET_HOST])
|
||||||
async def repair(self) -> None:
|
async def repair(self) -> None:
|
||||||
"""Repair local add-ons."""
|
"""Repair local add-ons."""
|
||||||
needs_repair: List[Addon] = []
|
needs_repair: list[Addon] = []
|
||||||
|
|
||||||
# Evaluate Add-ons to repair
|
# Evaluate Add-ons to repair
|
||||||
for addon in self.installed:
|
for addon in self.installed:
|
||||||
|
@@ -10,9 +10,10 @@ import secrets
|
|||||||
import shutil
|
import shutil
|
||||||
import tarfile
|
import tarfile
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from typing import Any, Awaitable, Dict, List, Optional, Set
|
from typing import Any, Awaitable, Final, Optional
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
from deepmerge import Merger
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous.humanize import humanize_error
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
@@ -22,6 +23,8 @@ from ..const import (
|
|||||||
ATTR_AUDIO_OUTPUT,
|
ATTR_AUDIO_OUTPUT,
|
||||||
ATTR_AUTO_UPDATE,
|
ATTR_AUTO_UPDATE,
|
||||||
ATTR_BOOT,
|
ATTR_BOOT,
|
||||||
|
ATTR_DATA,
|
||||||
|
ATTR_EVENT,
|
||||||
ATTR_IMAGE,
|
ATTR_IMAGE,
|
||||||
ATTR_INGRESS_ENTRY,
|
ATTR_INGRESS_ENTRY,
|
||||||
ATTR_INGRESS_PANEL,
|
ATTR_INGRESS_PANEL,
|
||||||
@@ -32,8 +35,10 @@ from ..const import (
|
|||||||
ATTR_PORTS,
|
ATTR_PORTS,
|
||||||
ATTR_PROTECTED,
|
ATTR_PROTECTED,
|
||||||
ATTR_SCHEMA,
|
ATTR_SCHEMA,
|
||||||
|
ATTR_SLUG,
|
||||||
ATTR_STATE,
|
ATTR_STATE,
|
||||||
ATTR_SYSTEM,
|
ATTR_SYSTEM,
|
||||||
|
ATTR_TYPE,
|
||||||
ATTR_USER,
|
ATTR_USER,
|
||||||
ATTR_UUID,
|
ATTR_UUID,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
@@ -50,20 +55,22 @@ from ..exceptions import (
|
|||||||
AddonConfigurationError,
|
AddonConfigurationError,
|
||||||
AddonsError,
|
AddonsError,
|
||||||
AddonsNotSupportedError,
|
AddonsNotSupportedError,
|
||||||
|
ConfigurationFileError,
|
||||||
DockerError,
|
DockerError,
|
||||||
DockerRequestError,
|
DockerRequestError,
|
||||||
HostAppArmorError,
|
HostAppArmorError,
|
||||||
JsonFileError,
|
|
||||||
)
|
)
|
||||||
from ..hardware.data import Device
|
from ..hardware.data import Device
|
||||||
|
from ..homeassistant.const import WSEvent, WSType
|
||||||
from ..utils import check_port
|
from ..utils import check_port
|
||||||
from ..utils.apparmor import adjust_profile
|
from ..utils.apparmor import adjust_profile
|
||||||
from ..utils.json import read_json_file, write_json_file
|
from ..utils.json import read_json_file, write_json_file
|
||||||
from ..utils.tar import atomic_contents_add, secure_path
|
from ..utils.tar import atomic_contents_add, secure_path
|
||||||
|
from .const import AddonBackupMode
|
||||||
from .model import AddonModel, Data
|
from .model import AddonModel, Data
|
||||||
from .options import AddonOptions
|
from .options import AddonOptions
|
||||||
from .utils import remove_data
|
from .utils import remove_data
|
||||||
from .validate import SCHEMA_ADDON_SNAPSHOT
|
from .validate import SCHEMA_ADDON_BACKUP
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -74,13 +81,19 @@ RE_WEBUI = re.compile(
|
|||||||
|
|
||||||
RE_WATCHDOG = re.compile(
|
RE_WATCHDOG = re.compile(
|
||||||
r"^(?:(?P<s_prefix>https?|tcp)|\[PROTO:(?P<t_proto>\w+)\])"
|
r"^(?:(?P<s_prefix>https?|tcp)|\[PROTO:(?P<t_proto>\w+)\])"
|
||||||
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$"
|
r":\/\/\[HOST\]:(?:\[PORT:)?(?P<t_port>\d+)\]?(?P<s_suffix>.*)$"
|
||||||
)
|
)
|
||||||
|
|
||||||
RE_OLD_AUDIO = re.compile(r"\d+,\d+")
|
RE_OLD_AUDIO = re.compile(r"\d+,\d+")
|
||||||
|
|
||||||
WATCHDOG_TIMEOUT = aiohttp.ClientTimeout(total=10)
|
WATCHDOG_TIMEOUT = aiohttp.ClientTimeout(total=10)
|
||||||
|
|
||||||
|
_OPTIONS_MERGER: Final = Merger(
|
||||||
|
type_strategies=[(dict, ["merge"])],
|
||||||
|
fallback_strategies=["override"],
|
||||||
|
type_conflict_strategies=["override"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Addon(AddonModel):
|
class Addon(AddonModel):
|
||||||
"""Hold data for add-on inside Supervisor."""
|
"""Hold data for add-on inside Supervisor."""
|
||||||
@@ -89,12 +102,34 @@ class Addon(AddonModel):
|
|||||||
"""Initialize data holder."""
|
"""Initialize data holder."""
|
||||||
super().__init__(coresys, slug)
|
super().__init__(coresys, slug)
|
||||||
self.instance: DockerAddon = DockerAddon(coresys, self)
|
self.instance: DockerAddon = DockerAddon(coresys, self)
|
||||||
self.state: AddonState = AddonState.UNKNOWN
|
self._state: AddonState = AddonState.UNKNOWN
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""Return internal representation."""
|
"""Return internal representation."""
|
||||||
return f"<Addon: {self.slug}>"
|
return f"<Addon: {self.slug}>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> AddonState:
|
||||||
|
"""Return state of the add-on."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@state.setter
|
||||||
|
def state(self, new_state: AddonState) -> None:
|
||||||
|
"""Set the add-on into new state."""
|
||||||
|
if self._state == new_state:
|
||||||
|
return
|
||||||
|
self._state = new_state
|
||||||
|
self.sys_homeassistant.websocket.send_command(
|
||||||
|
{
|
||||||
|
ATTR_TYPE: WSType.SUPERVISOR_EVENT,
|
||||||
|
ATTR_DATA: {
|
||||||
|
ATTR_EVENT: WSEvent.ADDON,
|
||||||
|
ATTR_SLUG: self.slug,
|
||||||
|
ATTR_STATE: new_state,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def in_progress(self) -> bool:
|
def in_progress(self) -> bool:
|
||||||
"""Return True if a task is in progress."""
|
"""Return True if a task is in progress."""
|
||||||
@@ -159,17 +194,19 @@ class Addon(AddonModel):
|
|||||||
return self.version != self.latest_version
|
return self.version != self.latest_version
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dns(self) -> List[str]:
|
def dns(self) -> list[str]:
|
||||||
"""Return list of DNS name for that add-on."""
|
"""Return list of DNS name for that add-on."""
|
||||||
return [f"{self.hostname}.{DNS_SUFFIX}"]
|
return [f"{self.hostname}.{DNS_SUFFIX}"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def options(self) -> Dict[str, Any]:
|
def options(self) -> dict[str, Any]:
|
||||||
"""Return options with local changes."""
|
"""Return options with local changes."""
|
||||||
return {**self.data[ATTR_OPTIONS], **self.persist[ATTR_OPTIONS]}
|
return _OPTIONS_MERGER.merge(
|
||||||
|
deepcopy(self.data[ATTR_OPTIONS]), deepcopy(self.persist[ATTR_OPTIONS])
|
||||||
|
)
|
||||||
|
|
||||||
@options.setter
|
@options.setter
|
||||||
def options(self, value: Optional[Dict[str, Any]]) -> None:
|
def options(self, value: Optional[dict[str, Any]]) -> None:
|
||||||
"""Store user add-on options."""
|
"""Store user add-on options."""
|
||||||
self.persist[ATTR_OPTIONS] = {} if value is None else deepcopy(value)
|
self.persist[ATTR_OPTIONS] = {} if value is None else deepcopy(value)
|
||||||
|
|
||||||
@@ -246,12 +283,12 @@ class Addon(AddonModel):
|
|||||||
self.persist[ATTR_PROTECTED] = value
|
self.persist[ATTR_PROTECTED] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ports(self) -> Optional[Dict[str, Optional[int]]]:
|
def ports(self) -> Optional[dict[str, Optional[int]]]:
|
||||||
"""Return ports of add-on."""
|
"""Return ports of add-on."""
|
||||||
return self.persist.get(ATTR_NETWORK, super().ports)
|
return self.persist.get(ATTR_NETWORK, super().ports)
|
||||||
|
|
||||||
@ports.setter
|
@ports.setter
|
||||||
def ports(self, value: Optional[Dict[str, Optional[int]]]) -> None:
|
def ports(self, value: Optional[dict[str, Optional[int]]]) -> None:
|
||||||
"""Set custom ports of add-on."""
|
"""Set custom ports of add-on."""
|
||||||
if value is None:
|
if value is None:
|
||||||
self.persist.pop(ATTR_NETWORK, None)
|
self.persist.pop(ATTR_NETWORK, None)
|
||||||
@@ -397,18 +434,22 @@ class Addon(AddonModel):
|
|||||||
return Path(self.sys_config.path_extern_tmp, f"{self.slug}_pulse")
|
return Path(self.sys_config.path_extern_tmp, f"{self.slug}_pulse")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def devices(self) -> Set[Device]:
|
def devices(self) -> set[Device]:
|
||||||
"""Create a schema for add-on options."""
|
"""Extract devices from add-on options."""
|
||||||
raw_schema = self.data[ATTR_SCHEMA]
|
options_schema = self.schema
|
||||||
if isinstance(raw_schema, bool) or not raw_schema:
|
|
||||||
return set()
|
|
||||||
|
|
||||||
# Validate devices
|
|
||||||
options_validator = AddonOptions(self.coresys, raw_schema)
|
|
||||||
with suppress(vol.Invalid):
|
with suppress(vol.Invalid):
|
||||||
options_validator(self.options)
|
options_schema.validate(self.options)
|
||||||
|
|
||||||
return options_validator.devices
|
return options_schema.devices
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pwned(self) -> set[str]:
|
||||||
|
"""Extract pwned data for add-on options."""
|
||||||
|
options_schema = self.schema
|
||||||
|
with suppress(vol.Invalid):
|
||||||
|
options_schema.validate(self.options)
|
||||||
|
|
||||||
|
return options_schema.pwned
|
||||||
|
|
||||||
def save_persist(self) -> None:
|
def save_persist(self) -> None:
|
||||||
"""Save data of add-on."""
|
"""Save data of add-on."""
|
||||||
@@ -422,7 +463,7 @@ class Addon(AddonModel):
|
|||||||
application = RE_WATCHDOG.match(url)
|
application = RE_WATCHDOG.match(url)
|
||||||
|
|
||||||
# extract arguments
|
# extract arguments
|
||||||
t_port = application.group("t_port")
|
t_port = int(application.group("t_port"))
|
||||||
t_proto = application.group("t_proto")
|
t_proto = application.group("t_proto")
|
||||||
s_prefix = application.group("s_prefix") or ""
|
s_prefix = application.group("s_prefix") or ""
|
||||||
s_suffix = application.group("s_suffix") or ""
|
s_suffix = application.group("s_suffix") or ""
|
||||||
@@ -446,8 +487,8 @@ class Addon(AddonModel):
|
|||||||
# Make HTTP request
|
# Make HTTP request
|
||||||
try:
|
try:
|
||||||
url = f"{proto}://{self.ip_address}:{port}{s_suffix}"
|
url = f"{proto}://{self.ip_address}:{port}{s_suffix}"
|
||||||
async with self.sys_websession_ssl.get(
|
async with self.sys_websession.get(
|
||||||
url, timeout=WATCHDOG_TIMEOUT
|
url, timeout=WATCHDOG_TIMEOUT, ssl=False
|
||||||
) as req:
|
) as req:
|
||||||
if req.status < 300:
|
if req.status < 300:
|
||||||
return True
|
return True
|
||||||
@@ -462,7 +503,7 @@ class Addon(AddonModel):
|
|||||||
await self.sys_homeassistant.secrets.reload()
|
await self.sys_homeassistant.secrets.reload()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
options = self.schema(self.options)
|
options = self.schema.validate(self.options)
|
||||||
write_json_file(self.path_options, options)
|
write_json_file(self.path_options, options)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
@@ -470,7 +511,7 @@ class Addon(AddonModel):
|
|||||||
self.slug,
|
self.slug,
|
||||||
humanize_error(self.options, ex),
|
humanize_error(self.options, ex),
|
||||||
)
|
)
|
||||||
except JsonFileError:
|
except ConfigurationFileError:
|
||||||
_LOGGER.error("Add-on %s can't write options", self.slug)
|
_LOGGER.error("Add-on %s can't write options", self.slug)
|
||||||
else:
|
else:
|
||||||
_LOGGER.debug("Add-on %s write options: %s", self.slug, options)
|
_LOGGER.debug("Add-on %s write options: %s", self.slug, options)
|
||||||
@@ -498,8 +539,7 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
# Write pulse config
|
# Write pulse config
|
||||||
try:
|
try:
|
||||||
with self.path_pulse.open("w") as config_file:
|
self.path_pulse.write_text(pulse_config, encoding="utf-8")
|
||||||
config_file.write(pulse_config)
|
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Add-on %s can't write pulse/client.config: %s", self.slug, err
|
"Add-on %s can't write pulse/client.config: %s", self.slug, err
|
||||||
@@ -551,7 +591,9 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
# create voluptuous
|
# create voluptuous
|
||||||
new_schema = vol.Schema(
|
new_schema = vol.Schema(
|
||||||
vol.All(dict, AddonOptions(self.coresys, new_raw_schema))
|
vol.All(
|
||||||
|
dict, AddonOptions(self.coresys, new_raw_schema, self.name, self.slug)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# validate
|
# validate
|
||||||
@@ -583,7 +625,7 @@ class Addon(AddonModel):
|
|||||||
try:
|
try:
|
||||||
await self.instance.run()
|
await self.instance.run()
|
||||||
except DockerRequestError as err:
|
except DockerRequestError as err:
|
||||||
self.state = AddonState.STOPPED
|
self.state = AddonState.ERROR
|
||||||
raise AddonsError() from err
|
raise AddonsError() from err
|
||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
self.state = AddonState.ERROR
|
self.state = AddonState.ERROR
|
||||||
@@ -596,6 +638,7 @@ class Addon(AddonModel):
|
|||||||
try:
|
try:
|
||||||
await self.instance.stop()
|
await self.instance.stop()
|
||||||
except DockerRequestError as err:
|
except DockerRequestError as err:
|
||||||
|
self.state = AddonState.ERROR
|
||||||
raise AddonsError() from err
|
raise AddonsError() from err
|
||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
self.state = AddonState.ERROR
|
self.state = AddonState.ERROR
|
||||||
@@ -636,16 +679,34 @@ class Addon(AddonModel):
|
|||||||
Return a coroutine.
|
Return a coroutine.
|
||||||
"""
|
"""
|
||||||
if not self.with_stdin:
|
if not self.with_stdin:
|
||||||
_LOGGER.error("Add-on %s does not support writing to stdin!", self.slug)
|
raise AddonsNotSupportedError(
|
||||||
raise AddonsNotSupportedError()
|
f"Add-on {self.slug} does not support writing to stdin!", _LOGGER.error
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await self.instance.write_stdin(data)
|
return await self.instance.write_stdin(data)
|
||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
raise AddonsError() from err
|
raise AddonsError() from err
|
||||||
|
|
||||||
async def snapshot(self, tar_file: tarfile.TarFile) -> None:
|
async def _backup_command(self, command: str) -> None:
|
||||||
"""Snapshot state of an add-on."""
|
try:
|
||||||
|
command_return = await self.instance.run_inside(command)
|
||||||
|
if command_return.exit_code != 0:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Pre-/Post backup command returned error code: %s",
|
||||||
|
command_return.exit_code,
|
||||||
|
)
|
||||||
|
raise AddonsError()
|
||||||
|
except DockerError as err:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Failed running pre-/post backup command %s: %s", command, err
|
||||||
|
)
|
||||||
|
raise AddonsError() from err
|
||||||
|
|
||||||
|
async def backup(self, tar_file: tarfile.TarFile) -> None:
|
||||||
|
"""Backup state of an add-on."""
|
||||||
|
is_running = await self.is_running()
|
||||||
|
|
||||||
with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp:
|
with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp:
|
||||||
temp_path = Path(temp)
|
temp_path = Path(temp)
|
||||||
|
|
||||||
@@ -666,9 +727,10 @@ class Addon(AddonModel):
|
|||||||
# Store local configs/state
|
# Store local configs/state
|
||||||
try:
|
try:
|
||||||
write_json_file(temp_path.joinpath("addon.json"), data)
|
write_json_file(temp_path.joinpath("addon.json"), data)
|
||||||
except JsonFileError as err:
|
except ConfigurationFileError as err:
|
||||||
_LOGGER.error("Can't save meta for %s", self.slug)
|
raise AddonsError(
|
||||||
raise AddonsError() from err
|
f"Can't save meta for {self.slug}", _LOGGER.error
|
||||||
|
) from err
|
||||||
|
|
||||||
# Store AppArmor Profile
|
# Store AppArmor Profile
|
||||||
if self.sys_host.apparmor.exists(self.slug):
|
if self.sys_host.apparmor.exists(self.slug):
|
||||||
@@ -676,61 +738,84 @@ class Addon(AddonModel):
|
|||||||
try:
|
try:
|
||||||
self.sys_host.apparmor.backup_profile(self.slug, profile)
|
self.sys_host.apparmor.backup_profile(self.slug, profile)
|
||||||
except HostAppArmorError as err:
|
except HostAppArmorError as err:
|
||||||
_LOGGER.error("Can't backup AppArmor profile")
|
raise AddonsError(
|
||||||
raise AddonsError() from err
|
"Can't backup AppArmor profile", _LOGGER.error
|
||||||
|
) from err
|
||||||
|
|
||||||
# write into tarfile
|
# write into tarfile
|
||||||
def _write_tarfile():
|
def _write_tarfile():
|
||||||
"""Write tar inside loop."""
|
"""Write tar inside loop."""
|
||||||
with tar_file as snapshot:
|
with tar_file as backup:
|
||||||
# Snapshot system
|
# Backup system
|
||||||
|
|
||||||
snapshot.add(temp, arcname=".")
|
backup.add(temp, arcname=".")
|
||||||
|
|
||||||
# Snapshot data
|
# Backup data
|
||||||
atomic_contents_add(
|
atomic_contents_add(
|
||||||
snapshot,
|
backup,
|
||||||
self.path_data,
|
self.path_data,
|
||||||
excludes=self.snapshot_exclude,
|
excludes=self.backup_exclude,
|
||||||
arcname="data",
|
arcname="data",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
is_running
|
||||||
|
and self.backup_mode == AddonBackupMode.HOT
|
||||||
|
and self.backup_pre is not None
|
||||||
|
):
|
||||||
|
await self._backup_command(self.backup_pre)
|
||||||
|
elif is_running and self.backup_mode == AddonBackupMode.COLD:
|
||||||
|
_LOGGER.info("Shutdown add-on %s for cold backup", self.slug)
|
||||||
|
await self.instance.stop()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_LOGGER.info("Building snapshot for add-on %s", self.slug)
|
_LOGGER.info("Building backup for add-on %s", self.slug)
|
||||||
await self.sys_run_in_executor(_write_tarfile)
|
await self.sys_run_in_executor(_write_tarfile)
|
||||||
except (tarfile.TarError, OSError) as err:
|
except (tarfile.TarError, OSError) as err:
|
||||||
_LOGGER.error("Can't write tarfile %s: %s", tar_file, err)
|
raise AddonsError(
|
||||||
raise AddonsError() from err
|
f"Can't write tarfile {tar_file}: {err}", _LOGGER.error
|
||||||
|
) from err
|
||||||
|
finally:
|
||||||
|
if (
|
||||||
|
is_running
|
||||||
|
and self.backup_mode == AddonBackupMode.HOT
|
||||||
|
and self.backup_post is not None
|
||||||
|
):
|
||||||
|
await self._backup_command(self.backup_post)
|
||||||
|
elif is_running and self.backup_mode is AddonBackupMode.COLD:
|
||||||
|
_LOGGER.info("Starting add-on %s again", self.slug)
|
||||||
|
await self.start()
|
||||||
|
|
||||||
_LOGGER.info("Finish snapshot for addon %s", self.slug)
|
_LOGGER.info("Finish backup for addon %s", self.slug)
|
||||||
|
|
||||||
async def restore(self, tar_file: tarfile.TarFile) -> None:
|
async def restore(self, tar_file: tarfile.TarFile) -> None:
|
||||||
"""Restore state of an add-on."""
|
"""Restore state of an add-on."""
|
||||||
with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp:
|
with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp:
|
||||||
# extract snapshot
|
# extract backup
|
||||||
def _extract_tarfile():
|
def _extract_tarfile():
|
||||||
"""Extract tar snapshot."""
|
"""Extract tar backup."""
|
||||||
with tar_file as snapshot:
|
with tar_file as backup:
|
||||||
snapshot.extractall(path=Path(temp), members=secure_path(snapshot))
|
backup.extractall(path=Path(temp), members=secure_path(backup))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.sys_run_in_executor(_extract_tarfile)
|
await self.sys_run_in_executor(_extract_tarfile)
|
||||||
except tarfile.TarError as err:
|
except tarfile.TarError as err:
|
||||||
_LOGGER.error("Can't read tarfile %s: %s", tar_file, err)
|
raise AddonsError(
|
||||||
raise AddonsError() from err
|
f"Can't read tarfile {tar_file}: {err}", _LOGGER.error
|
||||||
|
) from err
|
||||||
|
|
||||||
# Read snapshot data
|
# Read backup data
|
||||||
try:
|
try:
|
||||||
data = read_json_file(Path(temp, "addon.json"))
|
data = read_json_file(Path(temp, "addon.json"))
|
||||||
except JsonFileError as err:
|
except ConfigurationFileError as err:
|
||||||
raise AddonsError() from err
|
raise AddonsError() from err
|
||||||
|
|
||||||
# Validate
|
# Validate
|
||||||
try:
|
try:
|
||||||
data = SCHEMA_ADDON_SNAPSHOT(data)
|
data = SCHEMA_ADDON_BACKUP(data)
|
||||||
except vol.Invalid as err:
|
except vol.Invalid as err:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Can't validate %s, snapshot data: %s",
|
"Can't validate %s, backup data: %s",
|
||||||
self.slug,
|
self.slug,
|
||||||
humanize_error(data, err),
|
humanize_error(data, err),
|
||||||
)
|
)
|
||||||
@@ -738,8 +823,10 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
# If available
|
# If available
|
||||||
if not self._available(data[ATTR_SYSTEM]):
|
if not self._available(data[ATTR_SYSTEM]):
|
||||||
_LOGGER.error("Add-on %s is not available for this platform", self.slug)
|
raise AddonsNotSupportedError(
|
||||||
raise AddonsNotSupportedError()
|
f"Add-on {self.slug} is not available for this platform",
|
||||||
|
_LOGGER.error,
|
||||||
|
)
|
||||||
|
|
||||||
# Restore local add-on information
|
# Restore local add-on information
|
||||||
_LOGGER.info("Restore config for addon %s", self.slug)
|
_LOGGER.info("Restore config for addon %s", self.slug)
|
||||||
@@ -780,8 +867,9 @@ class Addon(AddonModel):
|
|||||||
try:
|
try:
|
||||||
await self.sys_run_in_executor(_restore_data)
|
await self.sys_run_in_executor(_restore_data)
|
||||||
except shutil.Error as err:
|
except shutil.Error as err:
|
||||||
_LOGGER.error("Can't restore origin data: %s", err)
|
raise AddonsError(
|
||||||
raise AddonsError() from err
|
f"Can't restore origin data: {err}", _LOGGER.error
|
||||||
|
) from err
|
||||||
|
|
||||||
# Restore AppArmor
|
# Restore AppArmor
|
||||||
profile_file = Path(temp, "apparmor.txt")
|
profile_file = Path(temp, "apparmor.txt")
|
||||||
|
@@ -2,20 +2,28 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Dict
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
|
|
||||||
from ..const import ATTR_ARGS, ATTR_BUILD_FROM, ATTR_SQUASH, META_ADDON
|
from ..const import (
|
||||||
|
ATTR_ARGS,
|
||||||
|
ATTR_BUILD_FROM,
|
||||||
|
ATTR_LABELS,
|
||||||
|
ATTR_SQUASH,
|
||||||
|
FILE_SUFFIX_CONFIGURATION,
|
||||||
|
META_ADDON,
|
||||||
|
)
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..utils.json import JsonConfig
|
from ..exceptions import ConfigurationFileError
|
||||||
|
from ..utils.common import FileConfiguration, find_one_filetype
|
||||||
from .validate import SCHEMA_BUILD_CONFIG
|
from .validate import SCHEMA_BUILD_CONFIG
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import AnyAddon
|
from . import AnyAddon
|
||||||
|
|
||||||
|
|
||||||
class AddonBuild(JsonConfig, CoreSysAttributes):
|
class AddonBuild(FileConfiguration, CoreSysAttributes):
|
||||||
"""Handle build options for add-ons."""
|
"""Handle build options for add-ons."""
|
||||||
|
|
||||||
def __init__(self, coresys: CoreSys, addon: AnyAddon) -> None:
|
def __init__(self, coresys: CoreSys, addon: AnyAddon) -> None:
|
||||||
@@ -23,9 +31,14 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
|||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self.addon = addon
|
self.addon = addon
|
||||||
|
|
||||||
super().__init__(
|
try:
|
||||||
Path(self.addon.path_location, "build.json"), SCHEMA_BUILD_CONFIG
|
build_file = find_one_filetype(
|
||||||
)
|
self.addon.path_location, "build", FILE_SUFFIX_CONFIGURATION
|
||||||
|
)
|
||||||
|
except ConfigurationFileError:
|
||||||
|
build_file = self.addon.path_location / "build.json"
|
||||||
|
|
||||||
|
super().__init__(build_file, SCHEMA_BUILD_CONFIG)
|
||||||
|
|
||||||
def save_data(self):
|
def save_data(self):
|
||||||
"""Ignore save function."""
|
"""Ignore save function."""
|
||||||
@@ -34,9 +47,12 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
|||||||
@property
|
@property
|
||||||
def base_image(self) -> str:
|
def base_image(self) -> str:
|
||||||
"""Return base image for this add-on."""
|
"""Return base image for this add-on."""
|
||||||
return self._data[ATTR_BUILD_FROM].get(
|
if not self._data[ATTR_BUILD_FROM]:
|
||||||
self.sys_arch.default, f"homeassistant/{self.sys_arch.default}-base:latest"
|
return f"ghcr.io/home-assistant/{self.sys_arch.default}-base:latest"
|
||||||
)
|
|
||||||
|
# Evaluate correct base image
|
||||||
|
arch = self.sys_arch.match(list(self._data[ATTR_BUILD_FROM].keys()))
|
||||||
|
return self._data[ATTR_BUILD_FROM][arch]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def squash(self) -> bool:
|
def squash(self) -> bool:
|
||||||
@@ -44,10 +60,15 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
|||||||
return self._data[ATTR_SQUASH]
|
return self._data[ATTR_SQUASH]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def additional_args(self) -> Dict[str, str]:
|
def additional_args(self) -> dict[str, str]:
|
||||||
"""Return additional Docker build arguments."""
|
"""Return additional Docker build arguments."""
|
||||||
return self._data[ATTR_ARGS]
|
return self._data[ATTR_ARGS]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def additional_labels(self) -> dict[str, str]:
|
||||||
|
"""Return additional Docker labels."""
|
||||||
|
return self._data[ATTR_LABELS]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_valid(self) -> bool:
|
def is_valid(self) -> bool:
|
||||||
"""Return true if the build env is valid."""
|
"""Return true if the build env is valid."""
|
||||||
@@ -64,7 +85,7 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
|||||||
"path": str(self.addon.path_location),
|
"path": str(self.addon.path_location),
|
||||||
"tag": f"{self.addon.image}:{version!s}",
|
"tag": f"{self.addon.image}:{version!s}",
|
||||||
"pull": True,
|
"pull": True,
|
||||||
"forcerm": True,
|
"forcerm": not self.sys_dev,
|
||||||
"squash": self.squash,
|
"squash": self.squash,
|
||||||
"labels": {
|
"labels": {
|
||||||
"io.hass.version": version,
|
"io.hass.version": version,
|
||||||
@@ -72,6 +93,7 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
|||||||
"io.hass.type": META_ADDON,
|
"io.hass.type": META_ADDON,
|
||||||
"io.hass.name": self._fix_label("name"),
|
"io.hass.name": self._fix_label("name"),
|
||||||
"io.hass.description": self._fix_label("description"),
|
"io.hass.description": self._fix_label("description"),
|
||||||
|
**self.additional_labels,
|
||||||
},
|
},
|
||||||
"buildargs": {
|
"buildargs": {
|
||||||
"BUILD_FROM": self.base_image,
|
"BUILD_FROM": self.base_image,
|
||||||
|
12
supervisor/addons/const.py
Normal file
12
supervisor/addons/const.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
"""Add-on static data."""
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class AddonBackupMode(str, Enum):
|
||||||
|
"""Backup mode of an Add-on."""
|
||||||
|
|
||||||
|
HOT = "hot"
|
||||||
|
COLD = "cold"
|
||||||
|
|
||||||
|
|
||||||
|
ATTR_BACKUP = "backup"
|
@@ -1,7 +1,6 @@
|
|||||||
"""Init file for Supervisor add-on data."""
|
"""Init file for Supervisor add-on data."""
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
import logging
|
from typing import Any
|
||||||
from typing import Any, Dict
|
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_IMAGE,
|
ATTR_IMAGE,
|
||||||
@@ -13,16 +12,14 @@ from ..const import (
|
|||||||
)
|
)
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..store.addon import AddonStore
|
from ..store.addon import AddonStore
|
||||||
from ..utils.json import JsonConfig
|
from ..utils.common import FileConfiguration
|
||||||
from .addon import Addon
|
from .addon import Addon
|
||||||
from .validate import SCHEMA_ADDONS_FILE
|
from .validate import SCHEMA_ADDONS_FILE
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
Config = dict[str, Any]
|
||||||
|
|
||||||
Config = Dict[str, Any]
|
|
||||||
|
|
||||||
|
|
||||||
class AddonsData(JsonConfig, CoreSysAttributes):
|
class AddonsData(FileConfiguration, CoreSysAttributes):
|
||||||
"""Hold data for installed Add-ons inside Supervisor."""
|
"""Hold data for installed Add-ons inside Supervisor."""
|
||||||
|
|
||||||
def __init__(self, coresys: CoreSys):
|
def __init__(self, coresys: CoreSys):
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
"""Init file for Supervisor add-ons."""
|
"""Init file for Supervisor add-ons."""
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Awaitable, Dict, List, Optional
|
from typing import Any, Awaitable, Optional
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersion, AwesomeVersionException
|
from awesomeversion import AwesomeVersion, AwesomeVersionException
|
||||||
import voluptuous as vol
|
|
||||||
|
from supervisor.addons.const import AddonBackupMode
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_ADVANCED,
|
ATTR_ADVANCED,
|
||||||
@@ -12,6 +13,9 @@ from ..const import (
|
|||||||
ATTR_ARCH,
|
ATTR_ARCH,
|
||||||
ATTR_AUDIO,
|
ATTR_AUDIO,
|
||||||
ATTR_AUTH_API,
|
ATTR_AUTH_API,
|
||||||
|
ATTR_BACKUP_EXCLUDE,
|
||||||
|
ATTR_BACKUP_POST,
|
||||||
|
ATTR_BACKUP_PRE,
|
||||||
ATTR_BOOT,
|
ATTR_BOOT,
|
||||||
ATTR_DESCRIPTON,
|
ATTR_DESCRIPTON,
|
||||||
ATTR_DEVICES,
|
ATTR_DEVICES,
|
||||||
@@ -31,7 +35,9 @@ from ..const import (
|
|||||||
ATTR_HOST_PID,
|
ATTR_HOST_PID,
|
||||||
ATTR_IMAGE,
|
ATTR_IMAGE,
|
||||||
ATTR_INGRESS,
|
ATTR_INGRESS,
|
||||||
|
ATTR_INGRESS_STREAM,
|
||||||
ATTR_INIT,
|
ATTR_INIT,
|
||||||
|
ATTR_JOURNALD,
|
||||||
ATTR_KERNEL_MODULES,
|
ATTR_KERNEL_MODULES,
|
||||||
ATTR_LEGACY,
|
ATTR_LEGACY,
|
||||||
ATTR_LOCATON,
|
ATTR_LOCATON,
|
||||||
@@ -45,16 +51,17 @@ from ..const import (
|
|||||||
ATTR_PORTS,
|
ATTR_PORTS,
|
||||||
ATTR_PORTS_DESCRIPTION,
|
ATTR_PORTS_DESCRIPTION,
|
||||||
ATTR_PRIVILEGED,
|
ATTR_PRIVILEGED,
|
||||||
|
ATTR_REALTIME,
|
||||||
ATTR_REPOSITORY,
|
ATTR_REPOSITORY,
|
||||||
ATTR_SCHEMA,
|
ATTR_SCHEMA,
|
||||||
ATTR_SERVICES,
|
ATTR_SERVICES,
|
||||||
ATTR_SLUG,
|
ATTR_SLUG,
|
||||||
ATTR_SNAPSHOT_EXCLUDE,
|
|
||||||
ATTR_STAGE,
|
ATTR_STAGE,
|
||||||
ATTR_STARTUP,
|
ATTR_STARTUP,
|
||||||
ATTR_STDIN,
|
ATTR_STDIN,
|
||||||
ATTR_TIMEOUT,
|
ATTR_TIMEOUT,
|
||||||
ATTR_TMPFS,
|
ATTR_TMPFS,
|
||||||
|
ATTR_TRANSLATIONS,
|
||||||
ATTR_UART,
|
ATTR_UART,
|
||||||
ATTR_UDEV,
|
ATTR_UDEV,
|
||||||
ATTR_URL,
|
ATTR_URL,
|
||||||
@@ -71,10 +78,12 @@ from ..const import (
|
|||||||
AddonStartup,
|
AddonStartup,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
|
from ..docker.const import Capabilities
|
||||||
|
from .const import ATTR_BACKUP
|
||||||
from .options import AddonOptions, UiOptions
|
from .options import AddonOptions, UiOptions
|
||||||
from .validate import RE_SERVICE, RE_VOLUME
|
from .validate import RE_SERVICE, RE_VOLUME
|
||||||
|
|
||||||
Data = Dict[str, Any]
|
Data = dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
class AddonModel(CoreSysAttributes, ABC):
|
class AddonModel(CoreSysAttributes, ABC):
|
||||||
@@ -106,7 +115,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self._available(self.data)
|
return self._available(self.data)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def options(self) -> Dict[str, Any]:
|
def options(self) -> dict[str, Any]:
|
||||||
"""Return options with local changes."""
|
"""Return options with local changes."""
|
||||||
return self.data[ATTR_OPTIONS]
|
return self.data[ATTR_OPTIONS]
|
||||||
|
|
||||||
@@ -131,7 +140,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.slug.replace("_", "-")
|
return self.slug.replace("_", "-")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dns(self) -> List[str]:
|
def dns(self) -> list[str]:
|
||||||
"""Return list of DNS name for that add-on."""
|
"""Return list of DNS name for that add-on."""
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -175,14 +184,18 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Return data
|
# Return data
|
||||||
with readme.open("r") as readme_file:
|
return readme.read_text(encoding="utf-8")
|
||||||
return readme_file.read()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def repository(self) -> str:
|
def repository(self) -> str:
|
||||||
"""Return repository of add-on."""
|
"""Return repository of add-on."""
|
||||||
return self.data[ATTR_REPOSITORY]
|
return self.data[ATTR_REPOSITORY]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def translations(self) -> dict:
|
||||||
|
"""Return add-on translations."""
|
||||||
|
return self.data[ATTR_TRANSLATIONS]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latest_version(self) -> AwesomeVersion:
|
def latest_version(self) -> AwesomeVersion:
|
||||||
"""Return latest version of add-on."""
|
"""Return latest version of add-on."""
|
||||||
@@ -214,7 +227,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data[ATTR_STAGE]
|
return self.data[ATTR_STAGE]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def services_role(self) -> Dict[str, str]:
|
def services_role(self) -> dict[str, str]:
|
||||||
"""Return dict of services with rights."""
|
"""Return dict of services with rights."""
|
||||||
services_list = self.data.get(ATTR_SERVICES, [])
|
services_list = self.data.get(ATTR_SERVICES, [])
|
||||||
|
|
||||||
@@ -227,17 +240,17 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return services
|
return services
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def discovery(self) -> List[str]:
|
def discovery(self) -> list[str]:
|
||||||
"""Return list of discoverable components/platforms."""
|
"""Return list of discoverable components/platforms."""
|
||||||
return self.data.get(ATTR_DISCOVERY, [])
|
return self.data.get(ATTR_DISCOVERY, [])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ports_description(self) -> Optional[Dict[str, str]]:
|
def ports_description(self) -> Optional[dict[str, str]]:
|
||||||
"""Return descriptions of ports."""
|
"""Return descriptions of ports."""
|
||||||
return self.data.get(ATTR_PORTS_DESCRIPTION)
|
return self.data.get(ATTR_PORTS_DESCRIPTION)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ports(self) -> Optional[Dict[str, Optional[int]]]:
|
def ports(self) -> Optional[dict[str, Optional[int]]]:
|
||||||
"""Return ports of add-on."""
|
"""Return ports of add-on."""
|
||||||
return self.data.get(ATTR_PORTS)
|
return self.data.get(ATTR_PORTS)
|
||||||
|
|
||||||
@@ -297,17 +310,17 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data[ATTR_HOST_DBUS]
|
return self.data[ATTR_HOST_DBUS]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def static_devices(self) -> List[Path]:
|
def static_devices(self) -> list[Path]:
|
||||||
"""Return static devices of add-on."""
|
"""Return static devices of add-on."""
|
||||||
return [Path(node) for node in self.data.get(ATTR_DEVICES, [])]
|
return [Path(node) for node in self.data.get(ATTR_DEVICES, [])]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def environment(self) -> Optional[Dict[str, str]]:
|
def environment(self) -> Optional[dict[str, str]]:
|
||||||
"""Return environment of add-on."""
|
"""Return environment of add-on."""
|
||||||
return self.data.get(ATTR_ENVIRONMENT)
|
return self.data.get(ATTR_ENVIRONMENT)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def privileged(self) -> List[str]:
|
def privileged(self) -> list[Capabilities]:
|
||||||
"""Return list of privilege."""
|
"""Return list of privilege."""
|
||||||
return self.data.get(ATTR_PRIVILEGED, [])
|
return self.data.get(ATTR_PRIVILEGED, [])
|
||||||
|
|
||||||
@@ -346,9 +359,24 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data[ATTR_HASSIO_ROLE]
|
return self.data[ATTR_HASSIO_ROLE]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def snapshot_exclude(self) -> List[str]:
|
def backup_exclude(self) -> list[str]:
|
||||||
"""Return Exclude list for snapshot."""
|
"""Return Exclude list for backup."""
|
||||||
return self.data.get(ATTR_SNAPSHOT_EXCLUDE, [])
|
return self.data.get(ATTR_BACKUP_EXCLUDE, [])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backup_pre(self) -> Optional[str]:
|
||||||
|
"""Return pre-backup command."""
|
||||||
|
return self.data.get(ATTR_BACKUP_PRE)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backup_post(self) -> Optional[str]:
|
||||||
|
"""Return post-backup command."""
|
||||||
|
return self.data.get(ATTR_BACKUP_POST)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backup_mode(self) -> AddonBackupMode:
|
||||||
|
"""Return if backup is hot/cold."""
|
||||||
|
return self.data[ATTR_BACKUP]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_init(self) -> bool:
|
def default_init(self) -> bool:
|
||||||
@@ -370,6 +398,11 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
"""Return True if the add-on access support ingress."""
|
"""Return True if the add-on access support ingress."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ingress_stream(self) -> bool:
|
||||||
|
"""Return True if post requests to ingress should be streamed."""
|
||||||
|
return self.data[ATTR_INGRESS_STREAM]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def with_gpio(self) -> bool:
|
def with_gpio(self) -> bool:
|
||||||
"""Return True if the add-on access to GPIO interface."""
|
"""Return True if the add-on access to GPIO interface."""
|
||||||
@@ -395,6 +428,11 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
"""Return True if the add-on access to kernel modules."""
|
"""Return True if the add-on access to kernel modules."""
|
||||||
return self.data[ATTR_KERNEL_MODULES]
|
return self.data[ATTR_KERNEL_MODULES]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_realtime(self) -> bool:
|
||||||
|
"""Return True if the add-on need realtime schedule functions."""
|
||||||
|
return self.data[ATTR_REALTIME]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def with_full_access(self) -> bool:
|
def with_full_access(self) -> bool:
|
||||||
"""Return True if the add-on want full access to hardware."""
|
"""Return True if the add-on want full access to hardware."""
|
||||||
@@ -456,12 +494,12 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.path_documentation.exists()
|
return self.path_documentation.exists()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_arch(self) -> List[str]:
|
def supported_arch(self) -> list[str]:
|
||||||
"""Return list of supported arch."""
|
"""Return list of supported arch."""
|
||||||
return self.data[ATTR_ARCH]
|
return self.data[ATTR_ARCH]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_machine(self) -> List[str]:
|
def supported_machine(self) -> list[str]:
|
||||||
"""Return list of supported machine."""
|
"""Return list of supported machine."""
|
||||||
return self.data.get(ATTR_MACHINE, [])
|
return self.data.get(ATTR_MACHINE, [])
|
||||||
|
|
||||||
@@ -476,7 +514,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return ATTR_IMAGE not in self.data
|
return ATTR_IMAGE not in self.data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def map_volumes(self) -> Dict[str, str]:
|
def map_volumes(self) -> dict[str, str]:
|
||||||
"""Return a dict of {volume: policy} from add-on."""
|
"""Return a dict of {volume: policy} from add-on."""
|
||||||
volumes = {}
|
volumes = {}
|
||||||
for volume in self.data[ATTR_MAP]:
|
for volume in self.data[ATTR_MAP]:
|
||||||
@@ -518,16 +556,16 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return Path(self.path_location, "apparmor.txt")
|
return Path(self.path_location, "apparmor.txt")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def schema(self) -> vol.Schema:
|
def schema(self) -> AddonOptions:
|
||||||
"""Create a schema for add-on options."""
|
"""Return Addon options validation object."""
|
||||||
raw_schema = self.data[ATTR_SCHEMA]
|
raw_schema = self.data[ATTR_SCHEMA]
|
||||||
|
|
||||||
if isinstance(raw_schema, bool):
|
if isinstance(raw_schema, bool):
|
||||||
raw_schema = {}
|
raw_schema = {}
|
||||||
return vol.Schema(vol.All(dict, AddonOptions(self.coresys, raw_schema)))
|
|
||||||
|
return AddonOptions(self.coresys, raw_schema, self.name, self.slug)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def schema_ui(self) -> Optional[List[Dict[str, Any]]]:
|
def schema_ui(self) -> Optional[list[dict[any, any]]]:
|
||||||
"""Create a UI schema for add-on options."""
|
"""Create a UI schema for add-on options."""
|
||||||
raw_schema = self.data[ATTR_SCHEMA]
|
raw_schema = self.data[ATTR_SCHEMA]
|
||||||
|
|
||||||
@@ -535,6 +573,11 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return None
|
return None
|
||||||
return UiOptions(self.coresys)(raw_schema)
|
return UiOptions(self.coresys)(raw_schema)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_journald(self) -> bool:
|
||||||
|
"""Return True if the add-on accesses the system journal."""
|
||||||
|
return self.data[ATTR_JOURNALD]
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
"""Compaired add-on objects."""
|
"""Compaired add-on objects."""
|
||||||
if not isinstance(other, AddonModel):
|
if not isinstance(other, AddonModel):
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
"""Add-on Options / UI rendering."""
|
"""Add-on Options / UI rendering."""
|
||||||
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict, List, Set, Union
|
from typing import Any, Union
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -58,14 +59,20 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
"""Validate Add-ons Options."""
|
"""Validate Add-ons Options."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, coresys: CoreSys, raw_schema: dict[str, Any], name: str, slug: str
|
||||||
coresys: CoreSys,
|
|
||||||
raw_schema: Dict[str, Any],
|
|
||||||
):
|
):
|
||||||
"""Validate schema."""
|
"""Validate schema."""
|
||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self.raw_schema: Dict[str, Any] = raw_schema
|
self.raw_schema: dict[str, Any] = raw_schema
|
||||||
self.devices: Set[Device] = set()
|
self.devices: set[Device] = set()
|
||||||
|
self.pwned: set[str] = set()
|
||||||
|
self._name = name
|
||||||
|
self._slug = slug
|
||||||
|
|
||||||
|
@property
|
||||||
|
def validate(self) -> vol.Schema:
|
||||||
|
"""Create a schema for add-on options."""
|
||||||
|
return vol.Schema(vol.All(dict, self))
|
||||||
|
|
||||||
def __call__(self, struct):
|
def __call__(self, struct):
|
||||||
"""Create schema validator for add-ons options."""
|
"""Create schema validator for add-ons options."""
|
||||||
@@ -75,7 +82,12 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
for key, value in struct.items():
|
for key, value in struct.items():
|
||||||
# Ignore unknown options / remove from list
|
# Ignore unknown options / remove from list
|
||||||
if key not in self.raw_schema:
|
if key not in self.raw_schema:
|
||||||
_LOGGER.warning("Unknown options %s", key)
|
_LOGGER.warning(
|
||||||
|
"Option '%s' does not exist in the schema for %s (%s)",
|
||||||
|
key,
|
||||||
|
self._name,
|
||||||
|
self._slug,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
typ = self.raw_schema[key]
|
typ = self.raw_schema[key]
|
||||||
@@ -90,7 +102,9 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
# normal value
|
# normal value
|
||||||
options[key] = self._single_validate(typ, value, key)
|
options[key] = self._single_validate(typ, value, key)
|
||||||
except (IndexError, KeyError):
|
except (IndexError, KeyError):
|
||||||
raise vol.Invalid(f"Type error for {key}") from None
|
raise vol.Invalid(
|
||||||
|
f"Type error for option '{key}' in {self._name} ({self._slug})"
|
||||||
|
) from None
|
||||||
|
|
||||||
self._check_missing_options(self.raw_schema, options, "root")
|
self._check_missing_options(self.raw_schema, options, "root")
|
||||||
return options
|
return options
|
||||||
@@ -100,20 +114,26 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
"""Validate a single element."""
|
"""Validate a single element."""
|
||||||
# if required argument
|
# if required argument
|
||||||
if value is None:
|
if value is None:
|
||||||
raise vol.Invalid(f"Missing required option '{key}'") from None
|
raise vol.Invalid(
|
||||||
|
f"Missing required option '{key}' in {self._name} ({self._slug})"
|
||||||
|
) from None
|
||||||
|
|
||||||
# Lookup secret
|
# Lookup secret
|
||||||
if str(value).startswith("!secret "):
|
if str(value).startswith("!secret "):
|
||||||
secret: str = value.partition(" ")[2]
|
secret: str = value.partition(" ")[2]
|
||||||
value = self.sys_homeassistant.secrets.get(secret)
|
value = self.sys_homeassistant.secrets.get(secret)
|
||||||
if value is None:
|
if value is None:
|
||||||
raise vol.Invalid(f"Unknown secret {secret}") from None
|
raise vol.Invalid(
|
||||||
|
f"Unknown secret '{secret}' in {self._name} ({self._slug})"
|
||||||
|
) from None
|
||||||
|
|
||||||
# parse extend data from type
|
# parse extend data from type
|
||||||
match = RE_SCHEMA_ELEMENT.match(typ)
|
match = RE_SCHEMA_ELEMENT.match(typ)
|
||||||
|
|
||||||
if not match:
|
if not match:
|
||||||
raise vol.Invalid(f"Unknown type {typ}") from None
|
raise vol.Invalid(
|
||||||
|
f"Unknown type '{typ}' in {self._name} ({self._slug})"
|
||||||
|
) from None
|
||||||
|
|
||||||
# prepare range
|
# prepare range
|
||||||
range_args = {}
|
range_args = {}
|
||||||
@@ -123,6 +143,8 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
range_args[group_name[2:]] = float(group_value)
|
range_args[group_name[2:]] = float(group_value)
|
||||||
|
|
||||||
if typ.startswith(_STR) or typ.startswith(_PASSWORD):
|
if typ.startswith(_STR) or typ.startswith(_PASSWORD):
|
||||||
|
if typ.startswith(_PASSWORD) and value:
|
||||||
|
self.pwned.add(hashlib.sha1(str(value).encode()).hexdigest())
|
||||||
return vol.All(str(value), vol.Range(**range_args))(value)
|
return vol.All(str(value), vol.Range(**range_args))(value)
|
||||||
elif typ.startswith(_INT):
|
elif typ.startswith(_INT):
|
||||||
return vol.All(vol.Coerce(int), vol.Range(**range_args))(value)
|
return vol.All(vol.Coerce(int), vol.Range(**range_args))(value)
|
||||||
@@ -144,7 +166,9 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
try:
|
try:
|
||||||
device = self.sys_hardware.get_by_path(Path(value))
|
device = self.sys_hardware.get_by_path(Path(value))
|
||||||
except HardwareNotFound:
|
except HardwareNotFound:
|
||||||
raise vol.Invalid(f"Device {value} does not exists!") from None
|
raise vol.Invalid(
|
||||||
|
f"Device '{value}' does not exists! in {self._name} ({self._slug})"
|
||||||
|
) from None
|
||||||
|
|
||||||
# Have filter
|
# Have filter
|
||||||
if match.group("filter"):
|
if match.group("filter"):
|
||||||
@@ -152,22 +176,26 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
device_filter = _create_device_filter(str_filter)
|
device_filter = _create_device_filter(str_filter)
|
||||||
if device not in self.sys_hardware.filter_devices(**device_filter):
|
if device not in self.sys_hardware.filter_devices(**device_filter):
|
||||||
raise vol.Invalid(
|
raise vol.Invalid(
|
||||||
f"Device {value} don't match the filter {str_filter}!"
|
f"Device '{value}' don't match the filter {str_filter}! in {self._name} ({self._slug})"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Device valid
|
# Device valid
|
||||||
self.devices.add(device)
|
self.devices.add(device)
|
||||||
return str(device.path)
|
return str(device.path)
|
||||||
|
|
||||||
raise vol.Invalid(f"Fatal error for {key} type {typ}") from None
|
raise vol.Invalid(
|
||||||
|
f"Fatal error for option '{key}' with type '{typ}' in {self._name} ({self._slug})"
|
||||||
|
) from None
|
||||||
|
|
||||||
def _nested_validate_list(self, typ: Any, data_list: List[Any], key: str):
|
def _nested_validate_list(self, typ: Any, data_list: list[Any], key: str):
|
||||||
"""Validate nested items."""
|
"""Validate nested items."""
|
||||||
options = []
|
options = []
|
||||||
|
|
||||||
# Make sure it is a list
|
# Make sure it is a list
|
||||||
if not isinstance(data_list, list):
|
if not isinstance(data_list, list):
|
||||||
raise vol.Invalid(f"Invalid list for {key}") from None
|
raise vol.Invalid(
|
||||||
|
f"Invalid list for option '{key}' in {self._name} ({self._slug})"
|
||||||
|
) from None
|
||||||
|
|
||||||
# Process list
|
# Process list
|
||||||
for element in data_list:
|
for element in data_list:
|
||||||
@@ -181,20 +209,24 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
return options
|
return options
|
||||||
|
|
||||||
def _nested_validate_dict(
|
def _nested_validate_dict(
|
||||||
self, typ: Dict[Any, Any], data_dict: Dict[Any, Any], key: str
|
self, typ: dict[Any, Any], data_dict: dict[Any, Any], key: str
|
||||||
):
|
):
|
||||||
"""Validate nested items."""
|
"""Validate nested items."""
|
||||||
options = {}
|
options = {}
|
||||||
|
|
||||||
# Make sure it is a dict
|
# Make sure it is a dict
|
||||||
if not isinstance(data_dict, dict):
|
if not isinstance(data_dict, dict):
|
||||||
raise vol.Invalid(f"Invalid dict for {key}") from None
|
raise vol.Invalid(
|
||||||
|
f"Invalid dict for option '{key}' in {self._name} ({self._slug})"
|
||||||
|
) from None
|
||||||
|
|
||||||
# Process dict
|
# Process dict
|
||||||
for c_key, c_value in data_dict.items():
|
for c_key, c_value in data_dict.items():
|
||||||
# Ignore unknown options / remove from list
|
# Ignore unknown options / remove from list
|
||||||
if c_key not in typ:
|
if c_key not in typ:
|
||||||
_LOGGER.warning("Unknown options %s", c_key)
|
_LOGGER.warning(
|
||||||
|
"Unknown option '%s' for %s (%s)", c_key, self._name, self._slug
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Nested?
|
# Nested?
|
||||||
@@ -209,14 +241,23 @@ class AddonOptions(CoreSysAttributes):
|
|||||||
return options
|
return options
|
||||||
|
|
||||||
def _check_missing_options(
|
def _check_missing_options(
|
||||||
self, origin: Dict[Any, Any], exists: Dict[Any, Any], root: str
|
self, origin: dict[Any, Any], exists: dict[Any, Any], root: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Check if all options are exists."""
|
"""Check if all options are exists."""
|
||||||
missing = set(origin) - set(exists)
|
missing = set(origin) - set(exists)
|
||||||
for miss_opt in missing:
|
for miss_opt in missing:
|
||||||
if isinstance(origin[miss_opt], str) and origin[miss_opt].endswith("?"):
|
miss_schema = origin[miss_opt]
|
||||||
|
|
||||||
|
# If its a list then value in list decides if its optional like ["str?"]
|
||||||
|
if isinstance(miss_schema, list) and len(miss_schema) > 0:
|
||||||
|
miss_schema = miss_schema[0]
|
||||||
|
|
||||||
|
if isinstance(miss_schema, str) and miss_schema.endswith("?"):
|
||||||
continue
|
continue
|
||||||
raise vol.Invalid(f"Missing option {miss_opt} in {root}") from None
|
|
||||||
|
raise vol.Invalid(
|
||||||
|
f"Missing option '{miss_opt}' in {root} in {self._name} ({self._slug})"
|
||||||
|
) from None
|
||||||
|
|
||||||
|
|
||||||
class UiOptions(CoreSysAttributes):
|
class UiOptions(CoreSysAttributes):
|
||||||
@@ -226,9 +267,9 @@ class UiOptions(CoreSysAttributes):
|
|||||||
"""Initialize UI option render."""
|
"""Initialize UI option render."""
|
||||||
self.coresys = coresys
|
self.coresys = coresys
|
||||||
|
|
||||||
def __call__(self, raw_schema: Dict[str, Any]) -> List[Dict[str, Any]]:
|
def __call__(self, raw_schema: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
"""Generate UI schema."""
|
"""Generate UI schema."""
|
||||||
ui_schema: List[Dict[str, Any]] = []
|
ui_schema: list[dict[str, Any]] = []
|
||||||
|
|
||||||
# read options
|
# read options
|
||||||
for key, value in raw_schema.items():
|
for key, value in raw_schema.items():
|
||||||
@@ -246,13 +287,13 @@ class UiOptions(CoreSysAttributes):
|
|||||||
|
|
||||||
def _single_ui_option(
|
def _single_ui_option(
|
||||||
self,
|
self,
|
||||||
ui_schema: List[Dict[str, Any]],
|
ui_schema: list[dict[str, Any]],
|
||||||
value: str,
|
value: str,
|
||||||
key: str,
|
key: str,
|
||||||
multiple: bool = False,
|
multiple: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Validate a single element."""
|
"""Validate a single element."""
|
||||||
ui_node: Dict[str, Union[str, bool, float, List[str]]] = {"name": key}
|
ui_node: dict[str, Union[str, bool, float, list[str]]] = {"name": key}
|
||||||
|
|
||||||
# If multiple
|
# If multiple
|
||||||
if multiple:
|
if multiple:
|
||||||
@@ -317,15 +358,15 @@ class UiOptions(CoreSysAttributes):
|
|||||||
else:
|
else:
|
||||||
ui_node["options"] = [
|
ui_node["options"] = [
|
||||||
(device.by_id or device.path).as_posix()
|
(device.by_id or device.path).as_posix()
|
||||||
for device in self.sys_hardware.devices()
|
for device in self.sys_hardware.devices
|
||||||
]
|
]
|
||||||
|
|
||||||
ui_schema.append(ui_node)
|
ui_schema.append(ui_node)
|
||||||
|
|
||||||
def _nested_ui_list(
|
def _nested_ui_list(
|
||||||
self,
|
self,
|
||||||
ui_schema: List[Dict[str, Any]],
|
ui_schema: list[dict[str, Any]],
|
||||||
option_list: List[Any],
|
option_list: list[Any],
|
||||||
key: str,
|
key: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""UI nested list items."""
|
"""UI nested list items."""
|
||||||
@@ -342,8 +383,8 @@ class UiOptions(CoreSysAttributes):
|
|||||||
|
|
||||||
def _nested_ui_dict(
|
def _nested_ui_dict(
|
||||||
self,
|
self,
|
||||||
ui_schema: List[Dict[str, Any]],
|
ui_schema: list[dict[str, Any]],
|
||||||
option_dict: Dict[str, Any],
|
option_dict: dict[str, Any],
|
||||||
key: str,
|
key: str,
|
||||||
multiple: bool = False,
|
multiple: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -367,7 +408,7 @@ class UiOptions(CoreSysAttributes):
|
|||||||
ui_schema.append(ui_node)
|
ui_schema.append(ui_node)
|
||||||
|
|
||||||
|
|
||||||
def _create_device_filter(str_filter: str) -> Dict[str, Any]:
|
def _create_device_filter(str_filter: str) -> dict[str, Any]:
|
||||||
"""Generate device Filter."""
|
"""Generate device Filter."""
|
||||||
raw_filter = dict(value.split("=") for value in str_filter.split(";"))
|
raw_filter = dict(value.split("=") for value in str_filter.split(";"))
|
||||||
|
|
||||||
|
@@ -6,18 +6,8 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from ..const import (
|
from ..const import ROLE_ADMIN, ROLE_MANAGER, SECURITY_DISABLE, SECURITY_PROFILE
|
||||||
PRIVILEGED_DAC_READ_SEARCH,
|
from ..docker.const import Capabilities
|
||||||
PRIVILEGED_NET_ADMIN,
|
|
||||||
PRIVILEGED_SYS_ADMIN,
|
|
||||||
PRIVILEGED_SYS_MODULE,
|
|
||||||
PRIVILEGED_SYS_PTRACE,
|
|
||||||
PRIVILEGED_SYS_RAWIO,
|
|
||||||
ROLE_ADMIN,
|
|
||||||
ROLE_MANAGER,
|
|
||||||
SECURITY_DISABLE,
|
|
||||||
SECURITY_PROFILE,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .model import AddonModel
|
from .model import AddonModel
|
||||||
@@ -46,16 +36,19 @@ def rating_security(addon: AddonModel) -> int:
|
|||||||
rating += 1
|
rating += 1
|
||||||
|
|
||||||
# Privileged options
|
# Privileged options
|
||||||
if any(
|
if (
|
||||||
privilege in addon.privileged
|
any(
|
||||||
for privilege in (
|
privilege in addon.privileged
|
||||||
PRIVILEGED_NET_ADMIN,
|
for privilege in (
|
||||||
PRIVILEGED_SYS_ADMIN,
|
Capabilities.NET_ADMIN,
|
||||||
PRIVILEGED_SYS_RAWIO,
|
Capabilities.SYS_ADMIN,
|
||||||
PRIVILEGED_SYS_PTRACE,
|
Capabilities.SYS_RAWIO,
|
||||||
PRIVILEGED_SYS_MODULE,
|
Capabilities.SYS_PTRACE,
|
||||||
PRIVILEGED_DAC_READ_SEARCH,
|
Capabilities.SYS_MODULE,
|
||||||
|
Capabilities.DAC_READ_SEARCH,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
or addon.with_kernel_modules
|
||||||
):
|
):
|
||||||
rating += -1
|
rating += -1
|
||||||
|
|
||||||
@@ -73,12 +66,8 @@ def rating_security(addon: AddonModel) -> int:
|
|||||||
if addon.host_pid:
|
if addon.host_pid:
|
||||||
rating += -2
|
rating += -2
|
||||||
|
|
||||||
# Full Access
|
# Docker Access & full Access
|
||||||
if addon.with_full_access:
|
if addon.access_docker_api or addon.with_full_access:
|
||||||
rating += -2
|
|
||||||
|
|
||||||
# Docker Access
|
|
||||||
if addon.access_docker_api:
|
|
||||||
rating = 1
|
rating = 1
|
||||||
|
|
||||||
return max(min(6, rating), 1)
|
return max(min(6, rating), 1)
|
||||||
|
@@ -2,11 +2,13 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
from typing import Any, Dict
|
from typing import Any
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from supervisor.addons.const import AddonBackupMode
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ARCH_ALL,
|
ARCH_ALL,
|
||||||
ATTR_ACCESS_TOKEN,
|
ATTR_ACCESS_TOKEN,
|
||||||
@@ -19,8 +21,12 @@ from ..const import (
|
|||||||
ATTR_AUDIO_OUTPUT,
|
ATTR_AUDIO_OUTPUT,
|
||||||
ATTR_AUTH_API,
|
ATTR_AUTH_API,
|
||||||
ATTR_AUTO_UPDATE,
|
ATTR_AUTO_UPDATE,
|
||||||
|
ATTR_BACKUP_EXCLUDE,
|
||||||
|
ATTR_BACKUP_POST,
|
||||||
|
ATTR_BACKUP_PRE,
|
||||||
ATTR_BOOT,
|
ATTR_BOOT,
|
||||||
ATTR_BUILD_FROM,
|
ATTR_BUILD_FROM,
|
||||||
|
ATTR_CONFIGURATION,
|
||||||
ATTR_DESCRIPTON,
|
ATTR_DESCRIPTON,
|
||||||
ATTR_DEVICES,
|
ATTR_DEVICES,
|
||||||
ATTR_DEVICETREE,
|
ATTR_DEVICETREE,
|
||||||
@@ -42,9 +48,12 @@ from ..const import (
|
|||||||
ATTR_INGRESS_ENTRY,
|
ATTR_INGRESS_ENTRY,
|
||||||
ATTR_INGRESS_PANEL,
|
ATTR_INGRESS_PANEL,
|
||||||
ATTR_INGRESS_PORT,
|
ATTR_INGRESS_PORT,
|
||||||
|
ATTR_INGRESS_STREAM,
|
||||||
ATTR_INGRESS_TOKEN,
|
ATTR_INGRESS_TOKEN,
|
||||||
ATTR_INIT,
|
ATTR_INIT,
|
||||||
|
ATTR_JOURNALD,
|
||||||
ATTR_KERNEL_MODULES,
|
ATTR_KERNEL_MODULES,
|
||||||
|
ATTR_LABELS,
|
||||||
ATTR_LEGACY,
|
ATTR_LEGACY,
|
||||||
ATTR_LOCATON,
|
ATTR_LOCATON,
|
||||||
ATTR_MACHINE,
|
ATTR_MACHINE,
|
||||||
@@ -59,11 +68,11 @@ from ..const import (
|
|||||||
ATTR_PORTS_DESCRIPTION,
|
ATTR_PORTS_DESCRIPTION,
|
||||||
ATTR_PRIVILEGED,
|
ATTR_PRIVILEGED,
|
||||||
ATTR_PROTECTED,
|
ATTR_PROTECTED,
|
||||||
|
ATTR_REALTIME,
|
||||||
ATTR_REPOSITORY,
|
ATTR_REPOSITORY,
|
||||||
ATTR_SCHEMA,
|
ATTR_SCHEMA,
|
||||||
ATTR_SERVICES,
|
ATTR_SERVICES,
|
||||||
ATTR_SLUG,
|
ATTR_SLUG,
|
||||||
ATTR_SNAPSHOT_EXCLUDE,
|
|
||||||
ATTR_SQUASH,
|
ATTR_SQUASH,
|
||||||
ATTR_STAGE,
|
ATTR_STAGE,
|
||||||
ATTR_STARTUP,
|
ATTR_STARTUP,
|
||||||
@@ -72,6 +81,7 @@ from ..const import (
|
|||||||
ATTR_SYSTEM,
|
ATTR_SYSTEM,
|
||||||
ATTR_TIMEOUT,
|
ATTR_TIMEOUT,
|
||||||
ATTR_TMPFS,
|
ATTR_TMPFS,
|
||||||
|
ATTR_TRANSLATIONS,
|
||||||
ATTR_UART,
|
ATTR_UART,
|
||||||
ATTR_UDEV,
|
ATTR_UDEV,
|
||||||
ATTR_URL,
|
ATTR_URL,
|
||||||
@@ -82,7 +92,6 @@ from ..const import (
|
|||||||
ATTR_VIDEO,
|
ATTR_VIDEO,
|
||||||
ATTR_WATCHDOG,
|
ATTR_WATCHDOG,
|
||||||
ATTR_WEBUI,
|
ATTR_WEBUI,
|
||||||
PRIVILEGED_ALL,
|
|
||||||
ROLE_ALL,
|
ROLE_ALL,
|
||||||
ROLE_DEFAULT,
|
ROLE_DEFAULT,
|
||||||
AddonBoot,
|
AddonBoot,
|
||||||
@@ -91,6 +100,7 @@ from ..const import (
|
|||||||
AddonState,
|
AddonState,
|
||||||
)
|
)
|
||||||
from ..discovery.validate import valid_discovery_service
|
from ..discovery.validate import valid_discovery_service
|
||||||
|
from ..docker.const import Capabilities
|
||||||
from ..validate import (
|
from ..validate import (
|
||||||
docker_image,
|
docker_image,
|
||||||
docker_ports,
|
docker_ports,
|
||||||
@@ -100,6 +110,7 @@ from ..validate import (
|
|||||||
uuid_match,
|
uuid_match,
|
||||||
version_tag,
|
version_tag,
|
||||||
)
|
)
|
||||||
|
from .const import ATTR_BACKUP
|
||||||
from .options import RE_SCHEMA_ELEMENT
|
from .options import RE_SCHEMA_ELEMENT
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@@ -117,6 +128,7 @@ SCHEMA_ELEMENT = vol.Match(RE_SCHEMA_ELEMENT)
|
|||||||
RE_MACHINE = re.compile(
|
RE_MACHINE = re.compile(
|
||||||
r"^!?(?:"
|
r"^!?(?:"
|
||||||
r"|intel-nuc"
|
r"|intel-nuc"
|
||||||
|
r"|generic-x86-64"
|
||||||
r"|odroid-c2"
|
r"|odroid-c2"
|
||||||
r"|odroid-c4"
|
r"|odroid-c4"
|
||||||
r"|odroid-n2"
|
r"|odroid-n2"
|
||||||
@@ -136,10 +148,38 @@ RE_MACHINE = re.compile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _warn_addon_config(config: dict[str, Any]):
|
||||||
|
"""Warn about miss configs."""
|
||||||
|
name = config.get(ATTR_NAME)
|
||||||
|
if not name:
|
||||||
|
raise vol.Invalid("Invalid Add-on config!")
|
||||||
|
|
||||||
|
if config.get(ATTR_FULL_ACCESS, False) and (
|
||||||
|
config.get(ATTR_DEVICES)
|
||||||
|
or config.get(ATTR_UART)
|
||||||
|
or config.get(ATTR_USB)
|
||||||
|
or config.get(ATTR_GPIO)
|
||||||
|
):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Add-on have full device access, and selective device access in the configuration. Please report this to the maintainer of %s",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
|
||||||
|
if config.get(ATTR_BACKUP, AddonBackupMode.HOT) == AddonBackupMode.COLD and (
|
||||||
|
config.get(ATTR_BACKUP_POST) or config.get(ATTR_BACKUP_PRE)
|
||||||
|
):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Add-on which only support COLD backups trying to use post/pre commands. Please report this to the maintainer of %s",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def _migrate_addon_config(protocol=False):
|
def _migrate_addon_config(protocol=False):
|
||||||
"""Migrate addon config."""
|
"""Migrate addon config."""
|
||||||
|
|
||||||
def _migrate(config: Dict[str, Any]):
|
def _migrate(config: dict[str, Any]):
|
||||||
name = config.get(ATTR_NAME)
|
name = config.get(ATTR_NAME)
|
||||||
if not name:
|
if not name:
|
||||||
raise vol.Invalid("Invalid Add-on config!")
|
raise vol.Invalid("Invalid Add-on config!")
|
||||||
@@ -185,6 +225,23 @@ def _migrate_addon_config(protocol=False):
|
|||||||
)
|
)
|
||||||
config[ATTR_TMPFS] = True
|
config[ATTR_TMPFS] = True
|
||||||
|
|
||||||
|
# 2021-06 "snapshot" renamed to "backup"
|
||||||
|
for entry in (
|
||||||
|
"snapshot_exclude",
|
||||||
|
"snapshot_post",
|
||||||
|
"snapshot_pre",
|
||||||
|
"snapshot",
|
||||||
|
):
|
||||||
|
if entry in config:
|
||||||
|
new_entry = entry.replace("snapshot", "backup")
|
||||||
|
config[new_entry] = config.pop(entry)
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Add-on config '%s' is deprecated, '%s' should be used instead. Please report this to the maintainer of %s",
|
||||||
|
entry,
|
||||||
|
new_entry,
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
return _migrate
|
return _migrate
|
||||||
@@ -210,7 +267,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
vol.Optional(ATTR_PORTS): docker_ports,
|
vol.Optional(ATTR_PORTS): docker_ports,
|
||||||
vol.Optional(ATTR_PORTS_DESCRIPTION): docker_ports_description,
|
vol.Optional(ATTR_PORTS_DESCRIPTION): docker_ports_description,
|
||||||
vol.Optional(ATTR_WATCHDOG): vol.Match(
|
vol.Optional(ATTR_WATCHDOG): vol.Match(
|
||||||
r"^(?:https?|\[PROTO:\w+\]|tcp):\/\/\[HOST\]:\[PORT:\d+\].*$"
|
r"^(?:https?|\[PROTO:\w+\]|tcp):\/\/\[HOST\]:(\[PORT:\d+\]|\d+).*$"
|
||||||
),
|
),
|
||||||
vol.Optional(ATTR_WEBUI): vol.Match(
|
vol.Optional(ATTR_WEBUI): vol.Match(
|
||||||
r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"
|
r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"
|
||||||
@@ -220,10 +277,11 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
network_port, vol.Equal(0)
|
network_port, vol.Equal(0)
|
||||||
),
|
),
|
||||||
vol.Optional(ATTR_INGRESS_ENTRY): str,
|
vol.Optional(ATTR_INGRESS_ENTRY): str,
|
||||||
|
vol.Optional(ATTR_INGRESS_STREAM, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_PANEL_ICON, default="mdi:puzzle"): str,
|
vol.Optional(ATTR_PANEL_ICON, default="mdi:puzzle"): str,
|
||||||
vol.Optional(ATTR_PANEL_TITLE): str,
|
vol.Optional(ATTR_PANEL_TITLE): str,
|
||||||
vol.Optional(ATTR_PANEL_ADMIN, default=True): vol.Boolean(),
|
vol.Optional(ATTR_PANEL_ADMIN, default=True): vol.Boolean(),
|
||||||
vol.Optional(ATTR_HOMEASSISTANT): vol.Maybe(version_tag),
|
vol.Optional(ATTR_HOMEASSISTANT): version_tag,
|
||||||
vol.Optional(ATTR_HOST_NETWORK, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HOST_NETWORK, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_HOST_PID, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HOST_PID, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_HOST_IPC, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HOST_IPC, default=False): vol.Boolean(),
|
||||||
@@ -233,7 +291,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
vol.Optional(ATTR_TMPFS, default=False): vol.Boolean(),
|
vol.Optional(ATTR_TMPFS, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_MAP, default=list): [vol.Match(RE_VOLUME)],
|
vol.Optional(ATTR_MAP, default=list): [vol.Match(RE_VOLUME)],
|
||||||
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): str},
|
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): str},
|
||||||
vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)],
|
vol.Optional(ATTR_PRIVILEGED): [vol.Coerce(Capabilities)],
|
||||||
vol.Optional(ATTR_APPARMOR, default=True): vol.Boolean(),
|
vol.Optional(ATTR_APPARMOR, default=True): vol.Boolean(),
|
||||||
vol.Optional(ATTR_FULL_ACCESS, default=False): vol.Boolean(),
|
vol.Optional(ATTR_FULL_ACCESS, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
|
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
|
||||||
@@ -243,6 +301,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
vol.Optional(ATTR_UART, default=False): vol.Boolean(),
|
vol.Optional(ATTR_UART, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(),
|
vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_KERNEL_MODULES, default=False): vol.Boolean(),
|
vol.Optional(ATTR_KERNEL_MODULES, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_REALTIME, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_HASSIO_ROLE, default=ROLE_DEFAULT): vol.In(ROLE_ALL),
|
vol.Optional(ATTR_HASSIO_ROLE, default=ROLE_DEFAULT): vol.In(ROLE_ALL),
|
||||||
vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(),
|
||||||
@@ -252,7 +311,12 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
vol.Optional(ATTR_AUTH_API, default=False): vol.Boolean(),
|
vol.Optional(ATTR_AUTH_API, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
|
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
|
||||||
vol.Optional(ATTR_DISCOVERY): [valid_discovery_service],
|
vol.Optional(ATTR_DISCOVERY): [valid_discovery_service],
|
||||||
vol.Optional(ATTR_SNAPSHOT_EXCLUDE): [str],
|
vol.Optional(ATTR_BACKUP_EXCLUDE): [str],
|
||||||
|
vol.Optional(ATTR_BACKUP_PRE): str,
|
||||||
|
vol.Optional(ATTR_BACKUP_POST): str,
|
||||||
|
vol.Optional(ATTR_BACKUP, default=AddonBackupMode.HOT): vol.Coerce(
|
||||||
|
AddonBackupMode
|
||||||
|
),
|
||||||
vol.Optional(ATTR_OPTIONS, default={}): dict,
|
vol.Optional(ATTR_OPTIONS, default={}): dict,
|
||||||
vol.Optional(ATTR_SCHEMA, default={}): vol.Any(
|
vol.Optional(ATTR_SCHEMA, default={}): vol.Any(
|
||||||
vol.Schema(
|
vol.Schema(
|
||||||
@@ -275,11 +339,14 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
vol.Optional(ATTR_TIMEOUT, default=10): vol.All(
|
vol.Optional(ATTR_TIMEOUT, default=10): vol.All(
|
||||||
vol.Coerce(int), vol.Range(min=10, max=300)
|
vol.Coerce(int), vol.Range(min=10, max=300)
|
||||||
),
|
),
|
||||||
|
vol.Optional(ATTR_JOURNALD, default=False): vol.Boolean(),
|
||||||
},
|
},
|
||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
SCHEMA_ADDON_CONFIG = vol.All(_migrate_addon_config(True), _SCHEMA_ADDON_CONFIG)
|
SCHEMA_ADDON_CONFIG = vol.All(
|
||||||
|
_migrate_addon_config(True), _warn_addon_config, _SCHEMA_ADDON_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
@@ -289,9 +356,25 @@ SCHEMA_BUILD_CONFIG = vol.Schema(
|
|||||||
{vol.In(ARCH_ALL): vol.Match(RE_DOCKER_IMAGE_BUILD)}
|
{vol.In(ARCH_ALL): vol.Match(RE_DOCKER_IMAGE_BUILD)}
|
||||||
),
|
),
|
||||||
vol.Optional(ATTR_SQUASH, default=False): vol.Boolean(),
|
vol.Optional(ATTR_SQUASH, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_ARGS, default=dict): vol.Schema(
|
vol.Optional(ATTR_ARGS, default=dict): vol.Schema({str: str}),
|
||||||
{vol.Coerce(str): vol.Coerce(str)}
|
vol.Optional(ATTR_LABELS, default=dict): vol.Schema({str: str}),
|
||||||
),
|
},
|
||||||
|
extra=vol.REMOVE_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
SCHEMA_TRANSLATION_CONFIGURATION = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_NAME): str,
|
||||||
|
vol.Optional(ATTR_DESCRIPTON): vol.Maybe(str),
|
||||||
|
},
|
||||||
|
extra=vol.REMOVE_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SCHEMA_ADDON_TRANSLATIONS = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_CONFIGURATION): {str: SCHEMA_TRANSLATION_CONFIGURATION},
|
||||||
|
vol.Optional(ATTR_NETWORK): {str: str},
|
||||||
},
|
},
|
||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
)
|
)
|
||||||
@@ -318,13 +401,15 @@ SCHEMA_ADDON_USER = vol.Schema(
|
|||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
SCHEMA_ADDON_SYSTEM = vol.All(
|
SCHEMA_ADDON_SYSTEM = vol.All(
|
||||||
_migrate_addon_config(),
|
_migrate_addon_config(),
|
||||||
_SCHEMA_ADDON_CONFIG.extend(
|
_SCHEMA_ADDON_CONFIG.extend(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_LOCATON): str,
|
vol.Required(ATTR_LOCATON): str,
|
||||||
vol.Required(ATTR_REPOSITORY): str,
|
vol.Required(ATTR_REPOSITORY): str,
|
||||||
|
vol.Required(ATTR_TRANSLATIONS, default=dict): {
|
||||||
|
str: SCHEMA_ADDON_TRANSLATIONS
|
||||||
|
},
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -339,7 +424,7 @@ SCHEMA_ADDONS_FILE = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
SCHEMA_ADDON_SNAPSHOT = vol.Schema(
|
SCHEMA_ADDON_BACKUP = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_USER): SCHEMA_ADDON_USER,
|
vol.Required(ATTR_USER): SCHEMA_ADDON_USER,
|
||||||
vol.Required(ATTR_SYSTEM): SCHEMA_ADDON_SYSTEM,
|
vol.Required(ATTR_SYSTEM): SCHEMA_ADDON_SYSTEM,
|
||||||
|
@@ -9,6 +9,7 @@ from ..coresys import CoreSys, CoreSysAttributes
|
|||||||
from .addons import APIAddons
|
from .addons import APIAddons
|
||||||
from .audio import APIAudio
|
from .audio import APIAudio
|
||||||
from .auth import APIAuth
|
from .auth import APIAuth
|
||||||
|
from .backups import APIBackups
|
||||||
from .cli import APICli
|
from .cli import APICli
|
||||||
from .discovery import APIDiscovery
|
from .discovery import APIDiscovery
|
||||||
from .dns import APICoreDNS
|
from .dns import APICoreDNS
|
||||||
@@ -19,15 +20,16 @@ from .host import APIHost
|
|||||||
from .info import APIInfo
|
from .info import APIInfo
|
||||||
from .ingress import APIIngress
|
from .ingress import APIIngress
|
||||||
from .jobs import APIJobs
|
from .jobs import APIJobs
|
||||||
|
from .middleware.security import SecurityMiddleware
|
||||||
from .multicast import APIMulticast
|
from .multicast import APIMulticast
|
||||||
from .network import APINetwork
|
from .network import APINetwork
|
||||||
from .observer import APIObserver
|
from .observer import APIObserver
|
||||||
from .os import APIOS
|
from .os import APIOS
|
||||||
from .proxy import APIProxy
|
from .proxy import APIProxy
|
||||||
from .resolution import APIResoulution
|
from .resolution import APIResoulution
|
||||||
from .security import SecurityMiddleware
|
from .security import APISecurity
|
||||||
from .services import APIServices
|
from .services import APIServices
|
||||||
from .snapshots import APISnapshots
|
from .store import APIStore
|
||||||
from .supervisor import APISupervisor
|
from .supervisor import APISupervisor
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@@ -60,6 +62,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self._register_addons()
|
self._register_addons()
|
||||||
self._register_audio()
|
self._register_audio()
|
||||||
self._register_auth()
|
self._register_auth()
|
||||||
|
self._register_backups()
|
||||||
self._register_cli()
|
self._register_cli()
|
||||||
self._register_discovery()
|
self._register_discovery()
|
||||||
self._register_dns()
|
self._register_dns()
|
||||||
@@ -78,8 +81,9 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self._register_proxy()
|
self._register_proxy()
|
||||||
self._register_resolution()
|
self._register_resolution()
|
||||||
self._register_services()
|
self._register_services()
|
||||||
self._register_snapshots()
|
|
||||||
self._register_supervisor()
|
self._register_supervisor()
|
||||||
|
self._register_store()
|
||||||
|
self._register_security()
|
||||||
|
|
||||||
await self.start()
|
await self.start()
|
||||||
|
|
||||||
@@ -141,6 +145,20 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.get("/os/info", api_os.info),
|
web.get("/os/info", api_os.info),
|
||||||
web.post("/os/update", api_os.update),
|
web.post("/os/update", api_os.update),
|
||||||
web.post("/os/config/sync", api_os.config_sync),
|
web.post("/os/config/sync", api_os.config_sync),
|
||||||
|
web.post("/os/datadisk/move", api_os.migrate_data),
|
||||||
|
web.get("/os/datadisk/list", api_os.list_data),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _register_security(self) -> None:
|
||||||
|
"""Register Security functions."""
|
||||||
|
api_security = APISecurity()
|
||||||
|
api_security.coresys = self.coresys
|
||||||
|
|
||||||
|
self.webapp.add_routes(
|
||||||
|
[
|
||||||
|
web.get("/security/info", api_security.info),
|
||||||
|
web.post("/security/options", api_security.options),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -207,7 +225,6 @@ class RestAPI(CoreSysAttributes):
|
|||||||
[
|
[
|
||||||
web.get("/hardware/info", api_hardware.info),
|
web.get("/hardware/info", api_hardware.info),
|
||||||
web.get("/hardware/audio", api_hardware.audio),
|
web.get("/hardware/audio", api_hardware.audio),
|
||||||
web.post("/hardware/trigger", api_hardware.trigger),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -226,6 +243,10 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self.webapp.add_routes(
|
self.webapp.add_routes(
|
||||||
[
|
[
|
||||||
web.get("/resolution/info", api_resolution.info),
|
web.get("/resolution/info", api_resolution.info),
|
||||||
|
web.post(
|
||||||
|
"/resolution/check/{check}/options", api_resolution.options_check
|
||||||
|
),
|
||||||
|
web.post("/resolution/check/{check}/run", api_resolution.run_check),
|
||||||
web.post(
|
web.post(
|
||||||
"/resolution/suggestion/{suggestion}",
|
"/resolution/suggestion/{suggestion}",
|
||||||
api_resolution.apply_suggestion,
|
api_resolution.apply_suggestion,
|
||||||
@@ -238,6 +259,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
"/resolution/issue/{issue}",
|
"/resolution/issue/{issue}",
|
||||||
api_resolution.dismiss_issue,
|
api_resolution.dismiss_issue,
|
||||||
),
|
),
|
||||||
|
web.post("/resolution/healthcheck", api_resolution.healthcheck),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -338,12 +360,10 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.get("/addons", api_addons.list),
|
web.get("/addons", api_addons.list),
|
||||||
web.post("/addons/reload", api_addons.reload),
|
web.post("/addons/reload", api_addons.reload),
|
||||||
web.get("/addons/{addon}/info", api_addons.info),
|
web.get("/addons/{addon}/info", api_addons.info),
|
||||||
web.post("/addons/{addon}/install", api_addons.install),
|
|
||||||
web.post("/addons/{addon}/uninstall", api_addons.uninstall),
|
web.post("/addons/{addon}/uninstall", api_addons.uninstall),
|
||||||
web.post("/addons/{addon}/start", api_addons.start),
|
web.post("/addons/{addon}/start", api_addons.start),
|
||||||
web.post("/addons/{addon}/stop", api_addons.stop),
|
web.post("/addons/{addon}/stop", api_addons.stop),
|
||||||
web.post("/addons/{addon}/restart", api_addons.restart),
|
web.post("/addons/{addon}/restart", api_addons.restart),
|
||||||
web.post("/addons/{addon}/update", api_addons.update),
|
|
||||||
web.post("/addons/{addon}/options", api_addons.options),
|
web.post("/addons/{addon}/options", api_addons.options),
|
||||||
web.post(
|
web.post(
|
||||||
"/addons/{addon}/options/validate", api_addons.options_validate
|
"/addons/{addon}/options/validate", api_addons.options_validate
|
||||||
@@ -375,30 +395,41 @@ class RestAPI(CoreSysAttributes):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def _register_snapshots(self) -> None:
|
def _register_backups(self) -> None:
|
||||||
"""Register snapshots functions."""
|
"""Register backups functions."""
|
||||||
api_snapshots = APISnapshots()
|
api_backups = APIBackups()
|
||||||
api_snapshots.coresys = self.coresys
|
api_backups.coresys = self.coresys
|
||||||
|
|
||||||
self.webapp.add_routes(
|
self.webapp.add_routes(
|
||||||
[
|
[
|
||||||
web.get("/snapshots", api_snapshots.list),
|
web.get("/snapshots", api_backups.list),
|
||||||
web.post("/snapshots/reload", api_snapshots.reload),
|
web.post("/snapshots/reload", api_backups.reload),
|
||||||
web.post("/snapshots/new/full", api_snapshots.snapshot_full),
|
web.post("/snapshots/new/full", api_backups.backup_full),
|
||||||
web.post("/snapshots/new/partial", api_snapshots.snapshot_partial),
|
web.post("/snapshots/new/partial", api_backups.backup_partial),
|
||||||
web.post("/snapshots/new/upload", api_snapshots.upload),
|
web.post("/snapshots/new/upload", api_backups.upload),
|
||||||
web.get("/snapshots/{snapshot}/info", api_snapshots.info),
|
web.get("/snapshots/{slug}/info", api_backups.info),
|
||||||
web.delete("/snapshots/{snapshot}", api_snapshots.remove),
|
web.delete("/snapshots/{slug}", api_backups.remove),
|
||||||
|
web.post("/snapshots/{slug}/restore/full", api_backups.restore_full),
|
||||||
web.post(
|
web.post(
|
||||||
"/snapshots/{snapshot}/restore/full", api_snapshots.restore_full
|
"/snapshots/{slug}/restore/partial",
|
||||||
|
api_backups.restore_partial,
|
||||||
),
|
),
|
||||||
|
web.get("/snapshots/{slug}/download", api_backups.download),
|
||||||
|
web.post("/snapshots/{slug}/remove", api_backups.remove),
|
||||||
|
# June 2021: /snapshots was renamed to /backups
|
||||||
|
web.get("/backups", api_backups.list),
|
||||||
|
web.post("/backups/reload", api_backups.reload),
|
||||||
|
web.post("/backups/new/full", api_backups.backup_full),
|
||||||
|
web.post("/backups/new/partial", api_backups.backup_partial),
|
||||||
|
web.post("/backups/new/upload", api_backups.upload),
|
||||||
|
web.get("/backups/{slug}/info", api_backups.info),
|
||||||
|
web.delete("/backups/{slug}", api_backups.remove),
|
||||||
|
web.post("/backups/{slug}/restore/full", api_backups.restore_full),
|
||||||
web.post(
|
web.post(
|
||||||
"/snapshots/{snapshot}/restore/partial",
|
"/backups/{slug}/restore/partial",
|
||||||
api_snapshots.restore_partial,
|
api_backups.restore_partial,
|
||||||
),
|
),
|
||||||
web.get("/snapshots/{snapshot}/download", api_snapshots.download),
|
web.get("/backups/{slug}/download", api_backups.download),
|
||||||
# Old, remove at end of 2020
|
|
||||||
web.post("/snapshots/{snapshot}/remove", api_snapshots.remove),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -469,6 +500,46 @@ class RestAPI(CoreSysAttributes):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _register_store(self) -> None:
|
||||||
|
"""Register store endpoints."""
|
||||||
|
api_store = APIStore()
|
||||||
|
api_store.coresys = self.coresys
|
||||||
|
|
||||||
|
self.webapp.add_routes(
|
||||||
|
[
|
||||||
|
web.get("/store", api_store.store_info),
|
||||||
|
web.get("/store/addons", api_store.addons_list),
|
||||||
|
web.get("/store/addons/{addon}", api_store.addons_addon_info),
|
||||||
|
web.get("/store/addons/{addon}/{version}", api_store.addons_addon_info),
|
||||||
|
web.post(
|
||||||
|
"/store/addons/{addon}/install", api_store.addons_addon_install
|
||||||
|
),
|
||||||
|
web.post(
|
||||||
|
"/store/addons/{addon}/install/{version}",
|
||||||
|
api_store.addons_addon_install,
|
||||||
|
),
|
||||||
|
web.post("/store/addons/{addon}/update", api_store.addons_addon_update),
|
||||||
|
web.post(
|
||||||
|
"/store/addons/{addon}/update/{version}",
|
||||||
|
api_store.addons_addon_update,
|
||||||
|
),
|
||||||
|
web.post("/store/reload", api_store.reload),
|
||||||
|
web.get("/store/repositories", api_store.repositories_list),
|
||||||
|
web.get(
|
||||||
|
"/store/repositories/{repository}",
|
||||||
|
api_store.repositories_repository_info,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reroute from legacy
|
||||||
|
self.webapp.add_routes(
|
||||||
|
[
|
||||||
|
web.post("/addons/{addon}/install", api_store.addons_addon_install),
|
||||||
|
web.post("/addons/{addon}/update", api_store.addons_addon_update),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
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")
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"""Init file for Supervisor Home Assistant RESTful API."""
|
"""Init file for Supervisor Home Assistant RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Awaitable, Dict, List
|
from typing import Any, Awaitable
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -71,6 +71,7 @@ from ..const import (
|
|||||||
ATTR_OPTIONS,
|
ATTR_OPTIONS,
|
||||||
ATTR_PRIVILEGED,
|
ATTR_PRIVILEGED,
|
||||||
ATTR_PROTECTED,
|
ATTR_PROTECTED,
|
||||||
|
ATTR_PWNED,
|
||||||
ATTR_RATING,
|
ATTR_RATING,
|
||||||
ATTR_REPOSITORIES,
|
ATTR_REPOSITORIES,
|
||||||
ATTR_REPOSITORY,
|
ATTR_REPOSITORY,
|
||||||
@@ -82,6 +83,7 @@ from ..const import (
|
|||||||
ATTR_STARTUP,
|
ATTR_STARTUP,
|
||||||
ATTR_STATE,
|
ATTR_STATE,
|
||||||
ATTR_STDIN,
|
ATTR_STDIN,
|
||||||
|
ATTR_TRANSLATIONS,
|
||||||
ATTR_UART,
|
ATTR_UART,
|
||||||
ATTR_UDEV,
|
ATTR_UDEV,
|
||||||
ATTR_UPDATE_AVAILABLE,
|
ATTR_UPDATE_AVAILABLE,
|
||||||
@@ -102,13 +104,13 @@ from ..const import (
|
|||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..docker.stats import DockerStats
|
from ..docker.stats import DockerStats
|
||||||
from ..exceptions import APIError, APIForbidden
|
from ..exceptions import APIError, APIForbidden, PwnedError, PwnedSecret
|
||||||
from ..validate import docker_ports
|
from ..validate import docker_ports
|
||||||
from .utils import api_process, api_process_raw, api_validate
|
from .utils import api_process, api_process_raw, api_validate, json_loads
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_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): str})
|
||||||
|
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
SCHEMA_OPTIONS = vol.Schema(
|
SCHEMA_OPTIONS = vol.Schema(
|
||||||
@@ -116,8 +118,8 @@ SCHEMA_OPTIONS = vol.Schema(
|
|||||||
vol.Optional(ATTR_BOOT): vol.Coerce(AddonBoot),
|
vol.Optional(ATTR_BOOT): vol.Coerce(AddonBoot),
|
||||||
vol.Optional(ATTR_NETWORK): vol.Maybe(docker_ports),
|
vol.Optional(ATTR_NETWORK): vol.Maybe(docker_ports),
|
||||||
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
|
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
|
||||||
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
|
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(str),
|
||||||
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
|
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(str),
|
||||||
vol.Optional(ATTR_INGRESS_PANEL): vol.Boolean(),
|
vol.Optional(ATTR_INGRESS_PANEL): vol.Boolean(),
|
||||||
vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
|
vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
|
||||||
}
|
}
|
||||||
@@ -154,7 +156,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
return addon
|
return addon
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def list(self, request: web.Request) -> Dict[str, Any]:
|
async def list(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Return all add-ons or repositories."""
|
"""Return all add-ons or repositories."""
|
||||||
data_addons = [
|
data_addons = [
|
||||||
{
|
{
|
||||||
@@ -171,6 +173,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_INSTALLED: addon.is_installed,
|
ATTR_INSTALLED: addon.is_installed,
|
||||||
ATTR_AVAILABLE: addon.available,
|
ATTR_AVAILABLE: addon.available,
|
||||||
ATTR_DETACHED: addon.is_detached,
|
ATTR_DETACHED: addon.is_detached,
|
||||||
|
ATTR_HOMEASSISTANT: addon.homeassistant_version,
|
||||||
ATTR_REPOSITORY: addon.repository,
|
ATTR_REPOSITORY: addon.repository,
|
||||||
ATTR_BUILD: addon.need_build,
|
ATTR_BUILD: addon.need_build,
|
||||||
ATTR_URL: addon.url,
|
ATTR_URL: addon.url,
|
||||||
@@ -198,7 +201,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
await asyncio.shield(self.sys_store.reload())
|
await asyncio.shield(self.sys_store.reload())
|
||||||
|
|
||||||
@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 add-on information."""
|
"""Return add-on information."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon: AnyAddon = self._extract_addon(request)
|
||||||
|
|
||||||
@@ -264,6 +267,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_SERVICES: _pretty_services(addon),
|
ATTR_SERVICES: _pretty_services(addon),
|
||||||
ATTR_DISCOVERY: addon.discovery,
|
ATTR_DISCOVERY: addon.discovery,
|
||||||
ATTR_IP_ADDRESS: None,
|
ATTR_IP_ADDRESS: None,
|
||||||
|
ATTR_TRANSLATIONS: addon.translations,
|
||||||
ATTR_INGRESS: addon.with_ingress,
|
ATTR_INGRESS: addon.with_ingress,
|
||||||
ATTR_INGRESS_ENTRY: None,
|
ATTR_INGRESS_ENTRY: None,
|
||||||
ATTR_INGRESS_URL: None,
|
ATTR_INGRESS_URL: None,
|
||||||
@@ -305,7 +309,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
|
|
||||||
# Extend schema with add-on specific validation
|
# Extend schema with add-on specific validation
|
||||||
addon_schema = SCHEMA_OPTIONS.extend(
|
addon_schema = SCHEMA_OPTIONS.extend(
|
||||||
{vol.Optional(ATTR_OPTIONS): vol.Any(None, addon.schema)}
|
{vol.Optional(ATTR_OPTIONS): vol.Maybe(addon.schema)}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate/Process Body
|
# Validate/Process Body
|
||||||
@@ -334,13 +338,39 @@ class APIAddons(CoreSysAttributes):
|
|||||||
async def options_validate(self, request: web.Request) -> None:
|
async def options_validate(self, request: web.Request) -> None:
|
||||||
"""Validate user options for add-on."""
|
"""Validate user options for add-on."""
|
||||||
addon = self._extract_addon_installed(request)
|
addon = self._extract_addon_installed(request)
|
||||||
data = {ATTR_MESSAGE: "", ATTR_VALID: True}
|
data = {ATTR_MESSAGE: "", ATTR_VALID: True, ATTR_PWNED: False}
|
||||||
|
|
||||||
|
options = await request.json(loads=json_loads) or addon.options
|
||||||
|
|
||||||
|
# Validate config
|
||||||
|
options_schema = addon.schema
|
||||||
try:
|
try:
|
||||||
addon.schema(addon.options)
|
options_schema.validate(options)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
data[ATTR_MESSAGE] = humanize_error(addon.options, ex)
|
data[ATTR_MESSAGE] = humanize_error(options, ex)
|
||||||
data[ATTR_VALID] = False
|
data[ATTR_VALID] = False
|
||||||
|
|
||||||
|
if not self.sys_security.pwned:
|
||||||
|
return data
|
||||||
|
|
||||||
|
# Pwned check
|
||||||
|
for secret in options_schema.pwned:
|
||||||
|
try:
|
||||||
|
await self.sys_security.verify_secret(secret)
|
||||||
|
continue
|
||||||
|
except PwnedSecret:
|
||||||
|
data[ATTR_PWNED] = True
|
||||||
|
except PwnedError:
|
||||||
|
data[ATTR_PWNED] = None
|
||||||
|
break
|
||||||
|
|
||||||
|
if self.sys_security.force and data[ATTR_PWNED] in (None, True):
|
||||||
|
data[ATTR_VALID] = False
|
||||||
|
if data[ATTR_PWNED] is None:
|
||||||
|
data[ATTR_MESSAGE] = "Error happening on pwned secrets check!"
|
||||||
|
else:
|
||||||
|
data[ATTR_MESSAGE] = "Add-on uses pwned secrets!"
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
@@ -352,7 +382,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
|
|
||||||
addon = self._extract_addon_installed(request)
|
addon = self._extract_addon_installed(request)
|
||||||
try:
|
try:
|
||||||
return addon.schema(addon.options)
|
return addon.schema.validate(addon.options)
|
||||||
except vol.Invalid:
|
except vol.Invalid:
|
||||||
raise APIError("Invalid configuration data for the add-on") from None
|
raise APIError("Invalid configuration data for the add-on") from None
|
||||||
|
|
||||||
@@ -360,7 +390,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
async def security(self, request: web.Request) -> None:
|
async def security(self, request: web.Request) -> None:
|
||||||
"""Store security options for add-on."""
|
"""Store security options for add-on."""
|
||||||
addon = self._extract_addon_installed(request)
|
addon = self._extract_addon_installed(request)
|
||||||
body: Dict[str, Any] = await api_validate(SCHEMA_SECURITY, request)
|
body: dict[str, Any] = await api_validate(SCHEMA_SECURITY, request)
|
||||||
|
|
||||||
if ATTR_PROTECTED in body:
|
if ATTR_PROTECTED in body:
|
||||||
_LOGGER.warning("Changing protected flag for %s!", addon.slug)
|
_LOGGER.warning("Changing protected flag for %s!", addon.slug)
|
||||||
@@ -369,7 +399,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
addon.save_persist()
|
addon.save_persist()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
async def stats(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
addon = self._extract_addon_installed(request)
|
addon = self._extract_addon_installed(request)
|
||||||
|
|
||||||
@@ -386,12 +416,6 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_BLK_WRITE: stats.blk_write,
|
ATTR_BLK_WRITE: stats.blk_write,
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
|
||||||
def install(self, request: web.Request) -> Awaitable[None]:
|
|
||||||
"""Install add-on."""
|
|
||||||
addon = self._extract_addon(request)
|
|
||||||
return asyncio.shield(addon.install())
|
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def uninstall(self, request: web.Request) -> Awaitable[None]:
|
def uninstall(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Uninstall add-on."""
|
"""Uninstall add-on."""
|
||||||
@@ -410,12 +434,6 @@ class APIAddons(CoreSysAttributes):
|
|||||||
addon = self._extract_addon_installed(request)
|
addon = self._extract_addon_installed(request)
|
||||||
return asyncio.shield(addon.stop())
|
return asyncio.shield(addon.stop())
|
||||||
|
|
||||||
@api_process
|
|
||||||
def update(self, request: web.Request) -> Awaitable[None]:
|
|
||||||
"""Update add-on."""
|
|
||||||
addon: Addon = self._extract_addon_installed(request)
|
|
||||||
return asyncio.shield(addon.update())
|
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Restart add-on."""
|
"""Restart add-on."""
|
||||||
@@ -485,6 +503,6 @@ class APIAddons(CoreSysAttributes):
|
|||||||
await asyncio.shield(addon.write_stdin(data))
|
await asyncio.shield(addon.write_stdin(data))
|
||||||
|
|
||||||
|
|
||||||
def _pretty_services(addon: AnyAddon) -> List[str]:
|
def _pretty_services(addon: AnyAddon) -> list[str]:
|
||||||
"""Return a simplified services role list."""
|
"""Return a simplified services role list."""
|
||||||
return [f"{name}:{access}" for name, access in addon.services_role.items()]
|
return [f"{name}:{access}" for name, access in addon.services_role.items()]
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"""Init file for Supervisor Audio RESTful API."""
|
"""Init file for Supervisor Audio RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Awaitable, Dict
|
from typing import Any, Awaitable
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import attr
|
import attr
|
||||||
@@ -56,10 +56,10 @@ SCHEMA_MUTE = vol.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
SCHEMA_DEFAULT = vol.Schema({vol.Required(ATTR_NAME): vol.Coerce(str)})
|
SCHEMA_DEFAULT = vol.Schema({vol.Required(ATTR_NAME): str})
|
||||||
|
|
||||||
SCHEMA_PROFILE = vol.Schema(
|
SCHEMA_PROFILE = vol.Schema(
|
||||||
{vol.Required(ATTR_CARD): vol.Coerce(str), vol.Required(ATTR_NAME): vol.Coerce(str)}
|
{vol.Required(ATTR_CARD): str, vol.Required(ATTR_NAME): str}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ class APIAudio(CoreSysAttributes):
|
|||||||
"""Handle RESTful API for Audio functions."""
|
"""Handle RESTful API for Audio 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 Audio information."""
|
"""Return Audio information."""
|
||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_plugins.audio.version,
|
ATTR_VERSION: self.sys_plugins.audio.version,
|
||||||
@@ -89,7 +89,7 @@ class APIAudio(CoreSysAttributes):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
async def stats(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
stats = await self.sys_plugins.audio.stats()
|
stats = await self.sys_plugins.audio.stats()
|
||||||
|
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
"""Init file for Supervisor auth/SSO RESTful API."""
|
"""Init file for Supervisor auth/SSO RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from aiohttp import BasicAuth, web
|
from aiohttp import BasicAuth, web
|
||||||
from aiohttp.hdrs import AUTHORIZATION, CONTENT_TYPE, WWW_AUTHENTICATE
|
from aiohttp.hdrs import AUTHORIZATION, CONTENT_TYPE, WWW_AUTHENTICATE
|
||||||
@@ -24,12 +23,12 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
SCHEMA_PASSWORD_RESET = vol.Schema(
|
SCHEMA_PASSWORD_RESET = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_USERNAME): vol.Coerce(str),
|
vol.Required(ATTR_USERNAME): str,
|
||||||
vol.Required(ATTR_PASSWORD): vol.Coerce(str),
|
vol.Required(ATTR_PASSWORD): str,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
REALM_HEADER: Dict[str, str] = {
|
REALM_HEADER: dict[str, str] = {
|
||||||
WWW_AUTHENTICATE: 'Basic realm="Home Assistant Authentication"'
|
WWW_AUTHENTICATE: 'Basic realm="Home Assistant Authentication"'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +45,7 @@ class APIAuth(CoreSysAttributes):
|
|||||||
return self.sys_auth.check_login(addon, auth.login, auth.password)
|
return self.sys_auth.check_login(addon, auth.login, auth.password)
|
||||||
|
|
||||||
def _process_dict(
|
def _process_dict(
|
||||||
self, request: web.Request, addon: Addon, data: Dict[str, str]
|
self, request: web.Request, addon: Addon, data: dict[str, str]
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Process login with dict data.
|
"""Process login with dict data.
|
||||||
|
|
||||||
@@ -86,7 +85,7 @@ class APIAuth(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def reset(self, request: web.Request) -> None:
|
async def reset(self, request: web.Request) -> None:
|
||||||
"""Process reset password request."""
|
"""Process reset password request."""
|
||||||
body: Dict[str, str] = await api_validate(SCHEMA_PASSWORD_RESET, request)
|
body: dict[str, str] = await api_validate(SCHEMA_PASSWORD_RESET, request)
|
||||||
await asyncio.shield(
|
await asyncio.shield(
|
||||||
self.sys_auth.change_password(body[ATTR_USERNAME], body[ATTR_PASSWORD])
|
self.sys_auth.change_password(body[ATTR_USERNAME], body[ATTR_PASSWORD])
|
||||||
)
|
)
|
||||||
|
217
supervisor/api/backups.py
Normal file
217
supervisor/api/backups.py
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
"""Backups RESTful API."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
from aiohttp.hdrs import CONTENT_DISPOSITION
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from ..backups.validate import ALL_FOLDERS
|
||||||
|
from ..const import (
|
||||||
|
ATTR_ADDONS,
|
||||||
|
ATTR_BACKUPS,
|
||||||
|
ATTR_CONTENT,
|
||||||
|
ATTR_DATE,
|
||||||
|
ATTR_FOLDERS,
|
||||||
|
ATTR_HOMEASSISTANT,
|
||||||
|
ATTR_NAME,
|
||||||
|
ATTR_PASSWORD,
|
||||||
|
ATTR_PROTECTED,
|
||||||
|
ATTR_REPOSITORIES,
|
||||||
|
ATTR_SIZE,
|
||||||
|
ATTR_SLUG,
|
||||||
|
ATTR_TYPE,
|
||||||
|
ATTR_VERSION,
|
||||||
|
CONTENT_TYPE_TAR,
|
||||||
|
)
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import APIError
|
||||||
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
RE_SLUGIFY_NAME = re.compile(r"[^A-Za-z0-9]+")
|
||||||
|
|
||||||
|
# pylint: disable=no-value-for-parameter
|
||||||
|
SCHEMA_RESTORE_PARTIAL = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_PASSWORD): vol.Maybe(str),
|
||||||
|
vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_ADDONS): vol.All([str], vol.Unique()),
|
||||||
|
vol.Optional(ATTR_FOLDERS): vol.All([vol.In(ALL_FOLDERS)], vol.Unique()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SCHEMA_RESTORE_FULL = vol.Schema({vol.Optional(ATTR_PASSWORD): vol.Maybe(str)})
|
||||||
|
|
||||||
|
SCHEMA_BACKUP_FULL = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_NAME): str,
|
||||||
|
vol.Optional(ATTR_PASSWORD): vol.Maybe(str),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_ADDONS): vol.All([str], vol.Unique()),
|
||||||
|
vol.Optional(ATTR_FOLDERS): vol.All([vol.In(ALL_FOLDERS)], vol.Unique()),
|
||||||
|
vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class APIBackups(CoreSysAttributes):
|
||||||
|
"""Handle RESTful API for backups functions."""
|
||||||
|
|
||||||
|
def _extract_slug(self, request):
|
||||||
|
"""Return backup, throw an exception if it doesn't exist."""
|
||||||
|
backup = self.sys_backups.get(request.match_info.get("slug"))
|
||||||
|
if not backup:
|
||||||
|
raise APIError("Backup does not exist")
|
||||||
|
return backup
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def list(self, request):
|
||||||
|
"""Return backup list."""
|
||||||
|
data_backups = []
|
||||||
|
for backup in self.sys_backups.list_backups:
|
||||||
|
data_backups.append(
|
||||||
|
{
|
||||||
|
ATTR_SLUG: backup.slug,
|
||||||
|
ATTR_NAME: backup.name,
|
||||||
|
ATTR_DATE: backup.date,
|
||||||
|
ATTR_TYPE: backup.sys_type,
|
||||||
|
ATTR_PROTECTED: backup.protected,
|
||||||
|
ATTR_CONTENT: {
|
||||||
|
ATTR_HOMEASSISTANT: backup.homeassistant_version is not None,
|
||||||
|
ATTR_ADDONS: backup.addon_list,
|
||||||
|
ATTR_FOLDERS: backup.folders,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.path == "/snapshots":
|
||||||
|
# Kept for backwards compability
|
||||||
|
return {"snapshots": data_backups}
|
||||||
|
|
||||||
|
return {ATTR_BACKUPS: data_backups}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def reload(self, request):
|
||||||
|
"""Reload backup list."""
|
||||||
|
await asyncio.shield(self.sys_backups.reload())
|
||||||
|
return True
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def info(self, request):
|
||||||
|
"""Return backup info."""
|
||||||
|
backup = self._extract_slug(request)
|
||||||
|
|
||||||
|
data_addons = []
|
||||||
|
for addon_data in backup.addons:
|
||||||
|
data_addons.append(
|
||||||
|
{
|
||||||
|
ATTR_SLUG: addon_data[ATTR_SLUG],
|
||||||
|
ATTR_NAME: addon_data[ATTR_NAME],
|
||||||
|
ATTR_VERSION: addon_data[ATTR_VERSION],
|
||||||
|
ATTR_SIZE: addon_data[ATTR_SIZE],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
ATTR_SLUG: backup.slug,
|
||||||
|
ATTR_TYPE: backup.sys_type,
|
||||||
|
ATTR_NAME: backup.name,
|
||||||
|
ATTR_DATE: backup.date,
|
||||||
|
ATTR_SIZE: backup.size,
|
||||||
|
ATTR_PROTECTED: backup.protected,
|
||||||
|
ATTR_HOMEASSISTANT: backup.homeassistant_version,
|
||||||
|
ATTR_ADDONS: data_addons,
|
||||||
|
ATTR_REPOSITORIES: backup.repositories,
|
||||||
|
ATTR_FOLDERS: backup.folders,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def backup_full(self, request):
|
||||||
|
"""Create full backup."""
|
||||||
|
body = await api_validate(SCHEMA_BACKUP_FULL, request)
|
||||||
|
backup = await asyncio.shield(self.sys_backups.do_backup_full(**body))
|
||||||
|
|
||||||
|
if backup:
|
||||||
|
return {ATTR_SLUG: backup.slug}
|
||||||
|
return False
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def backup_partial(self, request):
|
||||||
|
"""Create a partial backup."""
|
||||||
|
body = await api_validate(SCHEMA_BACKUP_PARTIAL, request)
|
||||||
|
backup = await asyncio.shield(self.sys_backups.do_backup_partial(**body))
|
||||||
|
|
||||||
|
if backup:
|
||||||
|
return {ATTR_SLUG: backup.slug}
|
||||||
|
return False
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def restore_full(self, request):
|
||||||
|
"""Full restore of a backup."""
|
||||||
|
backup = self._extract_slug(request)
|
||||||
|
body = await api_validate(SCHEMA_RESTORE_FULL, request)
|
||||||
|
|
||||||
|
return await asyncio.shield(self.sys_backups.do_restore_full(backup, **body))
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def restore_partial(self, request):
|
||||||
|
"""Partial restore a backup."""
|
||||||
|
backup = self._extract_slug(request)
|
||||||
|
body = await api_validate(SCHEMA_RESTORE_PARTIAL, request)
|
||||||
|
|
||||||
|
return await asyncio.shield(self.sys_backups.do_restore_partial(backup, **body))
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def remove(self, request):
|
||||||
|
"""Remove a backup."""
|
||||||
|
backup = self._extract_slug(request)
|
||||||
|
return self.sys_backups.remove(backup)
|
||||||
|
|
||||||
|
async def download(self, request):
|
||||||
|
"""Download a backup file."""
|
||||||
|
backup = self._extract_slug(request)
|
||||||
|
|
||||||
|
_LOGGER.info("Downloading backup %s", backup.slug)
|
||||||
|
response = web.FileResponse(backup.tarfile)
|
||||||
|
response.content_type = CONTENT_TYPE_TAR
|
||||||
|
response.headers[
|
||||||
|
CONTENT_DISPOSITION
|
||||||
|
] = f"attachment; filename={RE_SLUGIFY_NAME.sub('_', backup.name)}.tar"
|
||||||
|
return response
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def upload(self, request):
|
||||||
|
"""Upload a backup file."""
|
||||||
|
with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp_dir:
|
||||||
|
tar_file = Path(temp_dir, "backup.tar")
|
||||||
|
reader = await request.multipart()
|
||||||
|
contents = await reader.next()
|
||||||
|
try:
|
||||||
|
with tar_file.open("wb") as backup:
|
||||||
|
while True:
|
||||||
|
chunk = await contents.read_chunk()
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
backup.write(chunk)
|
||||||
|
|
||||||
|
except OSError as err:
|
||||||
|
_LOGGER.error("Can't write new backup file: %s", err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
backup = await asyncio.shield(self.sys_backups.import_backup(tar_file))
|
||||||
|
|
||||||
|
if backup:
|
||||||
|
return {ATTR_SLUG: backup.slug}
|
||||||
|
return False
|
@@ -1,7 +1,7 @@
|
|||||||
"""Init file for Supervisor HA cli RESTful API."""
|
"""Init file for Supervisor HA cli RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -32,7 +32,7 @@ class APICli(CoreSysAttributes):
|
|||||||
"""Handle RESTful API for HA Cli functions."""
|
"""Handle RESTful API for HA Cli 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 HA cli information."""
|
"""Return HA cli information."""
|
||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_plugins.cli.version,
|
ATTR_VERSION: self.sys_plugins.cli.version,
|
||||||
@@ -41,7 +41,7 @@ class APICli(CoreSysAttributes):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
async def stats(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
stats = await self.sys_plugins.cli.stats()
|
stats = await self.sys_plugins.cli.stats()
|
||||||
|
|
||||||
|
11
supervisor/api/const.py
Normal file
11
supervisor/api/const.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"""Const for API."""
|
||||||
|
|
||||||
|
ATTR_AGENT_VERSION = "agent_version"
|
||||||
|
ATTR_BOOT_TIMESTAMP = "boot_timestamp"
|
||||||
|
ATTR_DATA_DISK = "data_disk"
|
||||||
|
ATTR_DEVICE = "device"
|
||||||
|
ATTR_DT_SYNCHRONIZED = "dt_synchronized"
|
||||||
|
ATTR_DT_UTC = "dt_utc"
|
||||||
|
ATTR_STARTUP_TIME = "startup_time"
|
||||||
|
ATTR_USE_NTP = "use_ntp"
|
||||||
|
ATTR_USE_RTC = "use_rtc"
|
@@ -13,7 +13,7 @@ from ..const import (
|
|||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..discovery.validate import valid_discovery_service
|
from ..discovery.validate import valid_discovery_service
|
||||||
from ..exceptions import APIError, APIForbidden
|
from ..exceptions import APIError, APIForbidden
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, api_validate, require_home_assistant
|
||||||
|
|
||||||
SCHEMA_DISCOVERY = vol.Schema(
|
SCHEMA_DISCOVERY = vol.Schema(
|
||||||
{
|
{
|
||||||
@@ -33,15 +33,10 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
raise APIError("Discovery message not found")
|
raise APIError("Discovery message not found")
|
||||||
return message
|
return message
|
||||||
|
|
||||||
def _check_permission_ha(self, request):
|
|
||||||
"""Check permission for API call / Home Assistant."""
|
|
||||||
if request[REQUEST_FROM] != self.sys_homeassistant:
|
|
||||||
raise APIForbidden("Only HomeAssistant can use this API!")
|
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
|
@require_home_assistant
|
||||||
async def list(self, request):
|
async def list(self, request):
|
||||||
"""Show register services."""
|
"""Show register services."""
|
||||||
self._check_permission_ha(request)
|
|
||||||
|
|
||||||
# Get available discovery
|
# Get available discovery
|
||||||
discovery = []
|
discovery = []
|
||||||
@@ -79,13 +74,11 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
return {ATTR_UUID: message.uuid}
|
return {ATTR_UUID: message.uuid}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
|
@require_home_assistant
|
||||||
async def get_discovery(self, request):
|
async def get_discovery(self, request):
|
||||||
"""Read data into a discovery message."""
|
"""Read data into a discovery message."""
|
||||||
message = self._extract_message(request)
|
message = self._extract_message(request)
|
||||||
|
|
||||||
# HomeAssistant?
|
|
||||||
self._check_permission_ha(request)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ATTR_ADDON: message.addon,
|
ATTR_ADDON: message.addon,
|
||||||
ATTR_SERVICE: message.service,
|
ATTR_SERVICE: message.service,
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"""Init file for Supervisor DNS RESTful API."""
|
"""Init file for Supervisor DNS RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Awaitable, Dict
|
from typing import Any, Awaitable
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -40,7 +40,7 @@ class APICoreDNS(CoreSysAttributes):
|
|||||||
"""Handle RESTful API for DNS functions."""
|
"""Handle RESTful API for DNS 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 DNS information."""
|
"""Return DNS information."""
|
||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_plugins.dns.version,
|
ATTR_VERSION: self.sys_plugins.dns.version,
|
||||||
@@ -63,7 +63,7 @@ class APICoreDNS(CoreSysAttributes):
|
|||||||
self.sys_plugins.dns.save_data()
|
self.sys_plugins.dns.save_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
async def stats(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
stats = await self.sys_plugins.dns.stats()
|
stats = await self.sys_plugins.dns.stats()
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
"""Init file for Supervisor Home Assistant RESTful API."""
|
"""Init file for Supervisor Home Assistant RESTful API."""
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -21,7 +21,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
SCHEMA_DOCKER_REGISTRY = vol.Schema(
|
SCHEMA_DOCKER_REGISTRY = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Coerce(str): {
|
str: {
|
||||||
vol.Required(ATTR_USERNAME): str,
|
vol.Required(ATTR_USERNAME): str,
|
||||||
vol.Required(ATTR_PASSWORD): str,
|
vol.Required(ATTR_PASSWORD): str,
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ class APIDocker(CoreSysAttributes):
|
|||||||
"""Handle RESTful API for Docker configuration."""
|
"""Handle RESTful API for Docker configuration."""
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def registries(self, request) -> Dict[str, Any]:
|
async def registries(self, request) -> dict[str, Any]:
|
||||||
"""Return the list of registries."""
|
"""Return the list of registries."""
|
||||||
data_registries = {}
|
data_registries = {}
|
||||||
for hostname, registry in self.sys_docker.config.registries.items():
|
for hostname, registry in self.sys_docker.config.registries.items():
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
"""Init file for Supervisor hardware RESTful API."""
|
"""Init file for Supervisor hardware RESTful API."""
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Awaitable, Dict
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ from .utils import api_process
|
|||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def device_struct(device: Device) -> Dict[str, Any]:
|
def device_struct(device: Device) -> dict[str, Any]:
|
||||||
"""Return a dict with information of a interface to be used in th API."""
|
"""Return a dict with information of a interface to be used in th API."""
|
||||||
return {
|
return {
|
||||||
ATTR_NAME: device.name,
|
ATTR_NAME: device.name,
|
||||||
@@ -35,7 +35,7 @@ class APIHardware(CoreSysAttributes):
|
|||||||
"""Handle RESTful API for hardware functions."""
|
"""Handle RESTful API for hardware 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]:
|
||||||
"""Show hardware info."""
|
"""Show hardware info."""
|
||||||
return {
|
return {
|
||||||
ATTR_DEVICES: [
|
ATTR_DEVICES: [
|
||||||
@@ -44,7 +44,7 @@ class APIHardware(CoreSysAttributes):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def audio(self, request: web.Request) -> Dict[str, Any]:
|
async def audio(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Show pulse audio profiles."""
|
"""Show pulse audio profiles."""
|
||||||
return {
|
return {
|
||||||
ATTR_AUDIO: {
|
ATTR_AUDIO: {
|
||||||
@@ -58,8 +58,3 @@ class APIHardware(CoreSysAttributes):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
|
||||||
async def trigger(self, request: web.Request) -> Awaitable[None]:
|
|
||||||
"""Trigger a udev device reload."""
|
|
||||||
_LOGGER.debug("Ignoring DEPRECATED hardware trigger function call.")
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"""Init file for Supervisor Home Assistant RESTful API."""
|
"""Init file for Supervisor Home Assistant RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Awaitable, Dict
|
from typing import Any, Awaitable
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -48,9 +48,9 @@ SCHEMA_OPTIONS = vol.Schema(
|
|||||||
vol.Optional(ATTR_SSL): vol.Boolean(),
|
vol.Optional(ATTR_SSL): vol.Boolean(),
|
||||||
vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
|
vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
|
||||||
vol.Optional(ATTR_WAIT_BOOT): vol.All(vol.Coerce(int), vol.Range(min=60)),
|
vol.Optional(ATTR_WAIT_BOOT): vol.All(vol.Coerce(int), vol.Range(min=60)),
|
||||||
vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(vol.Coerce(str)),
|
vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(str),
|
||||||
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
|
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(str),
|
||||||
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
|
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(str),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
"""Handle RESTful API for Home Assistant functions."""
|
"""Handle RESTful API for Home Assistant 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 host information."""
|
"""Return host information."""
|
||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_homeassistant.version,
|
ATTR_VERSION: self.sys_homeassistant.version,
|
||||||
@@ -117,7 +117,7 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
self.sys_homeassistant.save_data()
|
self.sys_homeassistant.save_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> Dict[Any, str]:
|
async def stats(self, request: web.Request) -> dict[Any, str]:
|
||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
stats = await self.sys_homeassistant.core.stats()
|
stats = await self.sys_homeassistant.core.stats()
|
||||||
if not stats:
|
if not stats:
|
||||||
|
@@ -21,14 +21,24 @@ from ..const import (
|
|||||||
ATTR_OPERATING_SYSTEM,
|
ATTR_OPERATING_SYSTEM,
|
||||||
ATTR_SERVICES,
|
ATTR_SERVICES,
|
||||||
ATTR_STATE,
|
ATTR_STATE,
|
||||||
|
ATTR_TIMEZONE,
|
||||||
CONTENT_TYPE_BINARY,
|
CONTENT_TYPE_BINARY,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from .const import (
|
||||||
|
ATTR_AGENT_VERSION,
|
||||||
|
ATTR_BOOT_TIMESTAMP,
|
||||||
|
ATTR_DT_SYNCHRONIZED,
|
||||||
|
ATTR_DT_UTC,
|
||||||
|
ATTR_STARTUP_TIME,
|
||||||
|
ATTR_USE_NTP,
|
||||||
|
ATTR_USE_RTC,
|
||||||
|
)
|
||||||
from .utils import api_process, api_process_raw, api_validate
|
from .utils import api_process, api_process_raw, api_validate
|
||||||
|
|
||||||
SERVICE = "service"
|
SERVICE = "service"
|
||||||
|
|
||||||
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): vol.Coerce(str)})
|
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): str})
|
||||||
|
|
||||||
|
|
||||||
class APIHost(CoreSysAttributes):
|
class APIHost(CoreSysAttributes):
|
||||||
@@ -38,6 +48,7 @@ class APIHost(CoreSysAttributes):
|
|||||||
async def info(self, request):
|
async def info(self, request):
|
||||||
"""Return host information."""
|
"""Return host information."""
|
||||||
return {
|
return {
|
||||||
|
ATTR_AGENT_VERSION: self.sys_dbus.agent.version,
|
||||||
ATTR_CHASSIS: self.sys_host.info.chassis,
|
ATTR_CHASSIS: self.sys_host.info.chassis,
|
||||||
ATTR_CPE: self.sys_host.info.cpe,
|
ATTR_CPE: self.sys_host.info.cpe,
|
||||||
ATTR_DEPLOYMENT: self.sys_host.info.deployment,
|
ATTR_DEPLOYMENT: self.sys_host.info.deployment,
|
||||||
@@ -49,6 +60,13 @@ class APIHost(CoreSysAttributes):
|
|||||||
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
||||||
ATTR_KERNEL: self.sys_host.info.kernel,
|
ATTR_KERNEL: self.sys_host.info.kernel,
|
||||||
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
|
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
|
||||||
|
ATTR_TIMEZONE: self.sys_host.info.timezone,
|
||||||
|
ATTR_DT_UTC: self.sys_host.info.dt_utc,
|
||||||
|
ATTR_DT_SYNCHRONIZED: self.sys_host.info.dt_synchronized,
|
||||||
|
ATTR_USE_NTP: self.sys_host.info.use_ntp,
|
||||||
|
ATTR_USE_RTC: self.sys_host.info.use_rtc,
|
||||||
|
ATTR_STARTUP_TIME: self.sys_host.info.startup_time,
|
||||||
|
ATTR_BOOT_TIMESTAMP: self.sys_host.info.boot_timestamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
"""Init file for Supervisor info RESTful API."""
|
"""Init file for Supervisor info RESTful API."""
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ from ..const import (
|
|||||||
ATTR_LOGGING,
|
ATTR_LOGGING,
|
||||||
ATTR_MACHINE,
|
ATTR_MACHINE,
|
||||||
ATTR_OPERATING_SYSTEM,
|
ATTR_OPERATING_SYSTEM,
|
||||||
|
ATTR_STATE,
|
||||||
ATTR_SUPERVISOR,
|
ATTR_SUPERVISOR,
|
||||||
ATTR_SUPPORTED,
|
ATTR_SUPPORTED,
|
||||||
ATTR_SUPPORTED_ARCH,
|
ATTR_SUPPORTED_ARCH,
|
||||||
@@ -30,21 +31,22 @@ class APIInfo(CoreSysAttributes):
|
|||||||
"""Handle RESTful API for info functions."""
|
"""Handle RESTful API for info 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]:
|
||||||
"""Show system info."""
|
"""Show system info."""
|
||||||
return {
|
return {
|
||||||
ATTR_SUPERVISOR: self.sys_supervisor.version,
|
ATTR_SUPERVISOR: self.sys_supervisor.version,
|
||||||
ATTR_HOMEASSISTANT: self.sys_homeassistant.version,
|
ATTR_HOMEASSISTANT: self.sys_homeassistant.version,
|
||||||
ATTR_HASSOS: self.sys_hassos.version,
|
ATTR_HASSOS: self.sys_os.version,
|
||||||
ATTR_DOCKER: self.sys_docker.info.version,
|
ATTR_DOCKER: self.sys_docker.info.version,
|
||||||
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
||||||
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
|
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
|
||||||
ATTR_FEATURES: self.sys_host.features,
|
ATTR_FEATURES: self.sys_host.features,
|
||||||
ATTR_MACHINE: self.sys_machine,
|
ATTR_MACHINE: self.sys_machine,
|
||||||
ATTR_ARCH: self.sys_arch.default,
|
ATTR_ARCH: self.sys_arch.default,
|
||||||
|
ATTR_STATE: self.sys_core.state,
|
||||||
ATTR_SUPPORTED_ARCH: self.sys_arch.supported,
|
ATTR_SUPPORTED_ARCH: self.sys_arch.supported,
|
||||||
ATTR_SUPPORTED: self.sys_core.supported,
|
ATTR_SUPPORTED: self.sys_core.supported,
|
||||||
ATTR_CHANNEL: self.sys_updater.channel,
|
ATTR_CHANNEL: self.sys_updater.channel,
|
||||||
ATTR_LOGGING: self.sys_config.logging,
|
ATTR_LOGGING: self.sys_config.logging,
|
||||||
ATTR_TIMEZONE: self.sys_config.timezone,
|
ATTR_TIMEZONE: self.sys_timezone,
|
||||||
}
|
}
|
||||||
|
@@ -2,10 +2,10 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from ipaddress import ip_address
|
from ipaddress import ip_address
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Union
|
from typing import Any, Union
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp import hdrs, web
|
from aiohttp import ClientTimeout, hdrs, web
|
||||||
from aiohttp.web_exceptions import (
|
from aiohttp.web_exceptions import (
|
||||||
HTTPBadGateway,
|
HTTPBadGateway,
|
||||||
HTTPServiceUnavailable,
|
HTTPServiceUnavailable,
|
||||||
@@ -25,10 +25,9 @@ from ..const import (
|
|||||||
COOKIE_INGRESS,
|
COOKIE_INGRESS,
|
||||||
HEADER_TOKEN,
|
HEADER_TOKEN,
|
||||||
HEADER_TOKEN_OLD,
|
HEADER_TOKEN_OLD,
|
||||||
REQUEST_FROM,
|
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, api_validate, require_home_assistant
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -50,17 +49,12 @@ class APIIngress(CoreSysAttributes):
|
|||||||
|
|
||||||
return addon
|
return addon
|
||||||
|
|
||||||
def _check_ha_access(self, request: web.Request) -> None:
|
|
||||||
if request[REQUEST_FROM] != self.sys_homeassistant:
|
|
||||||
_LOGGER.warning("Ingress is only available behind Home Assistant")
|
|
||||||
raise HTTPUnauthorized()
|
|
||||||
|
|
||||||
def _create_url(self, addon: Addon, path: str) -> str:
|
def _create_url(self, addon: Addon, path: str) -> str:
|
||||||
"""Create URL to container."""
|
"""Create URL to container."""
|
||||||
return f"http://{addon.ip_address}:{addon.ingress_port}/{path}"
|
return f"http://{addon.ip_address}:{addon.ingress_port}/{path}"
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def panels(self, request: web.Request) -> Dict[str, Any]:
|
async def panels(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Create a list of panel data."""
|
"""Create a list of panel data."""
|
||||||
addons = {}
|
addons = {}
|
||||||
for addon in self.sys_ingress.addons:
|
for addon in self.sys_ingress.addons:
|
||||||
@@ -74,18 +68,16 @@ class APIIngress(CoreSysAttributes):
|
|||||||
return {ATTR_PANELS: addons}
|
return {ATTR_PANELS: addons}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def create_session(self, request: web.Request) -> Dict[str, Any]:
|
@require_home_assistant
|
||||||
|
async def create_session(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Create a new session."""
|
"""Create a new session."""
|
||||||
self._check_ha_access(request)
|
|
||||||
|
|
||||||
session = self.sys_ingress.create_session()
|
session = self.sys_ingress.create_session()
|
||||||
return {ATTR_SESSION: session}
|
return {ATTR_SESSION: session}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def validate_session(self, request: web.Request) -> Dict[str, Any]:
|
@require_home_assistant
|
||||||
|
async def validate_session(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Validate session and extending how long it's valid for."""
|
"""Validate session and extending how long it's valid for."""
|
||||||
self._check_ha_access(request)
|
|
||||||
|
|
||||||
data = await api_validate(VALIDATE_SESSION_DATA, request)
|
data = await api_validate(VALIDATE_SESSION_DATA, request)
|
||||||
|
|
||||||
# Check Ingress Session
|
# Check Ingress Session
|
||||||
@@ -93,11 +85,11 @@ class APIIngress(CoreSysAttributes):
|
|||||||
_LOGGER.warning("No valid ingress session %s", data[ATTR_SESSION])
|
_LOGGER.warning("No valid ingress session %s", data[ATTR_SESSION])
|
||||||
raise HTTPUnauthorized()
|
raise HTTPUnauthorized()
|
||||||
|
|
||||||
|
@require_home_assistant
|
||||||
async def handler(
|
async def handler(
|
||||||
self, request: web.Request
|
self, request: web.Request
|
||||||
) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]:
|
) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]:
|
||||||
"""Route data to Supervisor ingress service."""
|
"""Route data to Supervisor ingress service."""
|
||||||
self._check_ha_access(request)
|
|
||||||
|
|
||||||
# Check Ingress Session
|
# Check Ingress Session
|
||||||
session = request.cookies.get(COOKIE_INGRESS)
|
session = request.cookies.get(COOKIE_INGRESS)
|
||||||
@@ -170,9 +162,18 @@ class APIIngress(CoreSysAttributes):
|
|||||||
) -> Union[web.Response, web.StreamResponse]:
|
) -> Union[web.Response, web.StreamResponse]:
|
||||||
"""Ingress route for request."""
|
"""Ingress route for request."""
|
||||||
url = self._create_url(addon, path)
|
url = self._create_url(addon, path)
|
||||||
data = await request.read()
|
|
||||||
source_header = _init_header(request, addon)
|
source_header = _init_header(request, addon)
|
||||||
|
|
||||||
|
# Passing the raw stream breaks requests for some webservers
|
||||||
|
# since we just need it for POST requests really, for all other methods
|
||||||
|
# we read the bytes and pass that to the request to the add-on
|
||||||
|
# add-ons needs to add support with that in the configuration
|
||||||
|
data = (
|
||||||
|
request.content
|
||||||
|
if request.method == "POST" and addon.ingress_stream
|
||||||
|
else await request.read()
|
||||||
|
)
|
||||||
|
|
||||||
async with self.sys_websession.request(
|
async with self.sys_websession.request(
|
||||||
request.method,
|
request.method,
|
||||||
url,
|
url,
|
||||||
@@ -180,6 +181,7 @@ class APIIngress(CoreSysAttributes):
|
|||||||
params=request.query,
|
params=request.query,
|
||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
data=data,
|
data=data,
|
||||||
|
timeout=ClientTimeout(total=None),
|
||||||
) as result:
|
) as result:
|
||||||
headers = _response_header(result)
|
headers = _response_header(result)
|
||||||
|
|
||||||
@@ -218,7 +220,7 @@ class APIIngress(CoreSysAttributes):
|
|||||||
|
|
||||||
def _init_header(
|
def _init_header(
|
||||||
request: web.Request, addon: str
|
request: web.Request, addon: str
|
||||||
) -> Union[CIMultiDict, Dict[str, str]]:
|
) -> Union[CIMultiDict, dict[str, str]]:
|
||||||
"""Create initial header."""
|
"""Create initial header."""
|
||||||
headers = {}
|
headers = {}
|
||||||
|
|
||||||
@@ -227,6 +229,7 @@ def _init_header(
|
|||||||
if name in (
|
if name in (
|
||||||
hdrs.CONTENT_LENGTH,
|
hdrs.CONTENT_LENGTH,
|
||||||
hdrs.CONTENT_ENCODING,
|
hdrs.CONTENT_ENCODING,
|
||||||
|
hdrs.TRANSFER_ENCODING,
|
||||||
hdrs.SEC_WEBSOCKET_EXTENSIONS,
|
hdrs.SEC_WEBSOCKET_EXTENSIONS,
|
||||||
hdrs.SEC_WEBSOCKET_PROTOCOL,
|
hdrs.SEC_WEBSOCKET_PROTOCOL,
|
||||||
hdrs.SEC_WEBSOCKET_VERSION,
|
hdrs.SEC_WEBSOCKET_VERSION,
|
||||||
@@ -245,7 +248,7 @@ def _init_header(
|
|||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
|
||||||
def _response_header(response: aiohttp.ClientResponse) -> Dict[str, str]:
|
def _response_header(response: aiohttp.ClientResponse) -> dict[str, str]:
|
||||||
"""Create response header."""
|
"""Create response header."""
|
||||||
headers = {}
|
headers = {}
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
"""Init file for Supervisor Jobs RESTful API."""
|
"""Init file for Supervisor Jobs RESTful API."""
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -20,7 +20,7 @@ class APIJobs(CoreSysAttributes):
|
|||||||
"""Handle RESTful API for OS 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 JobManager information."""
|
"""Return JobManager information."""
|
||||||
return {
|
return {
|
||||||
ATTR_IGNORE_CONDITIONS: self.sys_jobs.ignore_conditions,
|
ATTR_IGNORE_CONDITIONS: self.sys_jobs.ignore_conditions,
|
||||||
@@ -36,6 +36,8 @@ class APIJobs(CoreSysAttributes):
|
|||||||
|
|
||||||
self.sys_jobs.save_data()
|
self.sys_jobs.save_data()
|
||||||
|
|
||||||
|
await self.sys_resolution.evaluate.evaluate_system()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def reset(self, request: web.Request) -> None:
|
async def reset(self, request: web.Request) -> None:
|
||||||
"""Reset options for JobManager."""
|
"""Reset options for JobManager."""
|
||||||
|
1
supervisor/api/middleware/__init__.py
Normal file
1
supervisor/api/middleware/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""API middleware for aiohttp."""
|
208
supervisor/api/middleware/security.py
Normal file
208
supervisor/api/middleware/security.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
"""Handle security part of this API."""
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from aiohttp.web import Request, RequestHandler, Response, middleware
|
||||||
|
from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized
|
||||||
|
|
||||||
|
from ...const import (
|
||||||
|
REQUEST_FROM,
|
||||||
|
ROLE_ADMIN,
|
||||||
|
ROLE_BACKUP,
|
||||||
|
ROLE_DEFAULT,
|
||||||
|
ROLE_HOMEASSISTANT,
|
||||||
|
ROLE_MANAGER,
|
||||||
|
CoreState,
|
||||||
|
)
|
||||||
|
from ...coresys import CoreSys, CoreSysAttributes
|
||||||
|
from ..utils import api_return_error, excract_supervisor_token
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
|
||||||
|
# Block Anytime
|
||||||
|
BLACKLIST = re.compile(
|
||||||
|
r"^(?:"
|
||||||
|
r"|/homeassistant/api/hassio/.*"
|
||||||
|
r"|/core/api/hassio/.*"
|
||||||
|
r")$"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Free to call or have own security concepts
|
||||||
|
NO_SECURITY_CHECK = re.compile(
|
||||||
|
r"^(?:"
|
||||||
|
r"|/homeassistant/api/.*"
|
||||||
|
r"|/homeassistant/websocket"
|
||||||
|
r"|/core/api/.*"
|
||||||
|
r"|/core/websocket"
|
||||||
|
r"|/supervisor/ping"
|
||||||
|
r")$"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Observer allow API calls
|
||||||
|
OBSERVER_CHECK = re.compile(
|
||||||
|
r"^(?:"
|
||||||
|
r"|/.+/info"
|
||||||
|
r")$"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Can called by every add-on
|
||||||
|
ADDONS_API_BYPASS = re.compile(
|
||||||
|
r"^(?:"
|
||||||
|
r"|/addons/self/(?!security|update)[^/]+"
|
||||||
|
r"|/addons/self/options/config"
|
||||||
|
r"|/info"
|
||||||
|
r"|/services.*"
|
||||||
|
r"|/discovery.*"
|
||||||
|
r"|/auth"
|
||||||
|
r")$"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Policy role add-on API access
|
||||||
|
ADDONS_ROLE_ACCESS = {
|
||||||
|
ROLE_DEFAULT: re.compile(
|
||||||
|
r"^(?:"
|
||||||
|
r"|/.+/info"
|
||||||
|
r")$"
|
||||||
|
),
|
||||||
|
ROLE_HOMEASSISTANT: re.compile(
|
||||||
|
r"^(?:"
|
||||||
|
r"|/.+/info"
|
||||||
|
r"|/core/.+"
|
||||||
|
r"|/homeassistant/.+"
|
||||||
|
r")$"
|
||||||
|
),
|
||||||
|
ROLE_BACKUP: re.compile(
|
||||||
|
r"^(?:"
|
||||||
|
r"|/.+/info"
|
||||||
|
r"|/backups.*"
|
||||||
|
r"|/snapshots.*"
|
||||||
|
r")$"
|
||||||
|
),
|
||||||
|
ROLE_MANAGER: re.compile(
|
||||||
|
r"^(?:"
|
||||||
|
r"|/.+/info"
|
||||||
|
r"|/addons(?:/[^/]+/(?!security).+|/reload)?"
|
||||||
|
r"|/audio/.+"
|
||||||
|
r"|/auth/cache"
|
||||||
|
r"|/cli/.+"
|
||||||
|
r"|/core/.+"
|
||||||
|
r"|/dns/.+"
|
||||||
|
r"|/docker/.+"
|
||||||
|
r"|/jobs/.+"
|
||||||
|
r"|/hardware/.+"
|
||||||
|
r"|/hassos/.+"
|
||||||
|
r"|/homeassistant/.+"
|
||||||
|
r"|/host/.+"
|
||||||
|
r"|/multicast/.+"
|
||||||
|
r"|/network/.+"
|
||||||
|
r"|/observer/.+"
|
||||||
|
r"|/os/.+"
|
||||||
|
r"|/resolution/.+"
|
||||||
|
r"|/backups.*"
|
||||||
|
r"|/snapshots.*"
|
||||||
|
r"|/store.*"
|
||||||
|
r"|/supervisor/.+"
|
||||||
|
r"|/security/.+"
|
||||||
|
r")$"
|
||||||
|
),
|
||||||
|
ROLE_ADMIN: re.compile(
|
||||||
|
r".*"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
|
class SecurityMiddleware(CoreSysAttributes):
|
||||||
|
"""Security middleware functions."""
|
||||||
|
|
||||||
|
def __init__(self, coresys: CoreSys):
|
||||||
|
"""Initialize security middleware."""
|
||||||
|
self.coresys: CoreSys = coresys
|
||||||
|
|
||||||
|
@middleware
|
||||||
|
async def system_validation(
|
||||||
|
self, request: Request, handler: RequestHandler
|
||||||
|
) -> Response:
|
||||||
|
"""Check if core is ready to response."""
|
||||||
|
if self.sys_core.state not in (
|
||||||
|
CoreState.STARTUP,
|
||||||
|
CoreState.RUNNING,
|
||||||
|
CoreState.FREEZE,
|
||||||
|
):
|
||||||
|
return api_return_error(
|
||||||
|
message=f"System is not ready with state: {self.sys_core.state.value}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
|
@middleware
|
||||||
|
async def token_validation(
|
||||||
|
self, request: Request, handler: RequestHandler
|
||||||
|
) -> Response:
|
||||||
|
"""Check security access of this layer."""
|
||||||
|
request_from = None
|
||||||
|
supervisor_token = excract_supervisor_token(request)
|
||||||
|
|
||||||
|
# Blacklist
|
||||||
|
if BLACKLIST.match(request.path):
|
||||||
|
_LOGGER.error("%s is blacklisted!", request.path)
|
||||||
|
raise HTTPForbidden()
|
||||||
|
|
||||||
|
# Ignore security check
|
||||||
|
if NO_SECURITY_CHECK.match(request.path):
|
||||||
|
_LOGGER.debug("Passthrough %s", request.path)
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
|
# Not token
|
||||||
|
if not supervisor_token:
|
||||||
|
_LOGGER.warning("No API token provided for %s", request.path)
|
||||||
|
raise HTTPUnauthorized()
|
||||||
|
|
||||||
|
# Home-Assistant
|
||||||
|
if supervisor_token == self.sys_homeassistant.supervisor_token:
|
||||||
|
_LOGGER.debug("%s access from Home Assistant", request.path)
|
||||||
|
request_from = self.sys_homeassistant
|
||||||
|
|
||||||
|
# Host
|
||||||
|
if supervisor_token == self.sys_plugins.cli.supervisor_token:
|
||||||
|
_LOGGER.debug("%s access from Host", request.path)
|
||||||
|
request_from = self.sys_host
|
||||||
|
|
||||||
|
# Observer
|
||||||
|
if supervisor_token == self.sys_plugins.observer.supervisor_token:
|
||||||
|
if not OBSERVER_CHECK.match(request.path):
|
||||||
|
_LOGGER.warning("%s invalid Observer access", request.path)
|
||||||
|
raise HTTPForbidden()
|
||||||
|
_LOGGER.debug("%s access from Observer", request.path)
|
||||||
|
request_from = self.sys_plugins.observer
|
||||||
|
|
||||||
|
# Add-on
|
||||||
|
addon = None
|
||||||
|
if supervisor_token and not request_from:
|
||||||
|
addon = self.sys_addons.from_token(supervisor_token)
|
||||||
|
|
||||||
|
# Check Add-on API access
|
||||||
|
if addon and ADDONS_API_BYPASS.match(request.path):
|
||||||
|
_LOGGER.debug("Passthrough %s from %s", request.path, addon.slug)
|
||||||
|
request_from = addon
|
||||||
|
elif addon and addon.access_hassio_api:
|
||||||
|
# Check Role
|
||||||
|
if ADDONS_ROLE_ACCESS[addon.hassio_role].match(request.path):
|
||||||
|
_LOGGER.info("%s access from %s", request.path, addon.slug)
|
||||||
|
request_from = addon
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("%s no role for %s", request.path, addon.slug)
|
||||||
|
elif addon:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"%s missing API permission for %s", addon.slug, request.path
|
||||||
|
)
|
||||||
|
|
||||||
|
if request_from:
|
||||||
|
request[REQUEST_FROM] = request_from
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
|
_LOGGER.error("Invalid token for access %s", request.path)
|
||||||
|
raise HTTPForbidden()
|
@@ -1,7 +1,7 @@
|
|||||||
"""Init file for Supervisor Multicast RESTful API."""
|
"""Init file for Supervisor Multicast RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Awaitable, Dict
|
from typing import Any, Awaitable
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -34,7 +34,7 @@ class APIMulticast(CoreSysAttributes):
|
|||||||
"""Handle RESTful API for Multicast functions."""
|
"""Handle RESTful API for Multicast 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 Multicast information."""
|
"""Return Multicast information."""
|
||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_plugins.multicast.version,
|
ATTR_VERSION: self.sys_plugins.multicast.version,
|
||||||
@@ -43,7 +43,7 @@ class APIMulticast(CoreSysAttributes):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
async def stats(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
stats = await self.sys_plugins.multicast.stats()
|
stats = await self.sys_plugins.multicast.stats()
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"""REST API for network."""
|
"""REST API for network."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from ipaddress import ip_address, ip_interface
|
from ipaddress import ip_address, ip_interface
|
||||||
from typing import Any, Awaitable, Dict
|
from typing import Any, Awaitable
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import attr
|
import attr
|
||||||
@@ -82,7 +82,7 @@ SCHEMA_UPDATE = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def ipconfig_struct(config: IpConfig) -> Dict[str, Any]:
|
def ipconfig_struct(config: IpConfig) -> dict[str, Any]:
|
||||||
"""Return a dict with information about ip configuration."""
|
"""Return a dict with information about ip configuration."""
|
||||||
return {
|
return {
|
||||||
ATTR_METHOD: config.method,
|
ATTR_METHOD: config.method,
|
||||||
@@ -92,7 +92,7 @@ def ipconfig_struct(config: IpConfig) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def wifi_struct(config: WifiConfig) -> Dict[str, Any]:
|
def wifi_struct(config: WifiConfig) -> dict[str, Any]:
|
||||||
"""Return a dict with information about wifi configuration."""
|
"""Return a dict with information about wifi configuration."""
|
||||||
return {
|
return {
|
||||||
ATTR_MODE: config.mode,
|
ATTR_MODE: config.mode,
|
||||||
@@ -102,7 +102,7 @@ def wifi_struct(config: WifiConfig) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def vlan_struct(config: VlanConfig) -> Dict[str, Any]:
|
def vlan_struct(config: VlanConfig) -> dict[str, Any]:
|
||||||
"""Return a dict with information about VLAN configuration."""
|
"""Return a dict with information about VLAN configuration."""
|
||||||
return {
|
return {
|
||||||
ATTR_ID: config.id,
|
ATTR_ID: config.id,
|
||||||
@@ -110,7 +110,7 @@ def vlan_struct(config: VlanConfig) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def interface_struct(interface: Interface) -> Dict[str, Any]:
|
def interface_struct(interface: Interface) -> dict[str, Any]:
|
||||||
"""Return a dict with information of a interface to be used in th API."""
|
"""Return a dict with information of a interface to be used in th API."""
|
||||||
return {
|
return {
|
||||||
ATTR_INTERFACE: interface.name,
|
ATTR_INTERFACE: interface.name,
|
||||||
@@ -125,7 +125,7 @@ def interface_struct(interface: Interface) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def accesspoint_struct(accesspoint: AccessPoint) -> Dict[str, Any]:
|
def accesspoint_struct(accesspoint: AccessPoint) -> dict[str, Any]:
|
||||||
"""Return a dict for AccessPoint."""
|
"""Return a dict for AccessPoint."""
|
||||||
return {
|
return {
|
||||||
ATTR_MODE: accesspoint.mode,
|
ATTR_MODE: accesspoint.mode,
|
||||||
@@ -158,7 +158,7 @@ class APINetwork(CoreSysAttributes):
|
|||||||
raise APIError(f"Interface {name} does not exist") from None
|
raise APIError(f"Interface {name} does not exist") from None
|
||||||
|
|
||||||
@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 network information."""
|
"""Return network information."""
|
||||||
return {
|
return {
|
||||||
ATTR_INTERFACES: [
|
ATTR_INTERFACES: [
|
||||||
@@ -176,7 +176,7 @@ class APINetwork(CoreSysAttributes):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def interface_info(self, request: web.Request) -> Dict[str, Any]:
|
async def interface_info(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Return network information for a interface."""
|
"""Return network information for a interface."""
|
||||||
interface = self._get_interface(request.match_info.get(ATTR_INTERFACE))
|
interface = self._get_interface(request.match_info.get(ATTR_INTERFACE))
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ class APINetwork(CoreSysAttributes):
|
|||||||
return asyncio.shield(self.sys_host.network.update())
|
return asyncio.shield(self.sys_host.network.update())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def scan_accesspoints(self, request: web.Request) -> Dict[str, Any]:
|
async def scan_accesspoints(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Scan and return a list of available networks."""
|
"""Scan and return a list of available networks."""
|
||||||
interface = self._get_interface(request.match_info.get(ATTR_INTERFACE))
|
interface = self._get_interface(request.match_info.get(ATTR_INTERFACE))
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"""Init file for Supervisor Observer RESTful API."""
|
"""Init file for Supervisor Observer RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -33,7 +33,7 @@ class APIObserver(CoreSysAttributes):
|
|||||||
"""Handle RESTful API for Observer functions."""
|
"""Handle RESTful API for Observer 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 HA Observer information."""
|
"""Return HA Observer information."""
|
||||||
return {
|
return {
|
||||||
ATTR_HOST: str(self.sys_docker.network.observer),
|
ATTR_HOST: str(self.sys_docker.network.observer),
|
||||||
@@ -43,7 +43,7 @@ class APIObserver(CoreSysAttributes):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
async def stats(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
stats = await self.sys_plugins.observer.stats()
|
stats = await self.sys_plugins.observer.stats()
|
||||||
|
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
"""Init file for Supervisor HassOS RESTful API."""
|
"""Init file for Supervisor HassOS RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Awaitable, Dict
|
from pathlib import Path
|
||||||
|
from typing import Any, Awaitable
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -9,42 +10,60 @@ import voluptuous as vol
|
|||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_BOARD,
|
ATTR_BOARD,
|
||||||
ATTR_BOOT,
|
ATTR_BOOT,
|
||||||
|
ATTR_DEVICES,
|
||||||
ATTR_UPDATE_AVAILABLE,
|
ATTR_UPDATE_AVAILABLE,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
ATTR_VERSION_LATEST,
|
ATTR_VERSION_LATEST,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..validate import version_tag
|
from ..validate import version_tag
|
||||||
|
from .const import ATTR_DATA_DISK, ATTR_DEVICE
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag})
|
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag})
|
||||||
|
SCHEMA_DISK = vol.Schema({vol.Required(ATTR_DEVICE): vol.All(str, vol.Coerce(Path))})
|
||||||
|
|
||||||
|
|
||||||
class APIOS(CoreSysAttributes):
|
class APIOS(CoreSysAttributes):
|
||||||
"""Handle RESTful API for OS 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 OS information."""
|
"""Return OS information."""
|
||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_hassos.version,
|
ATTR_VERSION: self.sys_os.version,
|
||||||
ATTR_VERSION_LATEST: self.sys_hassos.latest_version,
|
ATTR_VERSION_LATEST: self.sys_os.latest_version,
|
||||||
ATTR_UPDATE_AVAILABLE: self.sys_hassos.need_update,
|
ATTR_UPDATE_AVAILABLE: self.sys_os.need_update,
|
||||||
ATTR_BOARD: self.sys_hassos.board,
|
ATTR_BOARD: self.sys_os.board,
|
||||||
ATTR_BOOT: self.sys_dbus.rauc.boot_slot,
|
ATTR_BOOT: self.sys_dbus.rauc.boot_slot,
|
||||||
|
ATTR_DATA_DISK: self.sys_os.datadisk.disk_used,
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def update(self, request: web.Request) -> None:
|
async def update(self, request: web.Request) -> None:
|
||||||
"""Update OS."""
|
"""Update OS."""
|
||||||
body = await api_validate(SCHEMA_VERSION, request)
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
version = body.get(ATTR_VERSION, self.sys_hassos.latest_version)
|
version = body.get(ATTR_VERSION, self.sys_os.latest_version)
|
||||||
|
|
||||||
await asyncio.shield(self.sys_hassos.update(version))
|
await asyncio.shield(self.sys_os.update(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 OS."""
|
"""Trigger config reload on OS."""
|
||||||
return asyncio.shield(self.sys_hassos.config_sync())
|
return asyncio.shield(self.sys_os.config_sync())
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def migrate_data(self, request: web.Request) -> None:
|
||||||
|
"""Trigger data disk migration on Host."""
|
||||||
|
body = await api_validate(SCHEMA_DISK, request)
|
||||||
|
|
||||||
|
await asyncio.shield(self.sys_os.datadisk.migrate_disk(body[ATTR_DEVICE]))
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def list_data(self, request: web.Request) -> dict[str, Any]:
|
||||||
|
"""Return possible data targets."""
|
||||||
|
return {
|
||||||
|
ATTR_DEVICES: self.sys_os.datadisk.available_disks,
|
||||||
|
}
|
||||||
|
@@ -1,9 +1,16 @@
|
|||||||
|
|
||||||
try {
|
function loadES5() {
|
||||||
new Function("import('/api/hassio/app/frontend_latest/entrypoint.b537a8c0.js')")();
|
|
||||||
} catch (err) {
|
|
||||||
var el = document.createElement('script');
|
var el = document.createElement('script');
|
||||||
el.src = '/api/hassio/app/frontend_es5/entrypoint.f493e22d.js';
|
el.src = '/api/hassio/app/frontend_es5/entrypoint.4bb3aeeb.js';
|
||||||
document.body.appendChild(el);
|
document.body.appendChild(el);
|
||||||
}
|
}
|
||||||
|
if (/.*Version\/(?:11|12)(?:\.\d+)*.*Safari\//.test(navigator.userAgent)) {
|
||||||
|
loadES5();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
new Function("import('/api/hassio/app/frontend_latest/entrypoint.9dbee840.js')")();
|
||||||
|
} catch (err) {
|
||||||
|
loadES5();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
1
supervisor/api/panel/frontend_es5/069b642c.js
Normal file
1
supervisor/api/panel/frontend_es5/069b642c.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/069b642c.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/069b642c.js.gz
Normal file
Binary file not shown.
2
supervisor/api/panel/frontend_es5/0989d194.js
Normal file
2
supervisor/api/panel/frontend_es5/0989d194.js
Normal file
File diff suppressed because one or more lines are too long
20
supervisor/api/panel/frontend_es5/0989d194.js.LICENSE.txt
Normal file
20
supervisor/api/panel/frontend_es5/0989d194.js.LICENSE.txt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||||
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||||
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
Code distributed by Google as part of the polymer project is also
|
||||||
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
BIN
supervisor/api/panel/frontend_es5/0989d194.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/0989d194.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/28fc574d.js
Normal file
1
supervisor/api/panel/frontend_es5/28fc574d.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/28fc574d.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/28fc574d.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/2dbdaab4.js
Normal file
1
supervisor/api/panel/frontend_es5/2dbdaab4.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/2dbdaab4.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/2dbdaab4.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/37f808e6.js
Normal file
1
supervisor/api/panel/frontend_es5/37f808e6.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/37f808e6.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/37f808e6.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/432539dc.js
Normal file
1
supervisor/api/panel/frontend_es5/432539dc.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/432539dc.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/432539dc.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/44b59b80.js
Normal file
1
supervisor/api/panel/frontend_es5/44b59b80.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/44b59b80.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/44b59b80.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/4a274bef.js
Normal file
1
supervisor/api/panel/frontend_es5/4a274bef.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/4a274bef.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/4a274bef.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/64fa61b0.js
Normal file
1
supervisor/api/panel/frontend_es5/64fa61b0.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/64fa61b0.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/64fa61b0.js.gz
Normal file
Binary file not shown.
2
supervisor/api/panel/frontend_es5/69e58998.js
Normal file
2
supervisor/api/panel/frontend_es5/69e58998.js
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
/*! js-yaml 4.1.0 https://github.com/nodeca/js-yaml @license MIT */
|
BIN
supervisor/api/panel/frontend_es5/69e58998.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/69e58998.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/69f9cc55.js
Normal file
1
supervisor/api/panel/frontend_es5/69f9cc55.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/69f9cc55.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/69f9cc55.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/6b0926eb.js
Normal file
1
supervisor/api/panel/frontend_es5/6b0926eb.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
!function(){"use strict";var r,t,n={5425:function(r,t,n){var e=n(93217);n(58556);function o(r,t){return function(r){if(Array.isArray(r))return r}(r)||function(r,t){var n=null==r?null:"undefined"!=typeof Symbol&&r[Symbol.iterator]||r["@@iterator"];if(null==n)return;var e,o,u=[],i=!0,a=!1;try{for(n=n.call(r);!(i=(e=n.next()).done)&&(u.push(e.value),!t||u.length!==t);i=!0);}catch(f){a=!0,o=f}finally{try{i||null==n.return||n.return()}finally{if(a)throw o}}return u}(r,t)||function(r,t){if(!r)return;if("string"==typeof r)return u(r,t);var n=Object.prototype.toString.call(r).slice(8,-1);"Object"===n&&r.constructor&&(n=r.constructor.name);if("Map"===n||"Set"===n)return Array.from(r);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return u(r,t)}(r,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function u(r,t){(null==t||t>r.length)&&(t=r.length);for(var n=0,e=new Array(t);n<t;n++)e[n]=r[n];return e}var i={filterData:function(r,t,n){return n=n.toUpperCase(),r.filter((function(r){return Object.entries(t).some((function(t){var e=o(t,2),u=e[0],i=e[1];return!(!i.filterable||!String(i.filterKey?r[i.valueColumn||u][i.filterKey]:r[i.valueColumn||u]).toUpperCase().includes(n))}))}))},sortData:function(r,t,n,e){return r.sort((function(r,o){var u=1;"desc"===n&&(u=-1);var i=t.filterKey?r[t.valueColumn||e][t.filterKey]:r[t.valueColumn||e],a=t.filterKey?o[t.valueColumn||e][t.filterKey]:o[t.valueColumn||e];return"string"==typeof i&&(i=i.toUpperCase()),"string"==typeof a&&(a=a.toUpperCase()),void 0===i&&void 0!==a?1:void 0===a&&void 0!==i?-1:i<a?-1*u:i>a?1*u:0}))}};(0,e.Jj)(i)}},e={};function o(r){var t=e[r];if(void 0!==t)return t.exports;var u=e[r]={exports:{}};return n[r](u,u.exports,o),u.exports}o.m=n,o.x=function(){var r=o.O(void 0,[191],(function(){return o(5425)}));return r=o.O(r)},r=[],o.O=function(t,n,e,u){if(!n){var i=1/0;for(c=0;c<r.length;c++){n=r[c][0],e=r[c][1],u=r[c][2];for(var a=!0,f=0;f<n.length;f++)(!1&u||i>=u)&&Object.keys(o.O).every((function(r){return o.O[r](n[f])}))?n.splice(f--,1):(a=!1,u<i&&(i=u));if(a){r.splice(c--,1);var l=e();void 0!==l&&(t=l)}}return t}u=u||0;for(var c=r.length;c>0&&r[c-1][2]>u;c--)r[c]=r[c-1];r[c]=[n,e,u]},o.n=function(r){var t=r&&r.__esModule?function(){return r.default}:function(){return r};return o.d(t,{a:t}),t},o.d=function(r,t){for(var n in t)o.o(t,n)&&!o.o(r,n)&&Object.defineProperty(r,n,{enumerable:!0,get:t[n]})},o.f={},o.e=function(r){return Promise.all(Object.keys(o.f).reduce((function(t,n){return o.f[n](r,t),t}),[]))},o.u=function(r){return"2dbdaab4.js"},o.o=function(r,t){return Object.prototype.hasOwnProperty.call(r,t)},o.p="/api/hassio/app/frontend_es5/",function(){var r={477:1,425:1};o.f.i=function(t,n){r[t]||importScripts(o.p+o.u(t))};var t=self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[],n=t.push.bind(t);t.push=function(t){var e=t[0],u=t[1],i=t[2];for(var a in u)o.o(u,a)&&(o.m[a]=u[a]);for(i&&i(o);e.length;)r[e.pop()]=1;n(t)}}(),t=o.x,o.x=function(){return o.e(191).then(t)};o.x()}();
|
BIN
supervisor/api/panel/frontend_es5/6b0926eb.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/6b0926eb.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/6eb51463.js
Normal file
1
supervisor/api/panel/frontend_es5/6eb51463.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/6eb51463.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/6eb51463.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/72c04451.js
Normal file
1
supervisor/api/panel/frontend_es5/72c04451.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/72c04451.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/72c04451.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/b940d4c4.js
Normal file
1
supervisor/api/panel/frontend_es5/b940d4c4.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/b940d4c4.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/b940d4c4.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/c81c9790.js
Normal file
1
supervisor/api/panel/frontend_es5/c81c9790.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/c81c9790.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/c81c9790.js.gz
Normal file
Binary file not shown.
2
supervisor/api/panel/frontend_es5/ccd28201.js
Normal file
2
supervisor/api/panel/frontend_es5/ccd28201.js
Normal file
File diff suppressed because one or more lines are too long
14
supervisor/api/panel/frontend_es5/ccd28201.js.LICENSE.txt
Normal file
14
supervisor/api/panel/frontend_es5/ccd28201.js.LICENSE.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/*! *****************************************************************************
|
||||||
|
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||||
|
this file except in compliance with the License. You may obtain a copy of the
|
||||||
|
License at http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
|
||||||
|
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
|
||||||
|
MERCHANTABLITY OR NON-INFRINGEMENT.
|
||||||
|
|
||||||
|
See the Apache Version 2.0 License for specific language governing permissions
|
||||||
|
and limitations under the License.
|
||||||
|
***************************************************************************** */
|
BIN
supervisor/api/panel/frontend_es5/ccd28201.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/ccd28201.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"chunk.0aeb318c1dfa0b946242.js","sources":["webpack://home-assistant-frontend/chunk.0aeb318c1dfa0b946242.js"],"mappings":"AAAA","sourceRoot":""}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user