mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-10-09 19:59:34 +00:00
Compare commits
101 Commits
2024.08.0
...
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 |
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -38,6 +38,7 @@
|
||||
- This PR is related to issue:
|
||||
- Link to documentation pull request:
|
||||
- Link to cli pull request:
|
||||
- Link to client library pull request:
|
||||
|
||||
## Checklist
|
||||
|
||||
@@ -55,9 +56,11 @@
|
||||
- [ ] The code has been formatted using Ruff (`ruff format supervisor tests`)
|
||||
- [ ] 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]
|
||||
- [ ] [CLI][cli-repository] updated (if necessary)
|
||||
- [ ] [Client library][client-library-repository] updated (if necessary)
|
||||
|
||||
<!--
|
||||
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
|
||||
[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/
|
||||
|
18
.github/workflows/builder.yml
vendored
18
.github/workflows/builder.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
requirements: ${{ steps.requirements.outputs.changed }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -125,15 +125,15 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Install Cosign
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: sigstore/cosign-installer@v3.5.0
|
||||
uses: sigstore/cosign-installer@v3.7.0
|
||||
with:
|
||||
cosign-release: "v2.2.3"
|
||||
cosign-release: "v2.4.0"
|
||||
|
||||
- name: Install dirhash and calc hash
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV
|
||||
|
||||
- name: Build supervisor
|
||||
uses: home-assistant/builder@2024.03.5
|
||||
uses: home-assistant/builder@2024.08.2
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
|
||||
- name: Initialize git
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
@@ -203,11 +203,11 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
|
||||
- name: Build the Supervisor
|
||||
if: needs.init.outputs.publish != 'true'
|
||||
uses: home-assistant/builder@2024.03.5
|
||||
uses: home-assistant/builder@2024.08.2
|
||||
with:
|
||||
args: |
|
||||
--test \
|
||||
|
71
.github/workflows/ci.yaml
vendored
71
.github/workflows/ci.yaml
vendored
@@ -25,15 +25,15 @@ jobs:
|
||||
name: Prepare Python dependencies
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.1.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
pip install -r requirements.txt -r requirements_tests.txt
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.1.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
lookup-only: true
|
||||
@@ -67,15 +67,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.1.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.1.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: |
|
||||
@@ -110,15 +110,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.1.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.1.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: |
|
||||
@@ -153,7 +153,7 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
- name: Register hadolint problem matcher
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
||||
@@ -168,15 +168,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.1.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.1.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: |
|
||||
@@ -212,15 +212,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.1.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -232,7 +232,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.1.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
key: |
|
||||
@@ -256,15 +256,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.1.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -288,19 +288,19 @@ jobs:
|
||||
name: Run tests Python ${{ needs.prepare.outputs.python-version }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.5.0
|
||||
uses: sigstore/cosign-installer@v3.7.0
|
||||
with:
|
||||
cosign-release: "v2.2.3"
|
||||
cosign-release: "v2.4.0"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.1.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -313,7 +313,7 @@ jobs:
|
||||
- name: Install additional system dependencies
|
||||
run: |
|
||||
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
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/python.json"
|
||||
@@ -335,10 +335,11 @@ jobs:
|
||||
-o console_output_style=count \
|
||||
tests
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v4.3.5
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}
|
||||
path: .coverage
|
||||
include-hidden-files: true
|
||||
|
||||
coverage:
|
||||
name: Process test coverage
|
||||
@@ -346,15 +347,15 @@ jobs:
|
||||
needs: ["pytest", "prepare"]
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v5.1.1
|
||||
uses: actions/setup-python@v5.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.1.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -373,4 +374,4 @@ jobs:
|
||||
coverage report
|
||||
coverage xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4.5.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
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
2
.github/workflows/sentry.yaml
vendored
2
.github/workflows/sentry.yaml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.2.1
|
||||
- name: Sentry Release
|
||||
uses: getsentry/action-release@v1.7.0
|
||||
env:
|
||||
|
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.2.1
|
||||
rev: v0.5.7
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
|
@@ -30,3 +30,5 @@ Releases are done in 3 stages (channels) with this structure:
|
||||
|
||||
[development]: https://developers.home-assistant.io/docs/supervisor/development
|
||||
[stable]: https://github.com/home-assistant/version/blob/master/stable.json
|
||||
|
||||
[](https://www.openhomefoundation.org/)
|
||||
|
@@ -12,7 +12,7 @@ cosign:
|
||||
base_identity: https://github.com/home-assistant/docker-base/.*
|
||||
identity: https://github.com/home-assistant/supervisor/.*
|
||||
args:
|
||||
COSIGN_VERSION: 2.2.3
|
||||
COSIGN_VERSION: 2.4.0
|
||||
labels:
|
||||
io.hass.type: supervisor
|
||||
org.opencontainers.image.title: Home Assistant Supervisor
|
||||
|
@@ -215,6 +215,9 @@ expected-line-ending-format = "LF"
|
||||
[tool.pylint.EXCEPTIONS]
|
||||
overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"]
|
||||
|
||||
[tool.pylint.DESIGN]
|
||||
max-positional-arguments = 10
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
norecursedirs = [".git"]
|
||||
@@ -228,7 +231,7 @@ filterwarnings = [
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
select = [
|
||||
lint.select = [
|
||||
"B002", # Python does not support the unary prefix increment
|
||||
"B007", # Loop control variable {name} not used within loop body
|
||||
"B014", # Exception handler with duplicate exception
|
||||
@@ -291,7 +294,7 @@ select = [
|
||||
"W", # pycodestyle
|
||||
]
|
||||
|
||||
ignore = [
|
||||
lint.ignore = [
|
||||
"D202", # No blank lines allowed after function docstring
|
||||
"D203", # 1 blank line required before class docstring
|
||||
"D213", # Multi-line docstring summary should start at the second line
|
||||
@@ -338,16 +341,16 @@ ignore = [
|
||||
"PLE0605",
|
||||
]
|
||||
|
||||
[tool.ruff.flake8-import-conventions.extend-aliases]
|
||||
[tool.ruff.lint.flake8-import-conventions.extend-aliases]
|
||||
voluptuous = "vol"
|
||||
|
||||
[tool.ruff.flake8-pytest-style]
|
||||
[tool.ruff.lint.flake8-pytest-style]
|
||||
fixture-parentheses = false
|
||||
|
||||
[tool.ruff.flake8-tidy-imports.banned-api]
|
||||
[tool.ruff.lint.flake8-tidy-imports.banned-api]
|
||||
"pytz".msg = "use zoneinfo instead"
|
||||
|
||||
[tool.ruff.isort]
|
||||
[tool.ruff.lint.isort]
|
||||
force-sort-within-sections = true
|
||||
section-order = [
|
||||
"future",
|
||||
@@ -361,10 +364,10 @@ known-first-party = ["supervisor", "tests"]
|
||||
combine-as-imports = true
|
||||
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
|
||||
"tests/dbus_service_mocks/*.py" = ["F722", "F821", "N815"]
|
||||
|
||||
[tool.ruff.mccabe]
|
||||
[tool.ruff.lint.mccabe]
|
||||
max-complexity = 25
|
||||
|
@@ -1,30 +1,29 @@
|
||||
aiodns==3.2.0
|
||||
aiohttp==3.9.5
|
||||
aiohttp-fast-url-dispatcher==0.3.1
|
||||
aiohttp==3.10.10
|
||||
atomicwrites-homeassistant==1.4.1
|
||||
attrs==24.1.0
|
||||
attrs==24.2.0
|
||||
awesomeversion==24.6.0
|
||||
brotli==1.1.0
|
||||
ciso8601==2.3.1
|
||||
colorlog==6.8.2
|
||||
cpe==1.2.1
|
||||
cryptography==43.0.0
|
||||
debugpy==1.8.2
|
||||
deepmerge==1.1.1
|
||||
cpe==1.3.1
|
||||
cryptography==43.0.1
|
||||
debugpy==1.8.7
|
||||
deepmerge==2.0
|
||||
dirhash==0.5.0
|
||||
docker==7.1.0
|
||||
faust-cchardet==2.1.19
|
||||
gitpython==3.1.43
|
||||
jinja2==3.1.4
|
||||
orjson==3.9.15
|
||||
pulsectl==24.4.0
|
||||
orjson==3.10.7
|
||||
pulsectl==24.8.0
|
||||
pyudev==0.24.3
|
||||
PyYAML==6.0.1
|
||||
PyYAML==6.0.2
|
||||
requests==2.32.3
|
||||
securetar==2024.2.1
|
||||
sentry-sdk==2.10.0
|
||||
setuptools==72.1.0
|
||||
sentry-sdk==2.16.0
|
||||
setuptools==75.1.0
|
||||
voluptuous==0.15.2
|
||||
dbus-fast==2.22.1
|
||||
dbus-fast==2.24.3
|
||||
typing_extensions==4.12.2
|
||||
zlib-fast==0.2.0
|
||||
|
@@ -1,12 +1,12 @@
|
||||
coverage==7.6.1
|
||||
pre-commit==3.8.0
|
||||
pylint==3.2.6
|
||||
coverage==7.6.3
|
||||
pre-commit==4.0.1
|
||||
pylint==3.3.1
|
||||
pytest-aiohttp==1.0.5
|
||||
pytest-asyncio==0.23.6
|
||||
pytest-cov==5.0.0
|
||||
pytest-timeout==2.3.1
|
||||
pytest==8.3.2
|
||||
ruff==0.5.5
|
||||
time-machine==2.14.2
|
||||
pytest==8.3.3
|
||||
ruff==0.6.9
|
||||
time-machine==2.16.0
|
||||
typing_extensions==4.12.2
|
||||
urllib3==2.2.2
|
||||
urllib3==2.2.3
|
||||
|
1
setup.py
1
setup.py
@@ -1,4 +1,5 @@
|
||||
"""Home Assistant Supervisor setup."""
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Main file for Supervisor."""
|
||||
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import logging
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor add-ons."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
from contextlib import suppress
|
||||
@@ -46,6 +47,8 @@ from ..const import (
|
||||
ATTR_SLUG,
|
||||
ATTR_STATE,
|
||||
ATTR_SYSTEM,
|
||||
ATTR_SYSTEM_MANAGED,
|
||||
ATTR_SYSTEM_MANAGED_CONFIG_ENTRY,
|
||||
ATTR_TYPE,
|
||||
ATTR_USER,
|
||||
ATTR_UUID,
|
||||
@@ -54,6 +57,7 @@ from ..const import (
|
||||
ATTR_WATCHDOG,
|
||||
DNS_SUFFIX,
|
||||
AddonBoot,
|
||||
AddonBootConfig,
|
||||
AddonStartup,
|
||||
AddonState,
|
||||
BusEvent,
|
||||
@@ -308,7 +312,9 @@ class Addon(AddonModel):
|
||||
|
||||
@property
|
||||
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)
|
||||
|
||||
@boot.setter
|
||||
@@ -363,6 +369,37 @@ class Addon(AddonModel):
|
||||
else:
|
||||
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
|
||||
def uuid(self) -> str:
|
||||
"""Return an API token for this add-on."""
|
||||
@@ -729,10 +766,12 @@ class Addon(AddonModel):
|
||||
limit=JobExecutionLimit.GROUP_ONCE,
|
||||
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."""
|
||||
try:
|
||||
await self.instance.remove()
|
||||
await self.instance.remove(remove_image=remove_image)
|
||||
except DockerError as err:
|
||||
raise AddonsError() from err
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Supervisor add-on build environment."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import cached_property
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Add-on static data."""
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import StrEnum
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor add-on data."""
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Supervisor add-on manager."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
from contextlib import suppress
|
||||
@@ -184,7 +185,15 @@ class AddonManager(CoreSysAttributes):
|
||||
_LOGGER.warning("Add-on %s is not installed", slug)
|
||||
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)
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor add-ons."""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
from collections.abc import Awaitable, Callable
|
||||
@@ -82,6 +83,7 @@ from ..const import (
|
||||
SECURITY_DISABLE,
|
||||
SECURITY_PROFILE,
|
||||
AddonBoot,
|
||||
AddonBootConfig,
|
||||
AddonStage,
|
||||
AddonStartup,
|
||||
)
|
||||
@@ -149,10 +151,15 @@ class AddonModel(JobGroup, ABC):
|
||||
return self.data[ATTR_OPTIONS]
|
||||
|
||||
@property
|
||||
def boot(self) -> AddonBoot:
|
||||
"""Return boot config with prio local settings."""
|
||||
def boot_config(self) -> AddonBootConfig:
|
||||
"""Return boot config."""
|
||||
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
|
||||
def auto_update(self) -> bool | None:
|
||||
"""Return if auto update is enable."""
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Add-on Options / UI rendering."""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Util add-ons functions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Validate add-ons options schema."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
import secrets
|
||||
@@ -78,6 +79,8 @@ from ..const import (
|
||||
ATTR_STATE,
|
||||
ATTR_STDIN,
|
||||
ATTR_SYSTEM,
|
||||
ATTR_SYSTEM_MANAGED,
|
||||
ATTR_SYSTEM_MANAGED_CONFIG_ENTRY,
|
||||
ATTR_TIMEOUT,
|
||||
ATTR_TMPFS,
|
||||
ATTR_TRANSLATIONS,
|
||||
@@ -95,6 +98,7 @@ from ..const import (
|
||||
ROLE_ALL,
|
||||
ROLE_DEFAULT,
|
||||
AddonBoot,
|
||||
AddonBootConfig,
|
||||
AddonStage,
|
||||
AddonStartup,
|
||||
AddonState,
|
||||
@@ -318,7 +322,9 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
vol.Optional(ATTR_STARTUP, default=AddonStartup.APPLICATION): vol.Coerce(
|
||||
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_ADVANCED, default=False): vol.Boolean(),
|
||||
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_INGRESS_PANEL, 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,
|
||||
)
|
||||
|
@@ -1,11 +1,11 @@
|
||||
"""Init file for Supervisor RESTful API."""
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp_fast_url_dispatcher import FastUrlDispatcher, attach_fast_url_dispatcher
|
||||
|
||||
from ..const import AddonState
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
@@ -67,7 +67,6 @@ class RestAPI(CoreSysAttributes):
|
||||
"max_field_size": MAX_LINE_SIZE,
|
||||
},
|
||||
)
|
||||
attach_fast_url_dispatcher(self.webapp, FastUrlDispatcher())
|
||||
|
||||
# service stuff
|
||||
self._runner: web.AppRunner = web.AppRunner(self.webapp, shutdown_timeout=5)
|
||||
@@ -510,6 +509,7 @@ class RestAPI(CoreSysAttributes):
|
||||
web.post("/addons/{addon}/stop", api_addons.stop),
|
||||
web.post("/addons/{addon}/restart", api_addons.restart),
|
||||
web.post("/addons/{addon}/options", api_addons.options),
|
||||
web.post("/addons/{addon}/sys_options", api_addons.sys_options),
|
||||
web.post(
|
||||
"/addons/{addon}/options/validate", api_addons.options_validate
|
||||
),
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor Home Assistant RESTful API."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
import logging
|
||||
@@ -81,6 +82,8 @@ from ..const import (
|
||||
ATTR_STARTUP,
|
||||
ATTR_STATE,
|
||||
ATTR_STDIN,
|
||||
ATTR_SYSTEM_MANAGED,
|
||||
ATTR_SYSTEM_MANAGED_CONFIG_ENTRY,
|
||||
ATTR_TRANSLATIONS,
|
||||
ATTR_UART,
|
||||
ATTR_UDEV,
|
||||
@@ -95,6 +98,7 @@ from ..const import (
|
||||
ATTR_WEBUI,
|
||||
REQUEST_FROM,
|
||||
AddonBoot,
|
||||
AddonBootConfig,
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..docker.stats import DockerStats
|
||||
@@ -106,7 +110,7 @@ from ..exceptions import (
|
||||
PwnedSecret,
|
||||
)
|
||||
from ..validate import docker_ports
|
||||
from .const import ATTR_REMOVE_CONFIG, ATTR_SIGNED
|
||||
from .const import ATTR_BOOT_CONFIG, ATTR_REMOVE_CONFIG, ATTR_SIGNED
|
||||
from .utils import api_process, api_validate, json_loads
|
||||
|
||||
_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_UNINSTALL = vol.Schema(
|
||||
@@ -178,6 +189,7 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_URL: addon.url,
|
||||
ATTR_ICON: addon.with_icon,
|
||||
ATTR_LOGO: addon.with_logo,
|
||||
ATTR_SYSTEM_MANAGED: addon.system_managed,
|
||||
}
|
||||
for addon in self.sys_addons.installed
|
||||
]
|
||||
@@ -206,6 +218,7 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_VERSION_LATEST: addon.latest_version,
|
||||
ATTR_PROTECTED: addon.protected,
|
||||
ATTR_RATING: rating_security(addon),
|
||||
ATTR_BOOT_CONFIG: addon.boot_config,
|
||||
ATTR_BOOT: addon.boot,
|
||||
ATTR_OPTIONS: addon.options,
|
||||
ATTR_SCHEMA: addon.schema_ui,
|
||||
@@ -265,6 +278,8 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_WATCHDOG: addon.watchdog,
|
||||
ATTR_DEVICES: addon.static_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
|
||||
@@ -287,6 +302,10 @@ class APIAddons(CoreSysAttributes):
|
||||
if ATTR_OPTIONS in body:
|
||||
addon.options = body[ATTR_OPTIONS]
|
||||
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]
|
||||
if ATTR_AUTO_UPDATE in body:
|
||||
addon.auto_update = body[ATTR_AUTO_UPDATE]
|
||||
@@ -304,6 +323,20 @@ class APIAddons(CoreSysAttributes):
|
||||
|
||||
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
|
||||
async def options_validate(self, request: web.Request) -> None:
|
||||
"""Validate user options for add-on."""
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor Audio RESTful API."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
from dataclasses import asdict
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor auth/SSO RESTful API."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Backups RESTful API."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import errno
|
||||
@@ -342,9 +343,9 @@ class APIBackups(CoreSysAttributes):
|
||||
_LOGGER.info("Downloading backup %s", backup.slug)
|
||||
response = web.FileResponse(backup.tarfile)
|
||||
response.content_type = CONTENT_TYPE_TAR
|
||||
response.headers[
|
||||
CONTENT_DISPOSITION
|
||||
] = f"attachment; filename={RE_SLUGIFY_NAME.sub('_', backup.name)}.tar"
|
||||
response.headers[CONTENT_DISPOSITION] = (
|
||||
f"attachment; filename={RE_SLUGIFY_NAME.sub('_', backup.name)}.tar"
|
||||
)
|
||||
return response
|
||||
|
||||
@api_process
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor HA cli RESTful API."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
@@ -17,6 +17,7 @@ ATTR_APPARMOR_VERSION = "apparmor_version"
|
||||
ATTR_ATTRIBUTES = "attributes"
|
||||
ATTR_AVAILABLE_UPDATES = "available_updates"
|
||||
ATTR_BACKGROUND = "background"
|
||||
ATTR_BOOT_CONFIG = "boot_config"
|
||||
ATTR_BOOT_SLOT = "boot_slot"
|
||||
ATTR_BOOT_SLOTS = "boot_slots"
|
||||
ATTR_BOOT_TIMESTAMP = "boot_timestamp"
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor network RESTful API."""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor DNS RESTful API."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
import logging
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor Home Assistant RESTful API."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor hardware RESTful API."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
|
@@ -61,7 +61,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
IDENTIFIER = "identifier"
|
||||
BOOTID = "bootid"
|
||||
DEFAULT_RANGE = 100
|
||||
DEFAULT_LINES = 100
|
||||
|
||||
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): str})
|
||||
|
||||
@@ -222,13 +222,30 @@ class APIHost(CoreSysAttributes):
|
||||
"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
|
||||
|
||||
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)
|
||||
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(
|
||||
params=params, range_header=range_header, accept=LogFormat.JOURNAL
|
||||
@@ -236,8 +253,13 @@ class APIHost(CoreSysAttributes):
|
||||
try:
|
||||
response = web.StreamResponse()
|
||||
response.content_type = CONTENT_TYPE_TEXT
|
||||
await response.prepare(request)
|
||||
async for line in journal_logs_reader(resp, log_formatter):
|
||||
headers_returned = False
|
||||
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")
|
||||
except ConnectionResetError as ex:
|
||||
raise APIError(
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Supervisor Add-on ingress service."""
|
||||
|
||||
import asyncio
|
||||
from ipaddress import ip_address
|
||||
import logging
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor Jobs RESTful API."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Handle security part of this API."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
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 awesomeversion import AwesomeVersion
|
||||
|
||||
from supervisor.homeassistant.const import LANDINGPAGE
|
||||
|
||||
from ...addons.const import RE_SLUG
|
||||
from ...const import (
|
||||
REQUEST_FROM,
|
||||
@@ -77,6 +80,13 @@ ADDONS_API_BYPASS: Final = re.compile(
|
||||
r")$"
|
||||
)
|
||||
|
||||
# Home Assistant only
|
||||
CORE_ONLY_PATHS: Final = re.compile(
|
||||
r"^(?:"
|
||||
r"/addons/" + RE_SLUG + "/sys_options"
|
||||
r")$"
|
||||
)
|
||||
|
||||
# Policy role add-on API access
|
||||
ADDONS_ROLE_ACCESS: dict[str, re.Pattern] = {
|
||||
ROLE_DEFAULT: re.compile(
|
||||
@@ -232,6 +242,9 @@ class SecurityMiddleware(CoreSysAttributes):
|
||||
if supervisor_token == self.sys_homeassistant.supervisor_token:
|
||||
_LOGGER.debug("%s access from Home Assistant", request.path)
|
||||
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
|
||||
if supervisor_token == self.sys_plugins.cli.supervisor_token:
|
||||
@@ -277,8 +290,10 @@ class SecurityMiddleware(CoreSysAttributes):
|
||||
@middleware
|
||||
async def core_proxy(self, request: Request, handler: RequestHandler) -> Response:
|
||||
"""Validate user from Core API proxy."""
|
||||
if request[REQUEST_FROM] != self.sys_homeassistant or version_is_new_enough(
|
||||
self.sys_homeassistant.version, _CORE_VERSION
|
||||
if (
|
||||
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)
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor Multicast RESTful API."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
import logging
|
||||
|
@@ -1,8 +1,8 @@
|
||||
"""REST API for network."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
from dataclasses import replace
|
||||
from ipaddress import ip_address, ip_interface
|
||||
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import web
|
||||
@@ -48,18 +48,28 @@ from ..host.configuration import (
|
||||
Interface,
|
||||
InterfaceMethod,
|
||||
IpConfig,
|
||||
IpSetting,
|
||||
VlanConfig,
|
||||
WifiConfig,
|
||||
)
|
||||
from ..host.const import AuthMethod, InterfaceType, WifiMode
|
||||
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_GATEWAY): vol.Coerce(ip_address),
|
||||
vol.Optional(ATTR_NAMESERVERS): [vol.Coerce(ip_address)],
|
||||
vol.Optional(ATTR_GATEWAY): vol.Coerce(IPv4Address),
|
||||
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
|
||||
SCHEMA_UPDATE = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_IPV4): _SCHEMA_IP_CONFIG,
|
||||
vol.Optional(ATTR_IPV6): _SCHEMA_IP_CONFIG,
|
||||
vol.Optional(ATTR_IPV4): _SCHEMA_IPV4_CONFIG,
|
||||
vol.Optional(ATTR_IPV6): _SCHEMA_IPV6_CONFIG,
|
||||
vol.Optional(ATTR_WIFI): _SCHEMA_WIFI_CONFIG,
|
||||
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 {
|
||||
ATTR_METHOD: config.method,
|
||||
ATTR_METHOD: setting.method,
|
||||
ATTR_ADDRESS: [address.with_prefixlen for address in config.address],
|
||||
ATTR_NAMESERVERS: [str(address) for address in config.nameservers],
|
||||
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_PRIMARY: interface.primary,
|
||||
ATTR_MAC: interface.mac,
|
||||
ATTR_IPV4: ipconfig_struct(interface.ipv4) if interface.ipv4 else None,
|
||||
ATTR_IPV6: ipconfig_struct(interface.ipv6) if interface.ipv6 else None,
|
||||
ATTR_IPV4: ipconfig_struct(interface.ipv4, interface.ipv4setting),
|
||||
ATTR_IPV6: ipconfig_struct(interface.ipv6, interface.ipv6setting),
|
||||
ATTR_WIFI: wifi_struct(interface.wifi) if interface.wifi else None,
|
||||
ATTR_VLAN: vlan_struct(interface.vlan) if interface.vlan else None,
|
||||
}
|
||||
@@ -197,24 +207,26 @@ class APINetwork(CoreSysAttributes):
|
||||
# Apply config
|
||||
for key, config in body.items():
|
||||
if key == ATTR_IPV4:
|
||||
interface.ipv4 = replace(
|
||||
interface.ipv4
|
||||
or IpConfig(InterfaceMethod.STATIC, [], None, [], None),
|
||||
**config,
|
||||
interface.ipv4setting = IpSetting(
|
||||
config.get(ATTR_METHOD, InterfaceMethod.STATIC),
|
||||
config.get(ATTR_ADDRESS, []),
|
||||
config.get(ATTR_GATEWAY),
|
||||
config.get(ATTR_NAMESERVERS, []),
|
||||
)
|
||||
elif key == ATTR_IPV6:
|
||||
interface.ipv6 = replace(
|
||||
interface.ipv6
|
||||
or IpConfig(InterfaceMethod.STATIC, [], None, [], None),
|
||||
**config,
|
||||
interface.ipv6setting = IpSetting(
|
||||
config.get(ATTR_METHOD, InterfaceMethod.STATIC),
|
||||
config.get(ATTR_ADDRESS, []),
|
||||
config.get(ATTR_GATEWAY),
|
||||
config.get(ATTR_NAMESERVERS, []),
|
||||
)
|
||||
elif key == ATTR_WIFI:
|
||||
interface.wifi = replace(
|
||||
interface.wifi
|
||||
or WifiConfig(
|
||||
WifiMode.INFRASTRUCTURE, "", AuthMethod.OPEN, None, None
|
||||
),
|
||||
**config,
|
||||
interface.wifi = WifiConfig(
|
||||
config.get(ATTR_MODE, WifiMode.INFRASTRUCTURE),
|
||||
config.get(ATTR_SSID, ""),
|
||||
config.get(ATTR_AUTH, AuthMethod.OPEN),
|
||||
config.get(ATTR_PSK, None),
|
||||
None,
|
||||
)
|
||||
elif key == ATTR_ENABLED:
|
||||
interface.enabled = config
|
||||
@@ -256,24 +268,22 @@ class APINetwork(CoreSysAttributes):
|
||||
|
||||
vlan_config = VlanConfig(vlan, interface.name)
|
||||
|
||||
ipv4_config = None
|
||||
ipv4_setting = None
|
||||
if ATTR_IPV4 in body:
|
||||
ipv4_config = IpConfig(
|
||||
ipv4_setting = IpSetting(
|
||||
body[ATTR_IPV4].get(ATTR_METHOD, InterfaceMethod.AUTO),
|
||||
body[ATTR_IPV4].get(ATTR_ADDRESS, []),
|
||||
body[ATTR_IPV4].get(ATTR_GATEWAY, None),
|
||||
body[ATTR_IPV4].get(ATTR_NAMESERVERS, []),
|
||||
None,
|
||||
)
|
||||
|
||||
ipv6_config = None
|
||||
ipv6_setting = None
|
||||
if ATTR_IPV6 in body:
|
||||
ipv6_config = IpConfig(
|
||||
ipv6_setting = IpSetting(
|
||||
body[ATTR_IPV6].get(ATTR_METHOD, InterfaceMethod.AUTO),
|
||||
body[ATTR_IPV6].get(ATTR_ADDRESS, []),
|
||||
body[ATTR_IPV6].get(ATTR_GATEWAY, None),
|
||||
body[ATTR_IPV6].get(ATTR_NAMESERVERS, []),
|
||||
None,
|
||||
)
|
||||
|
||||
vlan_interface = Interface(
|
||||
@@ -284,8 +294,10 @@ class APINetwork(CoreSysAttributes):
|
||||
True,
|
||||
False,
|
||||
InterfaceType.VLAN,
|
||||
ipv4_config,
|
||||
ipv6_config,
|
||||
None,
|
||||
ipv4_setting,
|
||||
None,
|
||||
ipv6_setting,
|
||||
None,
|
||||
vlan_config,
|
||||
)
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor Observer RESTful API."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor HassOS RESTful API."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
import logging
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Utils for Home Assistant Proxy."""
|
||||
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Handle REST API for resoulution."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
from typing import Any
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor Root RESTful API."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor Security RESTful API."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor Home Assistant RESTful API."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
from typing import Any
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor Supervisor RESTful API."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
import logging
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor util for RESTful API."""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Handle Arch for underlay maschine/platforms."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import platform
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Manage SSO for Add-ons with Home Assistant user."""
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import logging
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Representation of a backup file."""
|
||||
|
||||
import asyncio
|
||||
from base64 import b64decode, b64encode
|
||||
from collections import defaultdict
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Backup consts."""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
BUF_SIZE = 2**20 * 4 # 4MB
|
||||
|
@@ -10,7 +10,10 @@ from pathlib import Path
|
||||
|
||||
from ..addons.addon import Addon
|
||||
from ..const import (
|
||||
ATTR_DATA,
|
||||
ATTR_DAYS_UNTIL_STALE,
|
||||
ATTR_SLUG,
|
||||
ATTR_TYPE,
|
||||
FILE_HASSIO_BACKUPS,
|
||||
FOLDER_HOMEASSISTANT,
|
||||
CoreState,
|
||||
@@ -21,7 +24,9 @@ from ..exceptions import (
|
||||
BackupInvalidError,
|
||||
BackupJobError,
|
||||
BackupMountDownError,
|
||||
HomeAssistantWSError,
|
||||
)
|
||||
from ..homeassistant.const import WSType
|
||||
from ..jobs.const import JOB_GROUP_BACKUP_MANAGER, JobCondition, JobExecutionLimit
|
||||
from ..jobs.decorator import Job
|
||||
from ..jobs.job_group import JobGroup
|
||||
@@ -299,6 +304,18 @@ class BackupManager(FileConfiguration, JobGroup):
|
||||
# Ignore exceptions from waiting for addon startup, addon errors handled elsewhere
|
||||
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
|
||||
finally:
|
||||
self.sys_core.state = CoreState.RUNNING
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Util add-on functions."""
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Validate some things around restore."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
@@ -1,4 +1,6 @@
|
||||
"""Bootstrap Supervisor."""
|
||||
|
||||
# ruff: noqa: T100
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Bus event system."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Bootstrap Supervisor."""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
import logging
|
||||
import os
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Constants file for Supervisor."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from ipaddress import ip_network
|
||||
@@ -309,6 +310,8 @@ ATTR_SUPERVISOR_VERSION = "supervisor_version"
|
||||
ATTR_SUPPORTED = "supported"
|
||||
ATTR_SUPPORTED_ARCH = "supported_arch"
|
||||
ATTR_SYSTEM = "system"
|
||||
ATTR_SYSTEM_MANAGED = "system_managed"
|
||||
ATTR_SYSTEM_MANAGED_CONFIG_ENTRY = "system_managed_config_entry"
|
||||
ATTR_TIMEOUT = "timeout"
|
||||
ATTR_TIMEZONE = "timezone"
|
||||
ATTR_TITLE = "title"
|
||||
@@ -379,12 +382,27 @@ ROLE_ADMIN = "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):
|
||||
"""Boot mode for the add-on."""
|
||||
|
||||
AUTO = "auto"
|
||||
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):
|
||||
"""Startup types of Add-on."""
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Main file for Supervisor."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
from contextlib import suppress
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Handle core shared data."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
@@ -62,7 +63,7 @@ class CoreSys:
|
||||
|
||||
# External objects
|
||||
self._loop: asyncio.BaseEventLoop = asyncio.get_running_loop()
|
||||
self._websession: aiohttp.ClientSession = aiohttp.ClientSession()
|
||||
self._websession = None
|
||||
|
||||
# Global objects
|
||||
self._config: CoreConfig = CoreConfig()
|
||||
@@ -95,10 +96,8 @@ class CoreSys:
|
||||
self._bus: Bus | None = None
|
||||
self._mounts: MountManager | None = None
|
||||
|
||||
# Set default header for aiohttp
|
||||
self._websession._default_headers = MappingProxyType(
|
||||
{aiohttp.hdrs.USER_AGENT: SERVER_SOFTWARE}
|
||||
)
|
||||
# Setup aiohttp session
|
||||
self.create_websession()
|
||||
|
||||
# Task factory attributes
|
||||
self._set_task_context: list[Callable[[Context], Context]] = []
|
||||
@@ -547,6 +546,16 @@ class CoreSys:
|
||||
|
||||
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:
|
||||
"""Create a new context for a task."""
|
||||
context = copy_context()
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""OS-Agent implementation for DBUS."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
import logging
|
||||
@@ -7,7 +8,7 @@ from typing import Any
|
||||
from awesomeversion import AwesomeVersion
|
||||
from dbus_fast.aio.message_bus import MessageBus
|
||||
|
||||
from ...exceptions import DBusError, DBusInterfaceError, DBusServiceUnkownError
|
||||
from ...exceptions import DBusInterfaceError, DBusServiceUnkownError
|
||||
from ..const import (
|
||||
DBUS_ATTR_DIAGNOSTICS,
|
||||
DBUS_ATTR_VERSION,
|
||||
@@ -95,13 +96,25 @@ class OSAgent(DBusInterfaceProxy):
|
||||
_LOGGER.info("Load dbus interface %s", self.name)
|
||||
try:
|
||||
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):
|
||||
_LOGGER.warning(
|
||||
_LOGGER.error(
|
||||
"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
|
||||
async def update(self, changed: dict[str, Any] | None = None) -> None:
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""AppArmor object for OS-Agent."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
|
@@ -1,9 +1,10 @@
|
||||
"""Board management for OS Agent."""
|
||||
|
||||
import logging
|
||||
|
||||
from dbus_fast.aio.message_bus import MessageBus
|
||||
|
||||
from ....exceptions import BoardInvalidError
|
||||
from ....exceptions import BoardInvalidError, DBusInterfaceError, DBusServiceUnkownError
|
||||
from ...const import (
|
||||
DBUS_ATTR_BOARD,
|
||||
DBUS_IFACE_HAOS_BOARDS,
|
||||
@@ -74,6 +75,10 @@ class BoardManager(DBusInterfaceProxy):
|
||||
self._board_proxy = Green()
|
||||
elif self.board == BOARD_NAME_SUPERVISED:
|
||||
self._board_proxy = Supervised()
|
||||
else:
|
||||
return
|
||||
|
||||
if self._board_proxy:
|
||||
try:
|
||||
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."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from supervisor.dbus.utils import dbus_connected
|
||||
|
||||
from .const import BOARD_NAME_SUPERVISED
|
||||
from .interface import BoardProxy
|
||||
|
||||
@@ -11,3 +15,11 @@ class Supervised(BoardProxy):
|
||||
"""Initialize properties."""
|
||||
super().__init__(BOARD_NAME_SUPERVISED)
|
||||
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."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ..const import (
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Constants for DBUS."""
|
||||
|
||||
from enum import IntEnum, StrEnum
|
||||
from socket import AF_INET, AF_INET6
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""D-Bus interface for hostname."""
|
||||
|
||||
import logging
|
||||
|
||||
from dbus_fast.aio.message_bus import MessageBus
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Interface class for D-Bus wrappers."""
|
||||
|
||||
from abc import ABC
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Interface to Logind over D-Bus."""
|
||||
|
||||
import logging
|
||||
|
||||
from dbus_fast.aio.message_bus import MessageBus
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""D-Bus interface objects."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
@@ -128,9 +129,11 @@ class DBusManager(CoreSysAttributes):
|
||||
|
||||
for err in errors:
|
||||
if err:
|
||||
dbus = self.all[errors.index(err)]
|
||||
_LOGGER.warning(
|
||||
"Can't load dbus interface %s: %s",
|
||||
self.all[errors.index(err)].name,
|
||||
"Can't load dbus interface %s %s: %s",
|
||||
dbus.name,
|
||||
dbus.object_path,
|
||||
err,
|
||||
)
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Network Manager implementation for DBUS."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""NetworkConnection objects for Network Manager."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
|
||||
@@ -58,11 +59,22 @@ class VlanProperties:
|
||||
parent: str | None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class IpAddress:
|
||||
"""IP address object for Network Manager."""
|
||||
|
||||
address: str
|
||||
prefix: int
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class IpProperties:
|
||||
"""IP properties object for Network Manager."""
|
||||
|
||||
method: str | None
|
||||
address_data: list[IpAddress] | None
|
||||
gateway: str | None
|
||||
dns: list[bytes | int] | None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Network Manager DNS Manager object."""
|
||||
|
||||
from ipaddress import ip_address
|
||||
import logging
|
||||
from typing import Any
|
||||
|
@@ -1,17 +1,18 @@
|
||||
"""Connection object for Network Manager."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from dbus_fast import Variant
|
||||
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 ...interface import DBusInterface
|
||||
from ...utils import dbus_connected
|
||||
from ..configuration import (
|
||||
ConnectionProperties,
|
||||
EthernetProperties,
|
||||
IpAddress,
|
||||
IpProperties,
|
||||
MatchProperties,
|
||||
VlanProperties,
|
||||
@@ -20,25 +21,46 @@ from ..configuration import (
|
||||
)
|
||||
|
||||
CONF_ATTR_CONNECTION = "connection"
|
||||
CONF_ATTR_MATCH = "match"
|
||||
CONF_ATTR_802_ETHERNET = "802-3-ethernet"
|
||||
CONF_ATTR_802_WIRELESS = "802-11-wireless"
|
||||
CONF_ATTR_802_WIRELESS_SECURITY = "802-11-wireless-security"
|
||||
CONF_ATTR_VLAN = "vlan"
|
||||
CONF_ATTR_IPV4 = "ipv4"
|
||||
CONF_ATTR_IPV6 = "ipv6"
|
||||
CONF_ATTR_MATCH = "match"
|
||||
CONF_ATTR_PATH = "path"
|
||||
|
||||
ATTR_ID = "id"
|
||||
ATTR_UUID = "uuid"
|
||||
ATTR_TYPE = "type"
|
||||
ATTR_PARENT = "parent"
|
||||
ATTR_ASSIGNED_MAC = "assigned-mac-address"
|
||||
ATTR_POWERSAVE = "powersave"
|
||||
ATTR_AUTH_ALG = "auth-alg"
|
||||
ATTR_KEY_MGMT = "key-mgmt"
|
||||
ATTR_INTERFACE_NAME = "interface-name"
|
||||
ATTR_PATH = "path"
|
||||
CONF_ATTR_CONNECTION_ID = "id"
|
||||
CONF_ATTR_CONNECTION_UUID = "uuid"
|
||||
CONF_ATTR_CONNECTION_TYPE = "type"
|
||||
CONF_ATTR_CONNECTION_LLMNR = "llmnr"
|
||||
CONF_ATTR_CONNECTION_MDNS = "mdns"
|
||||
CONF_ATTR_CONNECTION_AUTOCONNECT = "autoconnect"
|
||||
CONF_ATTR_CONNECTION_INTERFACE_NAME = "interface-name"
|
||||
|
||||
CONF_ATTR_MATCH_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 = [
|
||||
"addresses",
|
||||
@@ -74,7 +96,7 @@ def _merge_settings_attribute(
|
||||
class NetworkSetting(DBusInterface):
|
||||
"""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
|
||||
@@ -148,7 +170,7 @@ class NetworkSetting(DBusInterface):
|
||||
new_settings,
|
||||
settings,
|
||||
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_WIRELESS)
|
||||
@@ -193,47 +215,69 @@ class NetworkSetting(DBusInterface):
|
||||
# See: https://developer-old.gnome.org/NetworkManager/stable/ch01.html
|
||||
if CONF_ATTR_CONNECTION in data:
|
||||
self._connection = ConnectionProperties(
|
||||
data[CONF_ATTR_CONNECTION].get(ATTR_ID),
|
||||
data[CONF_ATTR_CONNECTION].get(ATTR_UUID),
|
||||
data[CONF_ATTR_CONNECTION].get(ATTR_TYPE),
|
||||
data[CONF_ATTR_CONNECTION].get(ATTR_INTERFACE_NAME),
|
||||
data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_ID),
|
||||
data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_UUID),
|
||||
data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_TYPE),
|
||||
data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_INTERFACE_NAME),
|
||||
)
|
||||
|
||||
if CONF_ATTR_802_ETHERNET in data:
|
||||
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:
|
||||
self._wireless = WirelessProperties(
|
||||
bytes(data[CONF_ATTR_802_WIRELESS].get(ATTR_SSID, [])).decode(),
|
||||
data[CONF_ATTR_802_WIRELESS].get(ATTR_ASSIGNED_MAC),
|
||||
data[CONF_ATTR_802_WIRELESS].get(ATTR_MODE),
|
||||
data[CONF_ATTR_802_WIRELESS].get(ATTR_POWERSAVE),
|
||||
bytes(
|
||||
data[CONF_ATTR_802_WIRELESS].get(CONF_ATTR_802_WIRELESS_SSID, [])
|
||||
).decode(),
|
||||
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:
|
||||
self._wireless_security = WirelessSecurityProperties(
|
||||
data[CONF_ATTR_802_WIRELESS_SECURITY].get(ATTR_AUTH_ALG),
|
||||
data[CONF_ATTR_802_WIRELESS_SECURITY].get(ATTR_KEY_MGMT),
|
||||
data[CONF_ATTR_802_WIRELESS_SECURITY].get(ATTR_PSK),
|
||||
data[CONF_ATTR_802_WIRELESS_SECURITY].get(
|
||||
CONF_ATTR_802_WIRELESS_SECURITY_AUTH_ALG
|
||||
),
|
||||
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:
|
||||
self._vlan = VlanProperties(
|
||||
data[CONF_ATTR_VLAN].get(ATTR_ID),
|
||||
data[CONF_ATTR_VLAN].get(ATTR_PARENT),
|
||||
data[CONF_ATTR_VLAN].get(CONF_ATTR_VLAN_ID),
|
||||
data[CONF_ATTR_VLAN].get(CONF_ATTR_VLAN_PARENT),
|
||||
)
|
||||
|
||||
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(
|
||||
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:
|
||||
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(
|
||||
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:
|
||||
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."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
@@ -10,22 +11,128 @@ from dbus_fast import Variant
|
||||
from ....host.const import InterfaceMethod, InterfaceType
|
||||
from .. import NetworkManager
|
||||
from . import (
|
||||
ATTR_ASSIGNED_MAC,
|
||||
CONF_ATTR_802_ETHERNET,
|
||||
CONF_ATTR_802_ETHERNET_ASSIGNED_MAC,
|
||||
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_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_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_ADDRESS_DATA,
|
||||
CONF_ATTR_IPV4_DNS,
|
||||
CONF_ATTR_IPV4_GATEWAY,
|
||||
CONF_ATTR_IPV4_METHOD,
|
||||
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_PATH,
|
||||
CONF_ATTR_MATCH_PATH,
|
||||
CONF_ATTR_VLAN,
|
||||
CONF_ATTR_VLAN_ID,
|
||||
CONF_ATTR_VLAN_PARENT,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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(
|
||||
interface: Interface,
|
||||
network_manager: NetworkManager,
|
||||
@@ -53,77 +160,31 @@ def get_connection_from_interface(
|
||||
|
||||
conn: dict[str, dict[str, Variant]] = {
|
||||
CONF_ATTR_CONNECTION: {
|
||||
"id": Variant("s", name),
|
||||
"type": Variant("s", iftype),
|
||||
"uuid": Variant("s", uuid),
|
||||
"llmnr": Variant("i", 2),
|
||||
"mdns": Variant("i", 2),
|
||||
"autoconnect": Variant("b", True),
|
||||
CONF_ATTR_CONNECTION_ID: Variant("s", name),
|
||||
CONF_ATTR_CONNECTION_UUID: Variant("s", uuid),
|
||||
CONF_ATTR_CONNECTION_TYPE: Variant("s", iftype),
|
||||
CONF_ATTR_CONNECTION_LLMNR: Variant("i", 2),
|
||||
CONF_ATTR_CONNECTION_MDNS: Variant("i", 2),
|
||||
CONF_ATTR_CONNECTION_AUTOCONNECT: Variant("b", True),
|
||||
},
|
||||
}
|
||||
|
||||
if interface.type != InterfaceType.VLAN:
|
||||
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:
|
||||
conn[CONF_ATTR_CONNECTION]["interface-name"] = Variant("s", interface.name)
|
||||
|
||||
ipv4 = {}
|
||||
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
|
||||
],
|
||||
)
|
||||
conn[CONF_ATTR_IPV4] = _get_ipv4_connection_settings(interface.ipv4setting)
|
||||
|
||||
adressdata = []
|
||||
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
|
||||
conn[CONF_ATTR_IPV6] = _get_ipv6_connection_settings(interface.ipv6setting)
|
||||
|
||||
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":
|
||||
parent = interface.vlan.interface
|
||||
if parent in network_manager and (
|
||||
@@ -132,30 +193,44 @@ def get_connection_from_interface(
|
||||
parent = parent_connection.uuid
|
||||
|
||||
conn[CONF_ATTR_VLAN] = {
|
||||
"id": Variant("u", interface.vlan.id),
|
||||
"parent": Variant("s", parent),
|
||||
CONF_ATTR_VLAN_ID: Variant("u", interface.vlan.id),
|
||||
CONF_ATTR_VLAN_PARENT: Variant("s", parent),
|
||||
}
|
||||
elif interface.type == InterfaceType.WIRELESS:
|
||||
wireless = {
|
||||
ATTR_ASSIGNED_MAC: Variant("s", "preserve"),
|
||||
"ssid": Variant("ay", interface.wifi.ssid.encode("UTF-8")),
|
||||
"mode": Variant("s", "infrastructure"),
|
||||
"powersave": Variant("i", 1),
|
||||
CONF_ATTR_802_WIRELESS_ASSIGNED_MAC: Variant("s", "preserve"),
|
||||
CONF_ATTR_802_WIRELESS_MODE: Variant("s", "infrastructure"),
|
||||
CONF_ATTR_802_WIRELESS_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
|
||||
|
||||
if interface.wifi.auth != "open":
|
||||
if interface.wifi and interface.wifi.auth != "open":
|
||||
wireless["security"] = Variant("s", CONF_ATTR_802_WIRELESS_SECURITY)
|
||||
wireless_security = {}
|
||||
if interface.wifi.auth == "wep":
|
||||
wireless_security["auth-alg"] = Variant("s", "open")
|
||||
wireless_security["key-mgmt"] = Variant("s", "none")
|
||||
wireless_security[CONF_ATTR_802_WIRELESS_SECURITY_AUTH_ALG] = Variant(
|
||||
"s", "open"
|
||||
)
|
||||
wireless_security[CONF_ATTR_802_WIRELESS_SECURITY_KEY_MGMT] = Variant(
|
||||
"s", "none"
|
||||
)
|
||||
elif interface.wifi.auth == "wpa-psk":
|
||||
wireless_security["auth-alg"] = Variant("s", "open")
|
||||
wireless_security["key-mgmt"] = Variant("s", "wpa-psk")
|
||||
wireless_security[CONF_ATTR_802_WIRELESS_SECURITY_AUTH_ALG] = Variant(
|
||||
"s", "open"
|
||||
)
|
||||
wireless_security[CONF_ATTR_802_WIRELESS_SECURITY_KEY_MGMT] = Variant(
|
||||
"s", "wpa-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
|
||||
|
||||
return conn
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Network Manager implementation for DBUS."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Wireless object for Network Manager."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""D-Bus interface for systemd-resolved."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Interface to systemd-timedate over D-Bus."""
|
||||
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Interface to UDisks2 over D-Bus."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Interface to UDisks2 Block Device over D-Bus."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Handle discover message for Home Assistant."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor add-on Docker object."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable
|
||||
@@ -708,6 +709,28 @@ class DockerAddon(DockerInterface):
|
||||
with suppress(DockerError):
|
||||
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(
|
||||
name="docker_addon_write_stdin",
|
||||
limit=JobExecutionLimit.GROUP_ONCE,
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Audio docker object."""
|
||||
|
||||
import logging
|
||||
|
||||
import docker
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""HA Cli docker object."""
|
||||
|
||||
import logging
|
||||
|
||||
from ..coresys import CoreSysAttributes
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Docker constants."""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
from docker.types import Mount
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""DNS docker object."""
|
||||
|
||||
import logging
|
||||
|
||||
from docker.types import Mount
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor Docker object."""
|
||||
|
||||
from collections.abc import Awaitable
|
||||
from ipaddress import IPv4Address
|
||||
import logging
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Interface class for Supervisor Docker object."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
@@ -428,15 +429,17 @@ class DockerInterface(JobGroup):
|
||||
limit=JobExecutionLimit.GROUP_ONCE,
|
||||
on_condition=DockerJobError,
|
||||
)
|
||||
async def remove(self) -> None:
|
||||
async def remove(self, *, remove_image: bool = True) -> None:
|
||||
"""Remove Docker images."""
|
||||
# Cleanup container
|
||||
with suppress(DockerError):
|
||||
await self.stop()
|
||||
|
||||
await self.sys_run_in_executor(
|
||||
self.sys_docker.remove_image, self.image, self.version
|
||||
)
|
||||
if remove_image:
|
||||
await self.sys_run_in_executor(
|
||||
self.sys_docker.remove_image, self.image, self.version
|
||||
)
|
||||
|
||||
self._meta = None
|
||||
|
||||
@Job(
|
||||
@@ -509,14 +512,14 @@ class DockerInterface(JobGroup):
|
||||
return b""
|
||||
|
||||
@Job(name="docker_interface_cleanup", limit=JobExecutionLimit.GROUP_WAIT)
|
||||
def cleanup(
|
||||
async def cleanup(
|
||||
self,
|
||||
old_image: str | None = None,
|
||||
image: str | None = None,
|
||||
version: AwesomeVersion | None = None,
|
||||
) -> Awaitable[None]:
|
||||
) -> None:
|
||||
"""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,
|
||||
image or self.image,
|
||||
version or self.version,
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Manager for Supervisor Docker."""
|
||||
|
||||
from contextlib import suppress
|
||||
from ipaddress import IPv4Address
|
||||
import logging
|
||||
@@ -547,10 +548,13 @@ class DockerAPI:
|
||||
current_image: str,
|
||||
current_version: AwesomeVersion,
|
||||
old_images: set[str] | None = None,
|
||||
*,
|
||||
keep_images: set[str] | None = None,
|
||||
) -> None:
|
||||
"""Clean up old versions of an image."""
|
||||
image = f"{current_image}:{current_version!s}"
|
||||
try:
|
||||
current: Image = self.images.get(f"{current_image}:{current_version!s}")
|
||||
keep: set[str] = {self.images.get(image).id}
|
||||
except ImageNotFound:
|
||||
raise DockerNotFound(
|
||||
f"{current_image} not found for cleanup", _LOGGER.warning
|
||||
@@ -560,6 +564,19 @@ class DockerAPI:
|
||||
f"Can't get {current_image} for cleanup", _LOGGER.warning
|
||||
) 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
|
||||
image_names = list(
|
||||
old_images | {current_image} if old_images else {current_image}
|
||||
@@ -572,7 +589,7 @@ class DockerAPI:
|
||||
) from err
|
||||
|
||||
for image in images_list:
|
||||
if current.id == image.id:
|
||||
if image.id in keep:
|
||||
continue
|
||||
|
||||
with suppress(DockerException, requests.RequestException):
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""HA Cli docker object."""
|
||||
|
||||
import logging
|
||||
|
||||
from ..coresys import CoreSysAttributes
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Internal network manager for Supervisor."""
|
||||
|
||||
from contextlib import suppress
|
||||
from ipaddress import IPv4Address
|
||||
import logging
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Observer docker object."""
|
||||
|
||||
import logging
|
||||
|
||||
from ..const import DOCKER_NETWORK_MASK
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Calc and represent docker stats data."""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Init file for Supervisor Docker object."""
|
||||
|
||||
from collections.abc import Awaitable
|
||||
from ipaddress import IPv4Address
|
||||
import logging
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Constants for hardware."""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Data representation of Hardware."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Read disk hardware info from system."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Read hardware info from system."""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user