mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-09-06 11:46:22 +00:00
Compare commits
282 Commits
2024.03.1
...
trigger-sy
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e415923553 | ||
![]() |
95c638991d | ||
![]() |
e2ada42001 | ||
![]() |
22e50b4ace | ||
![]() |
334484de7f | ||
![]() |
180a7c3990 | ||
![]() |
d5f33de808 | ||
![]() |
6539f0df6f | ||
![]() |
1504278223 | ||
![]() |
9f3767b23d | ||
![]() |
e0d7985369 | ||
![]() |
2968a5717c | ||
![]() |
e2b25fe7ce | ||
![]() |
8601f5c49a | ||
![]() |
42279461e0 | ||
![]() |
409447d6ca | ||
![]() |
5b313db49d | ||
![]() |
d64618600d | ||
![]() |
1ee01b1d5e | ||
![]() |
af590202c3 | ||
![]() |
12ca2fb624 | ||
![]() |
ea95f83742 | ||
![]() |
e4d4da601c | ||
![]() |
0582f6fd39 | ||
![]() |
f254af8326 | ||
![]() |
3333770246 | ||
![]() |
ee5ded29ac | ||
![]() |
f530db98ff | ||
![]() |
911f9d661f | ||
![]() |
9935eac146 | ||
![]() |
eae2c9e221 | ||
![]() |
1a67fe8a83 | ||
![]() |
3af565267b | ||
![]() |
d09460a971 | ||
![]() |
c65329442a | ||
![]() |
48430dfa28 | ||
![]() |
70e2de372d | ||
![]() |
75784480ab | ||
![]() |
8a70ba841d | ||
![]() |
77733829d7 | ||
![]() |
d4b67f1946 | ||
![]() |
51ab138bb1 | ||
![]() |
b81413c8b2 | ||
![]() |
2ec33c6ef3 | ||
![]() |
68b2c38c7c | ||
![]() |
1ca22799d1 | ||
![]() |
549dddcb11 | ||
![]() |
131af90469 | ||
![]() |
c7c39da7c6 | ||
![]() |
8310c426f0 | ||
![]() |
bb8f91e39a | ||
![]() |
a359b9a3d5 | ||
![]() |
e130ebad1f | ||
![]() |
f5b996b66c | ||
![]() |
05e0c7c3ab | ||
![]() |
6c1203e4bf | ||
![]() |
5fbcaa8edd | ||
![]() |
00d217b5f7 | ||
![]() |
c0e35376f3 | ||
![]() |
2be84e1282 | ||
![]() |
08f10c96ef | ||
![]() |
12f8ccdf02 | ||
![]() |
d63e78cf34 | ||
![]() |
65d97ca924 | ||
![]() |
5770cafea9 | ||
![]() |
0177cd9528 | ||
![]() |
91a8fae9b5 | ||
![]() |
f16a4ce3ef | ||
![]() |
306f63c75b | ||
![]() |
2a0312318d | ||
![]() |
695a23a454 | ||
![]() |
7366673eea | ||
![]() |
53fa0fe215 | ||
![]() |
1ba621be60 | ||
![]() |
5117364625 | ||
![]() |
986b92aee4 | ||
![]() |
12d26b05af | ||
![]() |
e6c9704505 | ||
![]() |
8ab396d77c | ||
![]() |
8438448843 | ||
![]() |
362edb9a61 | ||
![]() |
1ff53e1853 | ||
![]() |
cfd28dbb5c | ||
![]() |
cbec558289 | ||
![]() |
ca3a2937d0 | ||
![]() |
3e67fc12c5 | ||
![]() |
f6faa18409 | ||
![]() |
21ae2c2e54 | ||
![]() |
eb3986bea2 | ||
![]() |
5d6738ced8 | ||
![]() |
2f2fecddf2 | ||
![]() |
218ba3601e | ||
![]() |
4c3f60c44b | ||
![]() |
cb85e5e464 | ||
![]() |
5b46235872 | ||
![]() |
70f675ac82 | ||
![]() |
bf0c714ea4 | ||
![]() |
c95df56e8d | ||
![]() |
5f3d851954 | ||
![]() |
10c69dcdae | ||
![]() |
bdd81ce3a9 | ||
![]() |
17ee234be4 | ||
![]() |
61034dfa7b | ||
![]() |
185cd362fb | ||
![]() |
e2ca357774 | ||
![]() |
3dea7fc4e8 | ||
![]() |
01ba591bc9 | ||
![]() |
640b7d46e3 | ||
![]() |
d6560c51ee | ||
![]() |
3e9b1938c6 | ||
![]() |
44ce8de71f | ||
![]() |
0bbd15bfda | ||
![]() |
591b9a4d87 | ||
![]() |
5ee7d16687 | ||
![]() |
4ab4350c58 | ||
![]() |
4ea7133fa8 | ||
![]() |
627d67f9d0 | ||
![]() |
eb37655598 | ||
![]() |
19b62dd0d4 | ||
![]() |
b2ad1ceea3 | ||
![]() |
c1545b5b78 | ||
![]() |
2c2f04ba85 | ||
![]() |
77e7bf51b7 | ||
![]() |
a42d71dcef | ||
![]() |
1ff0432f4d | ||
![]() |
54afd6e1c8 | ||
![]() |
458c493a74 | ||
![]() |
8ac8ecb17e | ||
![]() |
eac167067e | ||
![]() |
aa7f4aafeb | ||
![]() |
d2183fa12b | ||
![]() |
928f32bb4f | ||
![]() |
cbe21303c4 | ||
![]() |
94987c04b8 | ||
![]() |
d4ba46a846 | ||
![]() |
1a22d83895 | ||
![]() |
6b73bf5c28 | ||
![]() |
c9c9451c36 | ||
![]() |
1882d448ea | ||
![]() |
2f11c9c9e3 | ||
![]() |
02bdc4b555 | ||
![]() |
1a1ee50d9d | ||
![]() |
50dc09d1a9 | ||
![]() |
130efd340c | ||
![]() |
00bc13c049 | ||
![]() |
3caad67f61 | ||
![]() |
13783f0d4a | ||
![]() |
eae97ba3f4 | ||
![]() |
134dad7357 | ||
![]() |
1c4d2e8dec | ||
![]() |
f2d7be3aac | ||
![]() |
d06edb2dd6 | ||
![]() |
7fa15b334a | ||
![]() |
ffb4e2d6d7 | ||
![]() |
bd8047ae9c | ||
![]() |
49bc0624af | ||
![]() |
5e1d764eb3 | ||
![]() |
0064d93d75 | ||
![]() |
5a838ecfe7 | ||
![]() |
c37b5effd7 | ||
![]() |
ca7f3e8acb | ||
![]() |
b0cdb91d5e | ||
![]() |
4829eb8ae1 | ||
![]() |
1bb814b793 | ||
![]() |
918fcb7d62 | ||
![]() |
bbfd899564 | ||
![]() |
12c4d9da87 | ||
![]() |
6b4fd9b6b8 | ||
![]() |
07c22f4a60 | ||
![]() |
252e1e2ac0 | ||
![]() |
b684c8673e | ||
![]() |
547f42439d | ||
![]() |
c51ceb000f | ||
![]() |
4cbede1bc8 | ||
![]() |
5eac8c7780 | ||
![]() |
ab78d87304 | ||
![]() |
09166e3867 | ||
![]() |
8a5c813cdd | ||
![]() |
4200622f43 | ||
![]() |
c4452a85b4 | ||
![]() |
e57de4a3c1 | ||
![]() |
9fd2c91c55 | ||
![]() |
fbd70013a8 | ||
![]() |
8d18f3e66e | ||
![]() |
5f5754e860 | ||
![]() |
974c882b9a | ||
![]() |
a9ea90096b | ||
![]() |
45c72c426e | ||
![]() |
4e5b75fe19 | ||
![]() |
3cd617e68f | ||
![]() |
ddff02f73b | ||
![]() |
b59347b3d3 | ||
![]() |
1dc769076f | ||
![]() |
f150a19c0f | ||
![]() |
c4bc1e3824 | ||
![]() |
eca99b69db | ||
![]() |
043af72847 | ||
![]() |
05c7b6c639 | ||
![]() |
3385c99f1f | ||
![]() |
895117f857 | ||
![]() |
9e3135e2de | ||
![]() |
9a1c517437 | ||
![]() |
c0c0c4b7ad | ||
![]() |
be6e39fed0 | ||
![]() |
b384921ee0 | ||
![]() |
0d05a6eae3 | ||
![]() |
430aef68c6 | ||
![]() |
eac6070e12 | ||
![]() |
6693b7c2e6 | ||
![]() |
7898c3e433 | ||
![]() |
420ecd064e | ||
![]() |
4289be53f8 | ||
![]() |
29b41b564e | ||
![]() |
998eb69583 | ||
![]() |
8ebc097ff4 | ||
![]() |
c05984ca49 | ||
![]() |
1a700c3013 | ||
![]() |
a9c92cdec8 | ||
![]() |
da8b938d5b | ||
![]() |
71e91328f1 | ||
![]() |
6356be4c52 | ||
![]() |
e26e5440b6 | ||
![]() |
fecfbd1a3e | ||
![]() |
c00d6dfc76 | ||
![]() |
85be66d90d | ||
![]() |
1ac506b391 | ||
![]() |
f7738b77de | ||
![]() |
824037bb7d | ||
![]() |
221292ad14 | ||
![]() |
16f8c75e9f | ||
![]() |
90a37079f1 | ||
![]() |
798092af5e | ||
![]() |
2a622a929d | ||
![]() |
ca8eeaa68c | ||
![]() |
d1b8ac1249 | ||
![]() |
3f629c4d60 | ||
![]() |
3fa910e68b | ||
![]() |
e3cf2989c9 | ||
![]() |
136b2f402d | ||
![]() |
8d18d2d9c6 | ||
![]() |
f18213361a | ||
![]() |
18d9d32bca | ||
![]() |
1246e429c9 | ||
![]() |
77bc46bc37 | ||
![]() |
ce16963c94 | ||
![]() |
a70e8cfe58 | ||
![]() |
ba922a1aaa | ||
![]() |
b09230a884 | ||
![]() |
f1cb9ca08e | ||
![]() |
06513e88c6 | ||
![]() |
b4a79bd068 | ||
![]() |
dfd8fe84e0 | ||
![]() |
4857c2e243 | ||
![]() |
7d384f6160 | ||
![]() |
672a7621f9 | ||
![]() |
f0e2fb3f57 | ||
![]() |
8c3a520512 | ||
![]() |
22e50d56db | ||
![]() |
a0735f3585 | ||
![]() |
50a2e8fde3 | ||
![]() |
55ed63cc79 | ||
![]() |
97e9dfff3f | ||
![]() |
501c9579fb | ||
![]() |
f9aedadee6 | ||
![]() |
c3c17b2bc3 | ||
![]() |
a894c4589e | ||
![]() |
56a8a1b5a1 | ||
![]() |
be3f7a6c37 | ||
![]() |
906e400ab7 | ||
![]() |
a9265afd4c | ||
![]() |
d26058ac80 | ||
![]() |
ebd1f30606 | ||
![]() |
c78e077649 | ||
![]() |
07619223b0 | ||
![]() |
25c326ec6c | ||
![]() |
df167b94c2 | ||
![]() |
3730908881 | ||
![]() |
975dc1bc11 | ||
![]() |
31409f0c32 | ||
![]() |
b19273227b | ||
![]() |
f89179fb03 | ||
![]() |
90c971f9f1 |
@@ -4,8 +4,12 @@
|
|||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
||||||
},
|
},
|
||||||
|
"remoteEnv": {
|
||||||
|
"PATH": "${containerEnv:VIRTUAL_ENV}/bin:${containerEnv:PATH}"
|
||||||
|
},
|
||||||
"appPort": ["9123:8123", "7357:4357"],
|
"appPort": ["9123:8123", "7357:4357"],
|
||||||
"postCreateCommand": "bash devcontainer_bootstrap",
|
"postCreateCommand": "bash devcontainer_setup",
|
||||||
|
"postStartCommand": "bash devcontainer_bootstrap",
|
||||||
"runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
|
"runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
@@ -19,17 +23,21 @@
|
|||||||
"GitHub.vscode-pull-request-github"
|
"GitHub.vscode-pull-request-github"
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"python.defaultInterpreterPath": "/home/vscode/.local/ha-venv/bin/python",
|
||||||
|
"python.pythonPath": "/home/vscode/.local/ha-venv/bin/python",
|
||||||
|
"python.terminal.activateEnvInCurrentTerminal": true,
|
||||||
|
"python.testing.pytestArgs": ["--no-cov"],
|
||||||
|
"pylint.importStrategy": "fromEnvironment",
|
||||||
|
"editor.formatOnPaste": false,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.formatOnType": true,
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
"terminal.integrated.profiles.linux": {
|
"terminal.integrated.profiles.linux": {
|
||||||
"zsh": {
|
"zsh": {
|
||||||
"path": "/usr/bin/zsh"
|
"path": "/usr/bin/zsh"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"terminal.integrated.defaultProfile.linux": "zsh",
|
"terminal.integrated.defaultProfile.linux": "zsh",
|
||||||
"editor.formatOnPaste": false,
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.formatOnType": true,
|
|
||||||
"files.trimTrailingWhitespace": true,
|
|
||||||
"python.pythonPath": "/usr/local/bin/python3",
|
|
||||||
"[python]": {
|
"[python]": {
|
||||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||||
}
|
}
|
||||||
|
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -38,6 +38,7 @@
|
|||||||
- This PR is related to issue:
|
- This PR is related to issue:
|
||||||
- Link to documentation pull request:
|
- Link to documentation pull request:
|
||||||
- Link to cli pull request:
|
- Link to cli pull request:
|
||||||
|
- Link to client library pull request:
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
@@ -55,9 +56,11 @@
|
|||||||
- [ ] The code has been formatted using Ruff (`ruff format supervisor tests`)
|
- [ ] The code has been formatted using Ruff (`ruff format supervisor tests`)
|
||||||
- [ ] Tests have been added to verify that the new code works.
|
- [ ] Tests have been added to verify that the new code works.
|
||||||
|
|
||||||
If API endpoints of add-on configuration are added/changed:
|
If API endpoints or add-on configuration are added/changed:
|
||||||
|
|
||||||
- [ ] Documentation added/updated for [developers.home-assistant.io][docs-repository]
|
- [ ] Documentation added/updated for [developers.home-assistant.io][docs-repository]
|
||||||
|
- [ ] [CLI][cli-repository] updated (if necessary)
|
||||||
|
- [ ] [Client library][client-library-repository] updated (if necessary)
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Thank you for contributing <3
|
Thank you for contributing <3
|
||||||
@@ -67,3 +70,5 @@ If API endpoints of add-on configuration are added/changed:
|
|||||||
|
|
||||||
[dev-checklist]: https://developers.home-assistant.io/docs/en/development_checklist.html
|
[dev-checklist]: https://developers.home-assistant.io/docs/en/development_checklist.html
|
||||||
[docs-repository]: https://github.com/home-assistant/developers.home-assistant
|
[docs-repository]: https://github.com/home-assistant/developers.home-assistant
|
||||||
|
[cli-repository]: https://github.com/home-assistant/cli
|
||||||
|
[client-library-repository]: https://github.com/home-assistant-libs/python-supervisor-client/
|
||||||
|
22
.github/workflows/builder.yml
vendored
22
.github/workflows/builder.yml
vendored
@@ -53,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@v4.1.2
|
uses: actions/checkout@v4.2.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.2
|
uses: actions/checkout@v4.2.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
if: needs.init.outputs.requirements == 'true'
|
if: needs.init.outputs.requirements == 'true'
|
||||||
uses: home-assistant/wheels@2024.01.0
|
uses: home-assistant/wheels@2024.07.1
|
||||||
with:
|
with:
|
||||||
abi: cp312
|
abi: cp312
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
@@ -125,15 +125,15 @@ jobs:
|
|||||||
|
|
||||||
- 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@v5.0.0
|
uses: actions/setup-python@v5.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
if: needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.publish == 'true'
|
||||||
uses: sigstore/cosign-installer@v3.4.0
|
uses: sigstore/cosign-installer@v3.7.0
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.2.3"
|
cosign-release: "v2.4.0"
|
||||||
|
|
||||||
- name: Install dirhash and calc hash
|
- name: Install dirhash and calc hash
|
||||||
if: needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.publish == 'true'
|
||||||
@@ -149,7 +149,7 @@ jobs:
|
|||||||
|
|
||||||
- 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@v3.1.0
|
uses: docker/login-action@v3.3.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -160,7 +160,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@2024.03.5
|
uses: home-assistant/builder@2024.08.2
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
$BUILD_ARGS \
|
$BUILD_ARGS \
|
||||||
@@ -178,7 +178,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@v4.1.2
|
uses: actions/checkout@v4.2.1
|
||||||
|
|
||||||
- name: Initialize git
|
- name: Initialize git
|
||||||
if: needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.publish == 'true'
|
||||||
@@ -203,11 +203,11 @@ jobs:
|
|||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.2
|
uses: actions/checkout@v4.2.1
|
||||||
|
|
||||||
- name: Build the Supervisor
|
- name: Build the Supervisor
|
||||||
if: needs.init.outputs.publish != 'true'
|
if: needs.init.outputs.publish != 'true'
|
||||||
uses: home-assistant/builder@2024.03.5
|
uses: home-assistant/builder@2024.08.2
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
--test \
|
--test \
|
||||||
|
73
.github/workflows/ci.yaml
vendored
73
.github/workflows/ci.yaml
vendored
@@ -25,15 +25,15 @@ jobs:
|
|||||||
name: Prepare Python dependencies
|
name: Prepare Python dependencies
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.2
|
uses: actions/checkout@v4.2.1
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.0.0
|
uses: actions/setup-python@v5.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.0.2
|
uses: actions/cache@v4.1.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -47,7 +47,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@v4.0.2
|
uses: actions/cache@v4.1.1
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
lookup-only: true
|
lookup-only: true
|
||||||
@@ -67,15 +67,15 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.2
|
uses: actions/checkout@v4.2.1
|
||||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v5.0.0
|
uses: actions/setup-python@v5.2.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
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@v4.0.2
|
uses: actions/cache@v4.1.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -87,7 +87,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@v4.0.2
|
uses: actions/cache@v4.1.1
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
key: |
|
key: |
|
||||||
@@ -110,15 +110,15 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.2
|
uses: actions/checkout@v4.2.1
|
||||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v5.0.0
|
uses: actions/setup-python@v5.2.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
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@v4.0.2
|
uses: actions/cache@v4.1.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -130,7 +130,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@v4.0.2
|
uses: actions/cache@v4.1.1
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
key: |
|
key: |
|
||||||
@@ -153,7 +153,7 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.2
|
uses: actions/checkout@v4.2.1
|
||||||
- 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"
|
||||||
@@ -168,15 +168,15 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.2
|
uses: actions/checkout@v4.2.1
|
||||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v5.0.0
|
uses: actions/setup-python@v5.2.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
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@v4.0.2
|
uses: actions/cache@v4.1.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -188,7 +188,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@v4.0.2
|
uses: actions/cache@v4.1.1
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
key: |
|
key: |
|
||||||
@@ -212,15 +212,15 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.2
|
uses: actions/checkout@v4.2.1
|
||||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v5.0.0
|
uses: actions/setup-python@v5.2.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
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@v4.0.2
|
uses: actions/cache@v4.1.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -232,7 +232,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@v4.0.2
|
uses: actions/cache@v4.1.1
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
key: |
|
key: |
|
||||||
@@ -256,15 +256,15 @@ jobs:
|
|||||||
needs: prepare
|
needs: prepare
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.2
|
uses: actions/checkout@v4.2.1
|
||||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v5.0.0
|
uses: actions/setup-python@v5.2.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
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@v4.0.2
|
uses: actions/cache@v4.1.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -288,19 +288,19 @@ jobs:
|
|||||||
name: Run tests Python ${{ needs.prepare.outputs.python-version }}
|
name: Run tests Python ${{ needs.prepare.outputs.python-version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.2
|
uses: actions/checkout@v4.2.1
|
||||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v5.0.0
|
uses: actions/setup-python@v5.2.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.4.0
|
uses: sigstore/cosign-installer@v3.7.0
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.2.3"
|
cosign-release: "v2.4.0"
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.0.2
|
uses: actions/cache@v4.1.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -313,7 +313,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 dbus dbus-x11
|
sudo apt-get install -y --no-install-recommends libpulse0 libudev1 dbus-daemon
|
||||||
- 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"
|
||||||
@@ -335,10 +335,11 @@ 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@v4.3.1
|
uses: actions/upload-artifact@v4.4.3
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}
|
name: coverage-${{ matrix.python-version }}
|
||||||
path: .coverage
|
path: .coverage
|
||||||
|
include-hidden-files: true
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
name: Process test coverage
|
name: Process test coverage
|
||||||
@@ -346,15 +347,15 @@ jobs:
|
|||||||
needs: ["pytest", "prepare"]
|
needs: ["pytest", "prepare"]
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.1.2
|
uses: actions/checkout@v4.2.1
|
||||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v5.0.0
|
uses: actions/setup-python@v5.2.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
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@v4.0.2
|
uses: actions/cache@v4.1.1
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -365,7 +366,7 @@ jobs:
|
|||||||
echo "Failed to restore Python virtual environment from cache"
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
exit 1
|
exit 1
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.1.4
|
uses: actions/download-artifact@v4.1.8
|
||||||
- name: Combine coverage results
|
- name: Combine coverage results
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
@@ -373,4 +374,4 @@ jobs:
|
|||||||
coverage report
|
coverage report
|
||||||
coverage xml
|
coverage xml
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v4.1.0
|
uses: codecov/codecov-action@v4.6.0
|
||||||
|
2
.github/workflows/release-drafter.yml
vendored
2
.github/workflows/release-drafter.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
name: Release Drafter
|
name: Release Drafter
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.2
|
uses: actions/checkout@v4.2.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
2
.github/workflows/sentry.yaml
vendored
2
.github/workflows/sentry.yaml
vendored
@@ -10,7 +10,7 @@ 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@v4.1.2
|
uses: actions/checkout@v4.2.1
|
||||||
- name: Sentry Release
|
- name: Sentry Release
|
||||||
uses: getsentry/action-release@v1.7.0
|
uses: getsentry/action-release@v1.7.0
|
||||||
env:
|
env:
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.2.1
|
rev: v0.5.7
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
|
14
Dockerfile
14
Dockerfile
@@ -4,7 +4,8 @@ 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
|
CRYPTOGRAPHY_OPENSSL_NO_LEGACY=1 \
|
||||||
|
UV_SYSTEM_PYTHON=true
|
||||||
|
|
||||||
ARG \
|
ARG \
|
||||||
COSIGN_VERSION \
|
COSIGN_VERSION \
|
||||||
@@ -26,14 +27,17 @@ RUN \
|
|||||||
yaml \
|
yaml \
|
||||||
\
|
\
|
||||||
&& curl -Lso /usr/bin/cosign "https://github.com/home-assistant/cosign/releases/download/${COSIGN_VERSION}/cosign_${BUILD_ARCH}" \
|
&& curl -Lso /usr/bin/cosign "https://github.com/home-assistant/cosign/releases/download/${COSIGN_VERSION}/cosign_${BUILD_ARCH}" \
|
||||||
&& chmod a+x /usr/bin/cosign
|
&& chmod a+x /usr/bin/cosign \
|
||||||
|
&& pip3 install uv==0.2.21
|
||||||
|
|
||||||
# Install requirements
|
# Install requirements
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN \
|
RUN \
|
||||||
export MAKEFLAGS="-j$(nproc)" \
|
if [ "${BUILD_ARCH}" = "i386" ]; then \
|
||||||
&& pip3 install --only-binary=:all: \
|
linux32 uv pip install --no-build -r requirements.txt; \
|
||||||
-r ./requirements.txt \
|
else \
|
||||||
|
uv pip install --no-build -r requirements.txt; \
|
||||||
|
fi \
|
||||||
&& rm -f requirements.txt
|
&& rm -f requirements.txt
|
||||||
|
|
||||||
# Install Home Assistant Supervisor
|
# Install Home Assistant Supervisor
|
||||||
|
@@ -30,3 +30,5 @@ Releases are done in 3 stages (channels) with this structure:
|
|||||||
|
|
||||||
[development]: https://developers.home-assistant.io/docs/supervisor/development
|
[development]: https://developers.home-assistant.io/docs/supervisor/development
|
||||||
[stable]: https://github.com/home-assistant/version/blob/master/stable.json
|
[stable]: https://github.com/home-assistant/version/blob/master/stable.json
|
||||||
|
|
||||||
|
[](https://www.openhomefoundation.org/)
|
||||||
|
12
build.yaml
12
build.yaml
@@ -1,10 +1,10 @@
|
|||||||
image: ghcr.io/home-assistant/{arch}-hassio-supervisor
|
image: ghcr.io/home-assistant/{arch}-hassio-supervisor
|
||||||
build_from:
|
build_from:
|
||||||
aarch64: ghcr.io/home-assistant/aarch64-base-python:3.12-alpine3.18
|
aarch64: ghcr.io/home-assistant/aarch64-base-python:3.12-alpine3.20
|
||||||
armhf: ghcr.io/home-assistant/armhf-base-python:3.12-alpine3.18
|
armhf: ghcr.io/home-assistant/armhf-base-python:3.12-alpine3.20
|
||||||
armv7: ghcr.io/home-assistant/armv7-base-python:3.12-alpine3.18
|
armv7: ghcr.io/home-assistant/armv7-base-python:3.12-alpine3.20
|
||||||
amd64: ghcr.io/home-assistant/amd64-base-python:3.12-alpine3.18
|
amd64: ghcr.io/home-assistant/amd64-base-python:3.12-alpine3.20
|
||||||
i386: ghcr.io/home-assistant/i386-base-python:3.12-alpine3.18
|
i386: ghcr.io/home-assistant/i386-base-python:3.12-alpine3.20
|
||||||
codenotary:
|
codenotary:
|
||||||
signer: notary@home-assistant.io
|
signer: notary@home-assistant.io
|
||||||
base_image: notary@home-assistant.io
|
base_image: notary@home-assistant.io
|
||||||
@@ -12,7 +12,7 @@ cosign:
|
|||||||
base_identity: https://github.com/home-assistant/docker-base/.*
|
base_identity: https://github.com/home-assistant/docker-base/.*
|
||||||
identity: https://github.com/home-assistant/supervisor/.*
|
identity: https://github.com/home-assistant/supervisor/.*
|
||||||
args:
|
args:
|
||||||
COSIGN_VERSION: 2.2.3
|
COSIGN_VERSION: 2.4.0
|
||||||
labels:
|
labels:
|
||||||
io.hass.type: supervisor
|
io.hass.type: supervisor
|
||||||
org.opencontainers.image.title: Home Assistant Supervisor
|
org.opencontainers.image.title: Home Assistant Supervisor
|
||||||
|
@@ -31,7 +31,7 @@ include-package-data = true
|
|||||||
include = ["supervisor*"]
|
include = ["supervisor*"]
|
||||||
|
|
||||||
[tool.pylint.MAIN]
|
[tool.pylint.MAIN]
|
||||||
py-version = "3.11"
|
py-version = "3.12"
|
||||||
# Use a conservative default here; 2 should speed up most setups and not hurt
|
# Use a conservative default here; 2 should speed up most setups and not hurt
|
||||||
# any too bad. Override on command line as appropriate.
|
# any too bad. Override on command line as appropriate.
|
||||||
jobs = 2
|
jobs = 2
|
||||||
@@ -215,6 +215,9 @@ expected-line-ending-format = "LF"
|
|||||||
[tool.pylint.EXCEPTIONS]
|
[tool.pylint.EXCEPTIONS]
|
||||||
overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"]
|
overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"]
|
||||||
|
|
||||||
|
[tool.pylint.DESIGN]
|
||||||
|
max-positional-arguments = 10
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
norecursedirs = [".git"]
|
norecursedirs = [".git"]
|
||||||
@@ -228,12 +231,13 @@ filterwarnings = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
select = [
|
lint.select = [
|
||||||
"B002", # Python does not support the unary prefix increment
|
"B002", # Python does not support the unary prefix increment
|
||||||
"B007", # Loop control variable {name} not used within loop body
|
"B007", # Loop control variable {name} not used within loop body
|
||||||
"B014", # Exception handler with duplicate exception
|
"B014", # Exception handler with duplicate exception
|
||||||
"B023", # Function definition does not bind loop variable {name}
|
"B023", # Function definition does not bind loop variable {name}
|
||||||
"B026", # Star-arg unpacking after a keyword argument is strongly discouraged
|
"B026", # Star-arg unpacking after a keyword argument is strongly discouraged
|
||||||
|
"B904", # Use raise from to specify exception cause
|
||||||
"C", # complexity
|
"C", # complexity
|
||||||
"COM818", # Trailing comma on bare tuple prohibited
|
"COM818", # Trailing comma on bare tuple prohibited
|
||||||
"D", # docstrings
|
"D", # docstrings
|
||||||
@@ -247,7 +251,6 @@ select = [
|
|||||||
"N804", # First argument of a class method should be named cls
|
"N804", # First argument of a class method should be named cls
|
||||||
"N805", # First argument of a method should be named self
|
"N805", # First argument of a method should be named self
|
||||||
"N815", # Variable {name} in class scope should not be mixedCase
|
"N815", # Variable {name} in class scope should not be mixedCase
|
||||||
"PGH001", # No builtin eval() allowed
|
|
||||||
"PGH004", # Use specific rule codes when using noqa
|
"PGH004", # Use specific rule codes when using noqa
|
||||||
"PLC0414", # Useless import alias. Import alias does not rename original package.
|
"PLC0414", # Useless import alias. Import alias does not rename original package.
|
||||||
"PLC", # pylint
|
"PLC", # pylint
|
||||||
@@ -286,13 +289,12 @@ select = [
|
|||||||
"T20", # flake8-print
|
"T20", # flake8-print
|
||||||
"TID251", # Banned imports
|
"TID251", # Banned imports
|
||||||
"TRY004", # Prefer TypeError exception for invalid type
|
"TRY004", # Prefer TypeError exception for invalid type
|
||||||
"TRY200", # Use raise from to specify exception cause
|
|
||||||
"TRY302", # Remove exception handler; error is immediately re-raised
|
"TRY302", # Remove exception handler; error is immediately re-raised
|
||||||
"UP", # pyupgrade
|
"UP", # pyupgrade
|
||||||
"W", # pycodestyle
|
"W", # pycodestyle
|
||||||
]
|
]
|
||||||
|
|
||||||
ignore = [
|
lint.ignore = [
|
||||||
"D202", # No blank lines allowed after function docstring
|
"D202", # No blank lines allowed after function docstring
|
||||||
"D203", # 1 blank line required before class docstring
|
"D203", # 1 blank line required before class docstring
|
||||||
"D213", # Multi-line docstring summary should start at the second line
|
"D213", # Multi-line docstring summary should start at the second line
|
||||||
@@ -339,16 +341,16 @@ ignore = [
|
|||||||
"PLE0605",
|
"PLE0605",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff.flake8-import-conventions.extend-aliases]
|
[tool.ruff.lint.flake8-import-conventions.extend-aliases]
|
||||||
voluptuous = "vol"
|
voluptuous = "vol"
|
||||||
|
|
||||||
[tool.ruff.flake8-pytest-style]
|
[tool.ruff.lint.flake8-pytest-style]
|
||||||
fixture-parentheses = false
|
fixture-parentheses = false
|
||||||
|
|
||||||
[tool.ruff.flake8-tidy-imports.banned-api]
|
[tool.ruff.lint.flake8-tidy-imports.banned-api]
|
||||||
"pytz".msg = "use zoneinfo instead"
|
"pytz".msg = "use zoneinfo instead"
|
||||||
|
|
||||||
[tool.ruff.isort]
|
[tool.ruff.lint.isort]
|
||||||
force-sort-within-sections = true
|
force-sort-within-sections = true
|
||||||
section-order = [
|
section-order = [
|
||||||
"future",
|
"future",
|
||||||
@@ -362,10 +364,10 @@ known-first-party = ["supervisor", "tests"]
|
|||||||
combine-as-imports = true
|
combine-as-imports = true
|
||||||
split-on-trailing-comma = false
|
split-on-trailing-comma = false
|
||||||
|
|
||||||
[tool.ruff.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
|
||||||
# DBus Service Mocks must use typing and names understood by dbus-fast
|
# DBus Service Mocks must use typing and names understood by dbus-fast
|
||||||
"tests/dbus_service_mocks/*.py" = ["F722", "F821", "N815"]
|
"tests/dbus_service_mocks/*.py" = ["F722", "F821", "N815"]
|
||||||
|
|
||||||
[tool.ruff.mccabe]
|
[tool.ruff.lint.mccabe]
|
||||||
max-complexity = 25
|
max-complexity = 25
|
||||||
|
@@ -1,29 +1,29 @@
|
|||||||
aiodns==3.1.1
|
aiodns==3.2.0
|
||||||
aiohttp==3.9.3
|
aiohttp==3.10.10
|
||||||
aiohttp-fast-url-dispatcher==0.3.0
|
|
||||||
atomicwrites-homeassistant==1.4.1
|
atomicwrites-homeassistant==1.4.1
|
||||||
attrs==23.2.0
|
attrs==24.2.0
|
||||||
awesomeversion==24.2.0
|
awesomeversion==24.6.0
|
||||||
brotli==1.1.0
|
brotli==1.1.0
|
||||||
ciso8601==2.3.1
|
ciso8601==2.3.1
|
||||||
colorlog==6.8.2
|
colorlog==6.8.2
|
||||||
cpe==1.2.1
|
cpe==1.3.1
|
||||||
cryptography==42.0.5
|
cryptography==43.0.1
|
||||||
debugpy==1.8.1
|
debugpy==1.8.7
|
||||||
deepmerge==1.1.1
|
deepmerge==2.0
|
||||||
dirhash==0.2.1
|
dirhash==0.5.0
|
||||||
docker==7.0.0
|
docker==7.1.0
|
||||||
faust-cchardet==2.1.19
|
faust-cchardet==2.1.19
|
||||||
gitpython==3.1.42
|
gitpython==3.1.43
|
||||||
jinja2==3.1.3
|
jinja2==3.1.4
|
||||||
orjson==3.9.15
|
orjson==3.10.7
|
||||||
pulsectl==23.5.2
|
pulsectl==24.8.0
|
||||||
pyudev==0.24.1
|
pyudev==0.24.3
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.2
|
||||||
|
requests==2.32.3
|
||||||
securetar==2024.2.1
|
securetar==2024.2.1
|
||||||
sentry-sdk==1.43.0
|
sentry-sdk==2.16.0
|
||||||
setuptools==69.2.0
|
setuptools==75.1.0
|
||||||
voluptuous==0.14.2
|
voluptuous==0.15.2
|
||||||
dbus-fast==2.21.1
|
dbus-fast==2.24.3
|
||||||
typing_extensions==4.10.0
|
typing_extensions==4.12.2
|
||||||
zlib-fast==0.2.0
|
zlib-fast==0.2.0
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
coverage==7.4.4
|
coverage==7.6.3
|
||||||
pre-commit==3.6.2
|
pre-commit==4.0.1
|
||||||
pylint==3.1.0
|
pylint==3.3.1
|
||||||
pytest-aiohttp==1.0.5
|
pytest-aiohttp==1.0.5
|
||||||
pytest-asyncio==0.23.5
|
pytest-asyncio==0.23.6
|
||||||
pytest-cov==4.1.0
|
pytest-cov==5.0.0
|
||||||
pytest-timeout==2.3.1
|
pytest-timeout==2.3.1
|
||||||
pytest==8.1.1
|
pytest==8.3.3
|
||||||
ruff==0.3.3
|
ruff==0.6.9
|
||||||
time-machine==2.14.0
|
time-machine==2.16.0
|
||||||
typing_extensions==4.10.0
|
typing_extensions==4.12.2
|
||||||
urllib3==2.2.1
|
urllib3==2.2.3
|
||||||
|
1
setup.py
1
setup.py
@@ -1,4 +1,5 @@
|
|||||||
"""Home Assistant Supervisor setup."""
|
"""Home Assistant Supervisor setup."""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Main file for Supervisor."""
|
"""Main file for Supervisor."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
import logging
|
import logging
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor add-ons."""
|
"""Init file for Supervisor add-ons."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
@@ -46,6 +47,8 @@ from ..const import (
|
|||||||
ATTR_SLUG,
|
ATTR_SLUG,
|
||||||
ATTR_STATE,
|
ATTR_STATE,
|
||||||
ATTR_SYSTEM,
|
ATTR_SYSTEM,
|
||||||
|
ATTR_SYSTEM_MANAGED,
|
||||||
|
ATTR_SYSTEM_MANAGED_CONFIG_ENTRY,
|
||||||
ATTR_TYPE,
|
ATTR_TYPE,
|
||||||
ATTR_USER,
|
ATTR_USER,
|
||||||
ATTR_UUID,
|
ATTR_UUID,
|
||||||
@@ -54,6 +57,7 @@ from ..const import (
|
|||||||
ATTR_WATCHDOG,
|
ATTR_WATCHDOG,
|
||||||
DNS_SUFFIX,
|
DNS_SUFFIX,
|
||||||
AddonBoot,
|
AddonBoot,
|
||||||
|
AddonBootConfig,
|
||||||
AddonStartup,
|
AddonStartup,
|
||||||
AddonState,
|
AddonState,
|
||||||
BusEvent,
|
BusEvent,
|
||||||
@@ -195,9 +199,20 @@ class Addon(AddonModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
await self._check_ingress_port()
|
await self._check_ingress_port()
|
||||||
with suppress(DockerError):
|
default_image = self._image(self.data)
|
||||||
|
try:
|
||||||
await self.instance.attach(version=self.version)
|
await self.instance.attach(version=self.version)
|
||||||
|
|
||||||
|
# Ensure we are using correct image for this system
|
||||||
|
await self.instance.check_image(self.version, default_image, self.arch)
|
||||||
|
except DockerError:
|
||||||
|
_LOGGER.info("No %s addon Docker image %s found", self.slug, self.image)
|
||||||
|
with suppress(DockerError):
|
||||||
|
await self.instance.install(self.version, default_image, arch=self.arch)
|
||||||
|
|
||||||
|
self.persist[ATTR_IMAGE] = default_image
|
||||||
|
self.save_persist()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ip_address(self) -> IPv4Address:
|
def ip_address(self) -> IPv4Address:
|
||||||
"""Return IP of add-on instance."""
|
"""Return IP of add-on instance."""
|
||||||
@@ -297,7 +312,9 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def boot(self) -> AddonBoot:
|
def boot(self) -> AddonBoot:
|
||||||
"""Return boot config with prio local settings."""
|
"""Return boot config with prio local settings unless config is forced."""
|
||||||
|
if self.boot_config == AddonBootConfig.MANUAL_ONLY:
|
||||||
|
return super().boot
|
||||||
return self.persist.get(ATTR_BOOT, super().boot)
|
return self.persist.get(ATTR_BOOT, super().boot)
|
||||||
|
|
||||||
@boot.setter
|
@boot.setter
|
||||||
@@ -352,6 +369,37 @@ class Addon(AddonModel):
|
|||||||
else:
|
else:
|
||||||
self.persist[ATTR_WATCHDOG] = value
|
self.persist[ATTR_WATCHDOG] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def system_managed(self) -> bool:
|
||||||
|
"""Return True if addon is managed by Home Assistant."""
|
||||||
|
return self.persist[ATTR_SYSTEM_MANAGED]
|
||||||
|
|
||||||
|
@system_managed.setter
|
||||||
|
def system_managed(self, value: bool) -> None:
|
||||||
|
"""Set system managed enable/disable."""
|
||||||
|
if not value and self.system_managed_config_entry:
|
||||||
|
self.system_managed_config_entry = None
|
||||||
|
|
||||||
|
self.persist[ATTR_SYSTEM_MANAGED] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def system_managed_config_entry(self) -> str | None:
|
||||||
|
"""Return id of config entry managing this addon (if any)."""
|
||||||
|
if not self.system_managed:
|
||||||
|
return None
|
||||||
|
return self.persist.get(ATTR_SYSTEM_MANAGED_CONFIG_ENTRY)
|
||||||
|
|
||||||
|
@system_managed_config_entry.setter
|
||||||
|
def system_managed_config_entry(self, value: str | None) -> None:
|
||||||
|
"""Set ID of config entry managing this addon."""
|
||||||
|
if not self.system_managed:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Ignoring system managed config entry for %s because it is not system managed",
|
||||||
|
self.slug,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.persist[ATTR_SYSTEM_MANAGED_CONFIG_ENTRY] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uuid(self) -> str:
|
def uuid(self) -> str:
|
||||||
"""Return an API token for this add-on."""
|
"""Return an API token for this add-on."""
|
||||||
@@ -718,10 +766,12 @@ class Addon(AddonModel):
|
|||||||
limit=JobExecutionLimit.GROUP_ONCE,
|
limit=JobExecutionLimit.GROUP_ONCE,
|
||||||
on_condition=AddonsJobError,
|
on_condition=AddonsJobError,
|
||||||
)
|
)
|
||||||
async def uninstall(self, *, remove_config: bool) -> None:
|
async def uninstall(
|
||||||
|
self, *, remove_config: bool, remove_image: bool = True
|
||||||
|
) -> None:
|
||||||
"""Uninstall and cleanup this addon."""
|
"""Uninstall and cleanup this addon."""
|
||||||
try:
|
try:
|
||||||
await self.instance.remove()
|
await self.instance.remove(remove_image=remove_image)
|
||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
raise AddonsError() from err
|
raise AddonsError() from err
|
||||||
|
|
||||||
@@ -1332,11 +1382,11 @@ class Addon(AddonModel):
|
|||||||
)
|
)
|
||||||
raise AddonsError() from err
|
raise AddonsError() from err
|
||||||
|
|
||||||
|
finally:
|
||||||
# Is add-on loaded
|
# Is add-on loaded
|
||||||
if not self.loaded:
|
if not self.loaded:
|
||||||
await self.load()
|
await self.load()
|
||||||
|
|
||||||
finally:
|
|
||||||
# Run add-on
|
# Run add-on
|
||||||
if data[ATTR_STATE] == AddonState.STARTED:
|
if data[ATTR_STATE] == AddonState.STARTED:
|
||||||
wait_for_start = await self.start()
|
wait_for_start = await self.start()
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Supervisor add-on build environment."""
|
"""Supervisor add-on build environment."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
@@ -102,11 +103,11 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
|
|||||||
except HassioArchNotFound:
|
except HassioArchNotFound:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_docker_args(self, version: AwesomeVersion):
|
def get_docker_args(self, version: AwesomeVersion, image: str | None = None):
|
||||||
"""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"{image or self.addon.image}:{version!s}",
|
||||||
"dockerfile": str(self.dockerfile),
|
"dockerfile": str(self.dockerfile),
|
||||||
"pull": True,
|
"pull": True,
|
||||||
"forcerm": not self.sys_dev,
|
"forcerm": not self.sys_dev,
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Add-on static data."""
|
"""Add-on static data."""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor add-on data."""
|
"""Init file for Supervisor add-on data."""
|
||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Supervisor add-on manager."""
|
"""Supervisor add-on manager."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
@@ -184,7 +185,15 @@ class AddonManager(CoreSysAttributes):
|
|||||||
_LOGGER.warning("Add-on %s is not installed", slug)
|
_LOGGER.warning("Add-on %s is not installed", slug)
|
||||||
return
|
return
|
||||||
|
|
||||||
await self.local[slug].uninstall(remove_config=remove_config)
|
shared_image = any(
|
||||||
|
self.local[slug].image == addon.image
|
||||||
|
and self.local[slug].version == addon.version
|
||||||
|
for addon in self.installed
|
||||||
|
if addon.slug != slug
|
||||||
|
)
|
||||||
|
await self.local[slug].uninstall(
|
||||||
|
remove_config=remove_config, remove_image=not shared_image
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER.info("Add-on '%s' successfully removed", slug)
|
_LOGGER.info("Add-on '%s' successfully removed", slug)
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor add-ons."""
|
"""Init file for Supervisor add-ons."""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
@@ -82,6 +83,7 @@ from ..const import (
|
|||||||
SECURITY_DISABLE,
|
SECURITY_DISABLE,
|
||||||
SECURITY_PROFILE,
|
SECURITY_PROFILE,
|
||||||
AddonBoot,
|
AddonBoot,
|
||||||
|
AddonBootConfig,
|
||||||
AddonStage,
|
AddonStage,
|
||||||
AddonStartup,
|
AddonStartup,
|
||||||
)
|
)
|
||||||
@@ -149,10 +151,15 @@ class AddonModel(JobGroup, ABC):
|
|||||||
return self.data[ATTR_OPTIONS]
|
return self.data[ATTR_OPTIONS]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def boot(self) -> AddonBoot:
|
def boot_config(self) -> AddonBootConfig:
|
||||||
"""Return boot config with prio local settings."""
|
"""Return boot config."""
|
||||||
return self.data[ATTR_BOOT]
|
return self.data[ATTR_BOOT]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def boot(self) -> AddonBoot:
|
||||||
|
"""Return boot config with prio local settings unless config is forced."""
|
||||||
|
return AddonBoot(self.data[ATTR_BOOT])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auto_update(self) -> bool | None:
|
def auto_update(self) -> bool | None:
|
||||||
"""Return if auto update is enable."""
|
"""Return if auto update is enable."""
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Add-on Options / UI rendering."""
|
"""Add-on Options / UI rendering."""
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Util add-ons functions."""
|
"""Util add-ons functions."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Validate add-ons options schema."""
|
"""Validate add-ons options schema."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
@@ -78,6 +79,8 @@ from ..const import (
|
|||||||
ATTR_STATE,
|
ATTR_STATE,
|
||||||
ATTR_STDIN,
|
ATTR_STDIN,
|
||||||
ATTR_SYSTEM,
|
ATTR_SYSTEM,
|
||||||
|
ATTR_SYSTEM_MANAGED,
|
||||||
|
ATTR_SYSTEM_MANAGED_CONFIG_ENTRY,
|
||||||
ATTR_TIMEOUT,
|
ATTR_TIMEOUT,
|
||||||
ATTR_TMPFS,
|
ATTR_TMPFS,
|
||||||
ATTR_TRANSLATIONS,
|
ATTR_TRANSLATIONS,
|
||||||
@@ -95,6 +98,7 @@ from ..const import (
|
|||||||
ROLE_ALL,
|
ROLE_ALL,
|
||||||
ROLE_DEFAULT,
|
ROLE_DEFAULT,
|
||||||
AddonBoot,
|
AddonBoot,
|
||||||
|
AddonBootConfig,
|
||||||
AddonStage,
|
AddonStage,
|
||||||
AddonStartup,
|
AddonStartup,
|
||||||
AddonState,
|
AddonState,
|
||||||
@@ -318,7 +322,9 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
vol.Optional(ATTR_STARTUP, default=AddonStartup.APPLICATION): vol.Coerce(
|
vol.Optional(ATTR_STARTUP, default=AddonStartup.APPLICATION): vol.Coerce(
|
||||||
AddonStartup
|
AddonStartup
|
||||||
),
|
),
|
||||||
vol.Optional(ATTR_BOOT, default=AddonBoot.AUTO): vol.Coerce(AddonBoot),
|
vol.Optional(ATTR_BOOT, default=AddonBootConfig.AUTO): vol.Coerce(
|
||||||
|
AddonBootConfig
|
||||||
|
),
|
||||||
vol.Optional(ATTR_INIT, default=True): vol.Boolean(),
|
vol.Optional(ATTR_INIT, default=True): vol.Boolean(),
|
||||||
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),
|
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_STAGE, default=AddonStage.STABLE): vol.Coerce(AddonStage),
|
vol.Optional(ATTR_STAGE, default=AddonStage.STABLE): vol.Coerce(AddonStage),
|
||||||
@@ -467,6 +473,8 @@ SCHEMA_ADDON_USER = vol.Schema(
|
|||||||
vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(),
|
vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(),
|
||||||
vol.Optional(ATTR_INGRESS_PANEL, default=False): vol.Boolean(),
|
vol.Optional(ATTR_INGRESS_PANEL, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_WATCHDOG, default=False): vol.Boolean(),
|
vol.Optional(ATTR_WATCHDOG, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_SYSTEM_MANAGED, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_SYSTEM_MANAGED_CONFIG_ENTRY, default=None): vol.Maybe(str),
|
||||||
},
|
},
|
||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
)
|
)
|
||||||
|
@@ -1,20 +1,22 @@
|
|||||||
"""Init file for Supervisor RESTful API."""
|
"""Init file for Supervisor RESTful API."""
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp_fast_url_dispatcher import FastUrlDispatcher, attach_fast_url_dispatcher
|
|
||||||
|
|
||||||
from ..const import AddonState
|
from ..const import AddonState
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..exceptions import APIAddonNotInstalled
|
from ..exceptions import APIAddonNotInstalled, HostNotSupportedError
|
||||||
|
from ..utils.sentry import capture_exception
|
||||||
from .addons import APIAddons
|
from .addons import APIAddons
|
||||||
from .audio import APIAudio
|
from .audio import APIAudio
|
||||||
from .auth import APIAuth
|
from .auth import APIAuth
|
||||||
from .backups import APIBackups
|
from .backups import APIBackups
|
||||||
from .cli import APICli
|
from .cli import APICli
|
||||||
|
from .const import CONTENT_TYPE_TEXT
|
||||||
from .discovery import APIDiscovery
|
from .discovery import APIDiscovery
|
||||||
from .dns import APICoreDNS
|
from .dns import APICoreDNS
|
||||||
from .docker import APIDocker
|
from .docker import APIDocker
|
||||||
@@ -36,7 +38,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
|
from .utils import api_process, api_process_raw
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -65,14 +67,19 @@ class RestAPI(CoreSysAttributes):
|
|||||||
"max_field_size": MAX_LINE_SIZE,
|
"max_field_size": MAX_LINE_SIZE,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
attach_fast_url_dispatcher(self.webapp, FastUrlDispatcher())
|
|
||||||
|
|
||||||
# service stuff
|
# service stuff
|
||||||
self._runner: web.AppRunner = web.AppRunner(self.webapp, shutdown_timeout=5)
|
self._runner: web.AppRunner = web.AppRunner(self.webapp, shutdown_timeout=5)
|
||||||
self._site: web.TCPSite | None = None
|
self._site: web.TCPSite | None = None
|
||||||
|
|
||||||
|
# share single host API handler for reuse in logging endpoints
|
||||||
|
self._api_host: APIHost | None = None
|
||||||
|
|
||||||
async def load(self) -> None:
|
async def load(self) -> None:
|
||||||
"""Register REST API Calls."""
|
"""Register REST API Calls."""
|
||||||
|
self._api_host = APIHost()
|
||||||
|
self._api_host.coresys = self.coresys
|
||||||
|
|
||||||
self._register_addons()
|
self._register_addons()
|
||||||
self._register_audio()
|
self._register_audio()
|
||||||
self._register_auth()
|
self._register_auth()
|
||||||
@@ -102,10 +109,41 @@ class RestAPI(CoreSysAttributes):
|
|||||||
|
|
||||||
await self.start()
|
await self.start()
|
||||||
|
|
||||||
|
def _register_advanced_logs(self, path: str, syslog_identifier: str):
|
||||||
|
"""Register logs endpoint for a given path, returning logs for single syslog identifier."""
|
||||||
|
|
||||||
|
self.webapp.add_routes(
|
||||||
|
[
|
||||||
|
web.get(
|
||||||
|
f"{path}/logs",
|
||||||
|
partial(self._api_host.advanced_logs, identifier=syslog_identifier),
|
||||||
|
),
|
||||||
|
web.get(
|
||||||
|
f"{path}/logs/follow",
|
||||||
|
partial(
|
||||||
|
self._api_host.advanced_logs,
|
||||||
|
identifier=syslog_identifier,
|
||||||
|
follow=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
web.get(
|
||||||
|
f"{path}/logs/boots/{{bootid}}",
|
||||||
|
partial(self._api_host.advanced_logs, identifier=syslog_identifier),
|
||||||
|
),
|
||||||
|
web.get(
|
||||||
|
f"{path}/logs/boots/{{bootid}}/follow",
|
||||||
|
partial(
|
||||||
|
self._api_host.advanced_logs,
|
||||||
|
identifier=syslog_identifier,
|
||||||
|
follow=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def _register_host(self) -> None:
|
def _register_host(self) -> None:
|
||||||
"""Register hostcontrol functions."""
|
"""Register hostcontrol functions."""
|
||||||
api_host = APIHost()
|
api_host = self._api_host
|
||||||
api_host.coresys = self.coresys
|
|
||||||
|
|
||||||
self.webapp.add_routes(
|
self.webapp.add_routes(
|
||||||
[
|
[
|
||||||
@@ -261,11 +299,11 @@ class RestAPI(CoreSysAttributes):
|
|||||||
[
|
[
|
||||||
web.get("/multicast/info", api_multicast.info),
|
web.get("/multicast/info", api_multicast.info),
|
||||||
web.get("/multicast/stats", api_multicast.stats),
|
web.get("/multicast/stats", api_multicast.stats),
|
||||||
web.get("/multicast/logs", api_multicast.logs),
|
|
||||||
web.post("/multicast/update", api_multicast.update),
|
web.post("/multicast/update", api_multicast.update),
|
||||||
web.post("/multicast/restart", api_multicast.restart),
|
web.post("/multicast/restart", api_multicast.restart),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
self._register_advanced_logs("/multicast", "hassio_multicast")
|
||||||
|
|
||||||
def _register_hardware(self) -> None:
|
def _register_hardware(self) -> None:
|
||||||
"""Register hardware functions."""
|
"""Register hardware functions."""
|
||||||
@@ -352,7 +390,6 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.get("/supervisor/ping", api_supervisor.ping),
|
web.get("/supervisor/ping", api_supervisor.ping),
|
||||||
web.get("/supervisor/info", api_supervisor.info),
|
web.get("/supervisor/info", api_supervisor.info),
|
||||||
web.get("/supervisor/stats", api_supervisor.stats),
|
web.get("/supervisor/stats", api_supervisor.stats),
|
||||||
web.get("/supervisor/logs", api_supervisor.logs),
|
|
||||||
web.post("/supervisor/update", api_supervisor.update),
|
web.post("/supervisor/update", api_supervisor.update),
|
||||||
web.post("/supervisor/reload", api_supervisor.reload),
|
web.post("/supervisor/reload", api_supervisor.reload),
|
||||||
web.post("/supervisor/restart", api_supervisor.restart),
|
web.post("/supervisor/restart", api_supervisor.restart),
|
||||||
@@ -361,6 +398,38 @@ class RestAPI(CoreSysAttributes):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def get_supervisor_logs(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return await self._api_host.advanced_logs_handler(
|
||||||
|
*args, identifier="hassio_supervisor", **kwargs
|
||||||
|
)
|
||||||
|
except Exception as err: # pylint: disable=broad-exception-caught
|
||||||
|
# Supervisor logs are critical, so catch everything, log the exception
|
||||||
|
# and try to return Docker container logs as the fallback
|
||||||
|
_LOGGER.exception(
|
||||||
|
"Failed to get supervisor logs using advanced_logs API"
|
||||||
|
)
|
||||||
|
if not isinstance(err, HostNotSupportedError):
|
||||||
|
# No need to capture HostNotSupportedError to Sentry, the cause
|
||||||
|
# is known and reported to the user using the resolution center.
|
||||||
|
capture_exception(err)
|
||||||
|
return await api_supervisor.logs(*args, **kwargs)
|
||||||
|
|
||||||
|
self.webapp.add_routes(
|
||||||
|
[
|
||||||
|
web.get("/supervisor/logs", get_supervisor_logs),
|
||||||
|
web.get(
|
||||||
|
"/supervisor/logs/follow",
|
||||||
|
partial(get_supervisor_logs, follow=True),
|
||||||
|
),
|
||||||
|
web.get("/supervisor/logs/boots/{bootid}", get_supervisor_logs),
|
||||||
|
web.get(
|
||||||
|
"/supervisor/logs/boots/{bootid}/follow",
|
||||||
|
partial(get_supervisor_logs, follow=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def _register_homeassistant(self) -> None:
|
def _register_homeassistant(self) -> None:
|
||||||
"""Register Home Assistant functions."""
|
"""Register Home Assistant functions."""
|
||||||
api_hass = APIHomeAssistant()
|
api_hass = APIHomeAssistant()
|
||||||
@@ -369,7 +438,6 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self.webapp.add_routes(
|
self.webapp.add_routes(
|
||||||
[
|
[
|
||||||
web.get("/core/info", api_hass.info),
|
web.get("/core/info", api_hass.info),
|
||||||
web.get("/core/logs", api_hass.logs),
|
|
||||||
web.get("/core/stats", api_hass.stats),
|
web.get("/core/stats", api_hass.stats),
|
||||||
web.post("/core/options", api_hass.options),
|
web.post("/core/options", api_hass.options),
|
||||||
web.post("/core/update", api_hass.update),
|
web.post("/core/update", api_hass.update),
|
||||||
@@ -381,11 +449,12 @@ class RestAPI(CoreSysAttributes):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._register_advanced_logs("/core", "homeassistant")
|
||||||
|
|
||||||
# Reroute from legacy
|
# Reroute from legacy
|
||||||
self.webapp.add_routes(
|
self.webapp.add_routes(
|
||||||
[
|
[
|
||||||
web.get("/homeassistant/info", api_hass.info),
|
web.get("/homeassistant/info", api_hass.info),
|
||||||
web.get("/homeassistant/logs", api_hass.logs),
|
|
||||||
web.get("/homeassistant/stats", api_hass.stats),
|
web.get("/homeassistant/stats", api_hass.stats),
|
||||||
web.post("/homeassistant/options", api_hass.options),
|
web.post("/homeassistant/options", api_hass.options),
|
||||||
web.post("/homeassistant/restart", api_hass.restart),
|
web.post("/homeassistant/restart", api_hass.restart),
|
||||||
@@ -397,6 +466,8 @@ class RestAPI(CoreSysAttributes):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._register_advanced_logs("/homeassistant", "homeassistant")
|
||||||
|
|
||||||
def _register_proxy(self) -> None:
|
def _register_proxy(self) -> None:
|
||||||
"""Register Home Assistant API Proxy."""
|
"""Register Home Assistant API Proxy."""
|
||||||
api_proxy = APIProxy()
|
api_proxy = APIProxy()
|
||||||
@@ -438,18 +509,39 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.post("/addons/{addon}/stop", api_addons.stop),
|
web.post("/addons/{addon}/stop", api_addons.stop),
|
||||||
web.post("/addons/{addon}/restart", api_addons.restart),
|
web.post("/addons/{addon}/restart", api_addons.restart),
|
||||||
web.post("/addons/{addon}/options", api_addons.options),
|
web.post("/addons/{addon}/options", api_addons.options),
|
||||||
|
web.post("/addons/{addon}/sys_options", api_addons.sys_options),
|
||||||
web.post(
|
web.post(
|
||||||
"/addons/{addon}/options/validate", api_addons.options_validate
|
"/addons/{addon}/options/validate", api_addons.options_validate
|
||||||
),
|
),
|
||||||
web.get("/addons/{addon}/options/config", api_addons.options_config),
|
web.get("/addons/{addon}/options/config", api_addons.options_config),
|
||||||
web.post("/addons/{addon}/rebuild", api_addons.rebuild),
|
web.post("/addons/{addon}/rebuild", api_addons.rebuild),
|
||||||
web.get("/addons/{addon}/logs", api_addons.logs),
|
|
||||||
web.post("/addons/{addon}/stdin", api_addons.stdin),
|
web.post("/addons/{addon}/stdin", api_addons.stdin),
|
||||||
web.post("/addons/{addon}/security", api_addons.security),
|
web.post("/addons/{addon}/security", api_addons.security),
|
||||||
web.get("/addons/{addon}/stats", api_addons.stats),
|
web.get("/addons/{addon}/stats", api_addons.stats),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@api_process_raw(CONTENT_TYPE_TEXT, error_type=CONTENT_TYPE_TEXT)
|
||||||
|
async def get_addon_logs(request, *args, **kwargs):
|
||||||
|
addon = api_addons.get_addon_for_request(request)
|
||||||
|
kwargs["identifier"] = f"addon_{addon.slug}"
|
||||||
|
return await self._api_host.advanced_logs(request, *args, **kwargs)
|
||||||
|
|
||||||
|
self.webapp.add_routes(
|
||||||
|
[
|
||||||
|
web.get("/addons/{addon}/logs", get_addon_logs),
|
||||||
|
web.get(
|
||||||
|
"/addons/{addon}/logs/follow",
|
||||||
|
partial(get_addon_logs, follow=True),
|
||||||
|
),
|
||||||
|
web.get("/addons/{addon}/logs/boots/{bootid}", get_addon_logs),
|
||||||
|
web.get(
|
||||||
|
"/addons/{addon}/logs/boots/{bootid}/follow",
|
||||||
|
partial(get_addon_logs, follow=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Legacy routing to support requests for not installed addons
|
# Legacy routing to support requests for not installed addons
|
||||||
api_store = APIStore()
|
api_store = APIStore()
|
||||||
api_store.coresys = self.coresys
|
api_store.coresys = self.coresys
|
||||||
@@ -547,7 +639,6 @@ class RestAPI(CoreSysAttributes):
|
|||||||
[
|
[
|
||||||
web.get("/dns/info", api_dns.info),
|
web.get("/dns/info", api_dns.info),
|
||||||
web.get("/dns/stats", api_dns.stats),
|
web.get("/dns/stats", api_dns.stats),
|
||||||
web.get("/dns/logs", api_dns.logs),
|
|
||||||
web.post("/dns/update", api_dns.update),
|
web.post("/dns/update", api_dns.update),
|
||||||
web.post("/dns/options", api_dns.options),
|
web.post("/dns/options", api_dns.options),
|
||||||
web.post("/dns/restart", api_dns.restart),
|
web.post("/dns/restart", api_dns.restart),
|
||||||
@@ -555,18 +646,17 @@ class RestAPI(CoreSysAttributes):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._register_advanced_logs("/dns", "hassio_dns")
|
||||||
|
|
||||||
def _register_audio(self) -> None:
|
def _register_audio(self) -> None:
|
||||||
"""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(
|
||||||
[
|
[
|
||||||
web.get("/audio/info", api_audio.info),
|
web.get("/audio/info", api_audio.info),
|
||||||
web.get("/audio/stats", api_audio.stats),
|
web.get("/audio/stats", api_audio.stats),
|
||||||
web.get("/audio/logs", api_audio.logs),
|
|
||||||
web.post("/audio/update", api_audio.update),
|
web.post("/audio/update", api_audio.update),
|
||||||
web.post("/audio/restart", api_audio.restart),
|
web.post("/audio/restart", api_audio.restart),
|
||||||
web.post("/audio/reload", api_audio.reload),
|
web.post("/audio/reload", api_audio.reload),
|
||||||
@@ -579,6 +669,8 @@ class RestAPI(CoreSysAttributes):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._register_advanced_logs("/audio", "hassio_audio")
|
||||||
|
|
||||||
def _register_mounts(self) -> None:
|
def _register_mounts(self) -> None:
|
||||||
"""Register mounts endpoints."""
|
"""Register mounts endpoints."""
|
||||||
api_mounts = APIMounts()
|
api_mounts = APIMounts()
|
||||||
@@ -605,7 +697,6 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.get("/store", api_store.store_info),
|
web.get("/store", api_store.store_info),
|
||||||
web.get("/store/addons", api_store.addons_list),
|
web.get("/store/addons", api_store.addons_list),
|
||||||
web.get("/store/addons/{addon}", api_store.addons_addon_info),
|
web.get("/store/addons/{addon}", api_store.addons_addon_info),
|
||||||
web.get("/store/addons/{addon}/{version}", api_store.addons_addon_info),
|
|
||||||
web.get("/store/addons/{addon}/icon", api_store.addons_addon_icon),
|
web.get("/store/addons/{addon}/icon", api_store.addons_addon_icon),
|
||||||
web.get("/store/addons/{addon}/logo", api_store.addons_addon_logo),
|
web.get("/store/addons/{addon}/logo", api_store.addons_addon_logo),
|
||||||
web.get(
|
web.get(
|
||||||
@@ -627,6 +718,8 @@ class RestAPI(CoreSysAttributes):
|
|||||||
"/store/addons/{addon}/update/{version}",
|
"/store/addons/{addon}/update/{version}",
|
||||||
api_store.addons_addon_update,
|
api_store.addons_addon_update,
|
||||||
),
|
),
|
||||||
|
# Must be below others since it has a wildcard in resource path
|
||||||
|
web.get("/store/addons/{addon}/{version}", api_store.addons_addon_info),
|
||||||
web.post("/store/reload", api_store.reload),
|
web.post("/store/reload", api_store.reload),
|
||||||
web.get("/store/repositories", api_store.repositories_list),
|
web.get("/store/repositories", api_store.repositories_list),
|
||||||
web.get(
|
web.get(
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""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
|
from collections.abc import Awaitable
|
||||||
import logging
|
import logging
|
||||||
@@ -81,6 +82,8 @@ from ..const import (
|
|||||||
ATTR_STARTUP,
|
ATTR_STARTUP,
|
||||||
ATTR_STATE,
|
ATTR_STATE,
|
||||||
ATTR_STDIN,
|
ATTR_STDIN,
|
||||||
|
ATTR_SYSTEM_MANAGED,
|
||||||
|
ATTR_SYSTEM_MANAGED_CONFIG_ENTRY,
|
||||||
ATTR_TRANSLATIONS,
|
ATTR_TRANSLATIONS,
|
||||||
ATTR_UART,
|
ATTR_UART,
|
||||||
ATTR_UDEV,
|
ATTR_UDEV,
|
||||||
@@ -95,6 +98,7 @@ from ..const import (
|
|||||||
ATTR_WEBUI,
|
ATTR_WEBUI,
|
||||||
REQUEST_FROM,
|
REQUEST_FROM,
|
||||||
AddonBoot,
|
AddonBoot,
|
||||||
|
AddonBootConfig,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..docker.stats import DockerStats
|
from ..docker.stats import DockerStats
|
||||||
@@ -106,8 +110,8 @@ from ..exceptions import (
|
|||||||
PwnedSecret,
|
PwnedSecret,
|
||||||
)
|
)
|
||||||
from ..validate import docker_ports
|
from ..validate import docker_ports
|
||||||
from .const import ATTR_REMOVE_CONFIG, ATTR_SIGNED, CONTENT_TYPE_BINARY
|
from .const import ATTR_BOOT_CONFIG, ATTR_REMOVE_CONFIG, ATTR_SIGNED
|
||||||
from .utils import api_process, api_process_raw, api_validate, json_loads
|
from .utils import api_process, api_validate, json_loads
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -126,6 +130,13 @@ SCHEMA_OPTIONS = vol.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SCHEMA_SYS_OPTIONS = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_SYSTEM_MANAGED): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_SYSTEM_MANAGED_CONFIG_ENTRY): vol.Maybe(str),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
SCHEMA_SECURITY = vol.Schema({vol.Optional(ATTR_PROTECTED): vol.Boolean()})
|
SCHEMA_SECURITY = vol.Schema({vol.Optional(ATTR_PROTECTED): vol.Boolean()})
|
||||||
|
|
||||||
SCHEMA_UNINSTALL = vol.Schema(
|
SCHEMA_UNINSTALL = vol.Schema(
|
||||||
@@ -137,8 +148,8 @@ SCHEMA_UNINSTALL = vol.Schema(
|
|||||||
class APIAddons(CoreSysAttributes):
|
class APIAddons(CoreSysAttributes):
|
||||||
"""Handle RESTful API for add-on functions."""
|
"""Handle RESTful API for add-on functions."""
|
||||||
|
|
||||||
def _extract_addon(self, request: web.Request) -> Addon:
|
def get_addon_for_request(self, request: web.Request) -> Addon:
|
||||||
"""Return addon, throw an exception it it doesn't exist."""
|
"""Return addon, throw an exception if it doesn't exist."""
|
||||||
addon_slug: str = request.match_info.get("addon")
|
addon_slug: str = request.match_info.get("addon")
|
||||||
|
|
||||||
# Lookup itself
|
# Lookup itself
|
||||||
@@ -178,6 +189,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_URL: addon.url,
|
ATTR_URL: addon.url,
|
||||||
ATTR_ICON: addon.with_icon,
|
ATTR_ICON: addon.with_icon,
|
||||||
ATTR_LOGO: addon.with_logo,
|
ATTR_LOGO: addon.with_logo,
|
||||||
|
ATTR_SYSTEM_MANAGED: addon.system_managed,
|
||||||
}
|
}
|
||||||
for addon in self.sys_addons.installed
|
for addon in self.sys_addons.installed
|
||||||
]
|
]
|
||||||
@@ -191,7 +203,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
|
|
||||||
async def info(self, request: web.Request) -> dict[str, Any]:
|
async def info(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Return add-on information."""
|
"""Return add-on information."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon: AnyAddon = self.get_addon_for_request(request)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
ATTR_NAME: addon.name,
|
ATTR_NAME: addon.name,
|
||||||
@@ -206,6 +218,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_VERSION_LATEST: addon.latest_version,
|
ATTR_VERSION_LATEST: addon.latest_version,
|
||||||
ATTR_PROTECTED: addon.protected,
|
ATTR_PROTECTED: addon.protected,
|
||||||
ATTR_RATING: rating_security(addon),
|
ATTR_RATING: rating_security(addon),
|
||||||
|
ATTR_BOOT_CONFIG: addon.boot_config,
|
||||||
ATTR_BOOT: addon.boot,
|
ATTR_BOOT: addon.boot,
|
||||||
ATTR_OPTIONS: addon.options,
|
ATTR_OPTIONS: addon.options,
|
||||||
ATTR_SCHEMA: addon.schema_ui,
|
ATTR_SCHEMA: addon.schema_ui,
|
||||||
@@ -265,6 +278,8 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_WATCHDOG: addon.watchdog,
|
ATTR_WATCHDOG: addon.watchdog,
|
||||||
ATTR_DEVICES: addon.static_devices
|
ATTR_DEVICES: addon.static_devices
|
||||||
+ [device.path for device in addon.devices],
|
+ [device.path for device in addon.devices],
|
||||||
|
ATTR_SYSTEM_MANAGED: addon.system_managed,
|
||||||
|
ATTR_SYSTEM_MANAGED_CONFIG_ENTRY: addon.system_managed_config_entry,
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@@ -272,7 +287,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def options(self, request: web.Request) -> None:
|
async def options(self, request: web.Request) -> None:
|
||||||
"""Store user options for add-on."""
|
"""Store user options for add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self.get_addon_for_request(request)
|
||||||
|
|
||||||
# Update secrets for validation
|
# Update secrets for validation
|
||||||
await self.sys_homeassistant.secrets.reload()
|
await self.sys_homeassistant.secrets.reload()
|
||||||
@@ -287,6 +302,10 @@ class APIAddons(CoreSysAttributes):
|
|||||||
if ATTR_OPTIONS in body:
|
if ATTR_OPTIONS in body:
|
||||||
addon.options = body[ATTR_OPTIONS]
|
addon.options = body[ATTR_OPTIONS]
|
||||||
if ATTR_BOOT in body:
|
if ATTR_BOOT in body:
|
||||||
|
if addon.boot_config == AddonBootConfig.MANUAL_ONLY:
|
||||||
|
raise APIError(
|
||||||
|
f"Addon {addon.slug} boot option is set to {addon.boot_config} so it cannot be changed"
|
||||||
|
)
|
||||||
addon.boot = body[ATTR_BOOT]
|
addon.boot = body[ATTR_BOOT]
|
||||||
if ATTR_AUTO_UPDATE in body:
|
if ATTR_AUTO_UPDATE in body:
|
||||||
addon.auto_update = body[ATTR_AUTO_UPDATE]
|
addon.auto_update = body[ATTR_AUTO_UPDATE]
|
||||||
@@ -304,10 +323,24 @@ class APIAddons(CoreSysAttributes):
|
|||||||
|
|
||||||
addon.save_persist()
|
addon.save_persist()
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def sys_options(self, request: web.Request) -> None:
|
||||||
|
"""Store system options for an add-on."""
|
||||||
|
addon = self.get_addon_for_request(request)
|
||||||
|
|
||||||
|
# Validate/Process Body
|
||||||
|
body = await api_validate(SCHEMA_SYS_OPTIONS, request)
|
||||||
|
if ATTR_SYSTEM_MANAGED in body:
|
||||||
|
addon.system_managed = body[ATTR_SYSTEM_MANAGED]
|
||||||
|
if ATTR_SYSTEM_MANAGED_CONFIG_ENTRY in body:
|
||||||
|
addon.system_managed_config_entry = body[ATTR_SYSTEM_MANAGED_CONFIG_ENTRY]
|
||||||
|
|
||||||
|
addon.save_persist()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def options_validate(self, request: web.Request) -> None:
|
async def options_validate(self, request: web.Request) -> None:
|
||||||
"""Validate user options for add-on."""
|
"""Validate user options for add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self.get_addon_for_request(request)
|
||||||
data = {ATTR_MESSAGE: "", ATTR_VALID: True, ATTR_PWNED: False}
|
data = {ATTR_MESSAGE: "", ATTR_VALID: True, ATTR_PWNED: False}
|
||||||
|
|
||||||
options = await request.json(loads=json_loads) or addon.options
|
options = await request.json(loads=json_loads) or addon.options
|
||||||
@@ -349,7 +382,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
slug: str = request.match_info.get("addon")
|
slug: str = request.match_info.get("addon")
|
||||||
if slug != "self":
|
if slug != "self":
|
||||||
raise APIForbidden("This can be only read by the Add-on itself!")
|
raise APIForbidden("This can be only read by the Add-on itself!")
|
||||||
addon = self._extract_addon(request)
|
addon = self.get_addon_for_request(request)
|
||||||
|
|
||||||
# Lookup/reload secrets
|
# Lookup/reload secrets
|
||||||
await self.sys_homeassistant.secrets.reload()
|
await self.sys_homeassistant.secrets.reload()
|
||||||
@@ -361,7 +394,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def security(self, request: web.Request) -> None:
|
async def security(self, request: web.Request) -> None:
|
||||||
"""Store security options for add-on."""
|
"""Store security options for add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self.get_addon_for_request(request)
|
||||||
body: dict[str, Any] = await api_validate(SCHEMA_SECURITY, request)
|
body: dict[str, Any] = await api_validate(SCHEMA_SECURITY, request)
|
||||||
|
|
||||||
if ATTR_PROTECTED in body:
|
if ATTR_PROTECTED in body:
|
||||||
@@ -373,7 +406,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> dict[str, Any]:
|
async def stats(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
addon = self._extract_addon(request)
|
addon = self.get_addon_for_request(request)
|
||||||
|
|
||||||
stats: DockerStats = await addon.stats()
|
stats: DockerStats = await addon.stats()
|
||||||
|
|
||||||
@@ -391,7 +424,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def uninstall(self, request: web.Request) -> Awaitable[None]:
|
async def uninstall(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Uninstall add-on."""
|
"""Uninstall add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self.get_addon_for_request(request)
|
||||||
body: dict[str, Any] = await api_validate(SCHEMA_UNINSTALL, request)
|
body: dict[str, Any] = await api_validate(SCHEMA_UNINSTALL, request)
|
||||||
return await asyncio.shield(
|
return await asyncio.shield(
|
||||||
self.sys_addons.uninstall(
|
self.sys_addons.uninstall(
|
||||||
@@ -402,40 +435,34 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def start(self, request: web.Request) -> None:
|
async def start(self, request: web.Request) -> None:
|
||||||
"""Start add-on."""
|
"""Start add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self.get_addon_for_request(request)
|
||||||
if start_task := await asyncio.shield(addon.start()):
|
if start_task := await asyncio.shield(addon.start()):
|
||||||
await start_task
|
await start_task
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def stop(self, request: web.Request) -> Awaitable[None]:
|
def stop(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Stop add-on."""
|
"""Stop add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self.get_addon_for_request(request)
|
||||||
return asyncio.shield(addon.stop())
|
return asyncio.shield(addon.stop())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def restart(self, request: web.Request) -> None:
|
async def restart(self, request: web.Request) -> None:
|
||||||
"""Restart add-on."""
|
"""Restart add-on."""
|
||||||
addon: Addon = self._extract_addon(request)
|
addon: Addon = self.get_addon_for_request(request)
|
||||||
if start_task := await asyncio.shield(addon.restart()):
|
if start_task := await asyncio.shield(addon.restart()):
|
||||||
await start_task
|
await start_task
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def rebuild(self, request: web.Request) -> None:
|
async def rebuild(self, request: web.Request) -> None:
|
||||||
"""Rebuild local build add-on."""
|
"""Rebuild local build add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self.get_addon_for_request(request)
|
||||||
if start_task := await asyncio.shield(self.sys_addons.rebuild(addon.slug)):
|
if start_task := await asyncio.shield(self.sys_addons.rebuild(addon.slug)):
|
||||||
await start_task
|
await start_task
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
|
||||||
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
|
||||||
"""Return logs from add-on."""
|
|
||||||
addon = self._extract_addon(request)
|
|
||||||
return addon.logs()
|
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def stdin(self, request: web.Request) -> None:
|
async def stdin(self, request: web.Request) -> None:
|
||||||
"""Write to stdin of add-on."""
|
"""Write to stdin of add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self.get_addon_for_request(request)
|
||||||
if not addon.with_stdin:
|
if not addon.with_stdin:
|
||||||
raise APIError(f"STDIN not supported the {addon.slug} add-on")
|
raise APIError(f"STDIN not supported the {addon.slug} add-on")
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor Audio RESTful API."""
|
"""Init file for Supervisor Audio RESTful API."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
@@ -35,8 +36,7 @@ from ..coresys import CoreSysAttributes
|
|||||||
from ..exceptions import APIError
|
from ..exceptions import APIError
|
||||||
from ..host.sound import StreamType
|
from ..host.sound import StreamType
|
||||||
from ..validate import version_tag
|
from ..validate import version_tag
|
||||||
from .const import CONTENT_TYPE_BINARY
|
from .utils import api_process, api_validate
|
||||||
from .utils import api_process, api_process_raw, api_validate
|
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -111,11 +111,6 @@ class APIAudio(CoreSysAttributes):
|
|||||||
raise APIError(f"Version {version} is already in use")
|
raise APIError(f"Version {version} is already in use")
|
||||||
await asyncio.shield(self.sys_plugins.audio.update(version))
|
await asyncio.shield(self.sys_plugins.audio.update(version))
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
|
||||||
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
|
||||||
"""Return Audio Docker logs."""
|
|
||||||
return self.sys_plugins.audio.logs()
|
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Restart Audio plugin."""
|
"""Restart Audio plugin."""
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor auth/SSO RESTful API."""
|
"""Init file for Supervisor auth/SSO RESTful API."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Backups RESTful API."""
|
"""Backups RESTful API."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
import errno
|
import errno
|
||||||
@@ -342,9 +343,9 @@ class APIBackups(CoreSysAttributes):
|
|||||||
_LOGGER.info("Downloading backup %s", backup.slug)
|
_LOGGER.info("Downloading backup %s", backup.slug)
|
||||||
response = web.FileResponse(backup.tarfile)
|
response = web.FileResponse(backup.tarfile)
|
||||||
response.content_type = CONTENT_TYPE_TAR
|
response.content_type = CONTENT_TYPE_TAR
|
||||||
response.headers[
|
response.headers[CONTENT_DISPOSITION] = (
|
||||||
CONTENT_DISPOSITION
|
f"attachment; filename={RE_SLUGIFY_NAME.sub('_', backup.name)}.tar"
|
||||||
] = f"attachment; filename={RE_SLUGIFY_NAME.sub('_', backup.name)}.tar"
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor HA cli RESTful API."""
|
"""Init file for Supervisor HA cli RESTful API."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@@ -17,6 +17,7 @@ ATTR_APPARMOR_VERSION = "apparmor_version"
|
|||||||
ATTR_ATTRIBUTES = "attributes"
|
ATTR_ATTRIBUTES = "attributes"
|
||||||
ATTR_AVAILABLE_UPDATES = "available_updates"
|
ATTR_AVAILABLE_UPDATES = "available_updates"
|
||||||
ATTR_BACKGROUND = "background"
|
ATTR_BACKGROUND = "background"
|
||||||
|
ATTR_BOOT_CONFIG = "boot_config"
|
||||||
ATTR_BOOT_SLOT = "boot_slot"
|
ATTR_BOOT_SLOT = "boot_slot"
|
||||||
ATTR_BOOT_SLOTS = "boot_slots"
|
ATTR_BOOT_SLOTS = "boot_slots"
|
||||||
ATTR_BOOT_TIMESTAMP = "boot_timestamp"
|
ATTR_BOOT_TIMESTAMP = "boot_timestamp"
|
||||||
@@ -36,6 +37,7 @@ ATTR_DT_UTC = "dt_utc"
|
|||||||
ATTR_EJECTABLE = "ejectable"
|
ATTR_EJECTABLE = "ejectable"
|
||||||
ATTR_FALLBACK = "fallback"
|
ATTR_FALLBACK = "fallback"
|
||||||
ATTR_FILESYSTEMS = "filesystems"
|
ATTR_FILESYSTEMS = "filesystems"
|
||||||
|
ATTR_FORCE = "force"
|
||||||
ATTR_GROUP_IDS = "group_ids"
|
ATTR_GROUP_IDS = "group_ids"
|
||||||
ATTR_IDENTIFIERS = "identifiers"
|
ATTR_IDENTIFIERS = "identifiers"
|
||||||
ATTR_IS_ACTIVE = "is_active"
|
ATTR_IS_ACTIVE = "is_active"
|
||||||
@@ -53,6 +55,7 @@ ATTR_PANEL_PATH = "panel_path"
|
|||||||
ATTR_REMOVABLE = "removable"
|
ATTR_REMOVABLE = "removable"
|
||||||
ATTR_REMOVE_CONFIG = "remove_config"
|
ATTR_REMOVE_CONFIG = "remove_config"
|
||||||
ATTR_REVISION = "revision"
|
ATTR_REVISION = "revision"
|
||||||
|
ATTR_SAFE_MODE = "safe_mode"
|
||||||
ATTR_SEAT = "seat"
|
ATTR_SEAT = "seat"
|
||||||
ATTR_SIGNED = "signed"
|
ATTR_SIGNED = "signed"
|
||||||
ATTR_STARTUP_TIME = "startup_time"
|
ATTR_STARTUP_TIME = "startup_time"
|
||||||
@@ -66,6 +69,7 @@ ATTR_USAGE = "usage"
|
|||||||
ATTR_USE_NTP = "use_ntp"
|
ATTR_USE_NTP = "use_ntp"
|
||||||
ATTR_USERS = "users"
|
ATTR_USERS = "users"
|
||||||
ATTR_VENDOR = "vendor"
|
ATTR_VENDOR = "vendor"
|
||||||
|
ATTR_VIRTUALIZATION = "virtualization"
|
||||||
|
|
||||||
|
|
||||||
class BootSlot(StrEnum):
|
class BootSlot(StrEnum):
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor network RESTful API."""
|
"""Init file for Supervisor network RESTful API."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor DNS RESTful API."""
|
"""Init file for Supervisor DNS RESTful API."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
import logging
|
import logging
|
||||||
@@ -26,8 +27,8 @@ from ..const import (
|
|||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import APIError
|
from ..exceptions import APIError
|
||||||
from ..validate import dns_server_list, version_tag
|
from ..validate import dns_server_list, version_tag
|
||||||
from .const import ATTR_FALLBACK, ATTR_LLMNR, ATTR_MDNS, CONTENT_TYPE_BINARY
|
from .const import ATTR_FALLBACK, ATTR_LLMNR, ATTR_MDNS
|
||||||
from .utils import api_process, api_process_raw, api_validate
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -105,11 +106,6 @@ class APICoreDNS(CoreSysAttributes):
|
|||||||
raise APIError(f"Version {version} is already in use")
|
raise APIError(f"Version {version} is already in use")
|
||||||
await asyncio.shield(self.sys_plugins.dns.update(version))
|
await asyncio.shield(self.sys_plugins.dns.update(version))
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
|
||||||
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
|
||||||
"""Return DNS Docker logs."""
|
|
||||||
return self.sys_plugins.dns.logs()
|
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Restart CoreDNS plugin."""
|
"""Restart CoreDNS plugin."""
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor Home Assistant RESTful API."""
|
"""Init file for Supervisor Home Assistant RESTful API."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor hardware RESTful API."""
|
"""Init file for Supervisor hardware RESTful API."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ from ..const import (
|
|||||||
ATTR_SYSTEM,
|
ATTR_SYSTEM,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..dbus.udisks2 import UDisks2
|
from ..dbus.udisks2 import UDisks2Manager
|
||||||
from ..dbus.udisks2.block import UDisks2Block
|
from ..dbus.udisks2.block import UDisks2Block
|
||||||
from ..dbus.udisks2.drive import UDisks2Drive
|
from ..dbus.udisks2.drive import UDisks2Drive
|
||||||
from ..hardware.data import Device
|
from ..hardware.data import Device
|
||||||
@@ -72,7 +73,7 @@ def filesystem_struct(fs_block: UDisks2Block) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def drive_struct(udisks2: UDisks2, drive: UDisks2Drive) -> dict[str, Any]:
|
def drive_struct(udisks2: UDisks2Manager, drive: UDisks2Drive) -> dict[str, Any]:
|
||||||
"""Return a dict with information of a disk to be used in the API."""
|
"""Return a dict with information of a disk to be used in the API."""
|
||||||
return {
|
return {
|
||||||
ATTR_VENDOR: drive.vendor,
|
ATTR_VENDOR: drive.vendor,
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""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
|
from collections.abc import Awaitable
|
||||||
import logging
|
import logging
|
||||||
@@ -34,10 +35,10 @@ from ..const import (
|
|||||||
ATTR_WATCHDOG,
|
ATTR_WATCHDOG,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import APIError
|
from ..exceptions import APIDBMigrationInProgress, APIError
|
||||||
from ..validate import docker_image, network_port, version_tag
|
from ..validate import docker_image, network_port, version_tag
|
||||||
from .const import CONTENT_TYPE_BINARY
|
from .const import ATTR_FORCE, ATTR_SAFE_MODE
|
||||||
from .utils import api_process, api_process_raw, api_validate
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -63,10 +64,34 @@ SCHEMA_UPDATE = vol.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SCHEMA_RESTART = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_SAFE_MODE, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_FORCE, default=False): vol.Boolean(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SCHEMA_STOP = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_FORCE, default=False): vol.Boolean(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class APIHomeAssistant(CoreSysAttributes):
|
class APIHomeAssistant(CoreSysAttributes):
|
||||||
"""Handle RESTful API for Home Assistant functions."""
|
"""Handle RESTful API for Home Assistant functions."""
|
||||||
|
|
||||||
|
async def _check_offline_migration(self, force: bool = False) -> None:
|
||||||
|
"""Check and raise if there's an offline DB migration in progress."""
|
||||||
|
if (
|
||||||
|
not force
|
||||||
|
and (state := await self.sys_homeassistant.api.get_api_state())
|
||||||
|
and state.offline_db_migration
|
||||||
|
):
|
||||||
|
raise APIDBMigrationInProgress(
|
||||||
|
"Offline database migration in progress, try again after it has completed"
|
||||||
|
)
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def info(self, request: web.Request) -> dict[str, Any]:
|
async def info(self, request: web.Request) -> dict[str, Any]:
|
||||||
"""Return host information."""
|
"""Return host information."""
|
||||||
@@ -94,6 +119,9 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
|
|
||||||
if ATTR_IMAGE in body:
|
if ATTR_IMAGE in body:
|
||||||
self.sys_homeassistant.image = body[ATTR_IMAGE]
|
self.sys_homeassistant.image = body[ATTR_IMAGE]
|
||||||
|
self.sys_homeassistant.override_image = (
|
||||||
|
self.sys_homeassistant.image != self.sys_homeassistant.default_image
|
||||||
|
)
|
||||||
|
|
||||||
if ATTR_BOOT in body:
|
if ATTR_BOOT in body:
|
||||||
self.sys_homeassistant.boot = body[ATTR_BOOT]
|
self.sys_homeassistant.boot = body[ATTR_BOOT]
|
||||||
@@ -145,6 +173,7 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
async def update(self, request: web.Request) -> None:
|
async def update(self, request: web.Request) -> None:
|
||||||
"""Update Home Assistant."""
|
"""Update Home Assistant."""
|
||||||
body = await api_validate(SCHEMA_UPDATE, request)
|
body = await api_validate(SCHEMA_UPDATE, request)
|
||||||
|
await self._check_offline_migration()
|
||||||
|
|
||||||
await asyncio.shield(
|
await asyncio.shield(
|
||||||
self.sys_homeassistant.core.update(
|
self.sys_homeassistant.core.update(
|
||||||
@@ -154,9 +183,12 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def stop(self, request: web.Request) -> Awaitable[None]:
|
async def stop(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Stop Home Assistant."""
|
"""Stop Home Assistant."""
|
||||||
return asyncio.shield(self.sys_homeassistant.core.stop())
|
body = await api_validate(SCHEMA_STOP, request)
|
||||||
|
await self._check_offline_migration(force=body[ATTR_FORCE])
|
||||||
|
|
||||||
|
return await asyncio.shield(self.sys_homeassistant.core.stop())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def start(self, request: web.Request) -> Awaitable[None]:
|
def start(self, request: web.Request) -> Awaitable[None]:
|
||||||
@@ -164,19 +196,24 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
return asyncio.shield(self.sys_homeassistant.core.start())
|
return asyncio.shield(self.sys_homeassistant.core.start())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
async def restart(self, request: web.Request) -> None:
|
||||||
"""Restart Home Assistant."""
|
"""Restart Home Assistant."""
|
||||||
return asyncio.shield(self.sys_homeassistant.core.restart())
|
body = await api_validate(SCHEMA_RESTART, request)
|
||||||
|
await self._check_offline_migration(force=body[ATTR_FORCE])
|
||||||
|
|
||||||
|
await asyncio.shield(
|
||||||
|
self.sys_homeassistant.core.restart(safe_mode=body[ATTR_SAFE_MODE])
|
||||||
|
)
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def rebuild(self, request: web.Request) -> Awaitable[None]:
|
async def rebuild(self, request: web.Request) -> None:
|
||||||
"""Rebuild Home Assistant."""
|
"""Rebuild Home Assistant."""
|
||||||
return asyncio.shield(self.sys_homeassistant.core.rebuild())
|
body = await api_validate(SCHEMA_RESTART, request)
|
||||||
|
await self._check_offline_migration(force=body[ATTR_FORCE])
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
await asyncio.shield(
|
||||||
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
self.sys_homeassistant.core.rebuild(safe_mode=body[ATTR_SAFE_MODE])
|
||||||
"""Return Home Assistant Docker logs."""
|
)
|
||||||
return self.sys_homeassistant.core.logs()
|
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def check(self, request: web.Request) -> None:
|
async def check(self, request: web.Request) -> None:
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor host RESTful API."""
|
"""Init file for Supervisor host RESTful API."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
import logging
|
import logging
|
||||||
@@ -27,7 +28,7 @@ from ..const import (
|
|||||||
ATTR_TIMEZONE,
|
ATTR_TIMEZONE,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import APIError, HostLogError
|
from ..exceptions import APIDBMigrationInProgress, APIError, HostLogError
|
||||||
from ..host.const import (
|
from ..host.const import (
|
||||||
PARAM_BOOT_ID,
|
PARAM_BOOT_ID,
|
||||||
PARAM_FOLLOW,
|
PARAM_FOLLOW,
|
||||||
@@ -45,27 +46,48 @@ from .const import (
|
|||||||
ATTR_BROADCAST_MDNS,
|
ATTR_BROADCAST_MDNS,
|
||||||
ATTR_DT_SYNCHRONIZED,
|
ATTR_DT_SYNCHRONIZED,
|
||||||
ATTR_DT_UTC,
|
ATTR_DT_UTC,
|
||||||
|
ATTR_FORCE,
|
||||||
ATTR_IDENTIFIERS,
|
ATTR_IDENTIFIERS,
|
||||||
ATTR_LLMNR_HOSTNAME,
|
ATTR_LLMNR_HOSTNAME,
|
||||||
ATTR_STARTUP_TIME,
|
ATTR_STARTUP_TIME,
|
||||||
ATTR_USE_NTP,
|
ATTR_USE_NTP,
|
||||||
|
ATTR_VIRTUALIZATION,
|
||||||
CONTENT_TYPE_TEXT,
|
CONTENT_TYPE_TEXT,
|
||||||
CONTENT_TYPE_X_LOG,
|
CONTENT_TYPE_X_LOG,
|
||||||
)
|
)
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, api_process_raw, api_validate
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
IDENTIFIER = "identifier"
|
IDENTIFIER = "identifier"
|
||||||
BOOTID = "bootid"
|
BOOTID = "bootid"
|
||||||
DEFAULT_RANGE = 100
|
DEFAULT_LINES = 100
|
||||||
|
|
||||||
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): str})
|
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): str})
|
||||||
|
|
||||||
|
# pylint: disable=no-value-for-parameter
|
||||||
|
SCHEMA_SHUTDOWN = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_FORCE, default=False): vol.Boolean(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# pylint: enable=no-value-for-parameter
|
||||||
|
|
||||||
|
|
||||||
class APIHost(CoreSysAttributes):
|
class APIHost(CoreSysAttributes):
|
||||||
"""Handle RESTful API for host functions."""
|
"""Handle RESTful API for host functions."""
|
||||||
|
|
||||||
|
async def _check_ha_offline_migration(self, force: bool) -> None:
|
||||||
|
"""Check if HA has an offline migration in progress and raise if not forced."""
|
||||||
|
if (
|
||||||
|
not force
|
||||||
|
and (state := await self.sys_homeassistant.api.get_api_state())
|
||||||
|
and state.offline_db_migration
|
||||||
|
):
|
||||||
|
raise APIDBMigrationInProgress(
|
||||||
|
"Home Assistant offline database migration in progress, please wait until complete before shutting down host"
|
||||||
|
)
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def info(self, request):
|
async def info(self, request):
|
||||||
"""Return host information."""
|
"""Return host information."""
|
||||||
@@ -73,6 +95,7 @@ class APIHost(CoreSysAttributes):
|
|||||||
ATTR_AGENT_VERSION: self.sys_dbus.agent.version,
|
ATTR_AGENT_VERSION: self.sys_dbus.agent.version,
|
||||||
ATTR_APPARMOR_VERSION: self.sys_host.apparmor.version,
|
ATTR_APPARMOR_VERSION: self.sys_host.apparmor.version,
|
||||||
ATTR_CHASSIS: self.sys_host.info.chassis,
|
ATTR_CHASSIS: self.sys_host.info.chassis,
|
||||||
|
ATTR_VIRTUALIZATION: self.sys_host.info.virtualization,
|
||||||
ATTR_CPE: self.sys_host.info.cpe,
|
ATTR_CPE: self.sys_host.info.cpe,
|
||||||
ATTR_DEPLOYMENT: self.sys_host.info.deployment,
|
ATTR_DEPLOYMENT: self.sys_host.info.deployment,
|
||||||
ATTR_DISK_FREE: self.sys_host.info.free_space,
|
ATTR_DISK_FREE: self.sys_host.info.free_space,
|
||||||
@@ -106,14 +129,20 @@ class APIHost(CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def reboot(self, request):
|
async def reboot(self, request):
|
||||||
"""Reboot host."""
|
"""Reboot host."""
|
||||||
return asyncio.shield(self.sys_host.control.reboot())
|
body = await api_validate(SCHEMA_SHUTDOWN, request)
|
||||||
|
await self._check_ha_offline_migration(force=body[ATTR_FORCE])
|
||||||
|
|
||||||
|
return await asyncio.shield(self.sys_host.control.reboot())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def shutdown(self, request):
|
async def shutdown(self, request):
|
||||||
"""Poweroff host."""
|
"""Poweroff host."""
|
||||||
return asyncio.shield(self.sys_host.control.shutdown())
|
body = await api_validate(SCHEMA_SHUTDOWN, request)
|
||||||
|
await self._check_ha_offline_migration(force=body[ATTR_FORCE])
|
||||||
|
|
||||||
|
return await asyncio.shield(self.sys_host.control.shutdown())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def reload(self, request):
|
def reload(self, request):
|
||||||
@@ -161,8 +190,7 @@ class APIHost(CoreSysAttributes):
|
|||||||
raise APIError() from err
|
raise APIError() from err
|
||||||
return possible_offset
|
return possible_offset
|
||||||
|
|
||||||
@api_process
|
async def advanced_logs_handler(
|
||||||
async def advanced_logs(
|
|
||||||
self, request: web.Request, identifier: str | None = None, follow: bool = False
|
self, request: web.Request, identifier: str | None = None, follow: bool = False
|
||||||
) -> web.StreamResponse:
|
) -> web.StreamResponse:
|
||||||
"""Return systemd-journald logs."""
|
"""Return systemd-journald logs."""
|
||||||
@@ -194,13 +222,30 @@ class APIHost(CoreSysAttributes):
|
|||||||
"supported for now."
|
"supported for now."
|
||||||
)
|
)
|
||||||
|
|
||||||
if request.headers[ACCEPT] == CONTENT_TYPE_X_LOG:
|
if "verbose" in request.query or request.headers[ACCEPT] == CONTENT_TYPE_X_LOG:
|
||||||
log_formatter = LogFormatter.VERBOSE
|
log_formatter = LogFormatter.VERBOSE
|
||||||
|
|
||||||
if RANGE in request.headers:
|
if "lines" in request.query:
|
||||||
|
lines = request.query.get("lines", DEFAULT_LINES)
|
||||||
|
try:
|
||||||
|
lines = int(lines)
|
||||||
|
except ValueError:
|
||||||
|
# If the user passed a non-integer value, just use the default instead of error.
|
||||||
|
lines = DEFAULT_LINES
|
||||||
|
finally:
|
||||||
|
# We can't use the entries= Range header syntax to refer to the last 1 line,
|
||||||
|
# and passing 1 to the calculation below would return the 1st line of the logs
|
||||||
|
# instead. Since this is really an edge case that doesn't matter much, we'll just
|
||||||
|
# return 2 lines at minimum.
|
||||||
|
lines = max(2, lines)
|
||||||
|
# entries=cursor[[:num_skip]:num_entries]
|
||||||
|
range_header = f"entries=:-{lines-1}:{'' if follow else lines}"
|
||||||
|
elif RANGE in request.headers:
|
||||||
range_header = request.headers.get(RANGE)
|
range_header = request.headers.get(RANGE)
|
||||||
else:
|
else:
|
||||||
range_header = f"entries=:-{DEFAULT_RANGE}:"
|
range_header = (
|
||||||
|
f"entries=:-{DEFAULT_LINES-1}:{'' if follow else DEFAULT_LINES}"
|
||||||
|
)
|
||||||
|
|
||||||
async with self.sys_host.logs.journald_logs(
|
async with self.sys_host.logs.journald_logs(
|
||||||
params=params, range_header=range_header, accept=LogFormat.JOURNAL
|
params=params, range_header=range_header, accept=LogFormat.JOURNAL
|
||||||
@@ -208,11 +253,23 @@ class APIHost(CoreSysAttributes):
|
|||||||
try:
|
try:
|
||||||
response = web.StreamResponse()
|
response = web.StreamResponse()
|
||||||
response.content_type = CONTENT_TYPE_TEXT
|
response.content_type = CONTENT_TYPE_TEXT
|
||||||
await response.prepare(request)
|
headers_returned = False
|
||||||
async for line in journal_logs_reader(resp, log_formatter):
|
async for cursor, line in journal_logs_reader(resp, log_formatter):
|
||||||
|
if not headers_returned:
|
||||||
|
if cursor:
|
||||||
|
response.headers["X-First-Cursor"] = cursor
|
||||||
|
await response.prepare(request)
|
||||||
|
headers_returned = True
|
||||||
await response.write(line.encode("utf-8") + b"\n")
|
await response.write(line.encode("utf-8") + b"\n")
|
||||||
except ConnectionResetError as ex:
|
except ConnectionResetError as ex:
|
||||||
raise APIError(
|
raise APIError(
|
||||||
"Connection reset when trying to fetch data from systemd-journald."
|
"Connection reset when trying to fetch data from systemd-journald."
|
||||||
) from ex
|
) from ex
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@api_process_raw(CONTENT_TYPE_TEXT, error_type=CONTENT_TYPE_TEXT)
|
||||||
|
async def advanced_logs(
|
||||||
|
self, request: web.Request, identifier: str | None = None, follow: bool = False
|
||||||
|
) -> web.StreamResponse:
|
||||||
|
"""Return systemd-journald logs. Wrapped as standard API handler."""
|
||||||
|
return await self.advanced_logs_handler(request, identifier, follow)
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Supervisor Add-on ingress service."""
|
"""Supervisor Add-on ingress service."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from ipaddress import ip_address
|
from ipaddress import ip_address
|
||||||
import logging
|
import logging
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor Jobs RESTful API."""
|
"""Init file for Supervisor Jobs RESTful API."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""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 typing import Final
|
||||||
@@ -8,6 +9,8 @@ from aiohttp.web import Request, RequestHandler, Response, middleware
|
|||||||
from aiohttp.web_exceptions import HTTPBadRequest, HTTPForbidden, HTTPUnauthorized
|
from aiohttp.web_exceptions import HTTPBadRequest, HTTPForbidden, HTTPUnauthorized
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
|
|
||||||
|
from supervisor.homeassistant.const import LANDINGPAGE
|
||||||
|
|
||||||
from ...addons.const import RE_SLUG
|
from ...addons.const import RE_SLUG
|
||||||
from ...const import (
|
from ...const import (
|
||||||
REQUEST_FROM,
|
REQUEST_FROM,
|
||||||
@@ -77,6 +80,13 @@ ADDONS_API_BYPASS: Final = re.compile(
|
|||||||
r")$"
|
r")$"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Home Assistant only
|
||||||
|
CORE_ONLY_PATHS: Final = re.compile(
|
||||||
|
r"^(?:"
|
||||||
|
r"/addons/" + RE_SLUG + "/sys_options"
|
||||||
|
r")$"
|
||||||
|
)
|
||||||
|
|
||||||
# Policy role add-on API access
|
# Policy role add-on API access
|
||||||
ADDONS_ROLE_ACCESS: dict[str, re.Pattern] = {
|
ADDONS_ROLE_ACCESS: dict[str, re.Pattern] = {
|
||||||
ROLE_DEFAULT: re.compile(
|
ROLE_DEFAULT: re.compile(
|
||||||
@@ -232,6 +242,9 @@ class SecurityMiddleware(CoreSysAttributes):
|
|||||||
if supervisor_token == self.sys_homeassistant.supervisor_token:
|
if supervisor_token == self.sys_homeassistant.supervisor_token:
|
||||||
_LOGGER.debug("%s access from Home Assistant", request.path)
|
_LOGGER.debug("%s access from Home Assistant", request.path)
|
||||||
request_from = self.sys_homeassistant
|
request_from = self.sys_homeassistant
|
||||||
|
elif CORE_ONLY_PATHS.match(request.path):
|
||||||
|
_LOGGER.warning("Attempted access to %s from client besides Home Assistant")
|
||||||
|
raise HTTPForbidden()
|
||||||
|
|
||||||
# Host
|
# Host
|
||||||
if supervisor_token == self.sys_plugins.cli.supervisor_token:
|
if supervisor_token == self.sys_plugins.cli.supervisor_token:
|
||||||
@@ -277,8 +290,10 @@ class SecurityMiddleware(CoreSysAttributes):
|
|||||||
@middleware
|
@middleware
|
||||||
async def core_proxy(self, request: Request, handler: RequestHandler) -> Response:
|
async def core_proxy(self, request: Request, handler: RequestHandler) -> Response:
|
||||||
"""Validate user from Core API proxy."""
|
"""Validate user from Core API proxy."""
|
||||||
if request[REQUEST_FROM] != self.sys_homeassistant or version_is_new_enough(
|
if (
|
||||||
self.sys_homeassistant.version, _CORE_VERSION
|
request[REQUEST_FROM] != self.sys_homeassistant
|
||||||
|
or self.sys_homeassistant.version == LANDINGPAGE
|
||||||
|
or version_is_new_enough(self.sys_homeassistant.version, _CORE_VERSION)
|
||||||
):
|
):
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor Multicast RESTful API."""
|
"""Init file for Supervisor Multicast RESTful API."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
import logging
|
import logging
|
||||||
@@ -23,8 +24,7 @@ from ..const import (
|
|||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import APIError
|
from ..exceptions import APIError
|
||||||
from ..validate import version_tag
|
from ..validate import version_tag
|
||||||
from .const import CONTENT_TYPE_BINARY
|
from .utils import api_process, api_validate
|
||||||
from .utils import api_process, api_process_raw, api_validate
|
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -69,11 +69,6 @@ class APIMulticast(CoreSysAttributes):
|
|||||||
raise APIError(f"Version {version} is already in use")
|
raise APIError(f"Version {version} is already in use")
|
||||||
await asyncio.shield(self.sys_plugins.multicast.update(version))
|
await asyncio.shield(self.sys_plugins.multicast.update(version))
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
|
||||||
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
|
||||||
"""Return Multicast Docker logs."""
|
|
||||||
return self.sys_plugins.multicast.logs()
|
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Restart Multicast plugin."""
|
"""Restart Multicast plugin."""
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
"""REST API for network."""
|
"""REST API for network."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
from dataclasses import replace
|
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface
|
||||||
from ipaddress import ip_address, ip_interface
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
@@ -48,18 +48,28 @@ from ..host.configuration import (
|
|||||||
Interface,
|
Interface,
|
||||||
InterfaceMethod,
|
InterfaceMethod,
|
||||||
IpConfig,
|
IpConfig,
|
||||||
|
IpSetting,
|
||||||
VlanConfig,
|
VlanConfig,
|
||||||
WifiConfig,
|
WifiConfig,
|
||||||
)
|
)
|
||||||
from ..host.const import AuthMethod, InterfaceType, WifiMode
|
from ..host.const import AuthMethod, InterfaceType, WifiMode
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
_SCHEMA_IP_CONFIG = vol.Schema(
|
_SCHEMA_IPV4_CONFIG = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(ATTR_ADDRESS): [vol.Coerce(ip_interface)],
|
vol.Optional(ATTR_ADDRESS): [vol.Coerce(IPv4Interface)],
|
||||||
vol.Optional(ATTR_METHOD): vol.Coerce(InterfaceMethod),
|
vol.Optional(ATTR_METHOD): vol.Coerce(InterfaceMethod),
|
||||||
vol.Optional(ATTR_GATEWAY): vol.Coerce(ip_address),
|
vol.Optional(ATTR_GATEWAY): vol.Coerce(IPv4Address),
|
||||||
vol.Optional(ATTR_NAMESERVERS): [vol.Coerce(ip_address)],
|
vol.Optional(ATTR_NAMESERVERS): [vol.Coerce(IPv4Address)],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
_SCHEMA_IPV6_CONFIG = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_ADDRESS): [vol.Coerce(IPv6Interface)],
|
||||||
|
vol.Optional(ATTR_METHOD): vol.Coerce(InterfaceMethod),
|
||||||
|
vol.Optional(ATTR_GATEWAY): vol.Coerce(IPv6Address),
|
||||||
|
vol.Optional(ATTR_NAMESERVERS): [vol.Coerce(IPv6Address)],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,18 +86,18 @@ _SCHEMA_WIFI_CONFIG = vol.Schema(
|
|||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
SCHEMA_UPDATE = vol.Schema(
|
SCHEMA_UPDATE = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(ATTR_IPV4): _SCHEMA_IP_CONFIG,
|
vol.Optional(ATTR_IPV4): _SCHEMA_IPV4_CONFIG,
|
||||||
vol.Optional(ATTR_IPV6): _SCHEMA_IP_CONFIG,
|
vol.Optional(ATTR_IPV6): _SCHEMA_IPV6_CONFIG,
|
||||||
vol.Optional(ATTR_WIFI): _SCHEMA_WIFI_CONFIG,
|
vol.Optional(ATTR_WIFI): _SCHEMA_WIFI_CONFIG,
|
||||||
vol.Optional(ATTR_ENABLED): vol.Boolean(),
|
vol.Optional(ATTR_ENABLED): vol.Boolean(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def ipconfig_struct(config: IpConfig) -> dict[str, Any]:
|
def ipconfig_struct(config: IpConfig, setting: IpSetting) -> dict[str, Any]:
|
||||||
"""Return a dict with information about ip configuration."""
|
"""Return a dict with information about ip configuration."""
|
||||||
return {
|
return {
|
||||||
ATTR_METHOD: config.method,
|
ATTR_METHOD: setting.method,
|
||||||
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,
|
||||||
@@ -122,8 +132,8 @@ def interface_struct(interface: Interface) -> dict[str, Any]:
|
|||||||
ATTR_CONNECTED: interface.connected,
|
ATTR_CONNECTED: interface.connected,
|
||||||
ATTR_PRIMARY: interface.primary,
|
ATTR_PRIMARY: interface.primary,
|
||||||
ATTR_MAC: interface.mac,
|
ATTR_MAC: interface.mac,
|
||||||
ATTR_IPV4: ipconfig_struct(interface.ipv4) if interface.ipv4 else None,
|
ATTR_IPV4: ipconfig_struct(interface.ipv4, interface.ipv4setting),
|
||||||
ATTR_IPV6: ipconfig_struct(interface.ipv6) if interface.ipv6 else None,
|
ATTR_IPV6: ipconfig_struct(interface.ipv6, interface.ipv6setting),
|
||||||
ATTR_WIFI: wifi_struct(interface.wifi) if interface.wifi else None,
|
ATTR_WIFI: wifi_struct(interface.wifi) if interface.wifi else None,
|
||||||
ATTR_VLAN: vlan_struct(interface.vlan) if interface.vlan else None,
|
ATTR_VLAN: vlan_struct(interface.vlan) if interface.vlan else None,
|
||||||
}
|
}
|
||||||
@@ -197,24 +207,26 @@ class APINetwork(CoreSysAttributes):
|
|||||||
# Apply config
|
# Apply config
|
||||||
for key, config in body.items():
|
for key, config in body.items():
|
||||||
if key == ATTR_IPV4:
|
if key == ATTR_IPV4:
|
||||||
interface.ipv4 = replace(
|
interface.ipv4setting = IpSetting(
|
||||||
interface.ipv4
|
config.get(ATTR_METHOD, InterfaceMethod.STATIC),
|
||||||
or IpConfig(InterfaceMethod.STATIC, [], None, [], None),
|
config.get(ATTR_ADDRESS, []),
|
||||||
**config,
|
config.get(ATTR_GATEWAY),
|
||||||
|
config.get(ATTR_NAMESERVERS, []),
|
||||||
)
|
)
|
||||||
elif key == ATTR_IPV6:
|
elif key == ATTR_IPV6:
|
||||||
interface.ipv6 = replace(
|
interface.ipv6setting = IpSetting(
|
||||||
interface.ipv6
|
config.get(ATTR_METHOD, InterfaceMethod.STATIC),
|
||||||
or IpConfig(InterfaceMethod.STATIC, [], None, [], None),
|
config.get(ATTR_ADDRESS, []),
|
||||||
**config,
|
config.get(ATTR_GATEWAY),
|
||||||
|
config.get(ATTR_NAMESERVERS, []),
|
||||||
)
|
)
|
||||||
elif key == ATTR_WIFI:
|
elif key == ATTR_WIFI:
|
||||||
interface.wifi = replace(
|
interface.wifi = WifiConfig(
|
||||||
interface.wifi
|
config.get(ATTR_MODE, WifiMode.INFRASTRUCTURE),
|
||||||
or WifiConfig(
|
config.get(ATTR_SSID, ""),
|
||||||
WifiMode.INFRASTRUCTURE, "", AuthMethod.OPEN, None, None
|
config.get(ATTR_AUTH, AuthMethod.OPEN),
|
||||||
),
|
config.get(ATTR_PSK, None),
|
||||||
**config,
|
None,
|
||||||
)
|
)
|
||||||
elif key == ATTR_ENABLED:
|
elif key == ATTR_ENABLED:
|
||||||
interface.enabled = config
|
interface.enabled = config
|
||||||
@@ -256,24 +268,22 @@ class APINetwork(CoreSysAttributes):
|
|||||||
|
|
||||||
vlan_config = VlanConfig(vlan, interface.name)
|
vlan_config = VlanConfig(vlan, interface.name)
|
||||||
|
|
||||||
ipv4_config = None
|
ipv4_setting = None
|
||||||
if ATTR_IPV4 in body:
|
if ATTR_IPV4 in body:
|
||||||
ipv4_config = IpConfig(
|
ipv4_setting = IpSetting(
|
||||||
body[ATTR_IPV4].get(ATTR_METHOD, InterfaceMethod.AUTO),
|
body[ATTR_IPV4].get(ATTR_METHOD, InterfaceMethod.AUTO),
|
||||||
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_setting = None
|
||||||
if ATTR_IPV6 in body:
|
if ATTR_IPV6 in body:
|
||||||
ipv6_config = IpConfig(
|
ipv6_setting = IpSetting(
|
||||||
body[ATTR_IPV6].get(ATTR_METHOD, InterfaceMethod.AUTO),
|
body[ATTR_IPV6].get(ATTR_METHOD, InterfaceMethod.AUTO),
|
||||||
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(
|
||||||
@@ -284,8 +294,10 @@ class APINetwork(CoreSysAttributes):
|
|||||||
True,
|
True,
|
||||||
False,
|
False,
|
||||||
InterfaceType.VLAN,
|
InterfaceType.VLAN,
|
||||||
ipv4_config,
|
None,
|
||||||
ipv6_config,
|
ipv4_setting,
|
||||||
|
None,
|
||||||
|
ipv6_setting,
|
||||||
None,
|
None,
|
||||||
vlan_config,
|
vlan_config,
|
||||||
)
|
)
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor Observer RESTful API."""
|
"""Init file for Supervisor Observer RESTful API."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor HassOS RESTful API."""
|
"""Init file for Supervisor HassOS RESTful API."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
import logging
|
import logging
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Utils for Home Assistant Proxy."""
|
"""Utils for Home Assistant Proxy."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
import logging
|
import logging
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Handle REST API for resoulution."""
|
"""Handle REST API for resoulution."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor Root RESTful API."""
|
"""Init file for Supervisor Root RESTful API."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor Security RESTful API."""
|
"""Init file for Supervisor Security RESTful API."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""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
|
from collections.abc import Awaitable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -249,9 +250,14 @@ class APIStore(CoreSysAttributes):
|
|||||||
@api_process_raw(CONTENT_TYPE_TEXT)
|
@api_process_raw(CONTENT_TYPE_TEXT)
|
||||||
async def addons_addon_changelog(self, request: web.Request) -> str:
|
async def addons_addon_changelog(self, request: web.Request) -> str:
|
||||||
"""Return changelog from add-on."""
|
"""Return changelog from add-on."""
|
||||||
addon = self._extract_addon(request)
|
# Frontend can't handle error response here, need to return 200 and error as text for now
|
||||||
|
try:
|
||||||
|
addon = self._extract_addon(request)
|
||||||
|
except APIError as err:
|
||||||
|
return str(err)
|
||||||
|
|
||||||
if not addon.with_changelog:
|
if not addon.with_changelog:
|
||||||
raise APIError(f"No changelog found for add-on {addon.slug}!")
|
return f"No changelog found for add-on {addon.slug}!"
|
||||||
|
|
||||||
with addon.path_changelog.open("r") as changelog:
|
with addon.path_changelog.open("r") as changelog:
|
||||||
return changelog.read()
|
return changelog.read()
|
||||||
@@ -259,9 +265,14 @@ class APIStore(CoreSysAttributes):
|
|||||||
@api_process_raw(CONTENT_TYPE_TEXT)
|
@api_process_raw(CONTENT_TYPE_TEXT)
|
||||||
async def addons_addon_documentation(self, request: web.Request) -> str:
|
async def addons_addon_documentation(self, request: web.Request) -> str:
|
||||||
"""Return documentation from add-on."""
|
"""Return documentation from add-on."""
|
||||||
addon = self._extract_addon(request)
|
# Frontend can't handle error response here, need to return 200 and error as text for now
|
||||||
|
try:
|
||||||
|
addon = self._extract_addon(request)
|
||||||
|
except APIError as err:
|
||||||
|
return str(err)
|
||||||
|
|
||||||
if not addon.with_documentation:
|
if not addon.with_documentation:
|
||||||
raise APIError(f"No documentation found for add-on {addon.slug}!")
|
return f"No documentation found for add-on {addon.slug}!"
|
||||||
|
|
||||||
with addon.path_documentation.open("r") as documentation:
|
with addon.path_documentation.open("r") as documentation:
|
||||||
return documentation.read()
|
return documentation.read()
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor Supervisor RESTful API."""
|
"""Init file for Supervisor Supervisor RESTful API."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
import logging
|
import logging
|
||||||
@@ -49,7 +50,7 @@ from ..store.validate import repositories
|
|||||||
from ..utils.sentry import close_sentry, init_sentry
|
from ..utils.sentry import close_sentry, init_sentry
|
||||||
from ..utils.validate import validate_timezone
|
from ..utils.validate import validate_timezone
|
||||||
from ..validate import version_tag, wait_boot
|
from ..validate import version_tag, wait_boot
|
||||||
from .const import CONTENT_TYPE_BINARY
|
from .const import CONTENT_TYPE_TEXT
|
||||||
from .utils import api_process, api_process_raw, api_validate
|
from .utils import api_process, api_process_raw, api_validate
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@@ -229,7 +230,7 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
"""Soft restart Supervisor."""
|
"""Soft restart Supervisor."""
|
||||||
return asyncio.shield(self.sys_supervisor.restart())
|
return asyncio.shield(self.sys_supervisor.restart())
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
@api_process_raw(CONTENT_TYPE_TEXT, error_type=CONTENT_TYPE_TEXT)
|
||||||
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
||||||
"""Return supervisor Docker logs."""
|
"""Return supervisor Docker logs."""
|
||||||
return self.sys_supervisor.logs()
|
return self.sys_supervisor.logs()
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor util for RESTful API."""
|
"""Init file for Supervisor util for RESTful API."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ from ..exceptions import APIError, APIForbidden, DockerAPIError, HassioError
|
|||||||
from ..utils import check_exception_chain, get_message_from_exception_chain
|
from ..utils import check_exception_chain, get_message_from_exception_chain
|
||||||
from ..utils.json import json_dumps, json_loads as json_loads_util
|
from ..utils.json import json_dumps, json_loads as json_loads_util
|
||||||
from ..utils.log_format import format_message
|
from ..utils.log_format import format_message
|
||||||
from .const import CONTENT_TYPE_BINARY
|
from . import const
|
||||||
|
|
||||||
|
|
||||||
def excract_supervisor_token(request: web.Request) -> str | None:
|
def excract_supervisor_token(request: web.Request) -> str | None:
|
||||||
@@ -91,7 +92,7 @@ def require_home_assistant(method):
|
|||||||
return wrap_api
|
return wrap_api
|
||||||
|
|
||||||
|
|
||||||
def api_process_raw(content):
|
def api_process_raw(content, *, error_type=None):
|
||||||
"""Wrap content_type into function."""
|
"""Wrap content_type into function."""
|
||||||
|
|
||||||
def wrap_method(method):
|
def wrap_method(method):
|
||||||
@@ -101,15 +102,15 @@ def api_process_raw(content):
|
|||||||
"""Return api information."""
|
"""Return api information."""
|
||||||
try:
|
try:
|
||||||
msg_data = await method(api, *args, **kwargs)
|
msg_data = await method(api, *args, **kwargs)
|
||||||
msg_type = content
|
except HassioError as err:
|
||||||
except (APIError, APIForbidden) as err:
|
return api_return_error(
|
||||||
msg_data = str(err).encode()
|
err, error_type=error_type or const.CONTENT_TYPE_BINARY
|
||||||
msg_type = CONTENT_TYPE_BINARY
|
)
|
||||||
except HassioError:
|
|
||||||
msg_data = b""
|
|
||||||
msg_type = CONTENT_TYPE_BINARY
|
|
||||||
|
|
||||||
return web.Response(body=msg_data, content_type=msg_type)
|
if isinstance(msg_data, (web.Response, web.StreamResponse)):
|
||||||
|
return msg_data
|
||||||
|
|
||||||
|
return web.Response(body=msg_data, content_type=content)
|
||||||
|
|
||||||
return wrap_api
|
return wrap_api
|
||||||
|
|
||||||
@@ -117,23 +118,36 @@ def api_process_raw(content):
|
|||||||
|
|
||||||
|
|
||||||
def api_return_error(
|
def api_return_error(
|
||||||
error: Exception | None = None, message: str | None = None
|
error: Exception | None = None,
|
||||||
|
message: str | None = None,
|
||||||
|
error_type: str | None = None,
|
||||||
) -> web.Response:
|
) -> web.Response:
|
||||||
"""Return an API error message."""
|
"""Return an API error message."""
|
||||||
if error and not message:
|
if error and not message:
|
||||||
message = get_message_from_exception_chain(error)
|
message = get_message_from_exception_chain(error)
|
||||||
if check_exception_chain(error, DockerAPIError):
|
if check_exception_chain(error, DockerAPIError):
|
||||||
message = format_message(message)
|
message = format_message(message)
|
||||||
|
if not message:
|
||||||
|
message = "Unknown error, see supervisor"
|
||||||
|
|
||||||
result = {
|
|
||||||
JSON_RESULT: RESULT_ERROR,
|
|
||||||
JSON_MESSAGE: message or "Unknown error, see supervisor",
|
|
||||||
}
|
|
||||||
status = 400
|
status = 400
|
||||||
if isinstance(error, APIError):
|
if is_api_error := isinstance(error, APIError):
|
||||||
status = error.status
|
status = error.status
|
||||||
if error.job_id:
|
|
||||||
result[JSON_JOB_ID] = error.job_id
|
match error_type:
|
||||||
|
case const.CONTENT_TYPE_TEXT:
|
||||||
|
return web.Response(body=message, content_type=error_type, status=status)
|
||||||
|
case const.CONTENT_TYPE_BINARY:
|
||||||
|
return web.Response(
|
||||||
|
body=message.encode(), content_type=error_type, status=status
|
||||||
|
)
|
||||||
|
case _:
|
||||||
|
result = {
|
||||||
|
JSON_RESULT: RESULT_ERROR,
|
||||||
|
JSON_MESSAGE: message,
|
||||||
|
}
|
||||||
|
if is_api_error and error.job_id:
|
||||||
|
result[JSON_JOB_ID] = error.job_id
|
||||||
|
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
result,
|
result,
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Handle Arch for underlay maschine/platforms."""
|
"""Handle Arch for underlay maschine/platforms."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import platform
|
import platform
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Manage SSO for Add-ons with Home Assistant user."""
|
"""Manage SSO for Add-ons with Home Assistant user."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Representation of a backup file."""
|
"""Representation of a backup file."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from base64 import b64decode, b64encode
|
from base64 import b64decode, b64encode
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Backup consts."""
|
"""Backup consts."""
|
||||||
|
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
|
||||||
BUF_SIZE = 2**20 * 4 # 4MB
|
BUF_SIZE = 2**20 * 4 # 4MB
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Backup manager."""
|
"""Backup manager."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -9,7 +10,10 @@ from pathlib import Path
|
|||||||
|
|
||||||
from ..addons.addon import Addon
|
from ..addons.addon import Addon
|
||||||
from ..const import (
|
from ..const import (
|
||||||
|
ATTR_DATA,
|
||||||
ATTR_DAYS_UNTIL_STALE,
|
ATTR_DAYS_UNTIL_STALE,
|
||||||
|
ATTR_SLUG,
|
||||||
|
ATTR_TYPE,
|
||||||
FILE_HASSIO_BACKUPS,
|
FILE_HASSIO_BACKUPS,
|
||||||
FOLDER_HOMEASSISTANT,
|
FOLDER_HOMEASSISTANT,
|
||||||
CoreState,
|
CoreState,
|
||||||
@@ -20,7 +24,9 @@ from ..exceptions import (
|
|||||||
BackupInvalidError,
|
BackupInvalidError,
|
||||||
BackupJobError,
|
BackupJobError,
|
||||||
BackupMountDownError,
|
BackupMountDownError,
|
||||||
|
HomeAssistantWSError,
|
||||||
)
|
)
|
||||||
|
from ..homeassistant.const import WSType
|
||||||
from ..jobs.const import JOB_GROUP_BACKUP_MANAGER, JobCondition, JobExecutionLimit
|
from ..jobs.const import JOB_GROUP_BACKUP_MANAGER, JobCondition, JobExecutionLimit
|
||||||
from ..jobs.decorator import Job
|
from ..jobs.decorator import Job
|
||||||
from ..jobs.job_group import JobGroup
|
from ..jobs.job_group import JobGroup
|
||||||
@@ -259,11 +265,6 @@ class BackupManager(FileConfiguration, JobGroup):
|
|||||||
self.sys_core.state = CoreState.FREEZE
|
self.sys_core.state = CoreState.FREEZE
|
||||||
|
|
||||||
async with backup:
|
async with backup:
|
||||||
# Backup add-ons
|
|
||||||
if addon_list:
|
|
||||||
self._change_stage(BackupJobStage.ADDONS, backup)
|
|
||||||
addon_start_tasks = await backup.store_addons(addon_list)
|
|
||||||
|
|
||||||
# HomeAssistant Folder is for v1
|
# HomeAssistant Folder is for v1
|
||||||
if homeassistant:
|
if homeassistant:
|
||||||
self._change_stage(BackupJobStage.HOME_ASSISTANT, backup)
|
self._change_stage(BackupJobStage.HOME_ASSISTANT, backup)
|
||||||
@@ -273,6 +274,11 @@ class BackupManager(FileConfiguration, JobGroup):
|
|||||||
else homeassistant_exclude_database
|
else homeassistant_exclude_database
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Backup add-ons
|
||||||
|
if addon_list:
|
||||||
|
self._change_stage(BackupJobStage.ADDONS, backup)
|
||||||
|
addon_start_tasks = await backup.store_addons(addon_list)
|
||||||
|
|
||||||
# Backup folders
|
# Backup folders
|
||||||
if folder_list:
|
if folder_list:
|
||||||
self._change_stage(BackupJobStage.FOLDERS, backup)
|
self._change_stage(BackupJobStage.FOLDERS, backup)
|
||||||
@@ -298,6 +304,18 @@ class BackupManager(FileConfiguration, JobGroup):
|
|||||||
# Ignore exceptions from waiting for addon startup, addon errors handled elsewhere
|
# Ignore exceptions from waiting for addon startup, addon errors handled elsewhere
|
||||||
await asyncio.gather(*addon_start_tasks, return_exceptions=True)
|
await asyncio.gather(*addon_start_tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.sys_homeassistant.websocket.async_send_command(
|
||||||
|
{
|
||||||
|
ATTR_TYPE: WSType.BACKUP_SYNC,
|
||||||
|
ATTR_DATA: {
|
||||||
|
ATTR_SLUG: backup.slug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except HomeAssistantWSError as err:
|
||||||
|
_LOGGER.error("Can't send backup sync to Home Assistant: %s", err)
|
||||||
|
|
||||||
return backup
|
return backup
|
||||||
finally:
|
finally:
|
||||||
self.sys_core.state = CoreState.RUNNING
|
self.sys_core.state = CoreState.RUNNING
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Util add-on functions."""
|
"""Util add-on functions."""
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Validate some things around restore."""
|
"""Validate some things around restore."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
"""Bootstrap Supervisor."""
|
"""Bootstrap Supervisor."""
|
||||||
|
|
||||||
|
# ruff: noqa: T100
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Bus event system."""
|
"""Bus event system."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Bootstrap Supervisor."""
|
"""Bootstrap Supervisor."""
|
||||||
|
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Constants file for Supervisor."""
|
"""Constants file for Supervisor."""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from ipaddress import ip_network
|
from ipaddress import ip_network
|
||||||
@@ -309,6 +310,8 @@ ATTR_SUPERVISOR_VERSION = "supervisor_version"
|
|||||||
ATTR_SUPPORTED = "supported"
|
ATTR_SUPPORTED = "supported"
|
||||||
ATTR_SUPPORTED_ARCH = "supported_arch"
|
ATTR_SUPPORTED_ARCH = "supported_arch"
|
||||||
ATTR_SYSTEM = "system"
|
ATTR_SYSTEM = "system"
|
||||||
|
ATTR_SYSTEM_MANAGED = "system_managed"
|
||||||
|
ATTR_SYSTEM_MANAGED_CONFIG_ENTRY = "system_managed_config_entry"
|
||||||
ATTR_TIMEOUT = "timeout"
|
ATTR_TIMEOUT = "timeout"
|
||||||
ATTR_TIMEZONE = "timezone"
|
ATTR_TIMEZONE = "timezone"
|
||||||
ATTR_TITLE = "title"
|
ATTR_TITLE = "title"
|
||||||
@@ -379,12 +382,27 @@ ROLE_ADMIN = "admin"
|
|||||||
ROLE_ALL = [ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_BACKUP, ROLE_MANAGER, ROLE_ADMIN]
|
ROLE_ALL = [ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_BACKUP, ROLE_MANAGER, ROLE_ADMIN]
|
||||||
|
|
||||||
|
|
||||||
|
class AddonBootConfig(StrEnum):
|
||||||
|
"""Boot mode config for the add-on."""
|
||||||
|
|
||||||
|
AUTO = "auto"
|
||||||
|
MANUAL = "manual"
|
||||||
|
MANUAL_ONLY = "manual_only"
|
||||||
|
|
||||||
|
|
||||||
class AddonBoot(StrEnum):
|
class AddonBoot(StrEnum):
|
||||||
"""Boot mode for the add-on."""
|
"""Boot mode for the add-on."""
|
||||||
|
|
||||||
AUTO = "auto"
|
AUTO = "auto"
|
||||||
MANUAL = "manual"
|
MANUAL = "manual"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _missing_(cls, value: str) -> Self | None:
|
||||||
|
"""Convert 'forced' config values to their counterpart."""
|
||||||
|
if value == AddonBootConfig.MANUAL_ONLY:
|
||||||
|
return AddonBoot.MANUAL
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class AddonStartup(StrEnum):
|
class AddonStartup(StrEnum):
|
||||||
"""Startup types of Add-on."""
|
"""Startup types of Add-on."""
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Main file for Supervisor."""
|
"""Main file for Supervisor."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
@@ -345,9 +346,6 @@ class Core(CoreSysAttributes):
|
|||||||
if self.state == CoreState.RUNNING:
|
if self.state == CoreState.RUNNING:
|
||||||
self.state = CoreState.SHUTDOWN
|
self.state = CoreState.SHUTDOWN
|
||||||
|
|
||||||
# Stop docker monitoring
|
|
||||||
await self.sys_docker.unload()
|
|
||||||
|
|
||||||
# Shutdown Application Add-ons, using Home Assistant API
|
# Shutdown Application Add-ons, using Home Assistant API
|
||||||
await self.sys_addons.shutdown(AddonStartup.APPLICATION)
|
await self.sys_addons.shutdown(AddonStartup.APPLICATION)
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Handle core shared data."""
|
"""Handle core shared data."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -62,7 +63,7 @@ class CoreSys:
|
|||||||
|
|
||||||
# External objects
|
# External objects
|
||||||
self._loop: asyncio.BaseEventLoop = asyncio.get_running_loop()
|
self._loop: asyncio.BaseEventLoop = asyncio.get_running_loop()
|
||||||
self._websession: aiohttp.ClientSession = aiohttp.ClientSession()
|
self._websession = None
|
||||||
|
|
||||||
# Global objects
|
# Global objects
|
||||||
self._config: CoreConfig = CoreConfig()
|
self._config: CoreConfig = CoreConfig()
|
||||||
@@ -95,10 +96,8 @@ class CoreSys:
|
|||||||
self._bus: Bus | None = None
|
self._bus: Bus | None = None
|
||||||
self._mounts: MountManager | None = None
|
self._mounts: MountManager | None = None
|
||||||
|
|
||||||
# Set default header for aiohttp
|
# Setup aiohttp session
|
||||||
self._websession._default_headers = MappingProxyType(
|
self.create_websession()
|
||||||
{aiohttp.hdrs.USER_AGENT: SERVER_SOFTWARE}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Task factory attributes
|
# Task factory attributes
|
||||||
self._set_task_context: list[Callable[[Context], Context]] = []
|
self._set_task_context: list[Callable[[Context], Context]] = []
|
||||||
@@ -113,8 +112,11 @@ class CoreSys:
|
|||||||
"""Return system timezone."""
|
"""Return system timezone."""
|
||||||
if self.config.timezone:
|
if self.config.timezone:
|
||||||
return self.config.timezone
|
return self.config.timezone
|
||||||
|
# pylint bug with python 3.12.4 (https://github.com/pylint-dev/pylint/issues/9811)
|
||||||
|
# pylint: disable=no-member
|
||||||
if self.host.info.timezone:
|
if self.host.info.timezone:
|
||||||
return self.host.info.timezone
|
return self.host.info.timezone
|
||||||
|
# pylint: enable=no-member
|
||||||
return "UTC"
|
return "UTC"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -544,6 +546,16 @@ class CoreSys:
|
|||||||
|
|
||||||
return self.loop.run_in_executor(None, funct, *args)
|
return self.loop.run_in_executor(None, funct, *args)
|
||||||
|
|
||||||
|
def create_websession(self) -> None:
|
||||||
|
"""Create a new aiohttp session."""
|
||||||
|
if self._websession:
|
||||||
|
self.create_task(self._websession.close())
|
||||||
|
|
||||||
|
# Create session and set default header for aiohttp
|
||||||
|
self._websession: aiohttp.ClientSession = aiohttp.ClientSession(
|
||||||
|
headers=MappingProxyType({aiohttp.hdrs.USER_AGENT: SERVER_SOFTWARE})
|
||||||
|
)
|
||||||
|
|
||||||
def _create_context(self) -> Context:
|
def _create_context(self) -> Context:
|
||||||
"""Create a new context for a task."""
|
"""Create a new context for a task."""
|
||||||
context = copy_context()
|
context = copy_context()
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""OS-Agent implementation for DBUS."""
|
"""OS-Agent implementation for DBUS."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
import logging
|
import logging
|
||||||
@@ -7,7 +8,7 @@ from typing import Any
|
|||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
from dbus_fast.aio.message_bus import MessageBus
|
from dbus_fast.aio.message_bus import MessageBus
|
||||||
|
|
||||||
from ...exceptions import DBusError, DBusInterfaceError, DBusServiceUnkownError
|
from ...exceptions import DBusInterfaceError, DBusServiceUnkownError
|
||||||
from ..const import (
|
from ..const import (
|
||||||
DBUS_ATTR_DIAGNOSTICS,
|
DBUS_ATTR_DIAGNOSTICS,
|
||||||
DBUS_ATTR_VERSION,
|
DBUS_ATTR_VERSION,
|
||||||
@@ -95,13 +96,25 @@ class OSAgent(DBusInterfaceProxy):
|
|||||||
_LOGGER.info("Load dbus interface %s", self.name)
|
_LOGGER.info("Load dbus interface %s", self.name)
|
||||||
try:
|
try:
|
||||||
await super().connect(bus)
|
await super().connect(bus)
|
||||||
await asyncio.gather(*[dbus.connect(bus) for dbus in self.all])
|
|
||||||
except DBusError:
|
|
||||||
_LOGGER.warning("Can't connect to OS-Agent")
|
|
||||||
except (DBusServiceUnkownError, DBusInterfaceError):
|
except (DBusServiceUnkownError, DBusInterfaceError):
|
||||||
_LOGGER.warning(
|
_LOGGER.error(
|
||||||
"No OS-Agent support on the host. Some Host functions have been disabled."
|
"No OS-Agent support on the host. Some Host functions have been disabled."
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
errors = await asyncio.gather(
|
||||||
|
*[dbus.connect(bus) for dbus in self.all], return_exceptions=True
|
||||||
|
)
|
||||||
|
|
||||||
|
for err in errors:
|
||||||
|
if err:
|
||||||
|
dbus = self.all[errors.index(err)]
|
||||||
|
_LOGGER.error(
|
||||||
|
"Can't load OS Agent dbus interface %s %s: %s",
|
||||||
|
dbus.bus_name,
|
||||||
|
dbus.object_path,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
@dbus_connected
|
@dbus_connected
|
||||||
async def update(self, changed: dict[str, Any] | None = None) -> None:
|
async def update(self, changed: dict[str, Any] | None = None) -> None:
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""AppArmor object for OS-Agent."""
|
"""AppArmor object for OS-Agent."""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
"""Board management for OS Agent."""
|
"""Board management for OS Agent."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dbus_fast.aio.message_bus import MessageBus
|
from dbus_fast.aio.message_bus import MessageBus
|
||||||
|
|
||||||
from ....exceptions import BoardInvalidError
|
from ....exceptions import BoardInvalidError, DBusInterfaceError, DBusServiceUnkownError
|
||||||
from ...const import (
|
from ...const import (
|
||||||
DBUS_ATTR_BOARD,
|
DBUS_ATTR_BOARD,
|
||||||
DBUS_IFACE_HAOS_BOARDS,
|
DBUS_IFACE_HAOS_BOARDS,
|
||||||
@@ -74,6 +75,10 @@ class BoardManager(DBusInterfaceProxy):
|
|||||||
self._board_proxy = Green()
|
self._board_proxy = Green()
|
||||||
elif self.board == BOARD_NAME_SUPERVISED:
|
elif self.board == BOARD_NAME_SUPERVISED:
|
||||||
self._board_proxy = Supervised()
|
self._board_proxy = Supervised()
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
if self._board_proxy:
|
try:
|
||||||
await self._board_proxy.connect(bus)
|
await self._board_proxy.connect(bus)
|
||||||
|
except (DBusServiceUnkownError, DBusInterfaceError) as ex:
|
||||||
|
_LOGGER.warning("OS-Agent board support initialization failed: %s", ex)
|
||||||
|
@@ -1,5 +1,9 @@
|
|||||||
"""Supervised board management."""
|
"""Supervised board management."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from supervisor.dbus.utils import dbus_connected
|
||||||
|
|
||||||
from .const import BOARD_NAME_SUPERVISED
|
from .const import BOARD_NAME_SUPERVISED
|
||||||
from .interface import BoardProxy
|
from .interface import BoardProxy
|
||||||
|
|
||||||
@@ -11,3 +15,11 @@ class Supervised(BoardProxy):
|
|||||||
"""Initialize properties."""
|
"""Initialize properties."""
|
||||||
super().__init__(BOARD_NAME_SUPERVISED)
|
super().__init__(BOARD_NAME_SUPERVISED)
|
||||||
self.sync_properties: bool = False
|
self.sync_properties: bool = False
|
||||||
|
|
||||||
|
@dbus_connected
|
||||||
|
async def update(self, changed: dict[str, Any] | None = None) -> None:
|
||||||
|
"""Do nothing as there are no properties.
|
||||||
|
|
||||||
|
Currently unused, avoid using the Properties interface to avoid a bug in
|
||||||
|
Go D-Bus, see: https://github.com/home-assistant/os-agent/issues/206
|
||||||
|
"""
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""DataDisk object for OS-Agent."""
|
"""DataDisk object for OS-Agent."""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Constants for DBUS."""
|
"""Constants for DBUS."""
|
||||||
|
|
||||||
from enum import IntEnum, StrEnum
|
from enum import IntEnum, StrEnum
|
||||||
from socket import AF_INET, AF_INET6
|
from socket import AF_INET, AF_INET6
|
||||||
|
|
||||||
@@ -61,7 +62,8 @@ DBUS_OBJECT_RESOLVED = "/org/freedesktop/resolve1"
|
|||||||
DBUS_OBJECT_SETTINGS = "/org/freedesktop/NetworkManager/Settings"
|
DBUS_OBJECT_SETTINGS = "/org/freedesktop/NetworkManager/Settings"
|
||||||
DBUS_OBJECT_SYSTEMD = "/org/freedesktop/systemd1"
|
DBUS_OBJECT_SYSTEMD = "/org/freedesktop/systemd1"
|
||||||
DBUS_OBJECT_TIMEDATE = "/org/freedesktop/timedate1"
|
DBUS_OBJECT_TIMEDATE = "/org/freedesktop/timedate1"
|
||||||
DBUS_OBJECT_UDISKS2 = "/org/freedesktop/UDisks2/Manager"
|
DBUS_OBJECT_UDISKS2 = "/org/freedesktop/UDisks2"
|
||||||
|
DBUS_OBJECT_UDISKS2_MANAGER = "/org/freedesktop/UDisks2/Manager"
|
||||||
|
|
||||||
DBUS_ATTR_ACTIVE_ACCESSPOINT = "ActiveAccessPoint"
|
DBUS_ATTR_ACTIVE_ACCESSPOINT = "ActiveAccessPoint"
|
||||||
DBUS_ATTR_ACTIVE_CONNECTION = "ActiveConnection"
|
DBUS_ATTR_ACTIVE_CONNECTION = "ActiveConnection"
|
||||||
@@ -180,6 +182,7 @@ DBUS_ATTR_UUID = "Uuid"
|
|||||||
DBUS_ATTR_VARIANT = "Variant"
|
DBUS_ATTR_VARIANT = "Variant"
|
||||||
DBUS_ATTR_VENDOR = "Vendor"
|
DBUS_ATTR_VENDOR = "Vendor"
|
||||||
DBUS_ATTR_VERSION = "Version"
|
DBUS_ATTR_VERSION = "Version"
|
||||||
|
DBUS_ATTR_VIRTUALIZATION = "Virtualization"
|
||||||
DBUS_ATTR_WHAT = "What"
|
DBUS_ATTR_WHAT = "What"
|
||||||
DBUS_ATTR_WWN = "WWN"
|
DBUS_ATTR_WWN = "WWN"
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""D-Bus interface for hostname."""
|
"""D-Bus interface for hostname."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dbus_fast.aio.message_bus import MessageBus
|
from dbus_fast.aio.message_bus import MessageBus
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Interface class for D-Bus wrappers."""
|
"""Interface class for D-Bus wrappers."""
|
||||||
|
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Interface to Logind over D-Bus."""
|
"""Interface to Logind over D-Bus."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dbus_fast.aio.message_bus import MessageBus
|
from dbus_fast.aio.message_bus import MessageBus
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""D-Bus interface objects."""
|
"""D-Bus interface objects."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ from .rauc import Rauc
|
|||||||
from .resolved import Resolved
|
from .resolved import Resolved
|
||||||
from .systemd import Systemd
|
from .systemd import Systemd
|
||||||
from .timedate import TimeDate
|
from .timedate import TimeDate
|
||||||
from .udisks2 import UDisks2
|
from .udisks2 import UDisks2Manager
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ class DBusManager(CoreSysAttributes):
|
|||||||
self._agent: OSAgent = OSAgent()
|
self._agent: OSAgent = OSAgent()
|
||||||
self._timedate: TimeDate = TimeDate()
|
self._timedate: TimeDate = TimeDate()
|
||||||
self._resolved: Resolved = Resolved()
|
self._resolved: Resolved = Resolved()
|
||||||
self._udisks2: UDisks2 = UDisks2()
|
self._udisks2: UDisks2Manager = UDisks2Manager()
|
||||||
self._bus: MessageBus | None = None
|
self._bus: MessageBus | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -81,7 +82,7 @@ class DBusManager(CoreSysAttributes):
|
|||||||
return self._resolved
|
return self._resolved
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def udisks2(self) -> UDisks2:
|
def udisks2(self) -> UDisks2Manager:
|
||||||
"""Return the udisks2 interface."""
|
"""Return the udisks2 interface."""
|
||||||
return self._udisks2
|
return self._udisks2
|
||||||
|
|
||||||
@@ -128,9 +129,11 @@ class DBusManager(CoreSysAttributes):
|
|||||||
|
|
||||||
for err in errors:
|
for err in errors:
|
||||||
if err:
|
if err:
|
||||||
|
dbus = self.all[errors.index(err)]
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Can't load dbus interface %s: %s",
|
"Can't load dbus interface %s %s: %s",
|
||||||
self.all[errors.index(err)].name,
|
dbus.name,
|
||||||
|
dbus.object_path,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Network Manager implementation for DBUS."""
|
"""Network Manager implementation for DBUS."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""NetworkConnection objects for Network Manager."""
|
"""NetworkConnection objects for Network Manager."""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from ipaddress import IPv4Address, IPv6Address
|
from ipaddress import IPv4Address, IPv6Address
|
||||||
|
|
||||||
@@ -58,11 +59,22 @@ class VlanProperties:
|
|||||||
parent: str | None
|
parent: str | None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class IpAddress:
|
||||||
|
"""IP address object for Network Manager."""
|
||||||
|
|
||||||
|
address: str
|
||||||
|
prefix: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class IpProperties:
|
class IpProperties:
|
||||||
"""IP properties object for Network Manager."""
|
"""IP properties object for Network Manager."""
|
||||||
|
|
||||||
method: str | None
|
method: str | None
|
||||||
|
address_data: list[IpAddress] | None
|
||||||
|
gateway: str | None
|
||||||
|
dns: list[bytes | int] | None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Network Manager DNS Manager object."""
|
"""Network Manager DNS Manager object."""
|
||||||
|
|
||||||
from ipaddress import ip_address
|
from ipaddress import ip_address
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@@ -1,17 +1,18 @@
|
|||||||
"""Connection object for Network Manager."""
|
"""Connection object for Network Manager."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from dbus_fast import Variant
|
from dbus_fast import Variant
|
||||||
from dbus_fast.aio.message_bus import MessageBus
|
from dbus_fast.aio.message_bus import MessageBus
|
||||||
|
|
||||||
from ....const import ATTR_METHOD, ATTR_MODE, ATTR_PSK, ATTR_SSID
|
|
||||||
from ...const import DBUS_NAME_NM
|
from ...const import DBUS_NAME_NM
|
||||||
from ...interface import DBusInterface
|
from ...interface import DBusInterface
|
||||||
from ...utils import dbus_connected
|
from ...utils import dbus_connected
|
||||||
from ..configuration import (
|
from ..configuration import (
|
||||||
ConnectionProperties,
|
ConnectionProperties,
|
||||||
EthernetProperties,
|
EthernetProperties,
|
||||||
|
IpAddress,
|
||||||
IpProperties,
|
IpProperties,
|
||||||
MatchProperties,
|
MatchProperties,
|
||||||
VlanProperties,
|
VlanProperties,
|
||||||
@@ -20,30 +21,52 @@ from ..configuration import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
CONF_ATTR_CONNECTION = "connection"
|
CONF_ATTR_CONNECTION = "connection"
|
||||||
|
CONF_ATTR_MATCH = "match"
|
||||||
CONF_ATTR_802_ETHERNET = "802-3-ethernet"
|
CONF_ATTR_802_ETHERNET = "802-3-ethernet"
|
||||||
CONF_ATTR_802_WIRELESS = "802-11-wireless"
|
CONF_ATTR_802_WIRELESS = "802-11-wireless"
|
||||||
CONF_ATTR_802_WIRELESS_SECURITY = "802-11-wireless-security"
|
CONF_ATTR_802_WIRELESS_SECURITY = "802-11-wireless-security"
|
||||||
CONF_ATTR_VLAN = "vlan"
|
CONF_ATTR_VLAN = "vlan"
|
||||||
CONF_ATTR_IPV4 = "ipv4"
|
CONF_ATTR_IPV4 = "ipv4"
|
||||||
CONF_ATTR_IPV6 = "ipv6"
|
CONF_ATTR_IPV6 = "ipv6"
|
||||||
CONF_ATTR_MATCH = "match"
|
|
||||||
CONF_ATTR_PATH = "path"
|
|
||||||
|
|
||||||
ATTR_ID = "id"
|
CONF_ATTR_CONNECTION_ID = "id"
|
||||||
ATTR_UUID = "uuid"
|
CONF_ATTR_CONNECTION_UUID = "uuid"
|
||||||
ATTR_TYPE = "type"
|
CONF_ATTR_CONNECTION_TYPE = "type"
|
||||||
ATTR_PARENT = "parent"
|
CONF_ATTR_CONNECTION_LLMNR = "llmnr"
|
||||||
ATTR_ASSIGNED_MAC = "assigned-mac-address"
|
CONF_ATTR_CONNECTION_MDNS = "mdns"
|
||||||
ATTR_POWERSAVE = "powersave"
|
CONF_ATTR_CONNECTION_AUTOCONNECT = "autoconnect"
|
||||||
ATTR_AUTH_ALG = "auth-alg"
|
CONF_ATTR_CONNECTION_INTERFACE_NAME = "interface-name"
|
||||||
ATTR_KEY_MGMT = "key-mgmt"
|
|
||||||
ATTR_INTERFACE_NAME = "interface-name"
|
CONF_ATTR_MATCH_PATH = "path"
|
||||||
ATTR_PATH = "path"
|
|
||||||
|
CONF_ATTR_VLAN_ID = "id"
|
||||||
|
CONF_ATTR_VLAN_PARENT = "parent"
|
||||||
|
|
||||||
|
CONF_ATTR_802_ETHERNET_ASSIGNED_MAC = "assigned-mac-address"
|
||||||
|
|
||||||
|
CONF_ATTR_802_WIRELESS_MODE = "mode"
|
||||||
|
CONF_ATTR_802_WIRELESS_ASSIGNED_MAC = "assigned-mac-address"
|
||||||
|
CONF_ATTR_802_WIRELESS_SSID = "ssid"
|
||||||
|
CONF_ATTR_802_WIRELESS_POWERSAVE = "powersave"
|
||||||
|
CONF_ATTR_802_WIRELESS_SECURITY_AUTH_ALG = "auth-alg"
|
||||||
|
CONF_ATTR_802_WIRELESS_SECURITY_KEY_MGMT = "key-mgmt"
|
||||||
|
CONF_ATTR_802_WIRELESS_SECURITY_PSK = "psk"
|
||||||
|
|
||||||
|
CONF_ATTR_IPV4_METHOD = "method"
|
||||||
|
CONF_ATTR_IPV4_ADDRESS_DATA = "address-data"
|
||||||
|
CONF_ATTR_IPV4_GATEWAY = "gateway"
|
||||||
|
CONF_ATTR_IPV4_DNS = "dns"
|
||||||
|
|
||||||
|
CONF_ATTR_IPV6_METHOD = "method"
|
||||||
|
CONF_ATTR_IPV6_ADDRESS_DATA = "address-data"
|
||||||
|
CONF_ATTR_IPV6_GATEWAY = "gateway"
|
||||||
|
CONF_ATTR_IPV6_DNS = "dns"
|
||||||
|
|
||||||
IPV4_6_IGNORE_FIELDS = [
|
IPV4_6_IGNORE_FIELDS = [
|
||||||
"addresses",
|
"addresses",
|
||||||
"address-data",
|
"address-data",
|
||||||
"dns",
|
"dns",
|
||||||
|
"dns-data",
|
||||||
"gateway",
|
"gateway",
|
||||||
"method",
|
"method",
|
||||||
]
|
]
|
||||||
@@ -73,7 +96,7 @@ def _merge_settings_attribute(
|
|||||||
class NetworkSetting(DBusInterface):
|
class NetworkSetting(DBusInterface):
|
||||||
"""Network connection setting object for Network Manager.
|
"""Network connection setting object for Network Manager.
|
||||||
|
|
||||||
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Settings.Connection.html
|
https://networkmanager.dev/docs/api/1.48.0/gdbus-org.freedesktop.NetworkManager.Settings.Connection.html
|
||||||
"""
|
"""
|
||||||
|
|
||||||
bus_name: str = DBUS_NAME_NM
|
bus_name: str = DBUS_NAME_NM
|
||||||
@@ -147,7 +170,7 @@ class NetworkSetting(DBusInterface):
|
|||||||
new_settings,
|
new_settings,
|
||||||
settings,
|
settings,
|
||||||
CONF_ATTR_CONNECTION,
|
CONF_ATTR_CONNECTION,
|
||||||
ignore_current_value=[ATTR_INTERFACE_NAME],
|
ignore_current_value=[CONF_ATTR_CONNECTION_INTERFACE_NAME],
|
||||||
)
|
)
|
||||||
_merge_settings_attribute(new_settings, settings, CONF_ATTR_802_ETHERNET)
|
_merge_settings_attribute(new_settings, settings, CONF_ATTR_802_ETHERNET)
|
||||||
_merge_settings_attribute(new_settings, settings, CONF_ATTR_802_WIRELESS)
|
_merge_settings_attribute(new_settings, settings, CONF_ATTR_802_WIRELESS)
|
||||||
@@ -192,47 +215,69 @@ class NetworkSetting(DBusInterface):
|
|||||||
# See: https://developer-old.gnome.org/NetworkManager/stable/ch01.html
|
# See: https://developer-old.gnome.org/NetworkManager/stable/ch01.html
|
||||||
if CONF_ATTR_CONNECTION in data:
|
if CONF_ATTR_CONNECTION in data:
|
||||||
self._connection = ConnectionProperties(
|
self._connection = ConnectionProperties(
|
||||||
data[CONF_ATTR_CONNECTION].get(ATTR_ID),
|
data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_ID),
|
||||||
data[CONF_ATTR_CONNECTION].get(ATTR_UUID),
|
data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_UUID),
|
||||||
data[CONF_ATTR_CONNECTION].get(ATTR_TYPE),
|
data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_TYPE),
|
||||||
data[CONF_ATTR_CONNECTION].get(ATTR_INTERFACE_NAME),
|
data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_INTERFACE_NAME),
|
||||||
)
|
)
|
||||||
|
|
||||||
if CONF_ATTR_802_ETHERNET in data:
|
if CONF_ATTR_802_ETHERNET in data:
|
||||||
self._ethernet = EthernetProperties(
|
self._ethernet = EthernetProperties(
|
||||||
data[CONF_ATTR_802_ETHERNET].get(ATTR_ASSIGNED_MAC),
|
data[CONF_ATTR_802_ETHERNET].get(CONF_ATTR_802_ETHERNET_ASSIGNED_MAC),
|
||||||
)
|
)
|
||||||
|
|
||||||
if CONF_ATTR_802_WIRELESS in data:
|
if CONF_ATTR_802_WIRELESS in data:
|
||||||
self._wireless = WirelessProperties(
|
self._wireless = WirelessProperties(
|
||||||
bytes(data[CONF_ATTR_802_WIRELESS].get(ATTR_SSID, [])).decode(),
|
bytes(
|
||||||
data[CONF_ATTR_802_WIRELESS].get(ATTR_ASSIGNED_MAC),
|
data[CONF_ATTR_802_WIRELESS].get(CONF_ATTR_802_WIRELESS_SSID, [])
|
||||||
data[CONF_ATTR_802_WIRELESS].get(ATTR_MODE),
|
).decode(),
|
||||||
data[CONF_ATTR_802_WIRELESS].get(ATTR_POWERSAVE),
|
data[CONF_ATTR_802_WIRELESS].get(CONF_ATTR_802_WIRELESS_ASSIGNED_MAC),
|
||||||
|
data[CONF_ATTR_802_WIRELESS].get(CONF_ATTR_802_WIRELESS_MODE),
|
||||||
|
data[CONF_ATTR_802_WIRELESS].get(CONF_ATTR_802_WIRELESS_POWERSAVE),
|
||||||
)
|
)
|
||||||
|
|
||||||
if CONF_ATTR_802_WIRELESS_SECURITY in data:
|
if CONF_ATTR_802_WIRELESS_SECURITY in data:
|
||||||
self._wireless_security = WirelessSecurityProperties(
|
self._wireless_security = WirelessSecurityProperties(
|
||||||
data[CONF_ATTR_802_WIRELESS_SECURITY].get(ATTR_AUTH_ALG),
|
data[CONF_ATTR_802_WIRELESS_SECURITY].get(
|
||||||
data[CONF_ATTR_802_WIRELESS_SECURITY].get(ATTR_KEY_MGMT),
|
CONF_ATTR_802_WIRELESS_SECURITY_AUTH_ALG
|
||||||
data[CONF_ATTR_802_WIRELESS_SECURITY].get(ATTR_PSK),
|
),
|
||||||
|
data[CONF_ATTR_802_WIRELESS_SECURITY].get(
|
||||||
|
CONF_ATTR_802_WIRELESS_SECURITY_KEY_MGMT
|
||||||
|
),
|
||||||
|
data[CONF_ATTR_802_WIRELESS_SECURITY].get(
|
||||||
|
CONF_ATTR_802_WIRELESS_SECURITY_PSK
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if CONF_ATTR_VLAN in data:
|
if CONF_ATTR_VLAN in data:
|
||||||
self._vlan = VlanProperties(
|
self._vlan = VlanProperties(
|
||||||
data[CONF_ATTR_VLAN].get(ATTR_ID),
|
data[CONF_ATTR_VLAN].get(CONF_ATTR_VLAN_ID),
|
||||||
data[CONF_ATTR_VLAN].get(ATTR_PARENT),
|
data[CONF_ATTR_VLAN].get(CONF_ATTR_VLAN_PARENT),
|
||||||
)
|
)
|
||||||
|
|
||||||
if CONF_ATTR_IPV4 in data:
|
if CONF_ATTR_IPV4 in data:
|
||||||
|
address_data = None
|
||||||
|
if ips := data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_ADDRESS_DATA):
|
||||||
|
address_data = [IpAddress(ip["address"], ip["prefix"]) for ip in ips]
|
||||||
self._ipv4 = IpProperties(
|
self._ipv4 = IpProperties(
|
||||||
data[CONF_ATTR_IPV4].get(ATTR_METHOD),
|
data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_METHOD),
|
||||||
|
address_data,
|
||||||
|
data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_GATEWAY),
|
||||||
|
data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_DNS),
|
||||||
)
|
)
|
||||||
|
|
||||||
if CONF_ATTR_IPV6 in data:
|
if CONF_ATTR_IPV6 in data:
|
||||||
|
address_data = None
|
||||||
|
if ips := data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_ADDRESS_DATA):
|
||||||
|
address_data = [IpAddress(ip["address"], ip["prefix"]) for ip in ips]
|
||||||
self._ipv6 = IpProperties(
|
self._ipv6 = IpProperties(
|
||||||
data[CONF_ATTR_IPV6].get(ATTR_METHOD),
|
data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_METHOD),
|
||||||
|
address_data,
|
||||||
|
data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_GATEWAY),
|
||||||
|
data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_DNS),
|
||||||
)
|
)
|
||||||
|
|
||||||
if CONF_ATTR_MATCH in data:
|
if CONF_ATTR_MATCH in data:
|
||||||
self._match = MatchProperties(data[CONF_ATTR_MATCH].get(ATTR_PATH))
|
self._match = MatchProperties(
|
||||||
|
data[CONF_ATTR_MATCH].get(CONF_ATTR_MATCH_PATH)
|
||||||
|
)
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Payload generators for DBUS communication."""
|
"""Payload generators for DBUS communication."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
@@ -10,22 +11,128 @@ from dbus_fast import Variant
|
|||||||
from ....host.const import InterfaceMethod, InterfaceType
|
from ....host.const import InterfaceMethod, InterfaceType
|
||||||
from .. import NetworkManager
|
from .. import NetworkManager
|
||||||
from . import (
|
from . import (
|
||||||
ATTR_ASSIGNED_MAC,
|
|
||||||
CONF_ATTR_802_ETHERNET,
|
CONF_ATTR_802_ETHERNET,
|
||||||
|
CONF_ATTR_802_ETHERNET_ASSIGNED_MAC,
|
||||||
CONF_ATTR_802_WIRELESS,
|
CONF_ATTR_802_WIRELESS,
|
||||||
|
CONF_ATTR_802_WIRELESS_ASSIGNED_MAC,
|
||||||
|
CONF_ATTR_802_WIRELESS_MODE,
|
||||||
|
CONF_ATTR_802_WIRELESS_POWERSAVE,
|
||||||
CONF_ATTR_802_WIRELESS_SECURITY,
|
CONF_ATTR_802_WIRELESS_SECURITY,
|
||||||
|
CONF_ATTR_802_WIRELESS_SECURITY_AUTH_ALG,
|
||||||
|
CONF_ATTR_802_WIRELESS_SECURITY_KEY_MGMT,
|
||||||
|
CONF_ATTR_802_WIRELESS_SECURITY_PSK,
|
||||||
|
CONF_ATTR_802_WIRELESS_SSID,
|
||||||
CONF_ATTR_CONNECTION,
|
CONF_ATTR_CONNECTION,
|
||||||
|
CONF_ATTR_CONNECTION_AUTOCONNECT,
|
||||||
|
CONF_ATTR_CONNECTION_ID,
|
||||||
|
CONF_ATTR_CONNECTION_LLMNR,
|
||||||
|
CONF_ATTR_CONNECTION_MDNS,
|
||||||
|
CONF_ATTR_CONNECTION_TYPE,
|
||||||
|
CONF_ATTR_CONNECTION_UUID,
|
||||||
CONF_ATTR_IPV4,
|
CONF_ATTR_IPV4,
|
||||||
|
CONF_ATTR_IPV4_ADDRESS_DATA,
|
||||||
|
CONF_ATTR_IPV4_DNS,
|
||||||
|
CONF_ATTR_IPV4_GATEWAY,
|
||||||
|
CONF_ATTR_IPV4_METHOD,
|
||||||
CONF_ATTR_IPV6,
|
CONF_ATTR_IPV6,
|
||||||
|
CONF_ATTR_IPV6_ADDRESS_DATA,
|
||||||
|
CONF_ATTR_IPV6_DNS,
|
||||||
|
CONF_ATTR_IPV6_GATEWAY,
|
||||||
|
CONF_ATTR_IPV6_METHOD,
|
||||||
CONF_ATTR_MATCH,
|
CONF_ATTR_MATCH,
|
||||||
CONF_ATTR_PATH,
|
CONF_ATTR_MATCH_PATH,
|
||||||
CONF_ATTR_VLAN,
|
CONF_ATTR_VLAN,
|
||||||
|
CONF_ATTR_VLAN_ID,
|
||||||
|
CONF_ATTR_VLAN_PARENT,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ....host.configuration import Interface
|
from ....host.configuration import Interface
|
||||||
|
|
||||||
|
|
||||||
|
def _get_ipv4_connection_settings(ipv4setting) -> dict:
|
||||||
|
ipv4 = {}
|
||||||
|
if not ipv4setting or ipv4setting.method == InterfaceMethod.AUTO:
|
||||||
|
ipv4[CONF_ATTR_IPV4_METHOD] = Variant("s", "auto")
|
||||||
|
elif ipv4setting.method == InterfaceMethod.DISABLED:
|
||||||
|
ipv4[CONF_ATTR_IPV4_METHOD] = Variant("s", "disabled")
|
||||||
|
elif ipv4setting.method == InterfaceMethod.STATIC:
|
||||||
|
ipv4[CONF_ATTR_IPV4_METHOD] = Variant("s", "manual")
|
||||||
|
|
||||||
|
address_data = []
|
||||||
|
for address in ipv4setting.address:
|
||||||
|
address_data.append(
|
||||||
|
{
|
||||||
|
"address": Variant("s", str(address.ip)),
|
||||||
|
"prefix": Variant("u", int(address.with_prefixlen.split("/")[-1])),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ipv4[CONF_ATTR_IPV4_ADDRESS_DATA] = Variant("aa{sv}", address_data)
|
||||||
|
if ipv4setting.gateway:
|
||||||
|
ipv4[CONF_ATTR_IPV4_GATEWAY] = Variant("s", str(ipv4setting.gateway))
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Invalid IPv4 InterfaceMethod")
|
||||||
|
|
||||||
|
if (
|
||||||
|
ipv4setting
|
||||||
|
and ipv4setting.nameservers
|
||||||
|
and ipv4setting.method
|
||||||
|
in (
|
||||||
|
InterfaceMethod.AUTO,
|
||||||
|
InterfaceMethod.STATIC,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
nameservers = ipv4setting.nameservers if ipv4setting else []
|
||||||
|
ipv4[CONF_ATTR_IPV4_DNS] = Variant(
|
||||||
|
"au",
|
||||||
|
[socket.htonl(int(ip_address)) for ip_address in nameservers],
|
||||||
|
)
|
||||||
|
|
||||||
|
return ipv4
|
||||||
|
|
||||||
|
|
||||||
|
def _get_ipv6_connection_settings(ipv6setting) -> dict:
|
||||||
|
ipv6 = {}
|
||||||
|
if not ipv6setting or ipv6setting.method == InterfaceMethod.AUTO:
|
||||||
|
ipv6[CONF_ATTR_IPV6_METHOD] = Variant("s", "auto")
|
||||||
|
elif ipv6setting.method == InterfaceMethod.DISABLED:
|
||||||
|
ipv6[CONF_ATTR_IPV6_METHOD] = Variant("s", "link-local")
|
||||||
|
elif ipv6setting.method == InterfaceMethod.STATIC:
|
||||||
|
ipv6[CONF_ATTR_IPV6_METHOD] = Variant("s", "manual")
|
||||||
|
|
||||||
|
address_data = []
|
||||||
|
for address in ipv6setting.address:
|
||||||
|
address_data.append(
|
||||||
|
{
|
||||||
|
"address": Variant("s", str(address.ip)),
|
||||||
|
"prefix": Variant("u", int(address.with_prefixlen.split("/")[-1])),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ipv6[CONF_ATTR_IPV6_ADDRESS_DATA] = Variant("aa{sv}", address_data)
|
||||||
|
if ipv6setting.gateway:
|
||||||
|
ipv6[CONF_ATTR_IPV6_GATEWAY] = Variant("s", str(ipv6setting.gateway))
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Invalid IPv6 InterfaceMethod")
|
||||||
|
|
||||||
|
if (
|
||||||
|
ipv6setting
|
||||||
|
and ipv6setting.nameservers
|
||||||
|
and ipv6setting.method
|
||||||
|
in (
|
||||||
|
InterfaceMethod.AUTO,
|
||||||
|
InterfaceMethod.STATIC,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
nameservers = ipv6setting.nameservers if ipv6setting else []
|
||||||
|
ipv6[CONF_ATTR_IPV6_DNS] = Variant(
|
||||||
|
"aay",
|
||||||
|
[ip_address.packed for ip_address in nameservers],
|
||||||
|
)
|
||||||
|
return ipv6
|
||||||
|
|
||||||
|
|
||||||
def get_connection_from_interface(
|
def get_connection_from_interface(
|
||||||
interface: Interface,
|
interface: Interface,
|
||||||
network_manager: NetworkManager,
|
network_manager: NetworkManager,
|
||||||
@@ -37,8 +144,8 @@ def get_connection_from_interface(
|
|||||||
# Generate/Update ID/name
|
# Generate/Update ID/name
|
||||||
if not name or not name.startswith("Supervisor"):
|
if not name or not name.startswith("Supervisor"):
|
||||||
name = f"Supervisor {interface.name}"
|
name = f"Supervisor {interface.name}"
|
||||||
if interface.type == InterfaceType.VLAN:
|
if interface.type == InterfaceType.VLAN:
|
||||||
name = f"{name}.{interface.vlan.id}"
|
name = f"{name}.{interface.vlan.id}"
|
||||||
|
|
||||||
if interface.type == InterfaceType.ETHERNET:
|
if interface.type == InterfaceType.ETHERNET:
|
||||||
iftype = "802-3-ethernet"
|
iftype = "802-3-ethernet"
|
||||||
@@ -53,77 +160,31 @@ def get_connection_from_interface(
|
|||||||
|
|
||||||
conn: dict[str, dict[str, Variant]] = {
|
conn: dict[str, dict[str, Variant]] = {
|
||||||
CONF_ATTR_CONNECTION: {
|
CONF_ATTR_CONNECTION: {
|
||||||
"id": Variant("s", name),
|
CONF_ATTR_CONNECTION_ID: Variant("s", name),
|
||||||
"type": Variant("s", iftype),
|
CONF_ATTR_CONNECTION_UUID: Variant("s", uuid),
|
||||||
"uuid": Variant("s", uuid),
|
CONF_ATTR_CONNECTION_TYPE: Variant("s", iftype),
|
||||||
"llmnr": Variant("i", 2),
|
CONF_ATTR_CONNECTION_LLMNR: Variant("i", 2),
|
||||||
"mdns": Variant("i", 2),
|
CONF_ATTR_CONNECTION_MDNS: Variant("i", 2),
|
||||||
"autoconnect": Variant("b", True),
|
CONF_ATTR_CONNECTION_AUTOCONNECT: Variant("b", True),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if interface.type != InterfaceType.VLAN:
|
if interface.type != InterfaceType.VLAN:
|
||||||
if interface.path:
|
if interface.path:
|
||||||
conn[CONF_ATTR_MATCH] = {CONF_ATTR_PATH: Variant("as", [interface.path])}
|
conn[CONF_ATTR_MATCH] = {
|
||||||
|
CONF_ATTR_MATCH_PATH: Variant("as", [interface.path])
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
conn[CONF_ATTR_CONNECTION]["interface-name"] = Variant("s", interface.name)
|
conn[CONF_ATTR_CONNECTION]["interface-name"] = Variant("s", interface.name)
|
||||||
|
|
||||||
ipv4 = {}
|
conn[CONF_ATTR_IPV4] = _get_ipv4_connection_settings(interface.ipv4setting)
|
||||||
if not interface.ipv4 or interface.ipv4.method == InterfaceMethod.AUTO:
|
|
||||||
ipv4["method"] = Variant("s", "auto")
|
|
||||||
elif interface.ipv4.method == InterfaceMethod.DISABLED:
|
|
||||||
ipv4["method"] = Variant("s", "disabled")
|
|
||||||
else:
|
|
||||||
ipv4["method"] = Variant("s", "manual")
|
|
||||||
ipv4["dns"] = Variant(
|
|
||||||
"au",
|
|
||||||
[
|
|
||||||
socket.htonl(int(ip_address))
|
|
||||||
for ip_address in interface.ipv4.nameservers
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
adressdata = []
|
conn[CONF_ATTR_IPV6] = _get_ipv6_connection_settings(interface.ipv6setting)
|
||||||
for address in interface.ipv4.address:
|
|
||||||
adressdata.append(
|
|
||||||
{
|
|
||||||
"address": Variant("s", str(address.ip)),
|
|
||||||
"prefix": Variant("u", int(address.with_prefixlen.split("/")[-1])),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ipv4["address-data"] = Variant("aa{sv}", adressdata)
|
|
||||||
ipv4["gateway"] = Variant("s", str(interface.ipv4.gateway))
|
|
||||||
|
|
||||||
conn[CONF_ATTR_IPV4] = ipv4
|
|
||||||
|
|
||||||
ipv6 = {}
|
|
||||||
if not interface.ipv6 or interface.ipv6.method == InterfaceMethod.AUTO:
|
|
||||||
ipv6["method"] = Variant("s", "auto")
|
|
||||||
elif interface.ipv6.method == InterfaceMethod.DISABLED:
|
|
||||||
ipv6["method"] = Variant("s", "link-local")
|
|
||||||
else:
|
|
||||||
ipv6["method"] = Variant("s", "manual")
|
|
||||||
ipv6["dns"] = Variant(
|
|
||||||
"aay", [ip_address.packed for ip_address in interface.ipv6.nameservers]
|
|
||||||
)
|
|
||||||
|
|
||||||
adressdata = []
|
|
||||||
for address in interface.ipv6.address:
|
|
||||||
adressdata.append(
|
|
||||||
{
|
|
||||||
"address": Variant("s", str(address.ip)),
|
|
||||||
"prefix": Variant("u", int(address.with_prefixlen.split("/")[-1])),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ipv6["address-data"] = Variant("aa{sv}", adressdata)
|
|
||||||
ipv6["gateway"] = Variant("s", str(interface.ipv6.gateway))
|
|
||||||
|
|
||||||
conn[CONF_ATTR_IPV6] = ipv6
|
|
||||||
|
|
||||||
if interface.type == InterfaceType.ETHERNET:
|
if interface.type == InterfaceType.ETHERNET:
|
||||||
conn[CONF_ATTR_802_ETHERNET] = {ATTR_ASSIGNED_MAC: Variant("s", "preserve")}
|
conn[CONF_ATTR_802_ETHERNET] = {
|
||||||
|
CONF_ATTR_802_ETHERNET_ASSIGNED_MAC: Variant("s", "preserve")
|
||||||
|
}
|
||||||
elif interface.type == "vlan":
|
elif interface.type == "vlan":
|
||||||
parent = interface.vlan.interface
|
parent = interface.vlan.interface
|
||||||
if parent in network_manager and (
|
if parent in network_manager and (
|
||||||
@@ -132,30 +193,44 @@ def get_connection_from_interface(
|
|||||||
parent = parent_connection.uuid
|
parent = parent_connection.uuid
|
||||||
|
|
||||||
conn[CONF_ATTR_VLAN] = {
|
conn[CONF_ATTR_VLAN] = {
|
||||||
"id": Variant("u", interface.vlan.id),
|
CONF_ATTR_VLAN_ID: Variant("u", interface.vlan.id),
|
||||||
"parent": Variant("s", parent),
|
CONF_ATTR_VLAN_PARENT: Variant("s", parent),
|
||||||
}
|
}
|
||||||
elif interface.type == InterfaceType.WIRELESS:
|
elif interface.type == InterfaceType.WIRELESS:
|
||||||
wireless = {
|
wireless = {
|
||||||
ATTR_ASSIGNED_MAC: Variant("s", "preserve"),
|
CONF_ATTR_802_WIRELESS_ASSIGNED_MAC: Variant("s", "preserve"),
|
||||||
"ssid": Variant("ay", interface.wifi.ssid.encode("UTF-8")),
|
CONF_ATTR_802_WIRELESS_MODE: Variant("s", "infrastructure"),
|
||||||
"mode": Variant("s", "infrastructure"),
|
CONF_ATTR_802_WIRELESS_POWERSAVE: Variant("i", 1),
|
||||||
"powersave": Variant("i", 1),
|
|
||||||
}
|
}
|
||||||
|
if interface.wifi and interface.wifi.ssid:
|
||||||
|
wireless[CONF_ATTR_802_WIRELESS_SSID] = Variant(
|
||||||
|
"ay", interface.wifi.ssid.encode("UTF-8")
|
||||||
|
)
|
||||||
|
|
||||||
conn[CONF_ATTR_802_WIRELESS] = wireless
|
conn[CONF_ATTR_802_WIRELESS] = wireless
|
||||||
|
|
||||||
if interface.wifi.auth != "open":
|
if interface.wifi and interface.wifi.auth != "open":
|
||||||
wireless["security"] = Variant("s", CONF_ATTR_802_WIRELESS_SECURITY)
|
wireless["security"] = Variant("s", CONF_ATTR_802_WIRELESS_SECURITY)
|
||||||
wireless_security = {}
|
wireless_security = {}
|
||||||
if interface.wifi.auth == "wep":
|
if interface.wifi.auth == "wep":
|
||||||
wireless_security["auth-alg"] = Variant("s", "open")
|
wireless_security[CONF_ATTR_802_WIRELESS_SECURITY_AUTH_ALG] = Variant(
|
||||||
wireless_security["key-mgmt"] = Variant("s", "none")
|
"s", "open"
|
||||||
|
)
|
||||||
|
wireless_security[CONF_ATTR_802_WIRELESS_SECURITY_KEY_MGMT] = Variant(
|
||||||
|
"s", "none"
|
||||||
|
)
|
||||||
elif interface.wifi.auth == "wpa-psk":
|
elif interface.wifi.auth == "wpa-psk":
|
||||||
wireless_security["auth-alg"] = Variant("s", "open")
|
wireless_security[CONF_ATTR_802_WIRELESS_SECURITY_AUTH_ALG] = Variant(
|
||||||
wireless_security["key-mgmt"] = Variant("s", "wpa-psk")
|
"s", "open"
|
||||||
|
)
|
||||||
|
wireless_security[CONF_ATTR_802_WIRELESS_SECURITY_KEY_MGMT] = Variant(
|
||||||
|
"s", "wpa-psk"
|
||||||
|
)
|
||||||
|
|
||||||
if interface.wifi.psk:
|
if interface.wifi.psk:
|
||||||
wireless_security["psk"] = Variant("s", interface.wifi.psk)
|
wireless_security[CONF_ATTR_802_WIRELESS_SECURITY_PSK] = Variant(
|
||||||
|
"s", interface.wifi.psk
|
||||||
|
)
|
||||||
conn[CONF_ATTR_802_WIRELESS_SECURITY] = wireless_security
|
conn[CONF_ATTR_802_WIRELESS_SECURITY] = wireless_security
|
||||||
|
|
||||||
return conn
|
return conn
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Network Manager implementation for DBUS."""
|
"""Network Manager implementation for DBUS."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Wireless object for Network Manager."""
|
"""Wireless object for Network Manager."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""D-Bus interface for systemd-resolved."""
|
"""D-Bus interface for systemd-resolved."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
@@ -20,6 +20,7 @@ from .const import (
|
|||||||
DBUS_ATTR_KERNEL_TIMESTAMP_MONOTONIC,
|
DBUS_ATTR_KERNEL_TIMESTAMP_MONOTONIC,
|
||||||
DBUS_ATTR_LOADER_TIMESTAMP_MONOTONIC,
|
DBUS_ATTR_LOADER_TIMESTAMP_MONOTONIC,
|
||||||
DBUS_ATTR_USERSPACE_TIMESTAMP_MONOTONIC,
|
DBUS_ATTR_USERSPACE_TIMESTAMP_MONOTONIC,
|
||||||
|
DBUS_ATTR_VIRTUALIZATION,
|
||||||
DBUS_ERR_SYSTEMD_NO_SUCH_UNIT,
|
DBUS_ERR_SYSTEMD_NO_SUCH_UNIT,
|
||||||
DBUS_IFACE_SYSTEMD_MANAGER,
|
DBUS_IFACE_SYSTEMD_MANAGER,
|
||||||
DBUS_NAME_SYSTEMD,
|
DBUS_NAME_SYSTEMD,
|
||||||
@@ -114,6 +115,12 @@ class Systemd(DBusInterfaceProxy):
|
|||||||
"""Return the boot timestamp."""
|
"""Return the boot timestamp."""
|
||||||
return self.properties[DBUS_ATTR_FINISH_TIMESTAMP]
|
return self.properties[DBUS_ATTR_FINISH_TIMESTAMP]
|
||||||
|
|
||||||
|
@property
|
||||||
|
@dbus_property
|
||||||
|
def virtualization(self) -> str:
|
||||||
|
"""Return virtualization hypervisor being used."""
|
||||||
|
return self.properties[DBUS_ATTR_VIRTUALIZATION]
|
||||||
|
|
||||||
@dbus_connected
|
@dbus_connected
|
||||||
async def reboot(self) -> None:
|
async def reboot(self) -> None:
|
||||||
"""Reboot host computer."""
|
"""Reboot host computer."""
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Interface to systemd-timedate over D-Bus."""
|
"""Interface to systemd-timedate over D-Bus."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Interface to UDisks2 over D-Bus."""
|
"""Interface to UDisks2 over D-Bus."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -15,12 +16,15 @@ from ...exceptions import (
|
|||||||
from ..const import (
|
from ..const import (
|
||||||
DBUS_ATTR_SUPPORTED_FILESYSTEMS,
|
DBUS_ATTR_SUPPORTED_FILESYSTEMS,
|
||||||
DBUS_ATTR_VERSION,
|
DBUS_ATTR_VERSION,
|
||||||
|
DBUS_IFACE_BLOCK,
|
||||||
|
DBUS_IFACE_DRIVE,
|
||||||
DBUS_IFACE_UDISKS2_MANAGER,
|
DBUS_IFACE_UDISKS2_MANAGER,
|
||||||
DBUS_NAME_UDISKS2,
|
DBUS_NAME_UDISKS2,
|
||||||
DBUS_OBJECT_BASE,
|
DBUS_OBJECT_BASE,
|
||||||
DBUS_OBJECT_UDISKS2,
|
DBUS_OBJECT_UDISKS2,
|
||||||
|
DBUS_OBJECT_UDISKS2_MANAGER,
|
||||||
)
|
)
|
||||||
from ..interface import DBusInterfaceProxy, dbus_property
|
from ..interface import DBusInterface, DBusInterfaceProxy, dbus_property
|
||||||
from ..utils import dbus_connected
|
from ..utils import dbus_connected
|
||||||
from .block import UDisks2Block
|
from .block import UDisks2Block
|
||||||
from .const import UDISKS2_DEFAULT_OPTIONS
|
from .const import UDISKS2_DEFAULT_OPTIONS
|
||||||
@@ -30,7 +34,15 @@ from .drive import UDisks2Drive
|
|||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class UDisks2(DBusInterfaceProxy):
|
class UDisks2(DBusInterface):
|
||||||
|
"""Handle D-Bus interface for UDisks2 root object."""
|
||||||
|
|
||||||
|
name: str = DBUS_NAME_UDISKS2
|
||||||
|
bus_name: str = DBUS_NAME_UDISKS2
|
||||||
|
object_path: str = DBUS_OBJECT_UDISKS2
|
||||||
|
|
||||||
|
|
||||||
|
class UDisks2Manager(DBusInterfaceProxy):
|
||||||
"""Handle D-Bus interface for UDisks2.
|
"""Handle D-Bus interface for UDisks2.
|
||||||
|
|
||||||
http://storaged.org/doc/udisks2-api/latest/
|
http://storaged.org/doc/udisks2-api/latest/
|
||||||
@@ -38,22 +50,36 @@ class UDisks2(DBusInterfaceProxy):
|
|||||||
|
|
||||||
name: str = DBUS_NAME_UDISKS2
|
name: str = DBUS_NAME_UDISKS2
|
||||||
bus_name: str = DBUS_NAME_UDISKS2
|
bus_name: str = DBUS_NAME_UDISKS2
|
||||||
object_path: str = DBUS_OBJECT_UDISKS2
|
object_path: str = DBUS_OBJECT_UDISKS2_MANAGER
|
||||||
properties_interface: str = DBUS_IFACE_UDISKS2_MANAGER
|
properties_interface: str = DBUS_IFACE_UDISKS2_MANAGER
|
||||||
|
|
||||||
_block_devices: dict[str, UDisks2Block] = {}
|
_block_devices: dict[str, UDisks2Block] = {}
|
||||||
_drives: dict[str, UDisks2Drive] = {}
|
_drives: dict[str, UDisks2Drive] = {}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize object."""
|
||||||
|
super().__init__()
|
||||||
|
self.udisks2_object_manager = UDisks2()
|
||||||
|
|
||||||
async def connect(self, bus: MessageBus):
|
async def connect(self, bus: MessageBus):
|
||||||
"""Connect to D-Bus."""
|
"""Connect to D-Bus."""
|
||||||
try:
|
try:
|
||||||
await super().connect(bus)
|
await super().connect(bus)
|
||||||
|
await self.udisks2_object_manager.connect(bus)
|
||||||
except DBusError:
|
except DBusError:
|
||||||
_LOGGER.warning("Can't connect to udisks2")
|
_LOGGER.warning("Can't connect to udisks2")
|
||||||
except (DBusServiceUnkownError, DBusInterfaceError):
|
except (DBusServiceUnkownError, DBusInterfaceError):
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"No udisks2 support on the host. Host control has been disabled."
|
"No udisks2 support on the host. Host control has been disabled."
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# Register for signals on devices added/removed
|
||||||
|
self.udisks2_object_manager.dbus.object_manager.on_interfaces_added(
|
||||||
|
self._interfaces_added
|
||||||
|
)
|
||||||
|
self.udisks2_object_manager.dbus.object_manager.on_interfaces_removed(
|
||||||
|
self._interfaces_removed
|
||||||
|
)
|
||||||
|
|
||||||
@dbus_connected
|
@dbus_connected
|
||||||
async def update(self, changed: dict[str, Any] | None = None) -> None:
|
async def update(self, changed: dict[str, Any] | None = None) -> None:
|
||||||
@@ -161,11 +187,47 @@ class UDisks2(DBusInterfaceProxy):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _interfaces_added(
|
||||||
|
self, object_path: str, properties: dict[str, dict[str, Any]]
|
||||||
|
) -> None:
|
||||||
|
"""Interfaces added to a UDisks2 object."""
|
||||||
|
if object_path in self._block_devices:
|
||||||
|
await self._block_devices[object_path].update()
|
||||||
|
return
|
||||||
|
if object_path in self._drives:
|
||||||
|
await self._drives[object_path].update()
|
||||||
|
return
|
||||||
|
|
||||||
|
if DBUS_IFACE_BLOCK in properties:
|
||||||
|
self._block_devices[object_path] = await UDisks2Block.new(
|
||||||
|
object_path, self.dbus.bus
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if DBUS_IFACE_DRIVE in properties:
|
||||||
|
self._drives[object_path] = await UDisks2Drive.new(
|
||||||
|
object_path, self.dbus.bus
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _interfaces_removed(
|
||||||
|
self, object_path: str, interfaces: list[str]
|
||||||
|
) -> None:
|
||||||
|
"""Interfaces removed from a UDisks2 object."""
|
||||||
|
if object_path in self._block_devices and DBUS_IFACE_BLOCK in interfaces:
|
||||||
|
self._block_devices[object_path].shutdown()
|
||||||
|
del self._block_devices[object_path]
|
||||||
|
return
|
||||||
|
|
||||||
|
if object_path in self._drives and DBUS_IFACE_DRIVE in interfaces:
|
||||||
|
self._drives[object_path].shutdown()
|
||||||
|
del self._drives[object_path]
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
def shutdown(self) -> None:
|
||||||
"""Shutdown the object and disconnect from D-Bus.
|
"""Shutdown the object and disconnect from D-Bus.
|
||||||
|
|
||||||
This method is irreversible.
|
This method is irreversible.
|
||||||
"""
|
"""
|
||||||
|
self.udisks2_object_manager.shutdown()
|
||||||
for block_device in self.block_devices:
|
for block_device in self.block_devices:
|
||||||
block_device.shutdown()
|
block_device.shutdown()
|
||||||
for drive in self.drives:
|
for drive in self.drives:
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Interface to UDisks2 Block Device over D-Bus."""
|
"""Interface to UDisks2 Block Device over D-Bus."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Handle discover message for Home Assistant."""
|
"""Handle discover message for Home Assistant."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor add-on Docker object."""
|
"""Init file for Supervisor add-on Docker object."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
@@ -641,11 +642,11 @@ class DockerAddon(DockerInterface):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Pull Docker image or build it."""
|
"""Pull Docker image or build it."""
|
||||||
if need_build is None and self.addon.need_build or need_build:
|
if need_build is None and self.addon.need_build or need_build:
|
||||||
await self._build(version)
|
await self._build(version, image)
|
||||||
else:
|
else:
|
||||||
await super().install(version, image, latest, arch)
|
await super().install(version, image, latest, arch)
|
||||||
|
|
||||||
async def _build(self, version: AwesomeVersion) -> None:
|
async def _build(self, version: AwesomeVersion, image: str | None = None) -> None:
|
||||||
"""Build a Docker container."""
|
"""Build a Docker container."""
|
||||||
build_env = AddonBuild(self.coresys, self.addon)
|
build_env = AddonBuild(self.coresys, self.addon)
|
||||||
if not build_env.is_valid:
|
if not build_env.is_valid:
|
||||||
@@ -657,7 +658,7 @@ class DockerAddon(DockerInterface):
|
|||||||
image, log = await self.sys_run_in_executor(
|
image, log = await self.sys_run_in_executor(
|
||||||
self.sys_docker.images.build,
|
self.sys_docker.images.build,
|
||||||
use_config_proxy=False,
|
use_config_proxy=False,
|
||||||
**build_env.get_docker_args(version),
|
**build_env.get_docker_args(version, image),
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER.debug("Build %s:%s done: %s", self.image, version, log)
|
_LOGGER.debug("Build %s:%s done: %s", self.image, version, log)
|
||||||
@@ -708,6 +709,28 @@ class DockerAddon(DockerInterface):
|
|||||||
with suppress(DockerError):
|
with suppress(DockerError):
|
||||||
await self.cleanup()
|
await self.cleanup()
|
||||||
|
|
||||||
|
@Job(name="docker_addon_cleanup", limit=JobExecutionLimit.GROUP_WAIT)
|
||||||
|
async def cleanup(
|
||||||
|
self,
|
||||||
|
old_image: str | None = None,
|
||||||
|
image: str | None = None,
|
||||||
|
version: AwesomeVersion | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Check if old version exists and cleanup other versions of image not in use."""
|
||||||
|
await self.sys_run_in_executor(
|
||||||
|
self.sys_docker.cleanup_old_images,
|
||||||
|
(image := image or self.image),
|
||||||
|
version or self.version,
|
||||||
|
{old_image} if old_image else None,
|
||||||
|
keep_images={
|
||||||
|
f"{addon.image}:{addon.version}"
|
||||||
|
for addon in self.sys_addons.installed
|
||||||
|
if addon.slug != self.addon.slug
|
||||||
|
and addon.image
|
||||||
|
and addon.image in {old_image, image}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
name="docker_addon_write_stdin",
|
name="docker_addon_write_stdin",
|
||||||
limit=JobExecutionLimit.GROUP_ONCE,
|
limit=JobExecutionLimit.GROUP_ONCE,
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Audio docker object."""
|
"""Audio docker object."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""HA Cli docker object."""
|
"""HA Cli docker object."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Docker constants."""
|
"""Docker constants."""
|
||||||
|
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
|
||||||
from docker.types import Mount
|
from docker.types import Mount
|
||||||
@@ -74,6 +75,7 @@ MOUNT_DBUS = Mount(
|
|||||||
type=MountType.BIND, source="/run/dbus", target="/run/dbus", read_only=True
|
type=MountType.BIND, source="/run/dbus", target="/run/dbus", read_only=True
|
||||||
)
|
)
|
||||||
MOUNT_DEV = Mount(type=MountType.BIND, source="/dev", target="/dev", read_only=True)
|
MOUNT_DEV = Mount(type=MountType.BIND, source="/dev", target="/dev", read_only=True)
|
||||||
|
MOUNT_DEV.setdefault("BindOptions", {})["ReadOnlyNonRecursive"] = True
|
||||||
MOUNT_DOCKER = Mount(
|
MOUNT_DOCKER = Mount(
|
||||||
type=MountType.BIND,
|
type=MountType.BIND,
|
||||||
source="/run/docker.sock",
|
source="/run/docker.sock",
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""DNS docker object."""
|
"""DNS docker object."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from docker.types import Mount
|
from docker.types import Mount
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor Docker object."""
|
"""Init file for Supervisor Docker object."""
|
||||||
|
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
import logging
|
import logging
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Interface class for Supervisor Docker object."""
|
"""Interface class for Supervisor Docker object."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@@ -14,6 +15,7 @@ from awesomeversion import AwesomeVersion
|
|||||||
from awesomeversion.strategy import AwesomeVersionStrategy
|
from awesomeversion.strategy import AwesomeVersionStrategy
|
||||||
import docker
|
import docker
|
||||||
from docker.models.containers import Container
|
from docker.models.containers import Container
|
||||||
|
from docker.models.images import Image
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
@@ -427,17 +429,57 @@ class DockerInterface(JobGroup):
|
|||||||
limit=JobExecutionLimit.GROUP_ONCE,
|
limit=JobExecutionLimit.GROUP_ONCE,
|
||||||
on_condition=DockerJobError,
|
on_condition=DockerJobError,
|
||||||
)
|
)
|
||||||
async def remove(self) -> None:
|
async def remove(self, *, remove_image: bool = True) -> None:
|
||||||
"""Remove Docker images."""
|
"""Remove Docker images."""
|
||||||
# Cleanup container
|
# Cleanup container
|
||||||
with suppress(DockerError):
|
with suppress(DockerError):
|
||||||
await self.stop()
|
await self.stop()
|
||||||
|
|
||||||
await self.sys_run_in_executor(
|
if remove_image:
|
||||||
self.sys_docker.remove_image, self.image, self.version
|
await self.sys_run_in_executor(
|
||||||
)
|
self.sys_docker.remove_image, self.image, self.version
|
||||||
|
)
|
||||||
|
|
||||||
self._meta = None
|
self._meta = None
|
||||||
|
|
||||||
|
@Job(
|
||||||
|
name="docker_interface_check_image",
|
||||||
|
limit=JobExecutionLimit.GROUP_ONCE,
|
||||||
|
on_condition=DockerJobError,
|
||||||
|
)
|
||||||
|
async def check_image(
|
||||||
|
self,
|
||||||
|
version: AwesomeVersion,
|
||||||
|
expected_image: str,
|
||||||
|
expected_arch: CpuArch | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Check we have expected image with correct arch."""
|
||||||
|
expected_arch = expected_arch or self.sys_arch.supervisor
|
||||||
|
image_name = f"{expected_image}:{version!s}"
|
||||||
|
if self.image == expected_image:
|
||||||
|
try:
|
||||||
|
image: Image = await self.sys_run_in_executor(
|
||||||
|
self.sys_docker.images.get, image_name
|
||||||
|
)
|
||||||
|
except (docker.errors.DockerException, requests.RequestException) as err:
|
||||||
|
raise DockerError(
|
||||||
|
f"Could not get {image_name} for check due to: {err!s}",
|
||||||
|
_LOGGER.error,
|
||||||
|
) from err
|
||||||
|
|
||||||
|
image_arch = f"{image.attrs['Os']}/{image.attrs['Architecture']}"
|
||||||
|
if "Variant" in image.attrs:
|
||||||
|
image_arch = f"{image_arch}/{image.attrs['Variant']}"
|
||||||
|
|
||||||
|
# If we have an image and its the right arch, all set
|
||||||
|
if MAP_ARCH[expected_arch] == image_arch:
|
||||||
|
return
|
||||||
|
|
||||||
|
# We're missing the image we need. Stop and clean up what we have then pull the right one
|
||||||
|
with suppress(DockerError):
|
||||||
|
await self.remove()
|
||||||
|
await self.install(version, expected_image, arch=expected_arch)
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
name="docker_interface_update",
|
name="docker_interface_update",
|
||||||
limit=JobExecutionLimit.GROUP_ONCE,
|
limit=JobExecutionLimit.GROUP_ONCE,
|
||||||
@@ -470,14 +512,14 @@ class DockerInterface(JobGroup):
|
|||||||
return b""
|
return b""
|
||||||
|
|
||||||
@Job(name="docker_interface_cleanup", limit=JobExecutionLimit.GROUP_WAIT)
|
@Job(name="docker_interface_cleanup", limit=JobExecutionLimit.GROUP_WAIT)
|
||||||
def cleanup(
|
async def cleanup(
|
||||||
self,
|
self,
|
||||||
old_image: str | None = None,
|
old_image: str | None = None,
|
||||||
image: str | None = None,
|
image: str | None = None,
|
||||||
version: AwesomeVersion | None = None,
|
version: AwesomeVersion | None = None,
|
||||||
) -> Awaitable[None]:
|
) -> None:
|
||||||
"""Check if old version exists and cleanup."""
|
"""Check if old version exists and cleanup."""
|
||||||
return self.sys_run_in_executor(
|
await self.sys_run_in_executor(
|
||||||
self.sys_docker.cleanup_old_images,
|
self.sys_docker.cleanup_old_images,
|
||||||
image or self.image,
|
image or self.image,
|
||||||
version or self.version,
|
version or self.version,
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Manager for Supervisor Docker."""
|
"""Manager for Supervisor Docker."""
|
||||||
|
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
import logging
|
import logging
|
||||||
@@ -177,6 +178,11 @@ class DockerAPI:
|
|||||||
if dns:
|
if dns:
|
||||||
kwargs["dns"] = [str(self.network.dns)]
|
kwargs["dns"] = [str(self.network.dns)]
|
||||||
kwargs["dns_search"] = [DNS_SUFFIX]
|
kwargs["dns_search"] = [DNS_SUFFIX]
|
||||||
|
# CoreDNS forward plug-in fails in ~6s, then fallback triggers.
|
||||||
|
# However, the default timeout of glibc and musl is 5s. Increase
|
||||||
|
# default timeout to make sure CoreDNS fallback is working
|
||||||
|
# on first query.
|
||||||
|
kwargs["dns_opt"] = ["timeout:10"]
|
||||||
if hostname:
|
if hostname:
|
||||||
kwargs["domainname"] = DNS_SUFFIX
|
kwargs["domainname"] = DNS_SUFFIX
|
||||||
|
|
||||||
@@ -542,10 +548,13 @@ class DockerAPI:
|
|||||||
current_image: str,
|
current_image: str,
|
||||||
current_version: AwesomeVersion,
|
current_version: AwesomeVersion,
|
||||||
old_images: set[str] | None = None,
|
old_images: set[str] | None = None,
|
||||||
|
*,
|
||||||
|
keep_images: set[str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Clean up old versions of an image."""
|
"""Clean up old versions of an image."""
|
||||||
|
image = f"{current_image}:{current_version!s}"
|
||||||
try:
|
try:
|
||||||
current: Image = self.images.get(f"{current_image}:{current_version!s}")
|
keep: set[str] = {self.images.get(image).id}
|
||||||
except ImageNotFound:
|
except ImageNotFound:
|
||||||
raise DockerNotFound(
|
raise DockerNotFound(
|
||||||
f"{current_image} not found for cleanup", _LOGGER.warning
|
f"{current_image} not found for cleanup", _LOGGER.warning
|
||||||
@@ -555,6 +564,19 @@ class DockerAPI:
|
|||||||
f"Can't get {current_image} for cleanup", _LOGGER.warning
|
f"Can't get {current_image} for cleanup", _LOGGER.warning
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
|
if keep_images:
|
||||||
|
keep_images -= {image}
|
||||||
|
try:
|
||||||
|
for image in keep_images:
|
||||||
|
# If its not found, no need to preserve it from getting removed
|
||||||
|
with suppress(ImageNotFound):
|
||||||
|
keep.add(self.images.get(image).id)
|
||||||
|
except (DockerException, requests.RequestException) as err:
|
||||||
|
raise DockerError(
|
||||||
|
f"Failed to get one or more images from {keep} during cleanup",
|
||||||
|
_LOGGER.warning,
|
||||||
|
) from err
|
||||||
|
|
||||||
# Cleanup old and current
|
# Cleanup old and current
|
||||||
image_names = list(
|
image_names = list(
|
||||||
old_images | {current_image} if old_images else {current_image}
|
old_images | {current_image} if old_images else {current_image}
|
||||||
@@ -567,7 +589,7 @@ class DockerAPI:
|
|||||||
) from err
|
) from err
|
||||||
|
|
||||||
for image in images_list:
|
for image in images_list:
|
||||||
if current.id == image.id:
|
if image.id in keep:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
with suppress(DockerException, requests.RequestException):
|
with suppress(DockerException, requests.RequestException):
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""HA Cli docker object."""
|
"""HA Cli docker object."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Internal network manager for Supervisor."""
|
"""Internal network manager for Supervisor."""
|
||||||
|
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
import logging
|
import logging
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Observer docker object."""
|
"""Observer docker object."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ..const import DOCKER_NETWORK_MASK
|
from ..const import DOCKER_NETWORK_MASK
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Calc and represent docker stats data."""
|
"""Calc and represent docker stats data."""
|
||||||
|
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor Docker object."""
|
"""Init file for Supervisor Docker object."""
|
||||||
|
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
import logging
|
import logging
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user