mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-08-12 10:39:21 +00:00
Compare commits
388 Commits
2022.08.2
...
remove-pas
Author | SHA1 | Date | |
---|---|---|---|
![]() |
de7ef86f52 | ||
![]() |
6f614c91d7 | ||
![]() |
8b4e8e9804 | ||
![]() |
5d1ef34f17 | ||
![]() |
9504eff889 | ||
![]() |
d5828a6815 | ||
![]() |
488f246f75 | ||
![]() |
000d4ec78a | ||
![]() |
6c0415163b | ||
![]() |
0205cbb78b | ||
![]() |
72db559adc | ||
![]() |
a57c145870 | ||
![]() |
759fd1077a | ||
![]() |
fb90e6d07e | ||
![]() |
86d17acd83 | ||
![]() |
6eb8de02eb | ||
![]() |
f4df298cb3 | ||
![]() |
9800955646 | ||
![]() |
1706d14c9c | ||
![]() |
cf68d9fd19 | ||
![]() |
6f2f8e88a6 | ||
![]() |
c896b60410 | ||
![]() |
0200c72db1 | ||
![]() |
fe5705b35b | ||
![]() |
3c3846240d | ||
![]() |
b86a6d292f | ||
![]() |
1feda7d89f | ||
![]() |
73d795e05e | ||
![]() |
e449205863 | ||
![]() |
841f68c175 | ||
![]() |
0df19cee91 | ||
![]() |
d3f490bcc3 | ||
![]() |
0fda5f6c4b | ||
![]() |
e984797f3c | ||
![]() |
334bcf48fb | ||
![]() |
73f3627ebd | ||
![]() |
0adf2864b4 | ||
![]() |
f542c8e790 | ||
![]() |
a7c1693911 | ||
![]() |
bb497c0c9f | ||
![]() |
95eee712a3 | ||
![]() |
6aeac271fa | ||
![]() |
1204852893 | ||
![]() |
f6c3bdb6a8 | ||
![]() |
fbb2776277 | ||
![]() |
5ced4e2f3b | ||
![]() |
61a7e6a87d | ||
![]() |
88d25fc14e | ||
![]() |
b5233cd398 | ||
![]() |
109b8b47a0 | ||
![]() |
c5566f40ca | ||
![]() |
9dd5d89458 | ||
![]() |
c6f31ce73f | ||
![]() |
da9787bb58 | ||
![]() |
4254b80c0a | ||
![]() |
b4fd5b28f6 | ||
![]() |
6a95f97ec9 | ||
![]() |
fc171b674e | ||
![]() |
17f5ff1cb1 | ||
![]() |
b017fed329 | ||
![]() |
4c69c7206e | ||
![]() |
caf094815f | ||
![]() |
4043503940 | ||
![]() |
4cd80c4228 | ||
![]() |
7fd38da403 | ||
![]() |
7688e1b9cb | ||
![]() |
61202db8b2 | ||
![]() |
34c394c3d1 | ||
![]() |
ebe9c32092 | ||
![]() |
2108b218d8 | ||
![]() |
b85b5041b4 | ||
![]() |
7c29c56b9a | ||
![]() |
207ae8ae4f | ||
![]() |
c13531e9e3 | ||
![]() |
0373030cb2 | ||
![]() |
9635c70f2b | ||
![]() |
ff54c5268c | ||
![]() |
c7141caa12 | ||
![]() |
d0bf2aa817 | ||
![]() |
ed2f57f3ca | ||
![]() |
744cd4ea39 | ||
![]() |
b3ca08f2c2 | ||
![]() |
afbafe44f9 | ||
![]() |
a54e0a8401 | ||
![]() |
df336dd493 | ||
![]() |
778134f096 | ||
![]() |
dc4a753fe3 | ||
![]() |
f5b6feec77 | ||
![]() |
08c40dfe98 | ||
![]() |
98110a26d4 | ||
![]() |
610b0e9adc | ||
![]() |
be39275cd0 | ||
![]() |
0c7fc10147 | ||
![]() |
6dd9b573fd | ||
![]() |
2c2f1afc48 | ||
![]() |
8cf71ffa81 | ||
![]() |
1123101c87 | ||
![]() |
5adddc97e3 | ||
![]() |
d09f35f079 | ||
![]() |
9a3459434f | ||
![]() |
fce0d2aaed | ||
![]() |
842e550dda | ||
![]() |
c9ee76f1d3 | ||
![]() |
852771fbcf | ||
![]() |
de1f3555b1 | ||
![]() |
c0b75edfb7 | ||
![]() |
a3204f4ebd | ||
![]() |
84e4d70a37 | ||
![]() |
cede47e95c | ||
![]() |
75b3ebec7c | ||
![]() |
b707a468d2 | ||
![]() |
4e41255a57 | ||
![]() |
3ceec044a8 | ||
![]() |
3646ae070e | ||
![]() |
a6caccd845 | ||
![]() |
c6ddc8e427 | ||
![]() |
8bfd07d66b | ||
![]() |
d764f00580 | ||
![]() |
d9b86fa2ab | ||
![]() |
0ddce4d9bc | ||
![]() |
8386b5cb3a | ||
![]() |
8fc036874a | ||
![]() |
2a625defc0 | ||
![]() |
3f1e72d69f | ||
![]() |
42374a3a3f | ||
![]() |
2adebd9da6 | ||
![]() |
3b2c75fbd7 | ||
![]() |
19f6e12936 | ||
![]() |
abe59ab1e5 | ||
![]() |
79d8db6015 | ||
![]() |
1b317f5e92 | ||
![]() |
c262a39c11 | ||
![]() |
6ee86ee062 | ||
![]() |
b3a869429f | ||
![]() |
e4e9dee02c | ||
![]() |
2887934dbe | ||
![]() |
daeec266cc | ||
![]() |
3887fcfc93 | ||
![]() |
ab83c51910 | ||
![]() |
2ae2d0e107 | ||
![]() |
613ef9010a | ||
![]() |
675bea7835 | ||
![]() |
3d74e07c5e | ||
![]() |
692d34a13c | ||
![]() |
440379680e | ||
![]() |
165af46f54 | ||
![]() |
4c2d729646 | ||
![]() |
8ffd227849 | ||
![]() |
64c5ba1635 | ||
![]() |
37a247160e | ||
![]() |
919f1e9149 | ||
![]() |
d73d8d00f0 | ||
![]() |
09c699a2fe | ||
![]() |
cb992762d1 | ||
![]() |
8f0cec10d5 | ||
![]() |
4a0e17f050 | ||
![]() |
b4c74404e3 | ||
![]() |
649091f3bd | ||
![]() |
a27be5d621 | ||
![]() |
939eb81581 | ||
![]() |
ee1daa0b35 | ||
![]() |
242c05a19b | ||
![]() |
9024085712 | ||
![]() |
e0abb98aaf | ||
![]() |
4ffa628a6e | ||
![]() |
417ee418f2 | ||
![]() |
0f79ba5a3d | ||
![]() |
47fd849319 | ||
![]() |
99e0eab958 | ||
![]() |
0a753c55ca | ||
![]() |
72d81e43dd | ||
![]() |
83e5359bd2 | ||
![]() |
51875bdcd5 | ||
![]() |
ecabf9dea7 | ||
![]() |
c1954f4426 | ||
![]() |
0991f52100 | ||
![]() |
fed4a05003 | ||
![]() |
089635f4d3 | ||
![]() |
15fa8de05c | ||
![]() |
8fc91f5288 | ||
![]() |
4461192fa7 | ||
![]() |
2fe7c0dce6 | ||
![]() |
e2e11faf18 | ||
![]() |
fcbef6b78b | ||
![]() |
10810fb1b9 | ||
![]() |
92408bb893 | ||
![]() |
61fc01915f | ||
![]() |
fea60c57a2 | ||
![]() |
c1ac6c0432 | ||
![]() |
64ca530e66 | ||
![]() |
08f290ca10 | ||
![]() |
03849258eb | ||
![]() |
32d0d84c53 | ||
![]() |
83265c4dc5 | ||
![]() |
a9cbeb21c9 | ||
![]() |
1af4a362c2 | ||
![]() |
b9e2cfad4d | ||
![]() |
726ded70d3 | ||
![]() |
ac56f1511f | ||
![]() |
3d7d52a62b | ||
![]() |
941e1f5c91 | ||
![]() |
1a2b13018a | ||
![]() |
da721f455e | ||
![]() |
4e91db10a9 | ||
![]() |
ba9bcd9e57 | ||
![]() |
c193c91fe7 | ||
![]() |
bdde24ae9e | ||
![]() |
b56995be27 | ||
![]() |
1f7199cf00 | ||
![]() |
e48e024bb3 | ||
![]() |
02c181c1ff | ||
![]() |
70cf6cc0d9 | ||
![]() |
9abf38f285 | ||
![]() |
54dfba1faa | ||
![]() |
ed778f09ee | ||
![]() |
b044095e57 | ||
![]() |
c41f13bf18 | ||
![]() |
2ddb5ca53f | ||
![]() |
fad75810ab | ||
![]() |
4d9e30adef | ||
![]() |
80a6171692 | ||
![]() |
815669e6e3 | ||
![]() |
a8133f0640 | ||
![]() |
2809f23391 | ||
![]() |
348fb56cb5 | ||
![]() |
4afbedfa3d | ||
![]() |
8d495aa437 | ||
![]() |
9559ac06b9 | ||
![]() |
e80d882395 | ||
![]() |
14fcda5d78 | ||
![]() |
14cd261b76 | ||
![]() |
783395a27d | ||
![]() |
a2dffe595e | ||
![]() |
a0b28ebb97 | ||
![]() |
89de909020 | ||
![]() |
672b220f69 | ||
![]() |
d59625e5b8 | ||
![]() |
2947e8e8e9 | ||
![]() |
5f04e4fb6a | ||
![]() |
4c5d54b7a3 | ||
![]() |
30932a83f8 | ||
![]() |
1df0a5db2a | ||
![]() |
9affa5316c | ||
![]() |
a13c8d86b9 | ||
![]() |
80248dc36d | ||
![]() |
2ad122ec18 | ||
![]() |
d7ec3646f9 | ||
![]() |
030e1a92f3 | ||
![]() |
3cf999b306 | ||
![]() |
2d2926f7ff | ||
![]() |
23ba0ad6a5 | ||
![]() |
38fffb7641 | ||
![]() |
03eda30e20 | ||
![]() |
10c87d5a39 | ||
![]() |
7a0c4c5060 | ||
![]() |
5d2b5bada7 | ||
![]() |
bde5c938a7 | ||
![]() |
34afcef4f1 | ||
![]() |
2ebb405871 | ||
![]() |
1f7c067c90 | ||
![]() |
9da4ea20a9 | ||
![]() |
767c2bd91a | ||
![]() |
7c1f03932e | ||
![]() |
f3d1904e28 | ||
![]() |
9cc87cabcd | ||
![]() |
18299cf274 | ||
![]() |
261c2431c6 | ||
![]() |
d36fc938b8 | ||
![]() |
dc0430f677 | ||
![]() |
1e2dc93158 | ||
![]() |
69a33777a7 | ||
![]() |
57f0c9af1b | ||
![]() |
14d26ad9aa | ||
![]() |
b36316416b | ||
![]() |
c634cc1f34 | ||
![]() |
646725bb08 | ||
![]() |
618c89c4d8 | ||
![]() |
0dc442d0cb | ||
![]() |
6ae664b448 | ||
![]() |
18b43ce767 | ||
![]() |
f9b474866b | ||
![]() |
1a76035682 | ||
![]() |
e332f4b2bd | ||
![]() |
ab27fd7b57 | ||
![]() |
12c0faf803 | ||
![]() |
c0a409b25f | ||
![]() |
2be33a80a7 | ||
![]() |
d684aab207 | ||
![]() |
ec6da7851e | ||
![]() |
eb621f6a2c | ||
![]() |
a1a9c55542 | ||
![]() |
d15a7c27ca | ||
![]() |
fb46335d16 | ||
![]() |
48e666e1fc | ||
![]() |
ff462ae976 | ||
![]() |
23731d9a6e | ||
![]() |
30df8ce5c7 | ||
![]() |
951efd6b29 | ||
![]() |
262fd05c6d | ||
![]() |
2a6fc512e7 | ||
![]() |
bb0d89f8fd | ||
![]() |
e9ccc7ee19 | ||
![]() |
a5103cc329 | ||
![]() |
c24b811180 | ||
![]() |
611963f5dd | ||
![]() |
0958cd0c06 | ||
![]() |
c406814794 | ||
![]() |
c3459fd32a | ||
![]() |
2072370ccc | ||
![]() |
615758a1df | ||
![]() |
cd10b597dd | ||
![]() |
50c277137d | ||
![]() |
99bc201688 | ||
![]() |
0b09eb3659 | ||
![]() |
a6795536ad | ||
![]() |
a46536e9be | ||
![]() |
c01bed9d97 | ||
![]() |
2f4e06aadf | ||
![]() |
b8249548ae | ||
![]() |
5f98ab7e3e | ||
![]() |
d195f19fa8 | ||
![]() |
c67d4d7c0b | ||
![]() |
5aa8028ff5 | ||
![]() |
b71c6c60da | ||
![]() |
4f272ad4fd | ||
![]() |
611128c014 | ||
![]() |
cbf73ceaa3 | ||
![]() |
01e24a3e74 | ||
![]() |
10dcf5c12f | ||
![]() |
ebae1e70ee | ||
![]() |
b1ddb917c8 | ||
![]() |
d6c25c4188 | ||
![]() |
170e85396e | ||
![]() |
bf48d48c51 | ||
![]() |
fc646db95f | ||
![]() |
0769af9383 | ||
![]() |
1f28e6ad93 | ||
![]() |
2dab39bf90 | ||
![]() |
dcd0592d44 | ||
![]() |
7c4b20380e | ||
![]() |
1d304bd6ff | ||
![]() |
4ea27f6311 | ||
![]() |
3dc36c3402 | ||
![]() |
bae7fe4184 | ||
![]() |
df030e6209 | ||
![]() |
09d60b4957 | ||
![]() |
004065ae33 | ||
![]() |
854d337dd3 | ||
![]() |
2c5bb3f714 | ||
![]() |
7b63544474 | ||
![]() |
97af1fc66e | ||
![]() |
32d65722e9 | ||
![]() |
d5f9fcfdc7 | ||
![]() |
ffa524d3a4 | ||
![]() |
9c7de4a6c3 | ||
![]() |
b4e1e3e853 | ||
![]() |
c7f7fbd41a | ||
![]() |
cbddca2658 | ||
![]() |
f4811a0243 | ||
![]() |
024b813865 | ||
![]() |
5919bc2252 | ||
![]() |
8bca34ec6b | ||
![]() |
8b5e96a8ad | ||
![]() |
2d908ffcec | ||
![]() |
c3f7a45d61 | ||
![]() |
97b05c2078 | ||
![]() |
aa9a774939 | ||
![]() |
3388a13693 | ||
![]() |
9957e3dd4c | ||
![]() |
01c2bd1b0c | ||
![]() |
2cd7f9d1b0 | ||
![]() |
5fc9484f73 | ||
![]() |
e6dfe83d62 | ||
![]() |
3f88236495 | ||
![]() |
96065ed704 | ||
![]() |
7754424cb8 | ||
![]() |
be842d5e6c | ||
![]() |
c8f184f24c | ||
![]() |
e82cb5da45 | ||
![]() |
a968f6e90a | ||
![]() |
3eac3a6178 | ||
![]() |
b831dce443 | ||
![]() |
e62324e43f | ||
![]() |
a92058e6fc | ||
![]() |
29b2de6998 | ||
![]() |
057a048504 | ||
![]() |
29a1e6f68b | ||
![]() |
702cb4f5be |
@@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "Supervisor dev",
|
"name": "Supervisor dev",
|
||||||
"image": "ghcr.io/home-assistant/devcontainer:supervisor",
|
"image": "ghcr.io/home-assistant/devcontainer:supervisor",
|
||||||
|
"containerEnv": {
|
||||||
|
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
||||||
|
},
|
||||||
"appPort": ["9123:8123", "7357:4357"],
|
"appPort": ["9123:8123", "7357:4357"],
|
||||||
"postCreateCommand": "bash devcontainer_bootstrap",
|
"postCreateCommand": "bash devcontainer_bootstrap",
|
||||||
"runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
|
"runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
|
||||||
@@ -10,7 +13,7 @@
|
|||||||
"visualstudioexptteam.vscodeintellicode",
|
"visualstudioexptteam.vscodeintellicode",
|
||||||
"esbenp.prettier-vscode"
|
"esbenp.prettier-vscode"
|
||||||
],
|
],
|
||||||
"mounts": [ "type=volume,target=/var/lib/docker" ],
|
"mounts": ["type=volume,target=/var/lib/docker"],
|
||||||
"settings": {
|
"settings": {
|
||||||
"terminal.integrated.profiles.linux": {
|
"terminal.integrated.profiles.linux": {
|
||||||
"zsh": {
|
"zsh": {
|
||||||
@@ -26,7 +29,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", "py39"],
|
"python.formatting.blackArgs": ["--target-version", "py310"],
|
||||||
"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",
|
||||||
|
52
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
52
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -20,22 +20,14 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
## Environment
|
## Environment
|
||||||
- type: input
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
attributes:
|
|
||||||
label: What is the used version of the Supervisor?
|
|
||||||
placeholder: supervisor-
|
|
||||||
description: >
|
|
||||||
Can be found in the Supervisor panel -> System tab. Starts with
|
|
||||||
`supervisor-....`.
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
attributes:
|
attributes:
|
||||||
label: What type of installation are you running?
|
label: What type of installation are you running?
|
||||||
description: >
|
description: >
|
||||||
If you don't know, you can find it in: Configuration panel -> Info.
|
If you don't know, can be found in [Settings -> System -> Repairs -> System Information](https://my.home-assistant.io/redirect/system_health/).
|
||||||
|
It is listed as the `Installation Type` value.
|
||||||
options:
|
options:
|
||||||
- Home Assistant OS
|
- Home Assistant OS
|
||||||
- Home Assistant Supervised
|
- Home Assistant Supervised
|
||||||
@@ -48,22 +40,6 @@ body:
|
|||||||
- 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
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
attributes:
|
|
||||||
label: What is the version of your installed operating system?
|
|
||||||
placeholder: "5.11"
|
|
||||||
description: Can be found in the Supervisor panel -> System tab.
|
|
||||||
- type: input
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
attributes:
|
|
||||||
label: What version of Home Assistant Core is installed?
|
|
||||||
placeholder: core-
|
|
||||||
description: >
|
|
||||||
Can be found in the Supervisor panel -> System tab. Starts with
|
|
||||||
`core-....`.
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
@@ -87,8 +63,30 @@ body:
|
|||||||
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?
|
||||||
description: >
|
description: >
|
||||||
The Supervisor logs can be found in the Supervisor panel -> System tab.
|
Supervisor Logs can be found in [Settings -> System -> Logs](https://my.home-assistant.io/redirect/logs/)
|
||||||
|
then choose `Supervisor` in the top right.
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/supervisor_logs/)
|
||||||
render: txt
|
render: txt
|
||||||
|
- type: textarea
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: System Health information
|
||||||
|
description: >
|
||||||
|
System Health information can be found in the top right menu in [Settings -> System -> Repairs](https://my.home-assistant.io/redirect/repairs/).
|
||||||
|
Click the copy button at the bottom of the pop-up and paste it here.
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/system_health/)
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Supervisor diagnostics
|
||||||
|
placeholder: "drag-and-drop the diagnostics data file here (do not copy-and-paste the content)"
|
||||||
|
description: >-
|
||||||
|
Supervisor diagnostics can be found in [Settings -> Integrations](https://my.home-assistant.io/redirect/integrations/).
|
||||||
|
Find the card that says `Home Assistant Supervisor`, open its menu and select 'Download diagnostics'.
|
||||||
|
|
||||||
|
**Please drag-and-drop the downloaded file into the textbox below. Do not copy and paste its contents.**
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional information
|
label: Additional information
|
||||||
|
57
.github/workflows/builder.yml
vendored
57
.github/workflows/builder.yml
vendored
@@ -33,10 +33,13 @@ on:
|
|||||||
- setup.py
|
- setup.py
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEFAULT_PYTHON: 3.9
|
DEFAULT_PYTHON: "3.11"
|
||||||
BUILD_NAME: supervisor
|
BUILD_NAME: supervisor
|
||||||
BUILD_TYPE: supervisor
|
BUILD_TYPE: supervisor
|
||||||
WHEELS_TAG: 3.9-alpine3.14
|
|
||||||
|
concurrency:
|
||||||
|
group: "${{ github.workflow }}-${{ github.ref }}"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
init:
|
init:
|
||||||
@@ -50,7 +53,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@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -85,21 +88,29 @@ 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@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Write env-file
|
||||||
|
if: needs.init.outputs.requirements == 'true'
|
||||||
|
run: |
|
||||||
|
(
|
||||||
|
# Fix out of memory issues with rust
|
||||||
|
echo "CARGO_NET_GIT_FETCH_WITH_CLI=true"
|
||||||
|
) > .env_file
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
if: needs.init.outputs.requirements == 'true'
|
if: needs.init.outputs.requirements == 'true'
|
||||||
uses: home-assistant/wheels@2022.01.2
|
uses: home-assistant/wheels@2023.04.0
|
||||||
with:
|
with:
|
||||||
tag: ${{ env.WHEELS_TAG }}
|
abi: cp311
|
||||||
|
tag: musllinux_1_2
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
wheels-host: wheels.hass.io
|
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
wheels-user: wheels
|
apk: "libffi-dev;openssl-dev"
|
||||||
apk: "build-base;libffi-dev;openssl-dev;cargo"
|
|
||||||
skip-binary: aiohttp
|
skip-binary: aiohttp
|
||||||
|
env-file: true
|
||||||
requirements: "requirements.txt"
|
requirements: "requirements.txt"
|
||||||
|
|
||||||
- name: Set version
|
- name: Set version
|
||||||
@@ -110,14 +121,14 @@ 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@v2.0.0
|
uses: docker/login-action@v2.2.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
|
- name: Login to GitHub Container Registry
|
||||||
if: needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.publish == 'true'
|
||||||
uses: docker/login-action@v2.0.0
|
uses: docker/login-action@v2.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -128,7 +139,7 @@ jobs:
|
|||||||
run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV
|
run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build supervisor
|
- name: Build supervisor
|
||||||
uses: home-assistant/builder@2022.06.2
|
uses: home-assistant/builder@2023.06.0
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
$BUILD_ARGS \
|
$BUILD_ARGS \
|
||||||
@@ -145,13 +156,13 @@ 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@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
if: needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.publish == 'true'
|
||||||
uses: actions/setup-python@v4.2.0
|
uses: actions/setup-python@v4.6.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@@ -184,7 +195,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@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
|
|
||||||
- name: Initialize git
|
- name: Initialize git
|
||||||
if: needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.publish == 'true'
|
||||||
@@ -209,11 +220,11 @@ jobs:
|
|||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
|
|
||||||
- name: Build the Supervisor
|
- name: Build the Supervisor
|
||||||
if: needs.init.outputs.publish != 'true'
|
if: needs.init.outputs.publish != 'true'
|
||||||
uses: home-assistant/builder@2022.06.2
|
uses: home-assistant/builder@2023.06.0
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
--test \
|
--test \
|
||||||
@@ -290,6 +301,12 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Make sure its state is started
|
||||||
|
test="$(docker exec hassio_cli ha addons info core_ssh --no-progress --raw-json | jq -r '.data.state')"
|
||||||
|
if [ "$test" != "started" ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Check the Supervisor code sign
|
- name: Check the Supervisor code sign
|
||||||
if: needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.publish == 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -362,6 +379,12 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Make sure its state is started
|
||||||
|
test="$(docker exec hassio_cli ha addons info core_ssh --no-progress --raw-json | jq -r '.data.state')"
|
||||||
|
if [ "$test" != "started" ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Restore SSL directory from backup
|
- name: Restore SSL directory from backup
|
||||||
run: |
|
run: |
|
||||||
test=$(docker exec hassio_cli ha backups restore ${{ steps.backup.outputs.slug }} --folders ssl --no-progress --raw-json | jq -r '.result')
|
test=$(docker exec hassio_cli ha backups restore ${{ steps.backup.outputs.slug }} --folders ssl --no-progress --raw-json | jq -r '.result')
|
||||||
|
156
.github/workflows/ci.yaml
vendored
156
.github/workflows/ci.yaml
vendored
@@ -8,30 +8,33 @@ on:
|
|||||||
pull_request: ~
|
pull_request: ~
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEFAULT_PYTHON: 3.9
|
DEFAULT_PYTHON: "3.11"
|
||||||
PRE_COMMIT_HOME: ~/.cache/pre-commit
|
PRE_COMMIT_HOME: ~/.cache/pre-commit
|
||||||
DEFAULT_CAS: v1.0.2
|
DEFAULT_CAS: v1.0.2
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: "${{ github.workflow }}-${{ github.ref }}"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Separate job to pre-populate the base dependency cache
|
# Separate job to pre-populate the base dependency cache
|
||||||
# This prevent upcoming jobs to do the same individually
|
# This prevent upcoming jobs to do the same individually
|
||||||
prepare:
|
prepare:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
outputs:
|
||||||
matrix:
|
python-version: ${{ steps.python.outputs.python-version }}
|
||||||
python-version: [3.9]
|
name: Prepare Python 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@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v4.2.0
|
uses: actions/setup-python@v4.6.1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v3.0.5
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -45,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@v3.0.5
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_HOME }}
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
key: |
|
key: |
|
||||||
@@ -64,19 +67,19 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v4.2.0
|
uses: actions/setup-python@v4.6.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v3.0.5
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
- name: Fail job if Python cache restore failed
|
- name: Fail job if Python cache restore failed
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -93,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@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
- 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"
|
||||||
@@ -108,19 +111,19 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v4.2.0
|
uses: actions/setup-python@v4.6.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v3.0.5
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
- name: Fail job if Python cache restore failed
|
- name: Fail job if Python cache restore failed
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -128,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@v3.0.5
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_HOME }}
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
key: |
|
key: |
|
||||||
@@ -152,19 +155,19 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v4.2.0
|
uses: actions/setup-python@v4.6.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v3.0.5
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
- name: Fail job if Python cache restore failed
|
- name: Fail job if Python cache restore failed
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -184,19 +187,19 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v4.2.0
|
uses: actions/setup-python@v4.6.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v3.0.5
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
- name: Fail job if Python cache restore failed
|
- name: Fail job if Python cache restore failed
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -204,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@v3.0.5
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_HOME }}
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
key: |
|
key: |
|
||||||
@@ -225,19 +228,19 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v4.2.0
|
uses: actions/setup-python@v4.6.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v3.0.5
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
- name: Fail job if Python cache restore failed
|
- name: Fail job if Python cache restore failed
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -245,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@v3.0.5
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_HOME }}
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
key: |
|
key: |
|
||||||
@@ -269,19 +272,19 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v4.2.0
|
uses: actions/setup-python@v4.6.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v3.0.5
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
- name: Fail job if Python cache restore failed
|
- name: Fail job if Python cache restore failed
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -301,19 +304,19 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v4.2.0
|
uses: actions/setup-python@v4.6.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v3.0.5
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
- name: Fail job if Python cache restore failed
|
- name: Fail job if Python cache restore failed
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -321,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@v3.0.5
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_HOME }}
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
key: |
|
key: |
|
||||||
@@ -339,29 +342,26 @@ jobs:
|
|||||||
pytest:
|
pytest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: prepare
|
needs: prepare
|
||||||
strategy:
|
name: Run tests Python ${{ needs.prepare.outputs.python-version }}
|
||||||
matrix:
|
|
||||||
python-version: [3.9]
|
|
||||||
name: Run tests Python ${{ matrix.python-version }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v4.2.0
|
uses: actions/setup-python@v4.6.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Install CAS tools
|
- name: Install CAS tools
|
||||||
uses: home-assistant/actions/helpers/cas@master
|
uses: home-assistant/actions/helpers/cas@master
|
||||||
with:
|
with:
|
||||||
version: ${{ env.DEFAULT_CAS }}
|
version: ${{ env.DEFAULT_CAS }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v3.0.5
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
- name: Fail job if Python cache restore failed
|
- name: Fail job if Python cache restore failed
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -370,7 +370,7 @@ jobs:
|
|||||||
- name: Install additional system dependencies
|
- name: Install additional system dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y --no-install-recommends libpulse0 libudev1
|
sudo apt-get install -y --no-install-recommends libpulse0 libudev1 dbus dbus-x11
|
||||||
- name: Register Python problem matcher
|
- name: Register Python problem matcher
|
||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/matchers/python.json"
|
echo "::add-matcher::.github/workflows/matchers/python.json"
|
||||||
@@ -392,7 +392,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@v3.1.0
|
uses: actions/upload-artifact@v3.1.2
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}
|
name: coverage-${{ matrix.python-version }}
|
||||||
path: .coverage
|
path: .coverage
|
||||||
@@ -400,22 +400,22 @@ jobs:
|
|||||||
coverage:
|
coverage:
|
||||||
name: Process test coverage
|
name: Process test coverage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: pytest
|
needs: ["pytest", "prepare"]
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v4.2.0
|
uses: actions/setup-python@v4.6.1
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v3.0.5
|
uses: actions/cache@v3.3.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
- name: Fail job if Python cache restore failed
|
- name: Fail job if Python cache restore failed
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
@@ -430,4 +430,4 @@ jobs:
|
|||||||
coverage report
|
coverage report
|
||||||
coverage xml
|
coverage xml
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v3.1.0
|
uses: codecov/codecov-action@v3.1.4
|
||||||
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v3.0.0
|
- uses: dessant/lock-threads@v4.0.1
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-inactive-days: "30"
|
issue-inactive-days: "30"
|
||||||
|
4
.github/workflows/release-drafter.yml
vendored
4
.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@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
echo "::set-output name=version::$datepre.$newpost"
|
echo "::set-output name=version::$datepre.$newpost"
|
||||||
|
|
||||||
- name: Run Release Drafter
|
- name: Run Release Drafter
|
||||||
uses: release-drafter/release-drafter@v5.20.0
|
uses: release-drafter/release-drafter@v5.23.0
|
||||||
with:
|
with:
|
||||||
tag: ${{ steps.version.outputs.version }}
|
tag: ${{ steps.version.outputs.version }}
|
||||||
name: ${{ steps.version.outputs.version }}
|
name: ${{ steps.version.outputs.version }}
|
||||||
|
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@v3.0.2
|
uses: actions/checkout@v3.5.3
|
||||||
- name: Sentry Release
|
- name: Sentry Release
|
||||||
uses: getsentry/action-release@v1.2.0
|
uses: getsentry/action-release@v1.4.1
|
||||||
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 }}
|
||||||
|
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
@@ -9,10 +9,10 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v5.1.1
|
- uses: actions/stale@v8.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 60
|
days-before-stale: 30
|
||||||
days-before-close: 7
|
days-before-close: 7
|
||||||
stale-issue-label: "stale"
|
stale-issue-label: "stale"
|
||||||
exempt-issue-labels: "no-stale,Help%20wanted,help-wanted,pinned,rfc,security"
|
exempt-issue-labels: "no-stale,Help%20wanted,help-wanted,pinned,rfc,security"
|
||||||
|
@@ -1,34 +1,34 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 22.6.0
|
rev: 23.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args:
|
args:
|
||||||
- --safe
|
- --safe
|
||||||
- --quiet
|
- --quiet
|
||||||
- --target-version
|
- --target-version
|
||||||
- py39
|
- py310
|
||||||
files: ^((supervisor|tests)/.+)?[^/]+\.py$
|
files: ^((supervisor|tests)/.+)?[^/]+\.py$
|
||||||
- repo: https://gitlab.com/pycqa/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 3.8.3
|
rev: 6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- flake8-docstrings==1.5.0
|
- flake8-docstrings==1.7.0
|
||||||
- pydocstyle==5.0.2
|
- pydocstyle==6.3.0
|
||||||
files: ^(supervisor|script|tests)/.+\.py$
|
files: ^(supervisor|script|tests)/.+\.py$
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v3.1.0
|
rev: v4.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-executables-have-shebangs
|
- id: check-executables-have-shebangs
|
||||||
stages: [manual]
|
stages: [manual]
|
||||||
- id: check-json
|
- id: check-json
|
||||||
- repo: https://github.com/PyCQA/isort
|
- repo: https://github.com/PyCQA/isort
|
||||||
rev: 5.9.3
|
rev: 5.12.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.32.1
|
rev: v3.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py39-plus]
|
args: [--py310-plus]
|
||||||
|
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -13,6 +13,13 @@
|
|||||||
"remoteRoot": "/usr/src/supervisor"
|
"remoteRoot": "/usr/src/supervisor"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug Tests",
|
||||||
|
"type": "python",
|
||||||
|
"request": "test",
|
||||||
|
"console": "internalConsole",
|
||||||
|
"justMyCode": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -3,10 +3,10 @@ FROM ${BUILD_FROM}
|
|||||||
|
|
||||||
ENV \
|
ENV \
|
||||||
S6_SERVICES_GRACETIME=10000 \
|
S6_SERVICES_GRACETIME=10000 \
|
||||||
SUPERVISOR_API=http://localhost
|
SUPERVISOR_API=http://localhost \
|
||||||
|
CRYPTOGRAPHY_OPENSSL_NO_LEGACY=1
|
||||||
|
|
||||||
ARG \
|
ARG \
|
||||||
BUILD_ARCH \
|
|
||||||
CAS_VERSION
|
CAS_VERSION
|
||||||
|
|
||||||
# Install base
|
# Install base
|
||||||
@@ -40,7 +40,7 @@ COPY requirements.txt .
|
|||||||
RUN \
|
RUN \
|
||||||
export MAKEFLAGS="-j$(nproc)" \
|
export MAKEFLAGS="-j$(nproc)" \
|
||||||
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links \
|
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links \
|
||||||
"https://wheels.home-assistant.io/alpine-$(cut -d '.' -f 1-2 < /etc/alpine-release)/${BUILD_ARCH}/" \
|
"https://wheels.home-assistant.io/musllinux/" \
|
||||||
-r ./requirements.txt \
|
-r ./requirements.txt \
|
||||||
&& rm -f requirements.txt
|
&& rm -f requirements.txt
|
||||||
|
|
||||||
|
10
build.yaml
10
build.yaml
@@ -1,11 +1,11 @@
|
|||||||
image: homeassistant/{arch}-hassio-supervisor
|
image: homeassistant/{arch}-hassio-supervisor
|
||||||
shadow_repository: ghcr.io/home-assistant
|
shadow_repository: ghcr.io/home-assistant
|
||||||
build_from:
|
build_from:
|
||||||
aarch64: ghcr.io/home-assistant/aarch64-base-python:3.9-alpine3.14
|
aarch64: ghcr.io/home-assistant/aarch64-base-python:3.11-alpine3.16
|
||||||
armhf: ghcr.io/home-assistant/armhf-base-python:3.9-alpine3.14
|
armhf: ghcr.io/home-assistant/armhf-base-python:3.11-alpine3.16
|
||||||
armv7: ghcr.io/home-assistant/armv7-base-python:3.9-alpine3.14
|
armv7: ghcr.io/home-assistant/armv7-base-python:3.11-alpine3.16
|
||||||
amd64: ghcr.io/home-assistant/amd64-base-python:3.9-alpine3.14
|
amd64: ghcr.io/home-assistant/amd64-base-python:3.11-alpine3.16
|
||||||
i386: ghcr.io/home-assistant/i386-base-python:3.9-alpine3.14
|
i386: ghcr.io/home-assistant/i386-base-python:3.11-alpine3.16
|
||||||
codenotary:
|
codenotary:
|
||||||
signer: notary@home-assistant.io
|
signer: notary@home-assistant.io
|
||||||
base_image: notary@home-assistant.io
|
base_image: notary@home-assistant.io
|
||||||
|
Submodule home-assistant-polymer updated: 414db83359...efa02c309b
2
pylintrc
2
pylintrc
@@ -38,7 +38,7 @@ disable=
|
|||||||
consider-using-with
|
consider-using-with
|
||||||
|
|
||||||
[EXCEPTIONS]
|
[EXCEPTIONS]
|
||||||
overgeneral-exceptions=Exception
|
overgeneral-exceptions=builtins.Exception
|
||||||
|
|
||||||
|
|
||||||
[TYPECHECK]
|
[TYPECHECK]
|
||||||
|
@@ -1,25 +1,26 @@
|
|||||||
aiodns==3.0.0
|
aiodns==3.0.0
|
||||||
aiohttp==3.8.1
|
aiohttp==3.8.4
|
||||||
async_timeout==4.0.2
|
async_timeout==4.0.2
|
||||||
atomicwrites-homeassistant==1.4.1
|
atomicwrites-homeassistant==1.4.1
|
||||||
attrs==22.1.0
|
attrs==23.1.0
|
||||||
awesomeversion==22.6.0
|
awesomeversion==23.5.0
|
||||||
brotli==1.0.9
|
brotli==1.0.9
|
||||||
cchardet==2.1.7
|
ciso8601==2.3.0
|
||||||
ciso8601==2.2.0
|
colorlog==6.7.0
|
||||||
colorlog==6.6.0
|
|
||||||
cpe==1.2.1
|
cpe==1.2.1
|
||||||
cryptography==37.0.4
|
cryptography==41.0.1
|
||||||
debugpy==1.6.2
|
debugpy==1.6.7
|
||||||
deepmerge==1.0.1
|
deepmerge==1.1.0
|
||||||
dirhash==0.2.1
|
dirhash==0.2.1
|
||||||
docker==5.0.3
|
docker==6.1.3
|
||||||
gitpython==3.1.27
|
faust-cchardet==2.1.18
|
||||||
|
gitpython==3.1.31
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
pulsectl==22.3.2
|
pulsectl==23.5.2
|
||||||
pyudev==0.23.2
|
pyudev==0.24.1
|
||||||
ruamel.yaml==0.17.17
|
ruamel.yaml==0.17.21
|
||||||
securetar==2022.2.0
|
securetar==2023.3.0
|
||||||
sentry-sdk==1.9.0
|
sentry-sdk==1.25.1
|
||||||
voluptuous==0.13.1
|
voluptuous==0.13.1
|
||||||
dbus-next==0.2.3
|
dbus-fast==1.86.0
|
||||||
|
typing_extensions==4.6.3
|
||||||
|
@@ -1,15 +1,16 @@
|
|||||||
black==22.6.0
|
black==23.3.0
|
||||||
codecov==2.1.12
|
coverage==7.2.7
|
||||||
coverage==6.4.2
|
flake8-docstrings==1.7.0
|
||||||
flake8-docstrings==1.6.0
|
flake8==6.0.0
|
||||||
flake8==5.0.4
|
pre-commit==3.3.3
|
||||||
pre-commit==2.20.0
|
pydocstyle==6.3.0
|
||||||
pydocstyle==6.1.1
|
pylint==2.17.4
|
||||||
pylint==2.14.5
|
|
||||||
pytest-aiohttp==1.0.4
|
pytest-aiohttp==1.0.4
|
||||||
pytest-asyncio==0.18.3
|
pytest-asyncio==0.18.3
|
||||||
pytest-cov==3.0.0
|
pytest-cov==4.1.0
|
||||||
pytest-timeout==2.1.0
|
pytest-timeout==2.1.0
|
||||||
pytest==7.1.2
|
pytest==7.3.2
|
||||||
pyupgrade==2.37.3
|
pyupgrade==3.6.0
|
||||||
time-machine==2.7.1
|
time-machine==2.9.0
|
||||||
|
typing_extensions==4.6.3
|
||||||
|
urllib3==2.0.3
|
||||||
|
@@ -27,3 +27,5 @@ ignore =
|
|||||||
E203,
|
E203,
|
||||||
D202,
|
D202,
|
||||||
W504
|
W504
|
||||||
|
per-file-ignores =
|
||||||
|
tests/dbus_service_mocks/*.py: F821,F722
|
||||||
|
@@ -28,7 +28,8 @@ if __name__ == "__main__":
|
|||||||
bootstrap.initialize_logging()
|
bootstrap.initialize_logging()
|
||||||
|
|
||||||
# Init async event loop
|
# Init async event loop
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
# Check if all information are available to setup Supervisor
|
# Check if all information are available to setup Supervisor
|
||||||
bootstrap.check_environment()
|
bootstrap.check_environment()
|
||||||
|
@@ -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 Optional, Union
|
from typing import Union
|
||||||
|
|
||||||
from ..const import AddonBoot, AddonStartup, AddonState
|
from ..const import AddonBoot, AddonStartup, AddonState
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
@@ -23,7 +23,9 @@ from ..jobs.decorator import Job, JobCondition
|
|||||||
from ..resolution.const import ContextType, IssueType, SuggestionType
|
from ..resolution.const import ContextType, IssueType, SuggestionType
|
||||||
from ..store.addon import AddonStore
|
from ..store.addon import AddonStore
|
||||||
from ..utils import check_exception_chain
|
from ..utils import check_exception_chain
|
||||||
|
from ..utils.sentry import capture_exception
|
||||||
from .addon import Addon
|
from .addon import Addon
|
||||||
|
from .const import ADDON_UPDATE_CONDITIONS
|
||||||
from .data import AddonsData
|
from .data import AddonsData
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@@ -52,7 +54,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
"""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())
|
||||||
|
|
||||||
def get(self, addon_slug: str, local_only: bool = False) -> Optional[AnyAddon]:
|
def get(self, addon_slug: str, local_only: bool = False) -> AnyAddon | None:
|
||||||
"""Return an add-on from slug.
|
"""Return an add-on from slug.
|
||||||
|
|
||||||
Prio:
|
Prio:
|
||||||
@@ -65,7 +67,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
return self.store.get(addon_slug)
|
return self.store.get(addon_slug)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def from_token(self, token: str) -> Optional[Addon]:
|
def from_token(self, token: str) -> Addon | None:
|
||||||
"""Return an add-on from Supervisor token."""
|
"""Return an add-on from Supervisor token."""
|
||||||
for addon in self.installed:
|
for addon in self.installed:
|
||||||
if token == addon.supervisor_token:
|
if token == addon.supervisor_token:
|
||||||
@@ -77,7 +79,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
tasks = []
|
tasks = []
|
||||||
for slug in self.data.system:
|
for slug in self.data.system:
|
||||||
addon = self.local[slug] = Addon(self.coresys, slug)
|
addon = self.local[slug] = Addon(self.coresys, slug)
|
||||||
tasks.append(addon.load())
|
tasks.append(self.sys_create_task(addon.load()))
|
||||||
|
|
||||||
# Run initial tasks
|
# Run initial tasks
|
||||||
_LOGGER.info("Found %d installed add-ons", len(tasks))
|
_LOGGER.info("Found %d installed add-ons", len(tasks))
|
||||||
@@ -113,7 +115,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
addon.boot = AddonBoot.MANUAL
|
addon.boot = AddonBoot.MANUAL
|
||||||
addon.save_persist()
|
addon.save_persist()
|
||||||
except Exception as err: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
self.sys_capture_exception(err)
|
capture_exception(err)
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -141,14 +143,10 @@ class AddonManager(CoreSysAttributes):
|
|||||||
await addon.stop()
|
await addon.stop()
|
||||||
except Exception as err: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
_LOGGER.warning("Can't stop Add-on %s: %s", addon.slug, err)
|
_LOGGER.warning("Can't stop Add-on %s: %s", addon.slug, err)
|
||||||
self.sys_capture_exception(err)
|
capture_exception(err)
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
conditions=[
|
conditions=ADDON_UPDATE_CONDITIONS,
|
||||||
JobCondition.FREE_SPACE,
|
|
||||||
JobCondition.INTERNET_HOST,
|
|
||||||
JobCondition.HEALTHY,
|
|
||||||
],
|
|
||||||
on_condition=AddonsJobError,
|
on_condition=AddonsJobError,
|
||||||
)
|
)
|
||||||
async def install(self, slug: str) -> None:
|
async def install(self, slug: str) -> None:
|
||||||
@@ -160,10 +158,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
if not store:
|
if not store:
|
||||||
raise AddonsError(f"Add-on {slug} does not exist", _LOGGER.error)
|
raise AddonsError(f"Add-on {slug} does not exist", _LOGGER.error)
|
||||||
|
|
||||||
if not store.available:
|
store.validate_availability()
|
||||||
raise AddonsNotSupportedError(
|
|
||||||
f"Add-on {slug} not supported on this platform", _LOGGER.error
|
|
||||||
)
|
|
||||||
|
|
||||||
self.data.install(store)
|
self.data.install(store)
|
||||||
addon = Addon(self.coresys, slug)
|
addon = Addon(self.coresys, slug)
|
||||||
@@ -183,8 +178,8 @@ class AddonManager(CoreSysAttributes):
|
|||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
self.data.uninstall(addon)
|
self.data.uninstall(addon)
|
||||||
raise AddonsError() from err
|
raise AddonsError() from err
|
||||||
else:
|
|
||||||
self.local[slug] = addon
|
self.local[slug] = addon
|
||||||
|
|
||||||
# Reload ingress tokens
|
# Reload ingress tokens
|
||||||
if addon.with_ingress:
|
if addon.with_ingress:
|
||||||
@@ -203,10 +198,10 @@ class AddonManager(CoreSysAttributes):
|
|||||||
await addon.instance.remove()
|
await addon.instance.remove()
|
||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
raise AddonsError() from err
|
raise AddonsError() from err
|
||||||
else:
|
|
||||||
addon.state = AddonState.UNKNOWN
|
|
||||||
|
|
||||||
await addon.remove_data()
|
addon.state = AddonState.UNKNOWN
|
||||||
|
|
||||||
|
await addon.unload()
|
||||||
|
|
||||||
# Cleanup audio settings
|
# Cleanup audio settings
|
||||||
if addon.path_pulse.exists():
|
if addon.path_pulse.exists():
|
||||||
@@ -246,14 +241,10 @@ class AddonManager(CoreSysAttributes):
|
|||||||
_LOGGER.info("Add-on '%s' successfully removed", slug)
|
_LOGGER.info("Add-on '%s' successfully removed", slug)
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
conditions=[
|
conditions=ADDON_UPDATE_CONDITIONS,
|
||||||
JobCondition.FREE_SPACE,
|
|
||||||
JobCondition.INTERNET_HOST,
|
|
||||||
JobCondition.HEALTHY,
|
|
||||||
],
|
|
||||||
on_condition=AddonsJobError,
|
on_condition=AddonsJobError,
|
||||||
)
|
)
|
||||||
async def update(self, slug: str, backup: Optional[bool] = False) -> None:
|
async def update(self, slug: str, backup: bool | None = False) -> None:
|
||||||
"""Update add-on."""
|
"""Update add-on."""
|
||||||
if slug not in self.local:
|
if slug not in self.local:
|
||||||
raise AddonsError(f"Add-on {slug} is not installed", _LOGGER.error)
|
raise AddonsError(f"Add-on {slug} is not installed", _LOGGER.error)
|
||||||
@@ -269,10 +260,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
raise AddonsError(f"No update available for add-on {slug}", _LOGGER.warning)
|
raise AddonsError(f"No update available for add-on {slug}", _LOGGER.warning)
|
||||||
|
|
||||||
# Check if available, Maybe something have changed
|
# Check if available, Maybe something have changed
|
||||||
if not store.available:
|
store.validate_availability()
|
||||||
raise AddonsNotSupportedError(
|
|
||||||
f"Add-on {slug} not supported on that platform", _LOGGER.error
|
|
||||||
)
|
|
||||||
|
|
||||||
if backup:
|
if backup:
|
||||||
await self.sys_backups.do_backup_partial(
|
await self.sys_backups.do_backup_partial(
|
||||||
@@ -340,9 +328,9 @@ class AddonManager(CoreSysAttributes):
|
|||||||
await addon.instance.install(addon.version)
|
await addon.instance.install(addon.version)
|
||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
raise AddonsError() from err
|
raise AddonsError() from err
|
||||||
else:
|
|
||||||
self.data.update(store)
|
self.data.update(store)
|
||||||
_LOGGER.info("Add-on '%s' successfully rebuilt", slug)
|
_LOGGER.info("Add-on '%s' successfully rebuilt", slug)
|
||||||
|
|
||||||
# restore state
|
# restore state
|
||||||
if last_state == AddonState.STARTED:
|
if last_state == AddonState.STARTED:
|
||||||
@@ -428,7 +416,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
reference=addon.slug,
|
reference=addon.slug,
|
||||||
suggestions=[SuggestionType.EXECUTE_REPAIR],
|
suggestions=[SuggestionType.EXECUTE_REPAIR],
|
||||||
)
|
)
|
||||||
self.sys_capture_exception(err)
|
capture_exception(err)
|
||||||
else:
|
else:
|
||||||
self.sys_plugins.dns.add_host(
|
self.sys_plugins.dns.add_host(
|
||||||
ipv4=addon.ip_address, names=[addon.hostname], write=False
|
ipv4=addon.ip_address, names=[addon.hostname], write=False
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
"""Init file for Supervisor add-ons."""
|
"""Init file for Supervisor add-ons."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Awaitable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
@@ -10,7 +11,7 @@ import secrets
|
|||||||
import shutil
|
import shutil
|
||||||
import tarfile
|
import tarfile
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from typing import Any, Awaitable, Final, Optional
|
from typing import Any, Final
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from deepmerge import Merger
|
from deepmerge import Merger
|
||||||
@@ -18,6 +19,7 @@ from securetar import atomic_contents_add, secure_path
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous.humanize import humanize_error
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
|
from ..bus import EventListener
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_ACCESS_TOKEN,
|
ATTR_ACCESS_TOKEN,
|
||||||
ATTR_AUDIO_INPUT,
|
ATTR_AUDIO_INPUT,
|
||||||
@@ -58,6 +60,7 @@ from ..docker.stats import DockerStats
|
|||||||
from ..exceptions import (
|
from ..exceptions import (
|
||||||
AddonConfigurationError,
|
AddonConfigurationError,
|
||||||
AddonsError,
|
AddonsError,
|
||||||
|
AddonsJobError,
|
||||||
AddonsNotSupportedError,
|
AddonsNotSupportedError,
|
||||||
ConfigurationFileError,
|
ConfigurationFileError,
|
||||||
DockerError,
|
DockerError,
|
||||||
@@ -65,10 +68,19 @@ from ..exceptions import (
|
|||||||
)
|
)
|
||||||
from ..hardware.data import Device
|
from ..hardware.data import Device
|
||||||
from ..homeassistant.const import WSEvent, WSType
|
from ..homeassistant.const import WSEvent, WSType
|
||||||
|
from ..jobs.const import JobExecutionLimit
|
||||||
|
from ..jobs.decorator import Job
|
||||||
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 .const import WATCHDOG_RETRY_SECONDS, AddonBackupMode
|
from ..utils.sentry import capture_exception
|
||||||
|
from .const import (
|
||||||
|
WATCHDOG_MAX_ATTEMPTS,
|
||||||
|
WATCHDOG_RETRY_SECONDS,
|
||||||
|
WATCHDOG_THROTTLE_MAX_CALLS,
|
||||||
|
WATCHDOG_THROTTLE_PERIOD,
|
||||||
|
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
|
||||||
@@ -103,6 +115,58 @@ class Addon(AddonModel):
|
|||||||
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
|
||||||
|
self._manual_stop: bool = (
|
||||||
|
self.sys_hardware.helper.last_boot != self.sys_config.last_boot
|
||||||
|
)
|
||||||
|
self._listeners: list[EventListener] = []
|
||||||
|
|
||||||
|
@Job(
|
||||||
|
name=f"addon_{slug}_restart_after_problem",
|
||||||
|
limit=JobExecutionLimit.THROTTLE_RATE_LIMIT,
|
||||||
|
throttle_period=WATCHDOG_THROTTLE_PERIOD,
|
||||||
|
throttle_max_calls=WATCHDOG_THROTTLE_MAX_CALLS,
|
||||||
|
on_condition=AddonsJobError,
|
||||||
|
)
|
||||||
|
async def restart_after_problem(addon: Addon, state: ContainerState):
|
||||||
|
"""Restart unhealthy or failed addon."""
|
||||||
|
attempts = 0
|
||||||
|
while await addon.instance.current_state() == state:
|
||||||
|
if not addon.in_progress:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Watchdog found addon %s is %s, restarting...",
|
||||||
|
addon.name,
|
||||||
|
state.value,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
if state == ContainerState.FAILED:
|
||||||
|
# Ensure failed container is removed before attempting reanimation
|
||||||
|
if attempts == 0:
|
||||||
|
with suppress(DockerError):
|
||||||
|
await addon.instance.stop(remove_container=True)
|
||||||
|
|
||||||
|
await addon.start()
|
||||||
|
else:
|
||||||
|
await addon.restart()
|
||||||
|
except AddonsError as err:
|
||||||
|
attempts = attempts + 1
|
||||||
|
_LOGGER.error(
|
||||||
|
"Watchdog restart of addon %s failed!", addon.name
|
||||||
|
)
|
||||||
|
capture_exception(err)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
if attempts >= WATCHDOG_MAX_ATTEMPTS:
|
||||||
|
_LOGGER.critical(
|
||||||
|
"Watchdog cannot restart addon %s, failed all %s attempts",
|
||||||
|
addon.name,
|
||||||
|
attempts,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
await asyncio.sleep(WATCHDOG_RETRY_SECONDS)
|
||||||
|
|
||||||
|
self._restart_after_problem = restart_after_problem
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""Return internal representation."""
|
"""Return internal representation."""
|
||||||
@@ -137,11 +201,15 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
async def load(self) -> None:
|
async def load(self) -> None:
|
||||||
"""Async initialize of object."""
|
"""Async initialize of object."""
|
||||||
self.sys_bus.register_event(
|
self._listeners.append(
|
||||||
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, self.container_state_changed
|
self.sys_bus.register_event(
|
||||||
|
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, self.container_state_changed
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.sys_bus.register_event(
|
self._listeners.append(
|
||||||
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, self.watchdog_container
|
self.sys_bus.register_event(
|
||||||
|
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, self.watchdog_container
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
with suppress(DockerError):
|
with suppress(DockerError):
|
||||||
@@ -183,7 +251,7 @@ class Addon(AddonModel):
|
|||||||
return self._available(self.data_store)
|
return self._available(self.data_store)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self) -> Optional[str]:
|
def version(self) -> str | None:
|
||||||
"""Return installed version."""
|
"""Return installed version."""
|
||||||
return self.persist[ATTR_VERSION]
|
return self.persist[ATTR_VERSION]
|
||||||
|
|
||||||
@@ -207,7 +275,7 @@ class Addon(AddonModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@options.setter
|
@options.setter
|
||||||
def options(self, value: Optional[dict[str, Any]]) -> None:
|
def options(self, value: dict[str, Any] | None) -> 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)
|
||||||
|
|
||||||
@@ -252,17 +320,17 @@ class Addon(AddonModel):
|
|||||||
return self.persist[ATTR_UUID]
|
return self.persist[ATTR_UUID]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supervisor_token(self) -> Optional[str]:
|
def supervisor_token(self) -> str | None:
|
||||||
"""Return access token for Supervisor API."""
|
"""Return access token for Supervisor API."""
|
||||||
return self.persist.get(ATTR_ACCESS_TOKEN)
|
return self.persist.get(ATTR_ACCESS_TOKEN)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ingress_token(self) -> Optional[str]:
|
def ingress_token(self) -> str | None:
|
||||||
"""Return access token for Supervisor API."""
|
"""Return access token for Supervisor API."""
|
||||||
return self.persist.get(ATTR_INGRESS_TOKEN)
|
return self.persist.get(ATTR_INGRESS_TOKEN)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ingress_entry(self) -> Optional[str]:
|
def ingress_entry(self) -> str | None:
|
||||||
"""Return ingress external URL."""
|
"""Return ingress external URL."""
|
||||||
if self.with_ingress:
|
if self.with_ingress:
|
||||||
return f"/api/hassio_ingress/{self.ingress_token}"
|
return f"/api/hassio_ingress/{self.ingress_token}"
|
||||||
@@ -284,12 +352,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) -> dict[str, int | None] | None:
|
||||||
"""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: dict[str, int | None] | None) -> 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)
|
||||||
@@ -304,7 +372,7 @@ class Addon(AddonModel):
|
|||||||
self.persist[ATTR_NETWORK] = new_ports
|
self.persist[ATTR_NETWORK] = new_ports
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ingress_url(self) -> Optional[str]:
|
def ingress_url(self) -> str | None:
|
||||||
"""Return URL to ingress url."""
|
"""Return URL to ingress url."""
|
||||||
if not self.with_ingress:
|
if not self.with_ingress:
|
||||||
return None
|
return None
|
||||||
@@ -315,7 +383,7 @@ class Addon(AddonModel):
|
|||||||
return url
|
return url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def webui(self) -> Optional[str]:
|
def webui(self) -> str | None:
|
||||||
"""Return URL to webui or None."""
|
"""Return URL to webui or None."""
|
||||||
url = super().webui
|
url = super().webui
|
||||||
if not url:
|
if not url:
|
||||||
@@ -343,7 +411,7 @@ class Addon(AddonModel):
|
|||||||
return f"{proto}://[HOST]:{port}{s_suffix}"
|
return f"{proto}://[HOST]:{port}{s_suffix}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ingress_port(self) -> Optional[int]:
|
def ingress_port(self) -> int | None:
|
||||||
"""Return Ingress port."""
|
"""Return Ingress port."""
|
||||||
if not self.with_ingress:
|
if not self.with_ingress:
|
||||||
return None
|
return None
|
||||||
@@ -354,7 +422,7 @@ class Addon(AddonModel):
|
|||||||
return port
|
return port
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ingress_panel(self) -> Optional[bool]:
|
def ingress_panel(self) -> bool | None:
|
||||||
"""Return True if the add-on access support ingress."""
|
"""Return True if the add-on access support ingress."""
|
||||||
if not self.with_ingress:
|
if not self.with_ingress:
|
||||||
return None
|
return None
|
||||||
@@ -367,19 +435,19 @@ class Addon(AddonModel):
|
|||||||
self.persist[ATTR_INGRESS_PANEL] = value
|
self.persist[ATTR_INGRESS_PANEL] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def audio_output(self) -> Optional[str]:
|
def audio_output(self) -> str | None:
|
||||||
"""Return a pulse profile for output or None."""
|
"""Return a pulse profile for output or None."""
|
||||||
if not self.with_audio:
|
if not self.with_audio:
|
||||||
return None
|
return None
|
||||||
return self.persist.get(ATTR_AUDIO_OUTPUT)
|
return self.persist.get(ATTR_AUDIO_OUTPUT)
|
||||||
|
|
||||||
@audio_output.setter
|
@audio_output.setter
|
||||||
def audio_output(self, value: Optional[str]):
|
def audio_output(self, value: str | None):
|
||||||
"""Set audio output profile settings."""
|
"""Set audio output profile settings."""
|
||||||
self.persist[ATTR_AUDIO_OUTPUT] = value
|
self.persist[ATTR_AUDIO_OUTPUT] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def audio_input(self) -> Optional[str]:
|
def audio_input(self) -> str | None:
|
||||||
"""Return pulse profile for input or None."""
|
"""Return pulse profile for input or None."""
|
||||||
if not self.with_audio:
|
if not self.with_audio:
|
||||||
return None
|
return None
|
||||||
@@ -387,12 +455,12 @@ class Addon(AddonModel):
|
|||||||
return self.persist.get(ATTR_AUDIO_INPUT)
|
return self.persist.get(ATTR_AUDIO_INPUT)
|
||||||
|
|
||||||
@audio_input.setter
|
@audio_input.setter
|
||||||
def audio_input(self, value: Optional[str]) -> None:
|
def audio_input(self, value: str | None) -> None:
|
||||||
"""Set audio input settings."""
|
"""Set audio input settings."""
|
||||||
self.persist[ATTR_AUDIO_INPUT] = value
|
self.persist[ATTR_AUDIO_INPUT] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self) -> Optional[str]:
|
def image(self) -> str | None:
|
||||||
"""Return image name of add-on."""
|
"""Return image name of add-on."""
|
||||||
return self.persist.get(ATTR_IMAGE)
|
return self.persist.get(ATTR_IMAGE)
|
||||||
|
|
||||||
@@ -401,6 +469,11 @@ class Addon(AddonModel):
|
|||||||
"""Return True if this add-on need a local build."""
|
"""Return True if this add-on need a local build."""
|
||||||
return ATTR_IMAGE not in self.data
|
return ATTR_IMAGE not in self.data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def latest_need_build(self) -> bool:
|
||||||
|
"""Return True if the latest version of the addon needs a local build."""
|
||||||
|
return ATTR_IMAGE not in self.data_store
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_data(self) -> Path:
|
def path_data(self) -> Path:
|
||||||
"""Return add-on data path inside Supervisor."""
|
"""Return add-on data path inside Supervisor."""
|
||||||
@@ -444,6 +517,11 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
return options_schema.pwned
|
return options_schema.pwned
|
||||||
|
|
||||||
|
@property
|
||||||
|
def loaded(self) -> bool:
|
||||||
|
"""Is add-on loaded."""
|
||||||
|
return bool(self._listeners)
|
||||||
|
|
||||||
def save_persist(self) -> None:
|
def save_persist(self) -> None:
|
||||||
"""Save data of add-on."""
|
"""Save data of add-on."""
|
||||||
self.sys_addons.data.save_data()
|
self.sys_addons.data.save_data()
|
||||||
@@ -512,8 +590,11 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
raise AddonConfigurationError()
|
raise AddonConfigurationError()
|
||||||
|
|
||||||
async def remove_data(self) -> None:
|
async def unload(self) -> None:
|
||||||
"""Remove add-on data."""
|
"""Unload add-on and remove data."""
|
||||||
|
for listener in self._listeners:
|
||||||
|
self.sys_bus.remove_listener(listener)
|
||||||
|
|
||||||
if not self.path_data.is_dir():
|
if not self.path_data.is_dir():
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -625,6 +706,7 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
"""Stop add-on."""
|
"""Stop add-on."""
|
||||||
|
self._manual_stop = True
|
||||||
try:
|
try:
|
||||||
await self.instance.stop()
|
await self.instance.stop()
|
||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
@@ -870,6 +952,10 @@ class Addon(AddonModel):
|
|||||||
)
|
)
|
||||||
raise AddonsError() from err
|
raise AddonsError() from err
|
||||||
|
|
||||||
|
# Is add-on loaded
|
||||||
|
if not self.loaded:
|
||||||
|
await self.load()
|
||||||
|
|
||||||
# Run add-on
|
# Run add-on
|
||||||
if data[ATTR_STATE] == AddonState.STARTED:
|
if data[ATTR_STATE] == AddonState.STARTED:
|
||||||
return await self.start()
|
return await self.start()
|
||||||
@@ -893,6 +979,7 @@ class Addon(AddonModel):
|
|||||||
ContainerState.HEALTHY,
|
ContainerState.HEALTHY,
|
||||||
ContainerState.UNHEALTHY,
|
ContainerState.UNHEALTHY,
|
||||||
]:
|
]:
|
||||||
|
self._manual_stop = False
|
||||||
self.state = AddonState.STARTED
|
self.state = AddonState.STARTED
|
||||||
elif event.state == ContainerState.STOPPED:
|
elif event.state == ContainerState.STOPPED:
|
||||||
self.state = AddonState.STOPPED
|
self.state = AddonState.STOPPED
|
||||||
@@ -901,43 +988,16 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
async def watchdog_container(self, event: DockerContainerStateEvent) -> None:
|
async def watchdog_container(self, event: DockerContainerStateEvent) -> None:
|
||||||
"""Process state changes in addon container and restart if necessary."""
|
"""Process state changes in addon container and restart if necessary."""
|
||||||
if not (event.name == self.instance.name and self.watchdog):
|
if event.name != self.instance.name:
|
||||||
return
|
return
|
||||||
|
|
||||||
if event.state == ContainerState.UNHEALTHY:
|
# Skip watchdog if not enabled or manual stopped
|
||||||
while await self.instance.current_state() == event.state:
|
if not self.watchdog or self._manual_stop:
|
||||||
if not self.in_progress:
|
return
|
||||||
_LOGGER.warning(
|
|
||||||
"Watchdog found addon %s is unhealthy, restarting...", self.name
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
await self.restart()
|
|
||||||
except AddonsError as err:
|
|
||||||
_LOGGER.error("Watchdog restart of addon %s failed!", self.name)
|
|
||||||
self.sys_capture_exception(err)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
await asyncio.sleep(WATCHDOG_RETRY_SECONDS)
|
if event.state in [
|
||||||
|
ContainerState.FAILED,
|
||||||
elif event.state == ContainerState.FAILED:
|
ContainerState.STOPPED,
|
||||||
# Ensure failed container is removed before attempting reanimation
|
ContainerState.UNHEALTHY,
|
||||||
with suppress(DockerError):
|
]:
|
||||||
await self.instance.stop(remove_container=True)
|
await self._restart_after_problem(self, event.state)
|
||||||
|
|
||||||
while await self.instance.current_state() == event.state:
|
|
||||||
if not self.in_progress:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Watchdog found addon %s failed, restarting...", self.name
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
await self.start()
|
|
||||||
except AddonsError as err:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Watchdog reanimation of addon %s failed!", self.name
|
|
||||||
)
|
|
||||||
self.sys_capture_exception(err)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
await asyncio.sleep(WATCHDOG_RETRY_SECONDS)
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
"""Supervisor add-on build environment."""
|
"""Supervisor add-on build environment."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from functools import cached_property
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -15,7 +16,8 @@ from ..const import (
|
|||||||
META_ADDON,
|
META_ADDON,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..exceptions import ConfigurationFileError
|
from ..docker.interface import MAP_ARCH
|
||||||
|
from ..exceptions import ConfigurationFileError, HassioArchNotFound
|
||||||
from ..utils.common import FileConfiguration, find_one_filetype
|
from ..utils.common import FileConfiguration, find_one_filetype
|
||||||
from .validate import SCHEMA_BUILD_CONFIG
|
from .validate import SCHEMA_BUILD_CONFIG
|
||||||
|
|
||||||
@@ -44,15 +46,33 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
|
|||||||
"""Ignore save function."""
|
"""Ignore save function."""
|
||||||
raise RuntimeError()
|
raise RuntimeError()
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def arch(self) -> str:
|
||||||
|
"""Return arch of the add-on."""
|
||||||
|
return self.sys_arch.match(self.addon.arch)
|
||||||
|
|
||||||
@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."""
|
||||||
if not self._data[ATTR_BUILD_FROM]:
|
if not self._data[ATTR_BUILD_FROM]:
|
||||||
return f"ghcr.io/home-assistant/{self.sys_arch.default}-base:latest"
|
return f"ghcr.io/home-assistant/{self.sys_arch.default}-base:latest"
|
||||||
|
|
||||||
|
if isinstance(self._data[ATTR_BUILD_FROM], str):
|
||||||
|
return self._data[ATTR_BUILD_FROM]
|
||||||
|
|
||||||
# Evaluate correct base image
|
# Evaluate correct base image
|
||||||
arch = self.sys_arch.match(list(self._data[ATTR_BUILD_FROM].keys()))
|
if self.arch not in self._data[ATTR_BUILD_FROM]:
|
||||||
return self._data[ATTR_BUILD_FROM][arch]
|
raise HassioArchNotFound(
|
||||||
|
f"Add-on {self.addon.slug} is not supported on {self.arch}"
|
||||||
|
)
|
||||||
|
return self._data[ATTR_BUILD_FROM][self.arch]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dockerfile(self) -> Path:
|
||||||
|
"""Return Dockerfile path."""
|
||||||
|
if self.addon.path_location.joinpath(f"Dockerfile.{self.arch}").exists():
|
||||||
|
return self.addon.path_location.joinpath(f"Dockerfile.{self.arch}")
|
||||||
|
return self.addon.path_location.joinpath("Dockerfile")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def squash(self) -> bool:
|
def squash(self) -> bool:
|
||||||
@@ -72,24 +92,29 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
|
|||||||
@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."""
|
||||||
return all(
|
try:
|
||||||
[
|
return all(
|
||||||
self.addon.path_location.is_dir(),
|
[
|
||||||
Path(self.addon.path_location, "Dockerfile").is_file(),
|
self.addon.path_location.is_dir(),
|
||||||
]
|
self.dockerfile.is_file(),
|
||||||
)
|
]
|
||||||
|
)
|
||||||
|
except HassioArchNotFound:
|
||||||
|
return False
|
||||||
|
|
||||||
def get_docker_args(self, version: AwesomeVersion):
|
def get_docker_args(self, version: AwesomeVersion):
|
||||||
"""Create a dict with Docker build arguments."""
|
"""Create a dict with Docker build arguments."""
|
||||||
args = {
|
args = {
|
||||||
"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}",
|
||||||
|
"dockerfile": str(self.dockerfile),
|
||||||
"pull": True,
|
"pull": True,
|
||||||
"forcerm": not self.sys_dev,
|
"forcerm": not self.sys_dev,
|
||||||
"squash": self.squash,
|
"squash": self.squash,
|
||||||
|
"platform": MAP_ARCH[self.arch],
|
||||||
"labels": {
|
"labels": {
|
||||||
"io.hass.version": version,
|
"io.hass.version": version,
|
||||||
"io.hass.arch": self.sys_arch.default,
|
"io.hass.arch": self.arch,
|
||||||
"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"),
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
"""Add-on static data."""
|
"""Add-on static data."""
|
||||||
|
from datetime import timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
from ..jobs.const import JobCondition
|
||||||
|
|
||||||
|
|
||||||
class AddonBackupMode(str, Enum):
|
class AddonBackupMode(str, Enum):
|
||||||
"""Backup mode of an Add-on."""
|
"""Backup mode of an Add-on."""
|
||||||
@@ -12,3 +15,16 @@ class AddonBackupMode(str, Enum):
|
|||||||
ATTR_BACKUP = "backup"
|
ATTR_BACKUP = "backup"
|
||||||
ATTR_CODENOTARY = "codenotary"
|
ATTR_CODENOTARY = "codenotary"
|
||||||
WATCHDOG_RETRY_SECONDS = 10
|
WATCHDOG_RETRY_SECONDS = 10
|
||||||
|
WATCHDOG_MAX_ATTEMPTS = 5
|
||||||
|
WATCHDOG_THROTTLE_PERIOD = timedelta(minutes=30)
|
||||||
|
WATCHDOG_THROTTLE_MAX_CALLS = 10
|
||||||
|
|
||||||
|
ADDON_UPDATE_CONDITIONS = [
|
||||||
|
JobCondition.FREE_SPACE,
|
||||||
|
JobCondition.HEALTHY,
|
||||||
|
JobCondition.INTERNET_HOST,
|
||||||
|
JobCondition.PLUGINS_UPDATED,
|
||||||
|
JobCondition.SUPERVISOR_UPDATED,
|
||||||
|
]
|
||||||
|
|
||||||
|
RE_SLUG = r"[-_.A-Za-z0-9]+"
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
"""Init file for Supervisor add-ons."""
|
"""Init file for Supervisor add-ons."""
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from contextlib import suppress
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Awaitable, Optional
|
from typing import Any
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersion, AwesomeVersionException
|
from awesomeversion import AwesomeVersion, AwesomeVersionException
|
||||||
|
|
||||||
from supervisor.addons.const import AddonBackupMode
|
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_ADVANCED,
|
ATTR_ADVANCED,
|
||||||
ATTR_APPARMOR,
|
ATTR_APPARMOR,
|
||||||
@@ -33,6 +34,7 @@ from ..const import (
|
|||||||
ATTR_HOST_IPC,
|
ATTR_HOST_IPC,
|
||||||
ATTR_HOST_NETWORK,
|
ATTR_HOST_NETWORK,
|
||||||
ATTR_HOST_PID,
|
ATTR_HOST_PID,
|
||||||
|
ATTR_HOST_UTS,
|
||||||
ATTR_IMAGE,
|
ATTR_IMAGE,
|
||||||
ATTR_INGRESS,
|
ATTR_INGRESS,
|
||||||
ATTR_INGRESS_STREAM,
|
ATTR_INGRESS_STREAM,
|
||||||
@@ -79,10 +81,13 @@ from ..const import (
|
|||||||
)
|
)
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..docker.const import Capabilities
|
from ..docker.const import Capabilities
|
||||||
from .const import ATTR_BACKUP, ATTR_CODENOTARY
|
from ..exceptions import AddonsNotSupportedError
|
||||||
|
from .const import ATTR_BACKUP, ATTR_CODENOTARY, AddonBackupMode
|
||||||
from .options import AddonOptions, UiOptions
|
from .options import AddonOptions, UiOptions
|
||||||
from .validate import RE_SERVICE, RE_VOLUME
|
from .validate import RE_SERVICE, RE_VOLUME
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
Data = dict[str, Any]
|
Data = dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
@@ -125,7 +130,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data[ATTR_BOOT]
|
return self.data[ATTR_BOOT]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auto_update(self) -> Optional[bool]:
|
def auto_update(self) -> bool | None:
|
||||||
"""Return if auto update is enable."""
|
"""Return if auto update is enable."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -150,22 +155,22 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data[ATTR_TIMEOUT]
|
return self.data[ATTR_TIMEOUT]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uuid(self) -> Optional[str]:
|
def uuid(self) -> str | None:
|
||||||
"""Return an API token for this add-on."""
|
"""Return an API token for this add-on."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supervisor_token(self) -> Optional[str]:
|
def supervisor_token(self) -> str | None:
|
||||||
"""Return access token for Supervisor API."""
|
"""Return access token for Supervisor API."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ingress_token(self) -> Optional[str]:
|
def ingress_token(self) -> str | None:
|
||||||
"""Return access token for Supervisor API."""
|
"""Return access token for Supervisor API."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ingress_entry(self) -> Optional[str]:
|
def ingress_entry(self) -> str | None:
|
||||||
"""Return ingress external URL."""
|
"""Return ingress external URL."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -175,7 +180,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data[ATTR_DESCRIPTON]
|
return self.data[ATTR_DESCRIPTON]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def long_description(self) -> Optional[str]:
|
def long_description(self) -> str | None:
|
||||||
"""Return README.md as long_description."""
|
"""Return README.md as long_description."""
|
||||||
readme = Path(self.path_location, "README.md")
|
readme = Path(self.path_location, "README.md")
|
||||||
|
|
||||||
@@ -245,32 +250,32 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
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) -> dict[str, str] | None:
|
||||||
"""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) -> dict[str, int | None] | None:
|
||||||
"""Return ports of add-on."""
|
"""Return ports of add-on."""
|
||||||
return self.data.get(ATTR_PORTS)
|
return self.data.get(ATTR_PORTS)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ingress_url(self) -> Optional[str]:
|
def ingress_url(self) -> str | None:
|
||||||
"""Return URL to ingress url."""
|
"""Return URL to ingress url."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def webui(self) -> Optional[str]:
|
def webui(self) -> str | None:
|
||||||
"""Return URL to webui or None."""
|
"""Return URL to webui or None."""
|
||||||
return self.data.get(ATTR_WEBUI)
|
return self.data.get(ATTR_WEBUI)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def watchdog(self) -> Optional[str]:
|
def watchdog(self) -> str | None:
|
||||||
"""Return URL to for watchdog or None."""
|
"""Return URL to for watchdog or None."""
|
||||||
return self.data.get(ATTR_WATCHDOG)
|
return self.data.get(ATTR_WATCHDOG)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ingress_port(self) -> Optional[int]:
|
def ingress_port(self) -> int | None:
|
||||||
"""Return Ingress port."""
|
"""Return Ingress port."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -304,6 +309,11 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
"""Return True if add-on run on host IPC namespace."""
|
"""Return True if add-on run on host IPC namespace."""
|
||||||
return self.data[ATTR_HOST_IPC]
|
return self.data[ATTR_HOST_IPC]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host_uts(self) -> bool:
|
||||||
|
"""Return True if add-on run on host UTS namespace."""
|
||||||
|
return self.data[ATTR_HOST_UTS]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def host_dbus(self) -> bool:
|
def host_dbus(self) -> bool:
|
||||||
"""Return True if add-on run on host D-BUS."""
|
"""Return True if add-on run on host D-BUS."""
|
||||||
@@ -315,7 +325,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
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) -> dict[str, str] | None:
|
||||||
"""Return environment of add-on."""
|
"""Return environment of add-on."""
|
||||||
return self.data.get(ATTR_ENVIRONMENT)
|
return self.data.get(ATTR_ENVIRONMENT)
|
||||||
|
|
||||||
@@ -364,12 +374,12 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data.get(ATTR_BACKUP_EXCLUDE, [])
|
return self.data.get(ATTR_BACKUP_EXCLUDE, [])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def backup_pre(self) -> Optional[str]:
|
def backup_pre(self) -> str | None:
|
||||||
"""Return pre-backup command."""
|
"""Return pre-backup command."""
|
||||||
return self.data.get(ATTR_BACKUP_PRE)
|
return self.data.get(ATTR_BACKUP_PRE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def backup_post(self) -> Optional[str]:
|
def backup_post(self) -> str | None:
|
||||||
"""Return post-backup command."""
|
"""Return post-backup command."""
|
||||||
return self.data.get(ATTR_BACKUP_POST)
|
return self.data.get(ATTR_BACKUP_POST)
|
||||||
|
|
||||||
@@ -394,7 +404,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data[ATTR_INGRESS]
|
return self.data[ATTR_INGRESS]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ingress_panel(self) -> Optional[bool]:
|
def ingress_panel(self) -> bool | None:
|
||||||
"""Return True if the add-on access support ingress."""
|
"""Return True if the add-on access support ingress."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -444,7 +454,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data[ATTR_DEVICETREE]
|
return self.data[ATTR_DEVICETREE]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def with_tmpfs(self) -> Optional[str]:
|
def with_tmpfs(self) -> str | None:
|
||||||
"""Return if tmp is in memory of add-on."""
|
"""Return if tmp is in memory of add-on."""
|
||||||
return self.data[ATTR_TMPFS]
|
return self.data[ATTR_TMPFS]
|
||||||
|
|
||||||
@@ -464,12 +474,12 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data[ATTR_VIDEO]
|
return self.data[ATTR_VIDEO]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def homeassistant_version(self) -> Optional[str]:
|
def homeassistant_version(self) -> str | None:
|
||||||
"""Return min Home Assistant version they needed by Add-on."""
|
"""Return min Home Assistant version they needed by Add-on."""
|
||||||
return self.data.get(ATTR_HOMEASSISTANT)
|
return self.data.get(ATTR_HOMEASSISTANT)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self) -> Optional[str]:
|
def url(self) -> str | None:
|
||||||
"""Return URL of add-on."""
|
"""Return URL of add-on."""
|
||||||
return self.data.get(ATTR_URL)
|
return self.data.get(ATTR_URL)
|
||||||
|
|
||||||
@@ -512,7 +522,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.sys_arch.default
|
return self.sys_arch.default
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self) -> Optional[str]:
|
def image(self) -> str | None:
|
||||||
"""Generate image name from data."""
|
"""Generate image name from data."""
|
||||||
return self._image(self.data)
|
return self._image(self.data)
|
||||||
|
|
||||||
@@ -522,14 +532,14 @@ 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, bool]:
|
||||||
"""Return a dict of {volume: policy} from add-on."""
|
"""Return a dict of {volume: read-only} from add-on."""
|
||||||
volumes = {}
|
volumes = {}
|
||||||
for volume in self.data[ATTR_MAP]:
|
for volume in self.data[ATTR_MAP]:
|
||||||
result = RE_VOLUME.match(volume)
|
result = RE_VOLUME.match(volume)
|
||||||
if not result:
|
if not result:
|
||||||
continue
|
continue
|
||||||
volumes[result.group(1)] = result.group(2) or "ro"
|
volumes[result.group(1)] = result.group(2) != "rw"
|
||||||
|
|
||||||
return volumes
|
return volumes
|
||||||
|
|
||||||
@@ -573,7 +583,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return AddonOptions(self.coresys, raw_schema, self.name, self.slug)
|
return AddonOptions(self.coresys, raw_schema, self.name, self.slug)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def schema_ui(self) -> Optional[list[dict[any, any]]]:
|
def schema_ui(self) -> list[dict[any, any]] | None:
|
||||||
"""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]
|
||||||
|
|
||||||
@@ -592,35 +602,58 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return ATTR_CODENOTARY in self.data
|
return ATTR_CODENOTARY in self.data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def codenotary(self) -> Optional[str]:
|
def codenotary(self) -> str | None:
|
||||||
"""Return Signer email address for CAS."""
|
"""Return Signer email address for CAS."""
|
||||||
return self.data.get(ATTR_CODENOTARY)
|
return self.data.get(ATTR_CODENOTARY)
|
||||||
|
|
||||||
|
def validate_availability(self) -> None:
|
||||||
|
"""Validate if addon is available for current system."""
|
||||||
|
return self._validate_availability(self.data, logger=_LOGGER.error)
|
||||||
|
|
||||||
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):
|
||||||
return False
|
return False
|
||||||
return self.slug == other.slug
|
return self.slug == other.slug
|
||||||
|
|
||||||
def _available(self, config) -> bool:
|
def _validate_availability(
|
||||||
"""Return True if this add-on is available on this platform."""
|
self, config, *, logger: Callable[..., None] | None = None
|
||||||
|
) -> None:
|
||||||
|
"""Validate if addon is available for current system."""
|
||||||
# Architecture
|
# Architecture
|
||||||
if not self.sys_arch.is_supported(config[ATTR_ARCH]):
|
if not self.sys_arch.is_supported(config[ATTR_ARCH]):
|
||||||
return False
|
raise AddonsNotSupportedError(
|
||||||
|
f"Add-on {self.slug} not supported on this platform, supported architectures: {', '.join(config[ATTR_ARCH])}",
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
|
||||||
# Machine / Hardware
|
# Machine / Hardware
|
||||||
machine = config.get(ATTR_MACHINE)
|
machine = config.get(ATTR_MACHINE)
|
||||||
if machine and f"!{self.sys_machine}" in machine:
|
if machine and (
|
||||||
return False
|
f"!{self.sys_machine}" in machine or self.sys_machine not in machine
|
||||||
elif machine and self.sys_machine not in machine:
|
):
|
||||||
return False
|
raise AddonsNotSupportedError(
|
||||||
|
f"Add-on {self.slug} not supported on this machine, supported machine types: {', '.join(machine)}",
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
|
||||||
# Home Assistant
|
# Home Assistant
|
||||||
version: Optional[AwesomeVersion] = config.get(ATTR_HOMEASSISTANT)
|
version: AwesomeVersion | None = config.get(ATTR_HOMEASSISTANT)
|
||||||
|
with suppress(AwesomeVersionException, TypeError):
|
||||||
|
if self.sys_homeassistant.version < version:
|
||||||
|
raise AddonsNotSupportedError(
|
||||||
|
f"Add-on {self.slug} not supported on this system, requires Home Assistant version {version} or greater",
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _available(self, config) -> bool:
|
||||||
|
"""Return True if this add-on is available on this platform."""
|
||||||
try:
|
try:
|
||||||
return self.sys_homeassistant.version >= version
|
self._validate_availability(config)
|
||||||
except (AwesomeVersionException, TypeError):
|
except AddonsNotSupportedError:
|
||||||
return True
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def _image(self, config) -> str:
|
def _image(self, config) -> str:
|
||||||
"""Generate image name from data."""
|
"""Generate image name from data."""
|
||||||
@@ -640,7 +673,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
"""Uninstall this add-on."""
|
"""Uninstall this add-on."""
|
||||||
return self.sys_addons.uninstall(self.slug)
|
return self.sys_addons.uninstall(self.slug)
|
||||||
|
|
||||||
def update(self, backup: Optional[bool] = False) -> Awaitable[None]:
|
def update(self, backup: bool | None = False) -> Awaitable[None]:
|
||||||
"""Update this add-on."""
|
"""Update this add-on."""
|
||||||
return self.sys_addons.update(self.slug, backup=backup)
|
return self.sys_addons.update(self.slug, backup=backup)
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@ import hashlib
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
from typing import Any, Union
|
from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -293,7 +293,7 @@ class UiOptions(CoreSysAttributes):
|
|||||||
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, str | bool | float | list[str]] = {"name": key}
|
||||||
|
|
||||||
# If multiple
|
# If multiple
|
||||||
if multiple:
|
if multiple:
|
||||||
|
@@ -44,12 +44,15 @@ def rating_security(addon: AddonModel) -> int:
|
|||||||
any(
|
any(
|
||||||
privilege in addon.privileged
|
privilege in addon.privileged
|
||||||
for privilege in (
|
for privilege in (
|
||||||
Capabilities.NET_ADMIN,
|
Capabilities.BPF,
|
||||||
Capabilities.SYS_ADMIN,
|
|
||||||
Capabilities.SYS_RAWIO,
|
|
||||||
Capabilities.SYS_PTRACE,
|
|
||||||
Capabilities.SYS_MODULE,
|
|
||||||
Capabilities.DAC_READ_SEARCH,
|
Capabilities.DAC_READ_SEARCH,
|
||||||
|
Capabilities.NET_ADMIN,
|
||||||
|
Capabilities.NET_RAW,
|
||||||
|
Capabilities.PERFMON,
|
||||||
|
Capabilities.SYS_ADMIN,
|
||||||
|
Capabilities.SYS_MODULE,
|
||||||
|
Capabilities.SYS_PTRACE,
|
||||||
|
Capabilities.SYS_RAWIO,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
or addon.with_kernel_modules
|
or addon.with_kernel_modules
|
||||||
@@ -70,6 +73,10 @@ def rating_security(addon: AddonModel) -> int:
|
|||||||
if addon.host_pid:
|
if addon.host_pid:
|
||||||
rating += -2
|
rating += -2
|
||||||
|
|
||||||
|
# UTS host namespace allows to set hostname only with SYS_ADMIN
|
||||||
|
if addon.host_uts and Capabilities.SYS_ADMIN in addon.privileged:
|
||||||
|
rating += -1
|
||||||
|
|
||||||
# Docker Access & full Access
|
# Docker Access & full Access
|
||||||
if addon.access_docker_api or addon.with_full_access:
|
if addon.access_docker_api or addon.with_full_access:
|
||||||
rating = 1
|
rating = 1
|
||||||
|
@@ -7,8 +7,6 @@ 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,
|
||||||
@@ -43,6 +41,7 @@ from ..const import (
|
|||||||
ATTR_HOST_IPC,
|
ATTR_HOST_IPC,
|
||||||
ATTR_HOST_NETWORK,
|
ATTR_HOST_NETWORK,
|
||||||
ATTR_HOST_PID,
|
ATTR_HOST_PID,
|
||||||
|
ATTR_HOST_UTS,
|
||||||
ATTR_IMAGE,
|
ATTR_IMAGE,
|
||||||
ATTR_INGRESS,
|
ATTR_INGRESS,
|
||||||
ATTR_INGRESS_ENTRY,
|
ATTR_INGRESS_ENTRY,
|
||||||
@@ -110,7 +109,7 @@ from ..validate import (
|
|||||||
uuid_match,
|
uuid_match,
|
||||||
version_tag,
|
version_tag,
|
||||||
)
|
)
|
||||||
from .const import ATTR_BACKUP, ATTR_CODENOTARY
|
from .const import ATTR_BACKUP, ATTR_CODENOTARY, RE_SLUG, AddonBackupMode
|
||||||
from .options import RE_SCHEMA_ELEMENT
|
from .options import RE_SCHEMA_ELEMENT
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@@ -131,6 +130,7 @@ RE_MACHINE = re.compile(
|
|||||||
r"|generic-x86-64"
|
r"|generic-x86-64"
|
||||||
r"|odroid-c2"
|
r"|odroid-c2"
|
||||||
r"|odroid-c4"
|
r"|odroid-c4"
|
||||||
|
r"|odroid-m1"
|
||||||
r"|odroid-n2"
|
r"|odroid-n2"
|
||||||
r"|odroid-xu"
|
r"|odroid-xu"
|
||||||
r"|qemuarm-64"
|
r"|qemuarm-64"
|
||||||
@@ -147,6 +147,8 @@ RE_MACHINE = re.compile(
|
|||||||
r")$"
|
r")$"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
RE_SLUG_FIELD = re.compile(r"^" + RE_SLUG + r"$")
|
||||||
|
|
||||||
|
|
||||||
def _warn_addon_config(config: dict[str, Any]):
|
def _warn_addon_config(config: dict[str, Any]):
|
||||||
"""Warn about miss configs."""
|
"""Warn about miss configs."""
|
||||||
@@ -252,7 +254,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
{
|
{
|
||||||
vol.Required(ATTR_NAME): str,
|
vol.Required(ATTR_NAME): str,
|
||||||
vol.Required(ATTR_VERSION): version_tag,
|
vol.Required(ATTR_VERSION): version_tag,
|
||||||
vol.Required(ATTR_SLUG): str,
|
vol.Required(ATTR_SLUG): vol.Match(RE_SLUG_FIELD),
|
||||||
vol.Required(ATTR_DESCRIPTON): str,
|
vol.Required(ATTR_DESCRIPTON): str,
|
||||||
vol.Required(ATTR_ARCH): [vol.In(ARCH_ALL)],
|
vol.Required(ATTR_ARCH): [vol.In(ARCH_ALL)],
|
||||||
vol.Optional(ATTR_MACHINE): vol.All([vol.Match(RE_MACHINE)], vol.Unique()),
|
vol.Optional(ATTR_MACHINE): vol.All([vol.Match(RE_MACHINE)], vol.Unique()),
|
||||||
@@ -285,6 +287,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
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(),
|
||||||
|
vol.Optional(ATTR_HOST_UTS, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_HOST_DBUS, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HOST_DBUS, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_DEVICES): [str],
|
vol.Optional(ATTR_DEVICES): [str],
|
||||||
vol.Optional(ATTR_UDEV, default=False): vol.Boolean(),
|
vol.Optional(ATTR_UDEV, default=False): vol.Boolean(),
|
||||||
@@ -353,8 +356,9 @@ SCHEMA_ADDON_CONFIG = vol.All(
|
|||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
SCHEMA_BUILD_CONFIG = vol.Schema(
|
SCHEMA_BUILD_CONFIG = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(ATTR_BUILD_FROM, default=dict): vol.Schema(
|
vol.Optional(ATTR_BUILD_FROM, default=dict): vol.Any(
|
||||||
{vol.In(ARCH_ALL): vol.Match(RE_DOCKER_IMAGE_BUILD)}
|
vol.Match(RE_DOCKER_IMAGE_BUILD),
|
||||||
|
vol.Schema({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({str: str}),
|
vol.Optional(ATTR_ARGS, default=dict): vol.Schema({str: str}),
|
||||||
|
@@ -1,15 +1,14 @@
|
|||||||
"""Init file for Supervisor RESTful API."""
|
"""Init file for Supervisor RESTful API."""
|
||||||
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from supervisor.api.utils import api_process
|
from ..const import AddonState
|
||||||
from supervisor.const import AddonState
|
|
||||||
from supervisor.exceptions import APIAddonNotInstalled
|
|
||||||
|
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
|
from ..exceptions import APIAddonNotInstalled
|
||||||
from .addons import APIAddons
|
from .addons import APIAddons
|
||||||
from .audio import APIAudio
|
from .audio import APIAudio
|
||||||
from .auth import APIAuth
|
from .auth import APIAuth
|
||||||
@@ -24,6 +23,7 @@ from .host import APIHost
|
|||||||
from .ingress import APIIngress
|
from .ingress import APIIngress
|
||||||
from .jobs import APIJobs
|
from .jobs import APIJobs
|
||||||
from .middleware.security import SecurityMiddleware
|
from .middleware.security import SecurityMiddleware
|
||||||
|
from .mounts import APIMounts
|
||||||
from .multicast import APIMulticast
|
from .multicast import APIMulticast
|
||||||
from .network import APINetwork
|
from .network import APINetwork
|
||||||
from .observer import APIObserver
|
from .observer import APIObserver
|
||||||
@@ -35,6 +35,7 @@ from .security import APISecurity
|
|||||||
from .services import APIServices
|
from .services import APIServices
|
||||||
from .store import APIStore
|
from .store import APIStore
|
||||||
from .supervisor import APISupervisor
|
from .supervisor import APISupervisor
|
||||||
|
from .utils import api_process
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -53,8 +54,10 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self.webapp: web.Application = web.Application(
|
self.webapp: web.Application = web.Application(
|
||||||
client_max_size=MAX_CLIENT_SIZE,
|
client_max_size=MAX_CLIENT_SIZE,
|
||||||
middlewares=[
|
middlewares=[
|
||||||
|
self.security.block_bad_requests,
|
||||||
self.security.system_validation,
|
self.security.system_validation,
|
||||||
self.security.token_validation,
|
self.security.token_validation,
|
||||||
|
self.security.core_proxy,
|
||||||
],
|
],
|
||||||
handler_args={
|
handler_args={
|
||||||
"max_line_size": MAX_LINE_SIZE,
|
"max_line_size": MAX_LINE_SIZE,
|
||||||
@@ -64,7 +67,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
|
|
||||||
# service stuff
|
# service stuff
|
||||||
self._runner: web.AppRunner = web.AppRunner(self.webapp)
|
self._runner: web.AppRunner = web.AppRunner(self.webapp)
|
||||||
self._site: Optional[web.TCPSite] = None
|
self._site: web.TCPSite | None = None
|
||||||
|
|
||||||
async def load(self) -> None:
|
async def load(self) -> None:
|
||||||
"""Register REST API Calls."""
|
"""Register REST API Calls."""
|
||||||
@@ -79,20 +82,21 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self._register_hardware()
|
self._register_hardware()
|
||||||
self._register_homeassistant()
|
self._register_homeassistant()
|
||||||
self._register_host()
|
self._register_host()
|
||||||
self._register_root()
|
self._register_jobs()
|
||||||
self._register_ingress()
|
self._register_ingress()
|
||||||
|
self._register_mounts()
|
||||||
self._register_multicast()
|
self._register_multicast()
|
||||||
self._register_network()
|
self._register_network()
|
||||||
self._register_observer()
|
self._register_observer()
|
||||||
self._register_os()
|
self._register_os()
|
||||||
self._register_jobs()
|
|
||||||
self._register_panel()
|
self._register_panel()
|
||||||
self._register_proxy()
|
self._register_proxy()
|
||||||
self._register_resolution()
|
self._register_resolution()
|
||||||
self._register_services()
|
self._register_root()
|
||||||
self._register_supervisor()
|
|
||||||
self._register_store()
|
|
||||||
self._register_security()
|
self._register_security()
|
||||||
|
self._register_services()
|
||||||
|
self._register_store()
|
||||||
|
self._register_supervisor()
|
||||||
|
|
||||||
await self.start()
|
await self.start()
|
||||||
|
|
||||||
@@ -104,16 +108,36 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self.webapp.add_routes(
|
self.webapp.add_routes(
|
||||||
[
|
[
|
||||||
web.get("/host/info", api_host.info),
|
web.get("/host/info", api_host.info),
|
||||||
web.get("/host/logs", api_host.logs),
|
web.get("/host/logs", api_host.advanced_logs),
|
||||||
|
web.get(
|
||||||
|
"/host/logs/follow",
|
||||||
|
partial(api_host.advanced_logs, follow=True),
|
||||||
|
),
|
||||||
|
web.get("/host/logs/identifiers", api_host.list_identifiers),
|
||||||
|
web.get("/host/logs/identifiers/{identifier}", api_host.advanced_logs),
|
||||||
|
web.get(
|
||||||
|
"/host/logs/identifiers/{identifier}/follow",
|
||||||
|
partial(api_host.advanced_logs, follow=True),
|
||||||
|
),
|
||||||
|
web.get("/host/logs/boots", api_host.list_boots),
|
||||||
|
web.get("/host/logs/boots/{bootid}", api_host.advanced_logs),
|
||||||
|
web.get(
|
||||||
|
"/host/logs/boots/{bootid}/follow",
|
||||||
|
partial(api_host.advanced_logs, follow=True),
|
||||||
|
),
|
||||||
|
web.get(
|
||||||
|
"/host/logs/boots/{bootid}/identifiers/{identifier}",
|
||||||
|
api_host.advanced_logs,
|
||||||
|
),
|
||||||
|
web.get(
|
||||||
|
"/host/logs/boots/{bootid}/identifiers/{identifier}/follow",
|
||||||
|
partial(api_host.advanced_logs, follow=True),
|
||||||
|
),
|
||||||
web.post("/host/reboot", api_host.reboot),
|
web.post("/host/reboot", api_host.reboot),
|
||||||
web.post("/host/shutdown", api_host.shutdown),
|
web.post("/host/shutdown", api_host.shutdown),
|
||||||
web.post("/host/reload", api_host.reload),
|
web.post("/host/reload", api_host.reload),
|
||||||
web.post("/host/options", api_host.options),
|
web.post("/host/options", api_host.options),
|
||||||
web.get("/host/services", api_host.services),
|
web.get("/host/services", api_host.services),
|
||||||
web.post("/host/services/{service}/stop", api_host.service_stop),
|
|
||||||
web.post("/host/services/{service}/start", api_host.service_start),
|
|
||||||
web.post("/host/services/{service}/restart", api_host.service_restart),
|
|
||||||
web.post("/host/services/{service}/reload", api_host.service_reload),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -159,6 +183,15 @@ class RestAPI(CoreSysAttributes):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Boards endpoints
|
||||||
|
self.webapp.add_routes(
|
||||||
|
[
|
||||||
|
web.get("/os/boards/yellow", api_os.boards_yellow_info),
|
||||||
|
web.post("/os/boards/yellow", api_os.boards_yellow_options),
|
||||||
|
web.get("/os/boards/{board}", api_os.boards_other_info),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def _register_security(self) -> None:
|
def _register_security(self) -> None:
|
||||||
"""Register Security functions."""
|
"""Register Security functions."""
|
||||||
api_security = APISecurity()
|
api_security = APISecurity()
|
||||||
@@ -278,6 +311,10 @@ class RestAPI(CoreSysAttributes):
|
|||||||
"/resolution/issue/{issue}",
|
"/resolution/issue/{issue}",
|
||||||
api_resolution.dismiss_issue,
|
api_resolution.dismiss_issue,
|
||||||
),
|
),
|
||||||
|
web.get(
|
||||||
|
"/resolution/issue/{issue}/suggestions",
|
||||||
|
api_resolution.suggestions_for_issue,
|
||||||
|
),
|
||||||
web.post("/resolution/healthcheck", api_resolution.healthcheck),
|
web.post("/resolution/healthcheck", api_resolution.healthcheck),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -445,11 +482,13 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self.webapp.add_routes(
|
self.webapp.add_routes(
|
||||||
[
|
[
|
||||||
web.get("/backups", api_backups.list),
|
web.get("/backups", api_backups.list),
|
||||||
|
web.get("/backups/info", api_backups.info),
|
||||||
|
web.post("/backups/options", api_backups.options),
|
||||||
web.post("/backups/reload", api_backups.reload),
|
web.post("/backups/reload", api_backups.reload),
|
||||||
web.post("/backups/new/full", api_backups.backup_full),
|
web.post("/backups/new/full", api_backups.backup_full),
|
||||||
web.post("/backups/new/partial", api_backups.backup_partial),
|
web.post("/backups/new/partial", api_backups.backup_partial),
|
||||||
web.post("/backups/new/upload", api_backups.upload),
|
web.post("/backups/new/upload", api_backups.upload),
|
||||||
web.get("/backups/{slug}/info", api_backups.info),
|
web.get("/backups/{slug}/info", api_backups.backup_info),
|
||||||
web.delete("/backups/{slug}", api_backups.remove),
|
web.delete("/backups/{slug}", api_backups.remove),
|
||||||
web.post("/backups/{slug}/restore/full", api_backups.restore_full),
|
web.post("/backups/{slug}/restore/full", api_backups.restore_full),
|
||||||
web.post(
|
web.post(
|
||||||
@@ -509,6 +548,8 @@ class RestAPI(CoreSysAttributes):
|
|||||||
"""Register Audio functions."""
|
"""Register Audio functions."""
|
||||||
api_audio = APIAudio()
|
api_audio = APIAudio()
|
||||||
api_audio.coresys = self.coresys
|
api_audio.coresys = self.coresys
|
||||||
|
api_host = APIHost()
|
||||||
|
api_host.coresys = self.coresys
|
||||||
|
|
||||||
self.webapp.add_routes(
|
self.webapp.add_routes(
|
||||||
[
|
[
|
||||||
@@ -527,6 +568,22 @@ class RestAPI(CoreSysAttributes):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _register_mounts(self) -> None:
|
||||||
|
"""Register mounts endpoints."""
|
||||||
|
api_mounts = APIMounts()
|
||||||
|
api_mounts.coresys = self.coresys
|
||||||
|
|
||||||
|
self.webapp.add_routes(
|
||||||
|
[
|
||||||
|
web.get("/mounts", api_mounts.info),
|
||||||
|
web.post("/mounts/options", api_mounts.options),
|
||||||
|
web.post("/mounts", api_mounts.create_mount),
|
||||||
|
web.put("/mounts/{mount}", api_mounts.update_mount),
|
||||||
|
web.delete("/mounts/{mount}", api_mounts.delete_mount),
|
||||||
|
web.post("/mounts/{mount}/reload", api_mounts.reload_mount),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def _register_store(self) -> None:
|
def _register_store(self) -> None:
|
||||||
"""Register store endpoints."""
|
"""Register store endpoints."""
|
||||||
api_store = APIStore()
|
api_store = APIStore()
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
"""Init file for Supervisor Home Assistant RESTful API."""
|
"""Init file for Supervisor Home Assistant RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Awaitable
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Awaitable
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -45,6 +46,7 @@ from ..const import (
|
|||||||
ATTR_HOST_IPC,
|
ATTR_HOST_IPC,
|
||||||
ATTR_HOST_NETWORK,
|
ATTR_HOST_NETWORK,
|
||||||
ATTR_HOST_PID,
|
ATTR_HOST_PID,
|
||||||
|
ATTR_HOST_UTS,
|
||||||
ATTR_HOSTNAME,
|
ATTR_HOSTNAME,
|
||||||
ATTR_ICON,
|
ATTR_ICON,
|
||||||
ATTR_INGRESS,
|
ATTR_INGRESS,
|
||||||
@@ -215,6 +217,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_HOST_NETWORK: addon.host_network,
|
ATTR_HOST_NETWORK: addon.host_network,
|
||||||
ATTR_HOST_PID: addon.host_pid,
|
ATTR_HOST_PID: addon.host_pid,
|
||||||
ATTR_HOST_IPC: addon.host_ipc,
|
ATTR_HOST_IPC: addon.host_ipc,
|
||||||
|
ATTR_HOST_UTS: addon.host_uts,
|
||||||
ATTR_HOST_DBUS: addon.host_dbus,
|
ATTR_HOST_DBUS: addon.host_dbus,
|
||||||
ATTR_PRIVILEGED: addon.privileged,
|
ATTR_PRIVILEGED: addon.privileged,
|
||||||
ATTR_FULL_ACCESS: addon.with_full_access,
|
ATTR_FULL_ACCESS: addon.with_full_access,
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
"""Init file for Supervisor Audio RESTful API."""
|
"""Init file for Supervisor Audio RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Awaitable
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Awaitable
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import attr
|
import attr
|
||||||
|
@@ -4,31 +4,36 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp.hdrs import CONTENT_DISPOSITION
|
from aiohttp.hdrs import CONTENT_DISPOSITION
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from ..backups.validate import ALL_FOLDERS, FOLDER_HOMEASSISTANT
|
from ..backups.validate import ALL_FOLDERS, FOLDER_HOMEASSISTANT, days_until_stale
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_ADDONS,
|
ATTR_ADDONS,
|
||||||
ATTR_BACKUPS,
|
ATTR_BACKUPS,
|
||||||
ATTR_COMPRESSED,
|
ATTR_COMPRESSED,
|
||||||
ATTR_CONTENT,
|
ATTR_CONTENT,
|
||||||
ATTR_DATE,
|
ATTR_DATE,
|
||||||
|
ATTR_DAYS_UNTIL_STALE,
|
||||||
ATTR_FOLDERS,
|
ATTR_FOLDERS,
|
||||||
ATTR_HOMEASSISTANT,
|
ATTR_HOMEASSISTANT,
|
||||||
|
ATTR_LOCATON,
|
||||||
ATTR_NAME,
|
ATTR_NAME,
|
||||||
ATTR_PASSWORD,
|
ATTR_PASSWORD,
|
||||||
ATTR_PROTECTED,
|
ATTR_PROTECTED,
|
||||||
ATTR_REPOSITORIES,
|
ATTR_REPOSITORIES,
|
||||||
ATTR_SIZE,
|
ATTR_SIZE,
|
||||||
ATTR_SLUG,
|
ATTR_SLUG,
|
||||||
|
ATTR_SUPERVISOR_VERSION,
|
||||||
ATTR_TYPE,
|
ATTR_TYPE,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import APIError
|
from ..exceptions import APIError
|
||||||
|
from ..mounts.const import MountUsage
|
||||||
from .const import CONTENT_TYPE_TAR
|
from .const import CONTENT_TYPE_TAR
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
@@ -57,6 +62,7 @@ SCHEMA_BACKUP_FULL = vol.Schema(
|
|||||||
vol.Optional(ATTR_NAME): str,
|
vol.Optional(ATTR_NAME): str,
|
||||||
vol.Optional(ATTR_PASSWORD): vol.Maybe(str),
|
vol.Optional(ATTR_PASSWORD): vol.Maybe(str),
|
||||||
vol.Optional(ATTR_COMPRESSED): vol.Maybe(vol.Boolean()),
|
vol.Optional(ATTR_COMPRESSED): vol.Maybe(vol.Boolean()),
|
||||||
|
vol.Optional(ATTR_LOCATON): vol.Maybe(str),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,6 +74,12 @@ SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SCHEMA_OPTIONS = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_DAYS_UNTIL_STALE): days_until_stale,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class APIBackups(CoreSysAttributes):
|
class APIBackups(CoreSysAttributes):
|
||||||
"""Handle RESTful API for backups functions."""
|
"""Handle RESTful API for backups functions."""
|
||||||
@@ -79,27 +91,31 @@ class APIBackups(CoreSysAttributes):
|
|||||||
raise APIError("Backup does not exist")
|
raise APIError("Backup does not exist")
|
||||||
return backup
|
return backup
|
||||||
|
|
||||||
|
def _list_backups(self):
|
||||||
|
"""Return list of backups."""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
ATTR_SLUG: backup.slug,
|
||||||
|
ATTR_NAME: backup.name,
|
||||||
|
ATTR_DATE: backup.date,
|
||||||
|
ATTR_TYPE: backup.sys_type,
|
||||||
|
ATTR_SIZE: backup.size,
|
||||||
|
ATTR_LOCATON: backup.location,
|
||||||
|
ATTR_PROTECTED: backup.protected,
|
||||||
|
ATTR_COMPRESSED: backup.compressed,
|
||||||
|
ATTR_CONTENT: {
|
||||||
|
ATTR_HOMEASSISTANT: backup.homeassistant_version is not None,
|
||||||
|
ATTR_ADDONS: backup.addon_list,
|
||||||
|
ATTR_FOLDERS: backup.folders,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for backup in self.sys_backups.list_backups
|
||||||
|
]
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def list(self, request):
|
async def list(self, request):
|
||||||
"""Return backup list."""
|
"""Return backup list."""
|
||||||
data_backups = []
|
data_backups = self._list_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_SIZE: backup.size,
|
|
||||||
ATTR_PROTECTED: backup.protected,
|
|
||||||
ATTR_COMPRESSED: backup.compressed,
|
|
||||||
ATTR_CONTENT: {
|
|
||||||
ATTR_HOMEASSISTANT: backup.homeassistant_version is not None,
|
|
||||||
ATTR_ADDONS: backup.addon_list,
|
|
||||||
ATTR_FOLDERS: backup.folders,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.path == "/snapshots":
|
if request.path == "/snapshots":
|
||||||
# Kept for backwards compability
|
# Kept for backwards compability
|
||||||
@@ -107,6 +123,24 @@ class APIBackups(CoreSysAttributes):
|
|||||||
|
|
||||||
return {ATTR_BACKUPS: data_backups}
|
return {ATTR_BACKUPS: data_backups}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def info(self, request):
|
||||||
|
"""Return backup list and manager info."""
|
||||||
|
return {
|
||||||
|
ATTR_BACKUPS: self._list_backups(),
|
||||||
|
ATTR_DAYS_UNTIL_STALE: self.sys_backups.days_until_stale,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def options(self, request):
|
||||||
|
"""Set backup manager options."""
|
||||||
|
body = await api_validate(SCHEMA_OPTIONS, request)
|
||||||
|
|
||||||
|
if ATTR_DAYS_UNTIL_STALE in body:
|
||||||
|
self.sys_backups.days_until_stale = body[ATTR_DAYS_UNTIL_STALE]
|
||||||
|
|
||||||
|
self.sys_backups.save_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def reload(self, request):
|
async def reload(self, request):
|
||||||
"""Reload backup list."""
|
"""Reload backup list."""
|
||||||
@@ -114,7 +148,7 @@ class APIBackups(CoreSysAttributes):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def info(self, request):
|
async def backup_info(self, request):
|
||||||
"""Return backup info."""
|
"""Return backup info."""
|
||||||
backup = self._extract_slug(request)
|
backup = self._extract_slug(request)
|
||||||
|
|
||||||
@@ -137,17 +171,35 @@ class APIBackups(CoreSysAttributes):
|
|||||||
ATTR_SIZE: backup.size,
|
ATTR_SIZE: backup.size,
|
||||||
ATTR_COMPRESSED: backup.compressed,
|
ATTR_COMPRESSED: backup.compressed,
|
||||||
ATTR_PROTECTED: backup.protected,
|
ATTR_PROTECTED: backup.protected,
|
||||||
|
ATTR_SUPERVISOR_VERSION: backup.supervisor_version,
|
||||||
ATTR_HOMEASSISTANT: backup.homeassistant_version,
|
ATTR_HOMEASSISTANT: backup.homeassistant_version,
|
||||||
|
ATTR_LOCATON: backup.location,
|
||||||
ATTR_ADDONS: data_addons,
|
ATTR_ADDONS: data_addons,
|
||||||
ATTR_REPOSITORIES: backup.repositories,
|
ATTR_REPOSITORIES: backup.repositories,
|
||||||
ATTR_FOLDERS: backup.folders,
|
ATTR_FOLDERS: backup.folders,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _location_to_mount(self, body: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Change location field to mount if necessary."""
|
||||||
|
if not body.get(ATTR_LOCATON):
|
||||||
|
return body
|
||||||
|
|
||||||
|
body[ATTR_LOCATON] = self.sys_mounts.get(body[ATTR_LOCATON])
|
||||||
|
if body[ATTR_LOCATON].usage != MountUsage.BACKUP:
|
||||||
|
raise APIError(
|
||||||
|
f"Mount {body[ATTR_LOCATON].name} is not used for backups, cannot backup to there"
|
||||||
|
)
|
||||||
|
|
||||||
|
return body
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def backup_full(self, request):
|
async def backup_full(self, request):
|
||||||
"""Create full backup."""
|
"""Create full backup."""
|
||||||
body = await api_validate(SCHEMA_BACKUP_FULL, request)
|
body = await api_validate(SCHEMA_BACKUP_FULL, request)
|
||||||
backup = await asyncio.shield(self.sys_backups.do_backup_full(**body))
|
|
||||||
|
backup = await asyncio.shield(
|
||||||
|
self.sys_backups.do_backup_full(**self._location_to_mount(body))
|
||||||
|
)
|
||||||
|
|
||||||
if backup:
|
if backup:
|
||||||
return {ATTR_SLUG: backup.slug}
|
return {ATTR_SLUG: backup.slug}
|
||||||
@@ -157,7 +209,9 @@ class APIBackups(CoreSysAttributes):
|
|||||||
async def backup_partial(self, request):
|
async def backup_partial(self, request):
|
||||||
"""Create a partial backup."""
|
"""Create a partial backup."""
|
||||||
body = await api_validate(SCHEMA_BACKUP_PARTIAL, request)
|
body = await api_validate(SCHEMA_BACKUP_PARTIAL, request)
|
||||||
backup = await asyncio.shield(self.sys_backups.do_backup_partial(**body))
|
backup = await asyncio.shield(
|
||||||
|
self.sys_backups.do_backup_partial(**self._location_to_mount(body))
|
||||||
|
)
|
||||||
|
|
||||||
if backup:
|
if backup:
|
||||||
return {ATTR_SLUG: backup.slug}
|
return {ATTR_SLUG: backup.slug}
|
||||||
|
@@ -9,31 +9,47 @@ CONTENT_TYPE_URL = "application/x-www-form-urlencoded"
|
|||||||
|
|
||||||
COOKIE_INGRESS = "ingress_session"
|
COOKIE_INGRESS = "ingress_session"
|
||||||
|
|
||||||
HEADER_TOKEN_OLD = "X-Hassio-Key"
|
|
||||||
HEADER_TOKEN = "X-Supervisor-Token"
|
|
||||||
|
|
||||||
ATTR_APPARMOR_VERSION = "apparmor_version"
|
|
||||||
ATTR_AGENT_VERSION = "agent_version"
|
ATTR_AGENT_VERSION = "agent_version"
|
||||||
|
ATTR_APPARMOR_VERSION = "apparmor_version"
|
||||||
|
ATTR_ATTRIBUTES = "attributes"
|
||||||
ATTR_AVAILABLE_UPDATES = "available_updates"
|
ATTR_AVAILABLE_UPDATES = "available_updates"
|
||||||
ATTR_BOOT_TIMESTAMP = "boot_timestamp"
|
ATTR_BOOT_TIMESTAMP = "boot_timestamp"
|
||||||
|
ATTR_BOOTS = "boots"
|
||||||
ATTR_BROADCAST_LLMNR = "broadcast_llmnr"
|
ATTR_BROADCAST_LLMNR = "broadcast_llmnr"
|
||||||
ATTR_BROADCAST_MDNS = "broadcast_mdns"
|
ATTR_BROADCAST_MDNS = "broadcast_mdns"
|
||||||
|
ATTR_BY_ID = "by_id"
|
||||||
|
ATTR_CHILDREN = "children"
|
||||||
|
ATTR_CONNECTION_BUS = "connection_bus"
|
||||||
ATTR_DATA_DISK = "data_disk"
|
ATTR_DATA_DISK = "data_disk"
|
||||||
ATTR_DEVICE = "device"
|
ATTR_DEVICE = "device"
|
||||||
|
ATTR_DEV_PATH = "dev_path"
|
||||||
|
ATTR_DISK_LED = "disk_led"
|
||||||
|
ATTR_DISKS = "disks"
|
||||||
|
ATTR_DRIVES = "drives"
|
||||||
ATTR_DT_SYNCHRONIZED = "dt_synchronized"
|
ATTR_DT_SYNCHRONIZED = "dt_synchronized"
|
||||||
ATTR_DT_UTC = "dt_utc"
|
ATTR_DT_UTC = "dt_utc"
|
||||||
|
ATTR_EJECTABLE = "ejectable"
|
||||||
ATTR_FALLBACK = "fallback"
|
ATTR_FALLBACK = "fallback"
|
||||||
|
ATTR_FILESYSTEMS = "filesystems"
|
||||||
|
ATTR_HEARTBEAT_LED = "heartbeat_led"
|
||||||
|
ATTR_IDENTIFIERS = "identifiers"
|
||||||
ATTR_LLMNR = "llmnr"
|
ATTR_LLMNR = "llmnr"
|
||||||
ATTR_LLMNR_HOSTNAME = "llmnr_hostname"
|
ATTR_LLMNR_HOSTNAME = "llmnr_hostname"
|
||||||
ATTR_MDNS = "mdns"
|
ATTR_MDNS = "mdns"
|
||||||
|
ATTR_MODEL = "model"
|
||||||
|
ATTR_MOUNTS = "mounts"
|
||||||
|
ATTR_MOUNT_POINTS = "mount_points"
|
||||||
ATTR_PANEL_PATH = "panel_path"
|
ATTR_PANEL_PATH = "panel_path"
|
||||||
|
ATTR_POWER_LED = "power_led"
|
||||||
|
ATTR_REMOVABLE = "removable"
|
||||||
|
ATTR_REVISION = "revision"
|
||||||
|
ATTR_SEAT = "seat"
|
||||||
ATTR_SIGNED = "signed"
|
ATTR_SIGNED = "signed"
|
||||||
ATTR_STARTUP_TIME = "startup_time"
|
ATTR_STARTUP_TIME = "startup_time"
|
||||||
ATTR_UPDATE_TYPE = "update_type"
|
|
||||||
ATTR_USE_NTP = "use_ntp"
|
|
||||||
ATTR_BY_ID = "by_id"
|
|
||||||
ATTR_SUBSYSTEM = "subsystem"
|
ATTR_SUBSYSTEM = "subsystem"
|
||||||
ATTR_SYSFS = "sysfs"
|
ATTR_SYSFS = "sysfs"
|
||||||
ATTR_DEV_PATH = "dev_path"
|
ATTR_TIME_DETECTED = "time_detected"
|
||||||
ATTR_ATTRIBUTES = "attributes"
|
ATTR_UPDATE_TYPE = "update_type"
|
||||||
ATTR_CHILDREN = "children"
|
ATTR_USE_NTP = "use_ntp"
|
||||||
|
ATTR_USAGE = "usage"
|
||||||
|
ATTR_VENDOR = "vendor"
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
"""Init file for Supervisor DNS RESTful API."""
|
"""Init file for Supervisor DNS RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Awaitable
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Awaitable
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@@ -4,16 +4,41 @@ from typing import Any
|
|||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from ..const import ATTR_AUDIO, ATTR_DEVICES, ATTR_INPUT, ATTR_NAME, ATTR_OUTPUT
|
from ..const import (
|
||||||
|
ATTR_AUDIO,
|
||||||
|
ATTR_DEVICES,
|
||||||
|
ATTR_ID,
|
||||||
|
ATTR_INPUT,
|
||||||
|
ATTR_NAME,
|
||||||
|
ATTR_OUTPUT,
|
||||||
|
ATTR_SERIAL,
|
||||||
|
ATTR_SIZE,
|
||||||
|
ATTR_SYSTEM,
|
||||||
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..dbus.udisks2 import UDisks2
|
||||||
|
from ..dbus.udisks2.block import UDisks2Block
|
||||||
|
from ..dbus.udisks2.drive import UDisks2Drive
|
||||||
from ..hardware.data import Device
|
from ..hardware.data import Device
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_ATTRIBUTES,
|
ATTR_ATTRIBUTES,
|
||||||
ATTR_BY_ID,
|
ATTR_BY_ID,
|
||||||
ATTR_CHILDREN,
|
ATTR_CHILDREN,
|
||||||
|
ATTR_CONNECTION_BUS,
|
||||||
ATTR_DEV_PATH,
|
ATTR_DEV_PATH,
|
||||||
|
ATTR_DEVICE,
|
||||||
|
ATTR_DRIVES,
|
||||||
|
ATTR_EJECTABLE,
|
||||||
|
ATTR_FILESYSTEMS,
|
||||||
|
ATTR_MODEL,
|
||||||
|
ATTR_MOUNT_POINTS,
|
||||||
|
ATTR_REMOVABLE,
|
||||||
|
ATTR_REVISION,
|
||||||
|
ATTR_SEAT,
|
||||||
ATTR_SUBSYSTEM,
|
ATTR_SUBSYSTEM,
|
||||||
ATTR_SYSFS,
|
ATTR_SYSFS,
|
||||||
|
ATTR_TIME_DETECTED,
|
||||||
|
ATTR_VENDOR,
|
||||||
)
|
)
|
||||||
from .utils import api_process
|
from .utils import api_process
|
||||||
|
|
||||||
@@ -21,7 +46,7 @@ _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 the API."""
|
||||||
return {
|
return {
|
||||||
ATTR_NAME: device.name,
|
ATTR_NAME: device.name,
|
||||||
ATTR_SYSFS: device.sysfs,
|
ATTR_SYSFS: device.sysfs,
|
||||||
@@ -33,6 +58,42 @@ def device_struct(device: Device) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def filesystem_struct(fs_block: UDisks2Block) -> dict[str, Any]:
|
||||||
|
"""Return a dict with information of a filesystem block device to be used in the API."""
|
||||||
|
return {
|
||||||
|
ATTR_DEVICE: str(fs_block.device),
|
||||||
|
ATTR_ID: fs_block.id,
|
||||||
|
ATTR_SIZE: fs_block.size,
|
||||||
|
ATTR_NAME: fs_block.id_label,
|
||||||
|
ATTR_SYSTEM: fs_block.hint_system,
|
||||||
|
ATTR_MOUNT_POINTS: [
|
||||||
|
str(mount_point) for mount_point in fs_block.filesystem.mount_points
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def drive_struct(udisks2: UDisks2, drive: UDisks2Drive) -> dict[str, Any]:
|
||||||
|
"""Return a dict with information of a disk to be used in the API."""
|
||||||
|
return {
|
||||||
|
ATTR_VENDOR: drive.vendor,
|
||||||
|
ATTR_MODEL: drive.model,
|
||||||
|
ATTR_REVISION: drive.revision,
|
||||||
|
ATTR_SERIAL: drive.serial,
|
||||||
|
ATTR_ID: drive.id,
|
||||||
|
ATTR_SIZE: drive.size,
|
||||||
|
ATTR_TIME_DETECTED: drive.time_detected.isoformat(),
|
||||||
|
ATTR_CONNECTION_BUS: drive.connection_bus,
|
||||||
|
ATTR_SEAT: drive.seat,
|
||||||
|
ATTR_REMOVABLE: drive.removable,
|
||||||
|
ATTR_EJECTABLE: drive.ejectable,
|
||||||
|
ATTR_FILESYSTEMS: [
|
||||||
|
filesystem_struct(block)
|
||||||
|
for block in udisks2.block_devices
|
||||||
|
if block.filesystem and block.drive == drive.object_path
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class APIHardware(CoreSysAttributes):
|
class APIHardware(CoreSysAttributes):
|
||||||
"""Handle RESTful API for hardware functions."""
|
"""Handle RESTful API for hardware functions."""
|
||||||
|
|
||||||
@@ -42,7 +103,11 @@ class APIHardware(CoreSysAttributes):
|
|||||||
return {
|
return {
|
||||||
ATTR_DEVICES: [
|
ATTR_DEVICES: [
|
||||||
device_struct(device) for device in self.sys_hardware.devices
|
device_struct(device) for device in self.sys_hardware.devices
|
||||||
]
|
],
|
||||||
|
ATTR_DRIVES: [
|
||||||
|
drive_struct(self.sys_dbus.udisks2, drive)
|
||||||
|
for drive in self.sys_dbus.udisks2.drives
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
"""Init file for Supervisor Home Assistant RESTful API."""
|
"""Init file for Supervisor Home Assistant RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Awaitable
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Awaitable
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
"""Init file for Supervisor host RESTful API."""
|
"""Init file for Supervisor host RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Awaitable
|
from contextlib import suppress
|
||||||
|
import logging
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
from aiohttp.hdrs import ACCEPT, RANGE
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
from voluptuous.error import CoerceInvalid
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_CHASSIS,
|
ATTR_CHASSIS,
|
||||||
@@ -24,22 +27,30 @@ from ..const import (
|
|||||||
ATTR_TIMEZONE,
|
ATTR_TIMEZONE,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import APIError, HostLogError
|
||||||
|
from ..host.const import PARAM_BOOT_ID, PARAM_FOLLOW, PARAM_SYSLOG_IDENTIFIER
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_AGENT_VERSION,
|
ATTR_AGENT_VERSION,
|
||||||
ATTR_APPARMOR_VERSION,
|
ATTR_APPARMOR_VERSION,
|
||||||
ATTR_BOOT_TIMESTAMP,
|
ATTR_BOOT_TIMESTAMP,
|
||||||
|
ATTR_BOOTS,
|
||||||
ATTR_BROADCAST_LLMNR,
|
ATTR_BROADCAST_LLMNR,
|
||||||
ATTR_BROADCAST_MDNS,
|
ATTR_BROADCAST_MDNS,
|
||||||
ATTR_DT_SYNCHRONIZED,
|
ATTR_DT_SYNCHRONIZED,
|
||||||
ATTR_DT_UTC,
|
ATTR_DT_UTC,
|
||||||
|
ATTR_IDENTIFIERS,
|
||||||
ATTR_LLMNR_HOSTNAME,
|
ATTR_LLMNR_HOSTNAME,
|
||||||
ATTR_STARTUP_TIME,
|
ATTR_STARTUP_TIME,
|
||||||
ATTR_USE_NTP,
|
ATTR_USE_NTP,
|
||||||
CONTENT_TYPE_BINARY,
|
CONTENT_TYPE_TEXT,
|
||||||
)
|
)
|
||||||
from .utils import api_process, api_process_raw, api_validate
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
SERVICE = "service"
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
IDENTIFIER = "identifier"
|
||||||
|
BOOTID = "bootid"
|
||||||
|
DEFAULT_RANGE = 100
|
||||||
|
|
||||||
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): str})
|
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): str})
|
||||||
|
|
||||||
@@ -117,30 +128,75 @@ class APIHost(CoreSysAttributes):
|
|||||||
return {ATTR_SERVICES: services}
|
return {ATTR_SERVICES: services}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def service_start(self, request):
|
async def list_boots(self, _: web.Request):
|
||||||
"""Start a service."""
|
"""Return a list of boot IDs."""
|
||||||
unit = request.match_info.get(SERVICE)
|
boot_ids = await self.sys_host.logs.get_boot_ids()
|
||||||
return asyncio.shield(self.sys_host.services.start(unit))
|
return {
|
||||||
|
ATTR_BOOTS: {
|
||||||
|
str(1 + i - len(boot_ids)): boot_id
|
||||||
|
for i, boot_id in enumerate(boot_ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def service_stop(self, request):
|
async def list_identifiers(self, _: web.Request):
|
||||||
"""Stop a service."""
|
"""Return a list of syslog identifiers."""
|
||||||
unit = request.match_info.get(SERVICE)
|
return {ATTR_IDENTIFIERS: await self.sys_host.logs.get_identifiers()}
|
||||||
return asyncio.shield(self.sys_host.services.stop(unit))
|
|
||||||
|
async def _get_boot_id(self, possible_offset: str) -> str:
|
||||||
|
"""Convert offset into boot ID if required."""
|
||||||
|
with suppress(CoerceInvalid):
|
||||||
|
offset = vol.Coerce(int)(possible_offset)
|
||||||
|
try:
|
||||||
|
return await self.sys_host.logs.get_boot_id(offset)
|
||||||
|
except (ValueError, HostLogError) as err:
|
||||||
|
raise APIError() from err
|
||||||
|
return possible_offset
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def service_reload(self, request):
|
async def advanced_logs(
|
||||||
"""Reload a service."""
|
self, request: web.Request, identifier: str | None = None, follow: bool = False
|
||||||
unit = request.match_info.get(SERVICE)
|
) -> web.StreamResponse:
|
||||||
return asyncio.shield(self.sys_host.services.reload(unit))
|
"""Return systemd-journald logs."""
|
||||||
|
params = {}
|
||||||
|
if identifier:
|
||||||
|
params[PARAM_SYSLOG_IDENTIFIER] = identifier
|
||||||
|
elif IDENTIFIER in request.match_info:
|
||||||
|
params[PARAM_SYSLOG_IDENTIFIER] = request.match_info.get(IDENTIFIER)
|
||||||
|
else:
|
||||||
|
params[PARAM_SYSLOG_IDENTIFIER] = self.sys_host.logs.default_identifiers
|
||||||
|
|
||||||
@api_process
|
if BOOTID in request.match_info:
|
||||||
def service_restart(self, request):
|
params[PARAM_BOOT_ID] = await self._get_boot_id(
|
||||||
"""Restart a service."""
|
request.match_info.get(BOOTID)
|
||||||
unit = request.match_info.get(SERVICE)
|
)
|
||||||
return asyncio.shield(self.sys_host.services.restart(unit))
|
if follow:
|
||||||
|
params[PARAM_FOLLOW] = ""
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
if ACCEPT in request.headers and request.headers[ACCEPT] not in [
|
||||||
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
CONTENT_TYPE_TEXT,
|
||||||
"""Return host kernel logs."""
|
"*/*",
|
||||||
return self.sys_host.info.get_dmesg()
|
]:
|
||||||
|
raise APIError(
|
||||||
|
"Invalid content type requested. Only text/plain supported for now."
|
||||||
|
)
|
||||||
|
|
||||||
|
if RANGE in request.headers:
|
||||||
|
range_header = request.headers.get(RANGE)
|
||||||
|
else:
|
||||||
|
range_header = f"entries=:-{DEFAULT_RANGE}:"
|
||||||
|
|
||||||
|
async with self.sys_host.logs.journald_logs(
|
||||||
|
params=params, range_header=range_header
|
||||||
|
) as resp:
|
||||||
|
try:
|
||||||
|
response = web.StreamResponse()
|
||||||
|
response.content_type = CONTENT_TYPE_TEXT
|
||||||
|
await response.prepare(request)
|
||||||
|
async for data in resp.content:
|
||||||
|
await response.write(data)
|
||||||
|
except ConnectionResetError as ex:
|
||||||
|
raise APIError(
|
||||||
|
"Connection reset when trying to fetch data from systemd-journald."
|
||||||
|
) from ex
|
||||||
|
return response
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from ipaddress import ip_address
|
from ipaddress import ip_address
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Union
|
from typing import Any
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp import ClientTimeout, hdrs, web
|
from aiohttp import ClientTimeout, hdrs, web
|
||||||
@@ -22,9 +22,11 @@ from ..const import (
|
|||||||
ATTR_PANELS,
|
ATTR_PANELS,
|
||||||
ATTR_SESSION,
|
ATTR_SESSION,
|
||||||
ATTR_TITLE,
|
ATTR_TITLE,
|
||||||
|
HEADER_TOKEN,
|
||||||
|
HEADER_TOKEN_OLD,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from .const import COOKIE_INGRESS, HEADER_TOKEN, HEADER_TOKEN_OLD
|
from .const import COOKIE_INGRESS
|
||||||
from .utils import api_process, api_validate, require_home_assistant
|
from .utils import api_process, api_validate, require_home_assistant
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@@ -83,10 +85,9 @@ 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]:
|
) -> web.Response | web.StreamResponse | web.WebSocketResponse:
|
||||||
"""Route data to Supervisor ingress service."""
|
"""Route data to Supervisor ingress service."""
|
||||||
|
|
||||||
# Check Ingress Session
|
# Check Ingress Session
|
||||||
@@ -147,8 +148,8 @@ class APIIngress(CoreSysAttributes):
|
|||||||
# Proxy requests
|
# Proxy requests
|
||||||
await asyncio.wait(
|
await asyncio.wait(
|
||||||
[
|
[
|
||||||
_websocket_forward(ws_server, ws_client),
|
self.sys_create_task(_websocket_forward(ws_server, ws_client)),
|
||||||
_websocket_forward(ws_client, ws_server),
|
self.sys_create_task(_websocket_forward(ws_client, ws_server)),
|
||||||
],
|
],
|
||||||
return_when=asyncio.FIRST_COMPLETED,
|
return_when=asyncio.FIRST_COMPLETED,
|
||||||
)
|
)
|
||||||
@@ -157,7 +158,7 @@ class APIIngress(CoreSysAttributes):
|
|||||||
|
|
||||||
async def _handle_request(
|
async def _handle_request(
|
||||||
self, request: web.Request, addon: Addon, path: str
|
self, request: web.Request, addon: Addon, path: str
|
||||||
) -> Union[web.Response, web.StreamResponse]:
|
) -> web.Response | web.StreamResponse:
|
||||||
"""Ingress route for request."""
|
"""Ingress route for request."""
|
||||||
url = self._create_url(addon, path)
|
url = self._create_url(addon, path)
|
||||||
source_header = _init_header(request, addon)
|
source_header = _init_header(request, addon)
|
||||||
@@ -180,6 +181,7 @@ class APIIngress(CoreSysAttributes):
|
|||||||
allow_redirects=False,
|
allow_redirects=False,
|
||||||
data=data,
|
data=data,
|
||||||
timeout=ClientTimeout(total=None),
|
timeout=ClientTimeout(total=None),
|
||||||
|
skip_auto_headers={hdrs.CONTENT_TYPE},
|
||||||
) as result:
|
) as result:
|
||||||
headers = _response_header(result)
|
headers = _response_header(result)
|
||||||
|
|
||||||
@@ -216,9 +218,7 @@ class APIIngress(CoreSysAttributes):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def _init_header(
|
def _init_header(request: web.Request, addon: str) -> CIMultiDict | dict[str, str]:
|
||||||
request: web.Request, addon: str
|
|
||||||
) -> Union[CIMultiDict, dict[str, str]]:
|
|
||||||
"""Create initial header."""
|
"""Create initial header."""
|
||||||
headers = {}
|
headers = {}
|
||||||
|
|
||||||
|
@@ -1,10 +1,14 @@
|
|||||||
"""Handle security part of this API."""
|
"""Handle security part of this API."""
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from typing import Final
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from aiohttp.web import Request, RequestHandler, Response, middleware
|
from aiohttp.web import Request, RequestHandler, Response, middleware
|
||||||
from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized
|
from aiohttp.web_exceptions import HTTPBadRequest, HTTPForbidden, HTTPUnauthorized
|
||||||
|
from awesomeversion import AwesomeVersion
|
||||||
|
|
||||||
|
from ...addons.const import RE_SLUG
|
||||||
from ...const import (
|
from ...const import (
|
||||||
REQUEST_FROM,
|
REQUEST_FROM,
|
||||||
ROLE_ADMIN,
|
ROLE_ADMIN,
|
||||||
@@ -18,11 +22,22 @@ from ...coresys import CoreSys, CoreSysAttributes
|
|||||||
from ..utils import api_return_error, excract_supervisor_token
|
from ..utils import api_return_error, excract_supervisor_token
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
_CORE_VERSION: Final = AwesomeVersion("2023.3.4")
|
||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
|
||||||
|
_CORE_FRONTEND_PATHS: Final = (
|
||||||
|
r"|/app/.*\.(?:js|gz|json|map|woff2)"
|
||||||
|
r"|/(store/)?addons/" + RE_SLUG + r"/(logo|icon)"
|
||||||
|
)
|
||||||
|
|
||||||
|
CORE_FRONTEND: Final = re.compile(
|
||||||
|
r"^(?:" + _CORE_FRONTEND_PATHS + r")$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Block Anytime
|
# Block Anytime
|
||||||
BLACKLIST = re.compile(
|
BLACKLIST: Final = re.compile(
|
||||||
r"^(?:"
|
r"^(?:"
|
||||||
r"|/homeassistant/api/hassio/.*"
|
r"|/homeassistant/api/hassio/.*"
|
||||||
r"|/core/api/hassio/.*"
|
r"|/core/api/hassio/.*"
|
||||||
@@ -30,25 +45,27 @@ BLACKLIST = re.compile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Free to call or have own security concepts
|
# Free to call or have own security concepts
|
||||||
NO_SECURITY_CHECK = re.compile(
|
NO_SECURITY_CHECK: Final = re.compile(
|
||||||
r"^(?:"
|
r"^(?:"
|
||||||
r"|/homeassistant/api/.*"
|
r"|/homeassistant/api/.*"
|
||||||
r"|/homeassistant/websocket"
|
r"|/homeassistant/websocket"
|
||||||
r"|/core/api/.*"
|
r"|/core/api/.*"
|
||||||
r"|/core/websocket"
|
r"|/core/websocket"
|
||||||
r"|/supervisor/ping"
|
r"|/supervisor/ping"
|
||||||
r")$"
|
r"|/ingress/[-_A-Za-z0-9]+/.*"
|
||||||
|
+ _CORE_FRONTEND_PATHS
|
||||||
|
+ r")$"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Observer allow API calls
|
# Observer allow API calls
|
||||||
OBSERVER_CHECK = re.compile(
|
OBSERVER_CHECK: Final = re.compile(
|
||||||
r"^(?:"
|
r"^(?:"
|
||||||
r"|/.+/info"
|
r"|/.+/info"
|
||||||
r")$"
|
r")$"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Can called by every add-on
|
# Can called by every add-on
|
||||||
ADDONS_API_BYPASS = re.compile(
|
ADDONS_API_BYPASS: Final = re.compile(
|
||||||
r"^(?:"
|
r"^(?:"
|
||||||
r"|/addons/self/(?!security|update)[^/]+"
|
r"|/addons/self/(?!security|update)[^/]+"
|
||||||
r"|/addons/self/options/config"
|
r"|/addons/self/options/config"
|
||||||
@@ -60,7 +77,7 @@ ADDONS_API_BYPASS = re.compile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Policy role add-on API access
|
# Policy role add-on API access
|
||||||
ADDONS_ROLE_ACCESS = {
|
ADDONS_ROLE_ACCESS: dict[str, re.Pattern] = {
|
||||||
ROLE_DEFAULT: re.compile(
|
ROLE_DEFAULT: re.compile(
|
||||||
r"^(?:"
|
r"^(?:"
|
||||||
r"|/.+/info"
|
r"|/.+/info"
|
||||||
@@ -82,7 +99,7 @@ ADDONS_ROLE_ACCESS = {
|
|||||||
ROLE_MANAGER: re.compile(
|
ROLE_MANAGER: re.compile(
|
||||||
r"^(?:"
|
r"^(?:"
|
||||||
r"|/.+/info"
|
r"|/.+/info"
|
||||||
r"|/addons(?:/[^/]+/(?!security).+|/reload)?"
|
r"|/addons(?:/" + RE_SLUG + r"/(?!security).+|/reload)?"
|
||||||
r"|/audio/.+"
|
r"|/audio/.+"
|
||||||
r"|/auth/cache"
|
r"|/auth/cache"
|
||||||
r"|/cli/.+"
|
r"|/cli/.+"
|
||||||
@@ -111,6 +128,26 @@ ADDONS_ROLE_ACCESS = {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FILTERS: Final = re.compile(
|
||||||
|
r"(?:"
|
||||||
|
|
||||||
|
# Common exploits
|
||||||
|
r"proc/self/environ"
|
||||||
|
r"|(<|%3C).*script.*(>|%3E)"
|
||||||
|
|
||||||
|
# File Injections
|
||||||
|
r"|(\.\.//?)+" # ../../anywhere
|
||||||
|
r"|[a-zA-Z0-9_]=/([a-z0-9_.]//?)+" # .html?v=/.//test
|
||||||
|
|
||||||
|
# SQL Injections
|
||||||
|
r"|union.*select.*\("
|
||||||
|
r"|union.*all.*select.*"
|
||||||
|
r"|concat.*\("
|
||||||
|
|
||||||
|
r")",
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
@@ -121,6 +158,32 @@ class SecurityMiddleware(CoreSysAttributes):
|
|||||||
"""Initialize security middleware."""
|
"""Initialize security middleware."""
|
||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
|
|
||||||
|
def _recursive_unquote(self, value: str) -> str:
|
||||||
|
"""Handle values that are encoded multiple times."""
|
||||||
|
if (unquoted := unquote(value)) != value:
|
||||||
|
unquoted = self._recursive_unquote(unquoted)
|
||||||
|
return unquoted
|
||||||
|
|
||||||
|
@middleware
|
||||||
|
async def block_bad_requests(
|
||||||
|
self, request: Request, handler: RequestHandler
|
||||||
|
) -> Response:
|
||||||
|
"""Process request and tblock commonly known exploit attempts."""
|
||||||
|
if FILTERS.search(self._recursive_unquote(request.path)):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Filtered a potential harmful request to: %s", request.raw_path
|
||||||
|
)
|
||||||
|
raise HTTPBadRequest
|
||||||
|
|
||||||
|
if FILTERS.search(self._recursive_unquote(request.query_string)):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Filtered a request with a potential harmful query string: %s",
|
||||||
|
request.raw_path,
|
||||||
|
)
|
||||||
|
raise HTTPBadRequest
|
||||||
|
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
@middleware
|
@middleware
|
||||||
async def system_validation(
|
async def system_validation(
|
||||||
self, request: Request, handler: RequestHandler
|
self, request: Request, handler: RequestHandler
|
||||||
@@ -153,6 +216,7 @@ class SecurityMiddleware(CoreSysAttributes):
|
|||||||
# Ignore security check
|
# Ignore security check
|
||||||
if NO_SECURITY_CHECK.match(request.path):
|
if NO_SECURITY_CHECK.match(request.path):
|
||||||
_LOGGER.debug("Passthrough %s", request.path)
|
_LOGGER.debug("Passthrough %s", request.path)
|
||||||
|
request[REQUEST_FROM] = None
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
|
|
||||||
# Not token
|
# Not token
|
||||||
@@ -205,3 +269,45 @@ class SecurityMiddleware(CoreSysAttributes):
|
|||||||
|
|
||||||
_LOGGER.error("Invalid token for access %s", request.path)
|
_LOGGER.error("Invalid token for access %s", request.path)
|
||||||
raise HTTPForbidden()
|
raise HTTPForbidden()
|
||||||
|
|
||||||
|
@middleware
|
||||||
|
async def core_proxy(self, request: Request, handler: RequestHandler) -> Response:
|
||||||
|
"""Validate user from Core API proxy."""
|
||||||
|
if (
|
||||||
|
request[REQUEST_FROM] != self.sys_homeassistant
|
||||||
|
or self.sys_homeassistant.version >= _CORE_VERSION
|
||||||
|
):
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
|
authorization_index: int | None = None
|
||||||
|
content_type_index: int | None = None
|
||||||
|
user_request: bool = False
|
||||||
|
admin_request: bool = False
|
||||||
|
ingress_request: bool = False
|
||||||
|
|
||||||
|
for idx, (key, value) in enumerate(request.raw_headers):
|
||||||
|
if key in (b"Authorization", b"X-Hassio-Key"):
|
||||||
|
authorization_index = idx
|
||||||
|
elif key == b"Content-Type":
|
||||||
|
content_type_index = idx
|
||||||
|
elif key == b"X-Hass-User-ID":
|
||||||
|
user_request = True
|
||||||
|
elif key == b"X-Hass-Is-Admin":
|
||||||
|
admin_request = value == b"1"
|
||||||
|
elif key == b"X-Ingress-Path":
|
||||||
|
ingress_request = True
|
||||||
|
|
||||||
|
if (user_request or admin_request) and not ingress_request:
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
|
is_proxy_request = (
|
||||||
|
authorization_index is not None
|
||||||
|
and content_type_index is not None
|
||||||
|
and content_type_index - authorization_index == 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
not CORE_FRONTEND.match(request.path) and is_proxy_request
|
||||||
|
) or ingress_request:
|
||||||
|
raise HTTPBadRequest()
|
||||||
|
return await handler(request)
|
||||||
|
124
supervisor/api/mounts.py
Normal file
124
supervisor/api/mounts.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
"""Inits file for supervisor mounts REST API."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from ..const import ATTR_NAME, ATTR_STATE
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import APIError
|
||||||
|
from ..mounts.const import ATTR_DEFAULT_BACKUP_MOUNT, MountUsage
|
||||||
|
from ..mounts.mount import Mount
|
||||||
|
from ..mounts.validate import SCHEMA_MOUNT_CONFIG
|
||||||
|
from .const import ATTR_MOUNTS
|
||||||
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
|
SCHEMA_OPTIONS = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_DEFAULT_BACKUP_MOUNT): vol.Maybe(str),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class APIMounts(CoreSysAttributes):
|
||||||
|
"""Handle REST API for mounting options."""
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def info(self, request: web.Request) -> dict[str, Any]:
|
||||||
|
"""Return MountManager info."""
|
||||||
|
return {
|
||||||
|
ATTR_DEFAULT_BACKUP_MOUNT: self.sys_mounts.default_backup_mount.name
|
||||||
|
if self.sys_mounts.default_backup_mount
|
||||||
|
else None,
|
||||||
|
ATTR_MOUNTS: [
|
||||||
|
mount.to_dict() | {ATTR_STATE: mount.state}
|
||||||
|
for mount in self.sys_mounts.mounts
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def options(self, request: web.Request) -> None:
|
||||||
|
"""Set Mount Manager options."""
|
||||||
|
body = await api_validate(SCHEMA_OPTIONS, request)
|
||||||
|
|
||||||
|
if ATTR_DEFAULT_BACKUP_MOUNT in body:
|
||||||
|
name: str | None = body[ATTR_DEFAULT_BACKUP_MOUNT]
|
||||||
|
if name is None:
|
||||||
|
self.sys_mounts.default_backup_mount = None
|
||||||
|
elif (mount := self.sys_mounts.get(name)).usage != MountUsage.BACKUP:
|
||||||
|
raise APIError(
|
||||||
|
f"Mount {name} is not used for backups, cannot use it as default backup mount"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.sys_mounts.default_backup_mount = mount
|
||||||
|
|
||||||
|
self.sys_mounts.save_data()
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def create_mount(self, request: web.Request) -> None:
|
||||||
|
"""Create a new mount in supervisor."""
|
||||||
|
body = await api_validate(SCHEMA_MOUNT_CONFIG, request)
|
||||||
|
|
||||||
|
if body[ATTR_NAME] in self.sys_mounts:
|
||||||
|
raise APIError(f"A mount already exists with name {body[ATTR_NAME]}")
|
||||||
|
|
||||||
|
mount = Mount.from_dict(self.coresys, body)
|
||||||
|
await self.sys_mounts.create_mount(mount)
|
||||||
|
|
||||||
|
# If it's a backup mount, reload backups
|
||||||
|
if mount.usage == MountUsage.BACKUP:
|
||||||
|
self.sys_create_task(self.sys_backups.reload())
|
||||||
|
|
||||||
|
# If there's no default backup mount, set it to the new mount
|
||||||
|
if not self.sys_mounts.default_backup_mount:
|
||||||
|
self.sys_mounts.default_backup_mount = mount
|
||||||
|
|
||||||
|
self.sys_mounts.save_data()
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def update_mount(self, request: web.Request) -> None:
|
||||||
|
"""Update an existing mount in supervisor."""
|
||||||
|
name = request.match_info.get("mount")
|
||||||
|
name_schema = vol.Schema(
|
||||||
|
{vol.Optional(ATTR_NAME, default=name): name}, extra=vol.ALLOW_EXTRA
|
||||||
|
)
|
||||||
|
body = await api_validate(vol.All(name_schema, SCHEMA_MOUNT_CONFIG), request)
|
||||||
|
|
||||||
|
if name not in self.sys_mounts:
|
||||||
|
raise APIError(f"No mount exists with name {name}")
|
||||||
|
|
||||||
|
mount = Mount.from_dict(self.coresys, body)
|
||||||
|
await self.sys_mounts.create_mount(mount)
|
||||||
|
|
||||||
|
# If it's a backup mount, reload backups
|
||||||
|
if mount.usage == MountUsage.BACKUP:
|
||||||
|
self.sys_create_task(self.sys_backups.reload())
|
||||||
|
|
||||||
|
# If this mount was the default backup mount and isn't for backups any more, remove it
|
||||||
|
elif self.sys_mounts.default_backup_mount == mount:
|
||||||
|
self.sys_mounts.default_backup_mount = None
|
||||||
|
|
||||||
|
self.sys_mounts.save_data()
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def delete_mount(self, request: web.Request) -> None:
|
||||||
|
"""Delete an existing mount in supervisor."""
|
||||||
|
name = request.match_info.get("mount")
|
||||||
|
mount = await self.sys_mounts.remove_mount(name)
|
||||||
|
|
||||||
|
# If it was a backup mount, reload backups
|
||||||
|
if mount.usage == MountUsage.BACKUP:
|
||||||
|
self.sys_create_task(self.sys_backups.reload())
|
||||||
|
|
||||||
|
self.sys_mounts.save_data()
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def reload_mount(self, request: web.Request) -> None:
|
||||||
|
"""Reload an existing mount in supervisor."""
|
||||||
|
name = request.match_info.get("mount")
|
||||||
|
await self.sys_mounts.reload_mount(name)
|
||||||
|
|
||||||
|
# If it's a backup mount, reload backups
|
||||||
|
if self.sys_mounts.get(name).usage == MountUsage.BACKUP:
|
||||||
|
self.sys_create_task(self.sys_backups.reload())
|
@@ -1,7 +1,8 @@
|
|||||||
"""Init file for Supervisor Multicast RESTful API."""
|
"""Init file for Supervisor Multicast RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Awaitable
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Awaitable
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
"""REST API for network."""
|
"""REST API for network."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Awaitable
|
||||||
from ipaddress import ip_address, ip_interface
|
from ipaddress import ip_address, ip_interface
|
||||||
from typing import Any, Awaitable
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import attr
|
import attr
|
||||||
@@ -30,6 +31,7 @@ from ..const import (
|
|||||||
ATTR_PARENT,
|
ATTR_PARENT,
|
||||||
ATTR_PRIMARY,
|
ATTR_PRIMARY,
|
||||||
ATTR_PSK,
|
ATTR_PSK,
|
||||||
|
ATTR_READY,
|
||||||
ATTR_SIGNAL,
|
ATTR_SIGNAL,
|
||||||
ATTR_SSID,
|
ATTR_SSID,
|
||||||
ATTR_SUPERVISOR_INTERNET,
|
ATTR_SUPERVISOR_INTERNET,
|
||||||
@@ -89,6 +91,7 @@ def ipconfig_struct(config: IpConfig) -> dict[str, Any]:
|
|||||||
ATTR_ADDRESS: [address.with_prefixlen for address in config.address],
|
ATTR_ADDRESS: [address.with_prefixlen for address in config.address],
|
||||||
ATTR_NAMESERVERS: [str(address) for address in config.nameservers],
|
ATTR_NAMESERVERS: [str(address) for address in config.nameservers],
|
||||||
ATTR_GATEWAY: str(config.gateway) if config.gateway else None,
|
ATTR_GATEWAY: str(config.gateway) if config.gateway else None,
|
||||||
|
ATTR_READY: config.ready,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -194,12 +197,14 @@ class APINetwork(CoreSysAttributes):
|
|||||||
for key, config in body.items():
|
for key, config in body.items():
|
||||||
if key == ATTR_IPV4:
|
if key == ATTR_IPV4:
|
||||||
interface.ipv4 = attr.evolve(
|
interface.ipv4 = attr.evolve(
|
||||||
interface.ipv4 or IpConfig(InterfaceMethod.STATIC, [], None, []),
|
interface.ipv4
|
||||||
|
or IpConfig(InterfaceMethod.STATIC, [], None, [], None),
|
||||||
**config,
|
**config,
|
||||||
)
|
)
|
||||||
elif key == ATTR_IPV6:
|
elif key == ATTR_IPV6:
|
||||||
interface.ipv6 = attr.evolve(
|
interface.ipv6 = attr.evolve(
|
||||||
interface.ipv6 or IpConfig(InterfaceMethod.STATIC, [], None, []),
|
interface.ipv6
|
||||||
|
or IpConfig(InterfaceMethod.STATIC, [], None, [], None),
|
||||||
**config,
|
**config,
|
||||||
)
|
)
|
||||||
elif key == ATTR_WIFI:
|
elif key == ATTR_WIFI:
|
||||||
@@ -218,7 +223,9 @@ class APINetwork(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
def reload(self, request: web.Request) -> Awaitable[None]:
|
def reload(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Reload network data."""
|
"""Reload network data."""
|
||||||
return asyncio.shield(self.sys_host.network.update())
|
return asyncio.shield(
|
||||||
|
self.sys_host.network.update(force_connectivity_check=True)
|
||||||
|
)
|
||||||
|
|
||||||
@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]:
|
||||||
@@ -255,6 +262,7 @@ class APINetwork(CoreSysAttributes):
|
|||||||
body[ATTR_IPV4].get(ATTR_ADDRESS, []),
|
body[ATTR_IPV4].get(ATTR_ADDRESS, []),
|
||||||
body[ATTR_IPV4].get(ATTR_GATEWAY, None),
|
body[ATTR_IPV4].get(ATTR_GATEWAY, None),
|
||||||
body[ATTR_IPV4].get(ATTR_NAMESERVERS, []),
|
body[ATTR_IPV4].get(ATTR_NAMESERVERS, []),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
ipv6_config = None
|
ipv6_config = None
|
||||||
@@ -264,6 +272,7 @@ class APINetwork(CoreSysAttributes):
|
|||||||
body[ATTR_IPV6].get(ATTR_ADDRESS, []),
|
body[ATTR_IPV6].get(ATTR_ADDRESS, []),
|
||||||
body[ATTR_IPV6].get(ATTR_GATEWAY, None),
|
body[ATTR_IPV6].get(ATTR_GATEWAY, None),
|
||||||
body[ATTR_IPV6].get(ATTR_NAMESERVERS, []),
|
body[ATTR_IPV6].get(ATTR_NAMESERVERS, []),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
vlan_interface = Interface(
|
vlan_interface = Interface(
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
"""Init file for Supervisor HassOS RESTful API."""
|
"""Init file for Supervisor HassOS RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Awaitable
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from typing import Any
|
||||||
from typing import Any, Awaitable
|
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -11,19 +11,44 @@ from ..const import (
|
|||||||
ATTR_BOARD,
|
ATTR_BOARD,
|
||||||
ATTR_BOOT,
|
ATTR_BOOT,
|
||||||
ATTR_DEVICES,
|
ATTR_DEVICES,
|
||||||
|
ATTR_ID,
|
||||||
|
ATTR_NAME,
|
||||||
|
ATTR_SERIAL,
|
||||||
|
ATTR_SIZE,
|
||||||
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 ..exceptions import BoardInvalidError
|
||||||
|
from ..resolution.const import ContextType, IssueType, SuggestionType
|
||||||
from ..validate import version_tag
|
from ..validate import version_tag
|
||||||
from .const import ATTR_DATA_DISK, ATTR_DEVICE
|
from .const import (
|
||||||
|
ATTR_DATA_DISK,
|
||||||
|
ATTR_DEV_PATH,
|
||||||
|
ATTR_DEVICE,
|
||||||
|
ATTR_DISK_LED,
|
||||||
|
ATTR_DISKS,
|
||||||
|
ATTR_HEARTBEAT_LED,
|
||||||
|
ATTR_MODEL,
|
||||||
|
ATTR_POWER_LED,
|
||||||
|
ATTR_VENDOR,
|
||||||
|
)
|
||||||
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))})
|
SCHEMA_DISK = vol.Schema({vol.Required(ATTR_DEVICE): str})
|
||||||
|
|
||||||
|
# pylint: disable=no-value-for-parameter
|
||||||
|
SCHEMA_YELLOW_OPTIONS = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_DISK_LED): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_HEARTBEAT_LED): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_POWER_LED): vol.Boolean(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class APIOS(CoreSysAttributes):
|
class APIOS(CoreSysAttributes):
|
||||||
@@ -38,7 +63,7 @@ class APIOS(CoreSysAttributes):
|
|||||||
ATTR_UPDATE_AVAILABLE: self.sys_os.need_update,
|
ATTR_UPDATE_AVAILABLE: self.sys_os.need_update,
|
||||||
ATTR_BOARD: self.sys_os.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,
|
ATTR_DATA_DISK: self.sys_os.datadisk.disk_used_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
@@ -65,5 +90,56 @@ class APIOS(CoreSysAttributes):
|
|||||||
async def list_data(self, request: web.Request) -> dict[str, Any]:
|
async def list_data(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Return possible data targets."""
|
"""Return possible data targets."""
|
||||||
return {
|
return {
|
||||||
ATTR_DEVICES: self.sys_os.datadisk.available_disks,
|
ATTR_DEVICES: [disk.id for disk in self.sys_os.datadisk.available_disks],
|
||||||
|
ATTR_DISKS: [
|
||||||
|
{
|
||||||
|
ATTR_NAME: disk.name,
|
||||||
|
ATTR_VENDOR: disk.vendor,
|
||||||
|
ATTR_MODEL: disk.model,
|
||||||
|
ATTR_SERIAL: disk.serial,
|
||||||
|
ATTR_SIZE: disk.size,
|
||||||
|
ATTR_ID: disk.id,
|
||||||
|
ATTR_DEV_PATH: disk.device_path.as_posix(),
|
||||||
|
}
|
||||||
|
for disk in self.sys_os.datadisk.available_disks
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def boards_yellow_info(self, request: web.Request) -> dict[str, Any]:
|
||||||
|
"""Get yellow board settings."""
|
||||||
|
return {
|
||||||
|
ATTR_DISK_LED: self.sys_dbus.agent.board.yellow.disk_led,
|
||||||
|
ATTR_HEARTBEAT_LED: self.sys_dbus.agent.board.yellow.heartbeat_led,
|
||||||
|
ATTR_POWER_LED: self.sys_dbus.agent.board.yellow.power_led,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def boards_yellow_options(self, request: web.Request) -> None:
|
||||||
|
"""Update yellow board settings."""
|
||||||
|
body = await api_validate(SCHEMA_YELLOW_OPTIONS, request)
|
||||||
|
|
||||||
|
if ATTR_DISK_LED in body:
|
||||||
|
self.sys_dbus.agent.board.yellow.disk_led = body[ATTR_DISK_LED]
|
||||||
|
|
||||||
|
if ATTR_HEARTBEAT_LED in body:
|
||||||
|
self.sys_dbus.agent.board.yellow.heartbeat_led = body[ATTR_HEARTBEAT_LED]
|
||||||
|
|
||||||
|
if ATTR_POWER_LED in body:
|
||||||
|
self.sys_dbus.agent.board.yellow.power_led = body[ATTR_POWER_LED]
|
||||||
|
|
||||||
|
self.sys_resolution.create_issue(
|
||||||
|
IssueType.REBOOT_REQUIRED,
|
||||||
|
ContextType.SYSTEM,
|
||||||
|
suggestions=[SuggestionType.EXECUTE_REBOOT],
|
||||||
|
)
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def boards_other_info(self, request: web.Request) -> dict[str, Any]:
|
||||||
|
"""Empty success return if board is in use, error otherwise."""
|
||||||
|
if request.match_info["board"] != self.sys_os.board:
|
||||||
|
raise BoardInvalidError(
|
||||||
|
f"{request.match_info['board']} board is not in use", _LOGGER.error
|
||||||
|
)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
@@ -1,16 +1 @@
|
|||||||
|
!function(){function n(n){var t=document.createElement("script");t.src=n,document.body.appendChild(t)}if(/.*Version\/(?:11|12)(?:\.\d+)*.*Safari\//.test(navigator.userAgent))n("/api/hassio/app/frontend_es5/entrypoint-NoHhvMA3Ku8.js");else try{new Function("import('/api/hassio/app/frontend_latest/entrypoint-G81gb268sps.js')")()}catch(t){n("/api/hassio/app/frontend_es5/entrypoint-NoHhvMA3Ku8.js")}}()
|
||||||
function loadES5() {
|
|
||||||
var el = document.createElement('script');
|
|
||||||
el.src = '/api/hassio/app/frontend_es5/entrypoint.2ca69b3e.js';
|
|
||||||
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.01671338.js')")();
|
|
||||||
} catch (err) {
|
|
||||||
loadES5();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
!function(){"use strict";var t,n,e={14971:function(t,n,e){var r,i,o=e(93217),u=e(69330),a=(e(58556),e(62173)),c=function(t,n,e){if("input"===t){if("type"===n&&"checkbox"===e||"checked"===n||"disabled"===n)return;return""}},f={renderMarkdown:function(t,n){var e,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return r||(r=Object.assign({},(0,a.getDefaultWhiteList)(),{input:["type","disabled","checked"],"ha-icon":["icon"],"ha-svg-icon":["path"],"ha-alert":["alert-type","title"]})),o.allowSvg?(i||(i=Object.assign({},r,{svg:["xmlns","height","width"],path:["transform","stroke","d"],img:["src"]})),e=i):e=r,(0,a.filterXSS)((0,u.TU)(t,n),{whiteList:e,onTagAttr:c})}};(0,o.Jj)(f)}},r={};function i(t){var n=r[t];if(void 0!==n)return n.exports;var o=r[t]={exports:{}};return e[t](o,o.exports,i),o.exports}i.m=e,i.x=function(){var t=i.O(void 0,[191,752],(function(){return i(14971)}));return t=i.O(t)},t=[],i.O=function(n,e,r,o){if(!e){var u=1/0;for(s=0;s<t.length;s++){e=t[s][0],r=t[s][1],o=t[s][2];for(var a=!0,c=0;c<e.length;c++)(!1&o||u>=o)&&Object.keys(i.O).every((function(t){return i.O[t](e[c])}))?e.splice(c--,1):(a=!1,o<u&&(u=o));if(a){t.splice(s--,1);var f=r();void 0!==f&&(n=f)}}return n}o=o||0;for(var s=t.length;s>0&&t[s-1][2]>o;s--)t[s]=t[s-1];t[s]=[e,r,o]},i.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return i.d(n,{a:n}),n},i.d=function(t,n){for(var e in n)i.o(n,e)&&!i.o(t,e)&&Object.defineProperty(t,e,{enumerable:!0,get:n[e]})},i.f={},i.e=function(t){return Promise.all(Object.keys(i.f).reduce((function(n,e){return i.f[e](t,n),n}),[]))},i.u=function(t){return{191:"2dbdaab4",752:"829db8ac"}[t]+".js"},i.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},i.p="/api/hassio/app/frontend_es5/",function(){var t={971:1};i.f.i=function(n,e){t[n]||importScripts(i.p+i.u(n))};var n=self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[],e=n.push.bind(n);n.push=function(n){var r=n[0],o=n[1],u=n[2];for(var a in o)i.o(o,a)&&(i.m[a]=o[a]);for(u&&u(i);r.length;)t[r.pop()]=1;e(n)}}(),n=i.x,i.x=function(){return Promise.all([i.e(191),i.e(752)]).then(n)};i.x()}();
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,14 +0,0 @@
|
|||||||
/*! *****************************************************************************
|
|
||||||
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.
|
|
||||||
***************************************************************************** */
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
2
supervisor/api/panel/frontend_es5/1036-6IMueKVv3m4.js
Normal file
2
supervisor/api/panel/frontend_es5/1036-6IMueKVv3m4.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1036-6IMueKVv3m4.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1036-6IMueKVv3m4.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
2
supervisor/api/panel/frontend_es5/1047-g7fFLS9eP4I.js
Normal file
2
supervisor/api/panel/frontend_es5/1047-g7fFLS9eP4I.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"use strict";(self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[]).push([[1047],{32594:function(e,t,r){r.d(t,{U:function(){return n}});var n=function(e){return e.stopPropagation()}},75054:function(e,t,r){r.r(t),r.d(t,{HaTimeDuration:function(){return f}});var n,a=r(88962),i=r(33368),o=r(71650),d=r(82390),u=r(69205),l=r(70906),s=r(91808),c=r(68144),v=r(79932),f=(r(47289),(0,s.Z)([(0,v.Mo)("ha-selector-duration")],(function(e,t){var r=function(t){(0,u.Z)(n,t);var r=(0,l.Z)(n);function n(){var t;(0,o.Z)(this,n);for(var a=arguments.length,i=new Array(a),u=0;u<a;u++)i[u]=arguments[u];return t=r.call.apply(r,[this].concat(i)),e((0,d.Z)(t)),t}return(0,i.Z)(n)}(t);return{F:r,d:[{kind:"field",decorators:[(0,v.Cb)({attribute:!1})],key:"hass",value:void 0},{kind:"field",decorators:[(0,v.Cb)({attribute:!1})],key:"selector",value:void 0},{kind:"field",decorators:[(0,v.Cb)({attribute:!1})],key:"value",value:void 0},{kind:"field",decorators:[(0,v.Cb)()],key:"label",value:void 0},{kind:"field",decorators:[(0,v.Cb)()],key:"helper",value:void 0},{kind:"field",decorators:[(0,v.Cb)({type:Boolean})],key:"disabled",value:function(){return!1}},{kind:"field",decorators:[(0,v.Cb)({type:Boolean})],key:"required",value:function(){return!0}},{kind:"method",key:"render",value:function(){var e;return(0,c.dy)(n||(n=(0,a.Z)([' <ha-duration-input .label="','" .helper="','" .data="','" .disabled="','" .required="','" ?enableDay="','"></ha-duration-input> '])),this.label,this.helper,this.value,this.disabled,this.required,null===(e=this.selector.duration)||void 0===e?void 0:e.enable_day)}}]}}),c.oi))}}]);
|
||||||
|
//# sourceMappingURL=1047-g7fFLS9eP4I.js.map
|
BIN
supervisor/api/panel/frontend_es5/1047-g7fFLS9eP4I.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1047-g7fFLS9eP4I.js.gz
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"1047-g7fFLS9eP4I.js","mappings":"yKAAO,IAAMA,EAAkB,SAACC,GAAE,OAAKA,EAAGD,iBAAiB,C,qLCQ9CE,G,UAAcC,EAAAA,EAAAA,GAAA,EAD1BC,EAAAA,EAAAA,IAAc,0BAAuB,SAAAC,EAAAC,GAAA,IACzBJ,EAAc,SAAAK,IAAAC,EAAAA,EAAAA,GAAAN,EAAAK,GAAA,IAAAE,GAAAC,EAAAA,EAAAA,GAAAR,GAAA,SAAAA,IAAA,IAAAS,GAAAC,EAAAA,EAAAA,GAAA,KAAAV,GAAA,QAAAW,EAAAC,UAAAC,OAAAC,EAAA,IAAAC,MAAAJ,GAAAK,EAAA,EAAAA,EAAAL,EAAAK,IAAAF,EAAAE,GAAAJ,UAAAI,GAAA,OAAAP,EAAAF,EAAAU,KAAAC,MAAAX,EAAA,OAAAY,OAAAL,IAAAX,GAAAiB,EAAAA,EAAAA,GAAAX,IAAAA,CAAA,QAAAY,EAAAA,EAAAA,GAAArB,EAAA,EAAAI,GAAA,OAAAkB,EAAdtB,EAAcuB,EAAA,EAAAC,KAAA,QAAAC,WAAA,EACxBC,EAAAA,EAAAA,IAAS,CAAEC,WAAW,KAAQC,IAAA,OAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAE9BC,EAAAA,EAAAA,IAAS,CAAEC,WAAW,KAAQC,IAAA,WAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAE9BC,EAAAA,EAAAA,IAAS,CAAEC,WAAW,KAAQC,IAAA,QAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAE9BC,EAAAA,EAAAA,OAAUE,IAAA,QAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAEVC,EAAAA,EAAAA,OAAUE,IAAA,SAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAEVC,EAAAA,EAAAA,IAAS,CAAEI,KAAMC,WAAUH,IAAA,WAAAC,MAAA,kBAAmB,CAAK,IAAAL,KAAA,QAAAC,WAAA,EAEnDC,EAAAA,EAAAA,IAAS,CAAEI,KAAMC,WAAUH,IAAA,WAAAC,MAAA,kBAAmB,CAAI,IAAAL,KAAA,SAAAI,IAAA,SAAAC,MAEnD,WAAmB,IAAAG,EACjB,OAAOC,EAAAA,EAAAA,IAAIC,IAAAA,GAAAC,EAAAA,EAAAA,GAAA,wIAEEC,KAAKC,MACJD,KAAKE,OACPF,KAAKP,MACDO,KAAKG,SACLH,KAAKI,SACkB,QADVR,EACZI,KAAKK,SAASC,gBAAQ,IAAAV,OAAA,EAAtBA,EAAwBW,WAG3C,IAAC,GA1BiCC,EAAAA,I","sources":["https://raw.githubusercontent.com/home-assistant/frontend/20230601.0/src/common/dom/stop_propagation.ts","https://raw.githubusercontent.com/home-assistant/frontend/20230601.0/src/components/ha-selector/ha-selector-duration.ts"],"names":["stopPropagation","ev","HaTimeDuration","_decorate","customElement","_initialize","_LitElement","_LitElement2","_inherits","_super","_createSuper","_this","_classCallCheck","_len","arguments","length","args","Array","_key","call","apply","concat","_assertThisInitialized","_createClass","F","d","kind","decorators","property","attribute","key","value","type","Boolean","_this$selector$durati","html","_templateObject","_taggedTemplateLiteral","this","label","helper","disabled","required","selector","duration","enable_day","LitElement"],"sourceRoot":""}
|
2
supervisor/api/panel/frontend_es5/1116-YCx9f7hKX80.js
Normal file
2
supervisor/api/panel/frontend_es5/1116-YCx9f7hKX80.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1116-YCx9f7hKX80.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1116-YCx9f7hKX80.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
2
supervisor/api/panel/frontend_es5/1193-AhESuEdTugg.js
Normal file
2
supervisor/api/panel/frontend_es5/1193-AhESuEdTugg.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1193-AhESuEdTugg.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1193-AhESuEdTugg.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
3
supervisor/api/panel/frontend_es5/1246-xNkZ7MzqHIg.js
Normal file
3
supervisor/api/panel/frontend_es5/1246-xNkZ7MzqHIg.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1246-xNkZ7MzqHIg.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1246-xNkZ7MzqHIg.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
2
supervisor/api/panel/frontend_es5/1265-DN3w24TEgis.js
Normal file
2
supervisor/api/panel/frontend_es5/1265-DN3w24TEgis.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1265-DN3w24TEgis.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1265-DN3w24TEgis.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
2
supervisor/api/panel/frontend_es5/1281-YwF-4nfc5C4.js
Normal file
2
supervisor/api/panel/frontend_es5/1281-YwF-4nfc5C4.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1281-YwF-4nfc5C4.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1281-YwF-4nfc5C4.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
2
supervisor/api/panel/frontend_es5/1601-TwbKqBiBtyc.js
Normal file
2
supervisor/api/panel/frontend_es5/1601-TwbKqBiBtyc.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1601-TwbKqBiBtyc.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1601-TwbKqBiBtyc.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
1
supervisor/api/panel/frontend_es5/1639-fgyA7IMZpwQ.js
Normal file
1
supervisor/api/panel/frontend_es5/1639-fgyA7IMZpwQ.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"use strict";(self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[]).push([[1639],{71639:function(s){s.exports=[]}}]);
|
BIN
supervisor/api/panel/frontend_es5/1639-fgyA7IMZpwQ.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1639-fgyA7IMZpwQ.js.gz
Normal file
Binary file not shown.
2
supervisor/api/panel/frontend_es5/1686-sxQDkz6nH90.js
Normal file
2
supervisor/api/panel/frontend_es5/1686-sxQDkz6nH90.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1686-sxQDkz6nH90.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1686-sxQDkz6nH90.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
"use strict";(self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[]).push([[639],{71639:function(s){s.exports=[]}}]);
|
|
2
supervisor/api/panel/frontend_es5/1838-XRSmhX2jL_I.js
Normal file
2
supervisor/api/panel/frontend_es5/1838-XRSmhX2jL_I.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1838-XRSmhX2jL_I.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1838-XRSmhX2jL_I.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
2
supervisor/api/panel/frontend_es5/184-HpOMRkmEP08.js
Normal file
2
supervisor/api/panel/frontend_es5/184-HpOMRkmEP08.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/184-HpOMRkmEP08.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/184-HpOMRkmEP08.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/184-HpOMRkmEP08.js.map
Normal file
1
supervisor/api/panel/frontend_es5/184-HpOMRkmEP08.js.map
Normal file
File diff suppressed because one or more lines are too long
2
supervisor/api/panel/frontend_es5/19-ZmrTYDQwVWg.js
Normal file
2
supervisor/api/panel/frontend_es5/19-ZmrTYDQwVWg.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/19-ZmrTYDQwVWg.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/19-ZmrTYDQwVWg.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/19-ZmrTYDQwVWg.js.map
Normal file
1
supervisor/api/panel/frontend_es5/19-ZmrTYDQwVWg.js.map
Normal file
File diff suppressed because one or more lines are too long
2
supervisor/api/panel/frontend_es5/1927-DBR9HQuAgWw.js
Normal file
2
supervisor/api/panel/frontend_es5/1927-DBR9HQuAgWw.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1927-DBR9HQuAgWw.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1927-DBR9HQuAgWw.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
2
supervisor/api/panel/frontend_es5/1985-TplXOIRc8_A.js
Normal file
2
supervisor/api/panel/frontend_es5/1985-TplXOIRc8_A.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1985-TplXOIRc8_A.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1985-TplXOIRc8_A.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user