mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-08-22 07:29:22 +00:00
Compare commits
43 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ad85fa29b6 | ||
![]() |
f57aeab9ae | ||
![]() |
383ea277b7 | ||
![]() |
a32d1668ee | ||
![]() |
e445a8aabf | ||
![]() |
0de190268f | ||
![]() |
9e5101aa39 | ||
![]() |
e2ac5042d8 | ||
![]() |
bfe1cb073c | ||
![]() |
3a1364dfcd | ||
![]() |
3f63414bb3 | ||
![]() |
8b3a09e5b8 | ||
![]() |
ca7dc8113b | ||
![]() |
6d2a603cf9 | ||
![]() |
d536ac8604 | ||
![]() |
c67317571c | ||
![]() |
d93def7f22 | ||
![]() |
20e45e3c00 | ||
![]() |
5758d42c91 | ||
![]() |
d2dc78ae6a | ||
![]() |
3fd3c02010 | ||
![]() |
a82b4aa6c8 | ||
![]() |
45e54d93c7 | ||
![]() |
435241bccf | ||
![]() |
1b8558ced3 | ||
![]() |
4339cae241 | ||
![]() |
4f2469fd98 | ||
![]() |
a90e8be6bc | ||
![]() |
dcaf36a8e5 | ||
![]() |
908df3b234 | ||
![]() |
1b445feaaa | ||
![]() |
c05504a069 | ||
![]() |
e37cee9818 | ||
![]() |
dd3a4a1f47 | ||
![]() |
b451e555d3 | ||
![]() |
5fb2b99917 | ||
![]() |
8984d4afd6 | ||
![]() |
7ae8dfe587 | ||
![]() |
c931a4c3e5 | ||
![]() |
c58fa816d9 | ||
![]() |
557f029aa0 | ||
![]() |
e8e3cc2f67 | ||
![]() |
b0e4983488 |
1
.github/release-drafter.yml
vendored
1
.github/release-drafter.yml
vendored
@@ -31,6 +31,7 @@ categories:
|
||||
|
||||
- title: ":arrow_up: Dependency Updates"
|
||||
label: "dependencies"
|
||||
collapse-after: 1
|
||||
|
||||
include-labels:
|
||||
- "breaking-change"
|
||||
|
78
.github/workflows/builder.yml
vendored
78
.github/workflows/builder.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
requirements: ${{ steps.requirements.outputs.changed }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -110,14 +110,14 @@ jobs:
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: docker/login-action@v1.13.0
|
||||
uses: docker/login-action@v1.14.1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: docker/login-action@v1.13.0
|
||||
uses: docker/login-action@v1.14.1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV
|
||||
|
||||
- name: Build supervisor
|
||||
uses: home-assistant/builder@2022.01.0
|
||||
uses: home-assistant/builder@2022.03.1
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@@ -145,13 +145,13 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: actions/setup-python@v2.3.2
|
||||
uses: actions/setup-python@v3.1.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -184,7 +184,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
|
||||
- name: Initialize git
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
@@ -209,11 +209,11 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
|
||||
- name: Build the Supervisor
|
||||
if: needs.init.outputs.publish != 'true'
|
||||
uses: home-assistant/builder@2022.01.0
|
||||
uses: home-assistant/builder@2022.03.1
|
||||
with:
|
||||
args: |
|
||||
--test \
|
||||
@@ -278,6 +278,12 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make sure it actually installed
|
||||
test=$(docker exec hassio_cli ha addons info core_ssh --no-progress --raw-json | jq -r '.data.version')
|
||||
if [[ "$test" == "null" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Start Core SSH Add-on"
|
||||
test=$(docker exec hassio_cli ha addons start core_ssh --no-progress --raw-json | jq -r '.result')
|
||||
if [ "$test" != "ok" ]; then
|
||||
@@ -311,6 +317,58 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create full backup
|
||||
id: backup
|
||||
run: |
|
||||
test=$(docker exec hassio_cli ha backups new --no-progress --raw-json)
|
||||
if [ "$(echo $test | jq -r '.result')" != "ok" ]; then
|
||||
exit 1
|
||||
fi
|
||||
echo "::set-output name=slug::$(echo $test | jq -r '.data.slug')"
|
||||
|
||||
- name: Uninstall SSH add-on
|
||||
run: |
|
||||
test=$(docker exec hassio_cli ha addons uninstall core_ssh --no-progress --raw-json | jq -r '.result')
|
||||
if [ "$test" != "ok" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Restart supervisor
|
||||
run: |
|
||||
test=$(docker exec hassio_cli ha supervisor restart --no-progress --raw-json | jq -r '.result')
|
||||
if [ "$test" != "ok" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Wait for Supervisor to come up
|
||||
run: |
|
||||
SUPERVISOR=$(docker inspect --format='{{.NetworkSettings.IPAddress}}' hassio_supervisor)
|
||||
ping="error"
|
||||
while [ "$ping" != "ok" ]; do
|
||||
ping=$(curl -sSL "http://$SUPERVISOR/supervisor/ping" | jq -r '.result')
|
||||
sleep 5
|
||||
done
|
||||
|
||||
- name: Restore SSH add-on from backup
|
||||
run: |
|
||||
test=$(docker exec hassio_cli ha backups restore ${{ steps.backup.outputs.slug }} --addons core_ssh --no-progress --raw-json | jq -r '.result')
|
||||
if [ "$test" != "ok" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make sure it actually installed
|
||||
test=$(docker exec hassio_cli ha addons info core_ssh --no-progress --raw-json | jq -r '.data.version')
|
||||
if [[ "$test" == "null" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Restore SSL directory from backup
|
||||
run: |
|
||||
test=$(docker exec hassio_cli ha backups restore ${{ steps.backup.outputs.slug }} --folders ssl --no-progress --raw-json | jq -r '.result')
|
||||
if [ "$test" != "ok" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Get supervisor logs on failiure
|
||||
if: ${{ cancelled() || failure() }}
|
||||
run: docker logs hassio_supervisor
|
||||
|
74
.github/workflows/ci.yaml
vendored
74
.github/workflows/ci.yaml
vendored
@@ -23,15 +23,15 @@ jobs:
|
||||
name: Prepare Python ${{ matrix.python-version }} dependencies
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v2.3.2
|
||||
uses: actions/setup-python@v3.1.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3.0.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
pip install -r requirements.txt -r requirements_tests.txt
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3.0.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -64,15 +64,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.3.2
|
||||
uses: actions/setup-python@v3.1.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3.0.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
- name: Register hadolint problem matcher
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
||||
@@ -108,15 +108,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.3.2
|
||||
uses: actions/setup-python@v3.1.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3.0.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3.0.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -152,15 +152,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.3.2
|
||||
uses: actions/setup-python@v3.1.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3.0.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -184,15 +184,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.3.2
|
||||
uses: actions/setup-python@v3.1.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3.0.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3.0.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -225,15 +225,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.3.2
|
||||
uses: actions/setup-python@v3.1.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3.0.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3.0.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -269,15 +269,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.3.2
|
||||
uses: actions/setup-python@v3.1.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3.0.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -301,15 +301,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.3.2
|
||||
uses: actions/setup-python@v3.1.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3.0.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -321,7 +321,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3.0.1
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -345,9 +345,9 @@ jobs:
|
||||
name: Run tests Python ${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2.3.2
|
||||
uses: actions/setup-python@v3.1.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
@@ -357,7 +357,7 @@ jobs:
|
||||
version: ${{ env.DEFAULT_CAS }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3.0.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -392,7 +392,7 @@ jobs:
|
||||
-o console_output_style=count \
|
||||
tests
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v2.3.1
|
||||
uses: actions/upload-artifact@v3.0.0
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}
|
||||
path: .coverage
|
||||
@@ -403,15 +403,15 @@ jobs:
|
||||
needs: pytest
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.3.2
|
||||
uses: actions/setup-python@v3.1.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3.0.1
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v3
|
||||
- uses: dessant/lock-threads@v3.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: "30"
|
||||
|
4
.github/workflows/release-drafter.yml
vendored
4
.github/workflows/release-drafter.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
name: Release Drafter
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2.4.0
|
||||
uses: actions/checkout@v3.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
echo "::set-output name=version::$datepre.$newpost"
|
||||
|
||||
- name: Run Release Drafter
|
||||
uses: release-drafter/release-drafter@v5
|
||||
uses: release-drafter/release-drafter@v5.19.0
|
||||
with:
|
||||
tag: ${{ steps.version.outputs.version }}
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
|
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@v2.4.0
|
||||
uses: actions/checkout@v3
|
||||
- name: Sentry Release
|
||||
uses: getsentry/action-release@v1.1.6
|
||||
env:
|
||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
- uses: actions/stale@v5.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 60
|
||||
|
@@ -2,23 +2,23 @@ aiohttp==3.8.1
|
||||
async_timeout==4.0.2
|
||||
atomicwrites==1.4.0
|
||||
attrs==21.4.0
|
||||
awesomeversion==22.2.0
|
||||
awesomeversion==22.4.0
|
||||
brotli==1.0.9
|
||||
cchardet==2.1.7
|
||||
ciso8601==2.2.0
|
||||
colorlog==6.6.0
|
||||
cpe==1.2.1
|
||||
cryptography==36.0.1
|
||||
debugpy==1.5.1
|
||||
cryptography==36.0.2
|
||||
debugpy==1.6.0
|
||||
deepmerge==1.0.1
|
||||
dirhash==0.2.1
|
||||
docker==5.0.3
|
||||
gitpython==3.1.27
|
||||
jinja2==3.0.3
|
||||
pulsectl==22.1.3
|
||||
jinja2==3.1.1
|
||||
pulsectl==22.3.2
|
||||
pyudev==0.23.2
|
||||
ruamel.yaml==0.17.17
|
||||
securetar==2022.2.0
|
||||
sentry-sdk==1.5.6
|
||||
voluptuous==0.12.2
|
||||
sentry-sdk==1.5.8
|
||||
voluptuous==0.13.0
|
||||
dbus-next==0.2.3
|
||||
|
@@ -1,14 +1,14 @@
|
||||
black==22.1.0
|
||||
black==22.3.0
|
||||
codecov==2.1.12
|
||||
coverage==6.3.2
|
||||
flake8-docstrings==1.6.0
|
||||
flake8==4.0.1
|
||||
pre-commit==2.17.0
|
||||
pre-commit==2.18.1
|
||||
pydocstyle==6.1.1
|
||||
pylint==2.12.2
|
||||
pylint==2.13.4
|
||||
pytest-aiohttp==0.3.0
|
||||
pytest-asyncio==0.12.0 # NB!: Versions over 0.12.0 breaks pytest-aiohttp (https://github.com/aio-libs/pytest-aiohttp/issues/16)
|
||||
pytest-cov==3.0.0
|
||||
pytest-timeout==2.1.0
|
||||
pytest==7.0.1
|
||||
pyupgrade==2.31.0
|
||||
pytest==7.1.1
|
||||
pyupgrade==2.31.1
|
||||
|
@@ -178,7 +178,7 @@ class AddonManager(CoreSysAttributes):
|
||||
await addon.install_apparmor()
|
||||
|
||||
try:
|
||||
await addon.instance.install(store.version, store.image)
|
||||
await addon.instance.install(store.version, store.image, arch=addon.arch)
|
||||
except DockerError as err:
|
||||
self.data.uninstall(addon)
|
||||
raise AddonsError() from err
|
||||
|
@@ -503,6 +503,14 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
"""Return list of supported machine."""
|
||||
return self.data.get(ATTR_MACHINE, [])
|
||||
|
||||
@property
|
||||
def arch(self) -> str:
|
||||
"""Return architecture to use for the addon's image."""
|
||||
if ATTR_IMAGE in self.data:
|
||||
return self.sys_arch.match(self.data[ATTR_ARCH])
|
||||
|
||||
return self.sys_arch.default
|
||||
|
||||
@property
|
||||
def image(self) -> Optional[str]:
|
||||
"""Generate image name from data."""
|
||||
|
@@ -9,7 +9,7 @@ from aiohttp import web
|
||||
from aiohttp.hdrs import CONTENT_DISPOSITION
|
||||
import voluptuous as vol
|
||||
|
||||
from ..backups.validate import ALL_FOLDERS
|
||||
from ..backups.validate import ALL_FOLDERS, FOLDER_HOMEASSISTANT
|
||||
from ..const import (
|
||||
ATTR_ADDONS,
|
||||
ATTR_BACKUPS,
|
||||
@@ -36,13 +36,16 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
RE_SLUGIFY_NAME = re.compile(r"[^A-Za-z0-9]+")
|
||||
|
||||
# Backwards compatible / Remove 2022.08
|
||||
_ALL_FOLDERS = ALL_FOLDERS + [FOLDER_HOMEASSISTANT]
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
SCHEMA_RESTORE_PARTIAL = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_PASSWORD): vol.Maybe(str),
|
||||
vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(),
|
||||
vol.Optional(ATTR_ADDONS): vol.All([str], vol.Unique()),
|
||||
vol.Optional(ATTR_FOLDERS): vol.All([vol.In(ALL_FOLDERS)], vol.Unique()),
|
||||
vol.Optional(ATTR_FOLDERS): vol.All([vol.In(_ALL_FOLDERS)], vol.Unique()),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -59,7 +62,7 @@ SCHEMA_BACKUP_FULL = vol.Schema(
|
||||
SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend(
|
||||
{
|
||||
vol.Optional(ATTR_ADDONS): vol.All([str], vol.Unique()),
|
||||
vol.Optional(ATTR_FOLDERS): vol.All([vol.In(ALL_FOLDERS)], vol.Unique()),
|
||||
vol.Optional(ATTR_FOLDERS): vol.All([vol.In(_ALL_FOLDERS)], vol.Unique()),
|
||||
vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(),
|
||||
}
|
||||
)
|
||||
|
@@ -444,7 +444,7 @@ class Backup(CoreSysAttributes):
|
||||
except (tarfile.TarError, OSError) as err:
|
||||
_LOGGER.warning("Can't restore folder %s: %s", name, err)
|
||||
|
||||
await self.sys_run_in_executor(_restore, name)
|
||||
await self.sys_run_in_executor(_restore)
|
||||
|
||||
# Restore folder sequential
|
||||
# avoid issue on slow IO
|
||||
|
@@ -144,13 +144,11 @@ class BackupManager(CoreSysAttributes):
|
||||
_LOGGER.info("Backing up %s store Add-ons", backup.slug)
|
||||
await backup.store_addons(addon_list)
|
||||
|
||||
# Backup folders
|
||||
# HomeAssistant Folder is for v1
|
||||
if homeassistant or FOLDER_HOMEASSISTANT in folder_list:
|
||||
if homeassistant:
|
||||
await backup.store_homeassistant()
|
||||
folder_list = list(folder_list)
|
||||
folder_list.remove(FOLDER_HOMEASSISTANT)
|
||||
|
||||
# Backup folders
|
||||
if folder_list:
|
||||
_LOGGER.info("Backing up %s store folders", backup.slug)
|
||||
await backup.store_folders(folder_list)
|
||||
@@ -186,12 +184,12 @@ class BackupManager(CoreSysAttributes):
|
||||
@Job(conditions=[JobCondition.FREE_SPACE, JobCondition.RUNNING])
|
||||
async def do_backup_partial(
|
||||
self,
|
||||
name="",
|
||||
addons=None,
|
||||
folders=None,
|
||||
password=None,
|
||||
homeassistant=True,
|
||||
compressed=True,
|
||||
name: str = "",
|
||||
addons: list[str] | None = None,
|
||||
folders: list[str] | None = None,
|
||||
password: str | None = None,
|
||||
homeassistant: bool = False,
|
||||
compressed: bool = True,
|
||||
):
|
||||
"""Create a partial backup."""
|
||||
if self.lock.locked():
|
||||
@@ -201,6 +199,11 @@ class BackupManager(CoreSysAttributes):
|
||||
addons = addons or []
|
||||
folders = folders or []
|
||||
|
||||
# HomeAssistant Folder is for v1
|
||||
if FOLDER_HOMEASSISTANT in folders:
|
||||
folders.remove(FOLDER_HOMEASSISTANT)
|
||||
homeassistant = True
|
||||
|
||||
if len(addons) == 0 and len(folders) == 0 and not homeassistant:
|
||||
_LOGGER.error("Nothing to create backup for")
|
||||
|
||||
@@ -227,15 +230,10 @@ class BackupManager(CoreSysAttributes):
|
||||
self,
|
||||
backup: Backup,
|
||||
addon_list: list[str],
|
||||
folder_list: list[Path],
|
||||
folder_list: list[str],
|
||||
homeassistant: bool,
|
||||
replace: bool,
|
||||
):
|
||||
# Version 1
|
||||
if FOLDER_HOMEASSISTANT in folder_list:
|
||||
folder_list.remove(FOLDER_HOMEASSISTANT)
|
||||
homeassistant = backup.homeassistant_version is not None
|
||||
|
||||
try:
|
||||
task_hass: asyncio.Task | None = None
|
||||
async with backup:
|
||||
@@ -356,6 +354,14 @@ class BackupManager(CoreSysAttributes):
|
||||
_LOGGER.error("A backup/restore process is already running")
|
||||
return False
|
||||
|
||||
addon_list = addons or []
|
||||
folder_list = folders or []
|
||||
|
||||
# Version 1
|
||||
if FOLDER_HOMEASSISTANT in folder_list:
|
||||
folder_list.remove(FOLDER_HOMEASSISTANT)
|
||||
homeassistant = True
|
||||
|
||||
if backup.protected and not backup.set_password(password):
|
||||
_LOGGER.error("Invalid password for backup %s", backup.slug)
|
||||
return False
|
||||
@@ -364,9 +370,6 @@ class BackupManager(CoreSysAttributes):
|
||||
_LOGGER.error("No Home Assistant Core data inside the backup")
|
||||
return False
|
||||
|
||||
addon_list = addons or []
|
||||
folder_list = folders or []
|
||||
|
||||
_LOGGER.info("Partial-Restore %s start", backup.slug)
|
||||
async with self.lock:
|
||||
self.sys_core.state = CoreState.FREEZE
|
||||
|
@@ -31,7 +31,6 @@ from ..const import (
|
||||
from ..validate import SCHEMA_DOCKER_CONFIG, repositories, version_tag
|
||||
|
||||
ALL_FOLDERS = [
|
||||
FOLDER_HOMEASSISTANT,
|
||||
FOLDER_SHARE,
|
||||
FOLDER_ADDONS,
|
||||
FOLDER_SSL,
|
||||
@@ -68,6 +67,13 @@ def v1_folderlist(folder_data: list[str]) -> list[str]:
|
||||
return folder_data
|
||||
|
||||
|
||||
def v1_protected(protected: bool | str) -> bool:
|
||||
"""Cleanup old protected handling."""
|
||||
if isinstance(protected, bool):
|
||||
return protected
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
SCHEMA_BACKUP = vol.Schema(
|
||||
{
|
||||
@@ -77,7 +83,9 @@ SCHEMA_BACKUP = vol.Schema(
|
||||
vol.Required(ATTR_NAME): str,
|
||||
vol.Required(ATTR_DATE): str,
|
||||
vol.Optional(ATTR_COMPRESSED, default=True): vol.Boolean(),
|
||||
vol.Optional(ATTR_PROTECTED, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_PROTECTED, default=False): vol.All(
|
||||
v1_protected, vol.Boolean()
|
||||
),
|
||||
vol.Optional(ATTR_CRYPTO, default=None): vol.Maybe(CRYPTO_AES128),
|
||||
vol.Optional(ATTR_HOMEASSISTANT, default=None): vol.All(
|
||||
v1_homeassistant,
|
||||
|
@@ -454,3 +454,13 @@ class BusEvent(str, Enum):
|
||||
|
||||
HARDWARE_NEW_DEVICE = "hardware_new_device"
|
||||
HARDWARE_REMOVE_DEVICE = "hardware_remove_device"
|
||||
|
||||
|
||||
class CpuArch(str, Enum):
|
||||
"""Supported CPU architectures."""
|
||||
|
||||
ARMV7 = "armv7"
|
||||
ARMHF = "armhf"
|
||||
AARCH64 = "aarch64"
|
||||
I386 = "i386"
|
||||
AMD64 = "amd64"
|
||||
|
@@ -1,11 +1,13 @@
|
||||
"""Constants for DBUS."""
|
||||
from enum import Enum
|
||||
from enum import Enum, IntEnum
|
||||
from socket import AF_INET, AF_INET6
|
||||
|
||||
DBUS_NAME_HAOS = "io.hass.os"
|
||||
DBUS_NAME_HOSTNAME = "org.freedesktop.hostname1"
|
||||
DBUS_NAME_LOGIND = "org.freedesktop.login1"
|
||||
DBUS_NAME_NM = "org.freedesktop.NetworkManager"
|
||||
DBUS_NAME_RAUC = "de.pengutronix.rauc"
|
||||
DBUS_NAME_RESOLVED = "org.freedesktop.resolve1"
|
||||
DBUS_NAME_SYSTEMD = "org.freedesktop.systemd1"
|
||||
DBUS_NAME_TIMEDATE = "org.freedesktop.timedate1"
|
||||
|
||||
@@ -24,6 +26,7 @@ DBUS_IFACE_IP4CONFIG = "org.freedesktop.NetworkManager.IP4Config"
|
||||
DBUS_IFACE_IP6CONFIG = "org.freedesktop.NetworkManager.IP6Config"
|
||||
DBUS_IFACE_NM = "org.freedesktop.NetworkManager"
|
||||
DBUS_IFACE_RAUC_INSTALLER = "de.pengutronix.rauc.Installer"
|
||||
DBUS_IFACE_RESOLVED_MANAGER = "org.freedesktop.resolve1.Manager"
|
||||
DBUS_IFACE_SETTINGS_CONNECTION = "org.freedesktop.NetworkManager.Settings.Connection"
|
||||
DBUS_IFACE_SYSTEMD_MANAGER = "org.freedesktop.systemd1.Manager"
|
||||
DBUS_IFACE_TIMEDATE = "org.freedesktop.timedate1"
|
||||
@@ -43,6 +46,7 @@ DBUS_OBJECT_HAOS_SYSTEM = "/io/hass/os/System"
|
||||
DBUS_OBJECT_HOSTNAME = "/org/freedesktop/hostname1"
|
||||
DBUS_OBJECT_LOGIND = "/org/freedesktop/login1"
|
||||
DBUS_OBJECT_NM = "/org/freedesktop/NetworkManager"
|
||||
DBUS_OBJECT_RESOLVED = "/org/freedesktop/resolve1"
|
||||
DBUS_OBJECT_SETTINGS = "/org/freedesktop/NetworkManager/Settings"
|
||||
DBUS_OBJECT_SYSTEMD = "/org/freedesktop/systemd1"
|
||||
DBUS_OBJECT_TIMEDATE = "/org/freedesktop/timedate1"
|
||||
@@ -52,19 +56,33 @@ DBUS_ATTR_ACTIVE_CONNECTION = "ActiveConnection"
|
||||
DBUS_ATTR_ACTIVE_CONNECTIONS = "ActiveConnections"
|
||||
DBUS_ATTR_ADDRESS_DATA = "AddressData"
|
||||
DBUS_ATTR_BOOT_SLOT = "BootSlot"
|
||||
DBUS_ATTR_CACHE_STATISTICS = "CacheStatistics"
|
||||
DBUS_ATTR_CHASSIS = "Chassis"
|
||||
DBUS_ATTR_COMPATIBLE = "Compatible"
|
||||
DBUS_ATTR_CONFIGURATION = "Configuration"
|
||||
DBUS_ATTR_CONNECTION = "Connection"
|
||||
DBUS_ATTR_CONNECTION_ENABLED = "ConnectivityCheckEnabled"
|
||||
DBUS_ATTR_CURRENT_DEVICE = "CurrentDevice"
|
||||
DBUS_ATTR_CURRENT_DNS_SERVER = "CurrentDNSServer"
|
||||
DBUS_ATTR_CURRENT_DNS_SERVER_EX = "CurrentDNSServerEx"
|
||||
DBUS_ATTR_DEFAULT = "Default"
|
||||
DBUS_ATTR_DEPLOYMENT = "Deployment"
|
||||
DBUS_ATTR_DEVICE_INTERFACE = "Interface"
|
||||
DBUS_ATTR_DEVICE_TYPE = "DeviceType"
|
||||
DBUS_ATTR_DEVICES = "Devices"
|
||||
DBUS_ATTR_DIAGNOSTICS = "Diagnostics"
|
||||
DBUS_ATTR_DNS = "DNS"
|
||||
DBUS_ATTR_DNS_EX = "DNSEx"
|
||||
DBUS_ATTR_DNS_OVER_TLS = "DNSOverTLS"
|
||||
DBUS_ATTR_DNS_STUB_LISTENER = "DNSStubListener"
|
||||
DBUS_ATTR_DNSSEC = "DNSSEC"
|
||||
DBUS_ATTR_DNSSEC_NEGATIVE_TRUST_ANCHORS = "DNSSECNegativeTrustAnchors"
|
||||
DBUS_ATTR_DNSSEC_STATISTICS = "DNSSECStatistics"
|
||||
DBUS_ATTR_DNSSEC_SUPPORTED = "DNSSECSupported"
|
||||
DBUS_ATTR_DOMAINS = "Domains"
|
||||
DBUS_ATTR_DRIVER = "Driver"
|
||||
DBUS_ATTR_FALLBACK_DNS = "FallbackDNS"
|
||||
DBUS_ATTR_FALLBACK_DNS_EX = "FallbackDNSEx"
|
||||
DBUS_ATTR_FINISH_TIMESTAMP = "FinishTimestamp"
|
||||
DBUS_ATTR_FIRMWARE_TIMESTAMP_MONOTONIC = "FirmwareTimestampMonotonic"
|
||||
DBUS_ATTR_FREQUENCY = "Frequency"
|
||||
@@ -76,11 +94,13 @@ DBUS_ATTR_IP6CONFIG = "Ip6Config"
|
||||
DBUS_ATTR_KERNEL_RELEASE = "KernelRelease"
|
||||
DBUS_ATTR_KERNEL_TIMESTAMP_MONOTONIC = "KernelTimestampMonotonic"
|
||||
DBUS_ATTR_LAST_ERROR = "LastError"
|
||||
DBUS_ATTR_LLMNR = "LLMNR"
|
||||
DBUS_ATTR_LLMNR_HOSTNAME = "LLMNRHostname"
|
||||
DBUS_ATTR_LOADER_TIMESTAMP_MONOTONIC = "LoaderTimestampMonotonic"
|
||||
DBUS_ATTR_LOCALRTC = "LocalRTC"
|
||||
DBUS_ATTR_MANAGED = "Managed"
|
||||
DBUS_ATTR_MODE = "Mode"
|
||||
DBUS_ATTR_MODE = "Mode"
|
||||
DBUS_ATTR_MULTICAST_DNS = "MulticastDNS"
|
||||
DBUS_ATTR_NAMESERVER_DATA = "NameserverData"
|
||||
DBUS_ATTR_NAMESERVERS = "Nameservers"
|
||||
DBUS_ATTR_NTP = "NTP"
|
||||
@@ -89,6 +109,7 @@ DBUS_ATTR_OPERATING_SYSTEM_PRETTY_NAME = "OperatingSystemPrettyName"
|
||||
DBUS_ATTR_OPERATION = "Operation"
|
||||
DBUS_ATTR_PARSER_VERSION = "ParserVersion"
|
||||
DBUS_ATTR_PRIMARY_CONNECTION = "PrimaryConnection"
|
||||
DBUS_ATTR_RESOLV_CONF_MODE = "ResolvConfMode"
|
||||
DBUS_ATTR_RCMANAGER = "RcManager"
|
||||
DBUS_ATTR_SSID = "Ssid"
|
||||
DBUS_ATTR_STATE = "State"
|
||||
@@ -97,6 +118,7 @@ DBUS_ATTR_STATIC_OPERATING_SYSTEM_CPE_NAME = "OperatingSystemCPEName"
|
||||
DBUS_ATTR_STRENGTH = "Strength"
|
||||
DBUS_ATTR_TIMEUSEC = "TimeUSec"
|
||||
DBUS_ATTR_TIMEZONE = "Timezone"
|
||||
DBUS_ATTR_TRANSACTION_STATISTICS = "TransactionStatistics"
|
||||
DBUS_ATTR_TYPE = "Type"
|
||||
DBUS_ATTR_USERSPACE_TIMESTAMP_MONOTONIC = "UserspaceTimestampMonotonic"
|
||||
DBUS_ATTR_UUID = "Uuid"
|
||||
@@ -176,3 +198,53 @@ class WirelessMethodType(int, Enum):
|
||||
INFRASTRUCTURE = 2
|
||||
ACCESSPOINT = 3
|
||||
MESH = 4
|
||||
|
||||
|
||||
class DNSAddressFamily(IntEnum):
|
||||
"""Address family for DNS server."""
|
||||
|
||||
INET = AF_INET
|
||||
INET6 = AF_INET6
|
||||
|
||||
|
||||
class MulticastProtocolEnabled(str, Enum):
|
||||
"""Multicast protocol enabled or resolve."""
|
||||
|
||||
YES = "yes"
|
||||
NO = "no"
|
||||
RESOLVE = "resolve"
|
||||
|
||||
|
||||
class DNSOverTLSEnabled(str, Enum):
|
||||
"""DNS over TLS enabled."""
|
||||
|
||||
YES = "yes"
|
||||
NO = "no"
|
||||
OPPORTUNISTIC = "opportunistic"
|
||||
|
||||
|
||||
class DNSSECValidation(str, Enum):
|
||||
"""DNSSEC validation enforced."""
|
||||
|
||||
YES = "yes"
|
||||
NO = "no"
|
||||
ALLOW_DOWNGRADE = "allow-downgrade"
|
||||
|
||||
|
||||
class DNSStubListenerEnabled(str, Enum):
|
||||
"""DNS stub listener enabled."""
|
||||
|
||||
YES = "yes"
|
||||
NO = "no"
|
||||
TCP_ONLY = "tcp"
|
||||
UDP_ONLY = "udp"
|
||||
|
||||
|
||||
class ResolvConfMode(str, Enum):
|
||||
"""Resolv.conf management mode."""
|
||||
|
||||
FOREIGN = "foreign"
|
||||
MISSING = "missing"
|
||||
STATIC = "static"
|
||||
STUB = "stub"
|
||||
UPLINK = "uplink"
|
||||
|
@@ -9,6 +9,7 @@ from .interface import DBusInterface
|
||||
from .logind import Logind
|
||||
from .network import NetworkManager
|
||||
from .rauc import Rauc
|
||||
from .resolved import Resolved
|
||||
from .systemd import Systemd
|
||||
from .timedate import TimeDate
|
||||
|
||||
@@ -29,6 +30,7 @@ class DBusManager(CoreSysAttributes):
|
||||
self._network: NetworkManager = NetworkManager()
|
||||
self._agent: OSAgent = OSAgent()
|
||||
self._timedate: TimeDate = TimeDate()
|
||||
self._resolved: Resolved = Resolved()
|
||||
|
||||
@property
|
||||
def systemd(self) -> Systemd:
|
||||
@@ -65,6 +67,11 @@ class DBusManager(CoreSysAttributes):
|
||||
"""Return the timedate interface."""
|
||||
return self._timedate
|
||||
|
||||
@property
|
||||
def resolved(self) -> Resolved:
|
||||
"""Return the resolved interface."""
|
||||
return self._resolved
|
||||
|
||||
async def load(self) -> None:
|
||||
"""Connect interfaces to D-Bus."""
|
||||
if not SOCKET_DBUS.exists():
|
||||
@@ -81,6 +88,7 @@ class DBusManager(CoreSysAttributes):
|
||||
self.timedate,
|
||||
self.network,
|
||||
self.rauc,
|
||||
self.resolved,
|
||||
]
|
||||
for dbus in dbus_loads:
|
||||
_LOGGER.info("Load dbus interface %s", dbus.name)
|
||||
|
188
supervisor/dbus/resolved.py
Normal file
188
supervisor/dbus/resolved.py
Normal file
@@ -0,0 +1,188 @@
|
||||
"""D-Bus interface for systemd-resolved."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ..exceptions import DBusError, DBusInterfaceError
|
||||
from ..utils.dbus import DBus
|
||||
from .const import (
|
||||
DBUS_ATTR_CACHE_STATISTICS,
|
||||
DBUS_ATTR_CURRENT_DNS_SERVER,
|
||||
DBUS_ATTR_CURRENT_DNS_SERVER_EX,
|
||||
DBUS_ATTR_DNS,
|
||||
DBUS_ATTR_DNS_EX,
|
||||
DBUS_ATTR_DNS_OVER_TLS,
|
||||
DBUS_ATTR_DNS_STUB_LISTENER,
|
||||
DBUS_ATTR_DNSSEC,
|
||||
DBUS_ATTR_DNSSEC_NEGATIVE_TRUST_ANCHORS,
|
||||
DBUS_ATTR_DNSSEC_STATISTICS,
|
||||
DBUS_ATTR_DNSSEC_SUPPORTED,
|
||||
DBUS_ATTR_DOMAINS,
|
||||
DBUS_ATTR_FALLBACK_DNS,
|
||||
DBUS_ATTR_FALLBACK_DNS_EX,
|
||||
DBUS_ATTR_LLMNR,
|
||||
DBUS_ATTR_LLMNR_HOSTNAME,
|
||||
DBUS_ATTR_MULTICAST_DNS,
|
||||
DBUS_ATTR_RESOLV_CONF_MODE,
|
||||
DBUS_ATTR_TRANSACTION_STATISTICS,
|
||||
DBUS_IFACE_RESOLVED_MANAGER,
|
||||
DBUS_NAME_RESOLVED,
|
||||
DBUS_OBJECT_RESOLVED,
|
||||
DNSAddressFamily,
|
||||
DNSOverTLSEnabled,
|
||||
DNSSECValidation,
|
||||
DNSStubListenerEnabled,
|
||||
MulticastProtocolEnabled,
|
||||
ResolvConfMode,
|
||||
)
|
||||
from .interface import DBusInterface, dbus_property
|
||||
from .utils import dbus_connected
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Resolved(DBusInterface):
|
||||
"""Handle D-Bus interface for systemd-resolved."""
|
||||
|
||||
name = DBUS_NAME_RESOLVED
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize Properties."""
|
||||
self.properties: dict[str, Any] = {}
|
||||
|
||||
async def connect(self):
|
||||
"""Connect to D-Bus."""
|
||||
try:
|
||||
self.dbus = await DBus.connect(DBUS_NAME_RESOLVED, DBUS_OBJECT_RESOLVED)
|
||||
except DBusError:
|
||||
_LOGGER.warning("Can't connect to systemd-resolved.")
|
||||
except DBusInterfaceError:
|
||||
_LOGGER.warning(
|
||||
"Host has no systemd-resolved support. DNS will not work correctly."
|
||||
)
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def cache_statistics(self) -> tuple[int, int, int] | None:
|
||||
"""Return current cache entries and hits and misses since last reset."""
|
||||
return self.properties[DBUS_ATTR_CACHE_STATISTICS]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def current_dns_server(
|
||||
self,
|
||||
) -> list[tuple[int, DNSAddressFamily, bytes]] | None:
|
||||
"""Return current DNS server."""
|
||||
return self.properties[DBUS_ATTR_CURRENT_DNS_SERVER]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def current_dns_server_ex(
|
||||
self,
|
||||
) -> list[tuple[int, DNSAddressFamily, bytes, int, str]] | None:
|
||||
"""Return current DNS server including port and server name."""
|
||||
return self.properties[DBUS_ATTR_CURRENT_DNS_SERVER_EX]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def dns(self) -> list[tuple[int, DNSAddressFamily, bytes]] | None:
|
||||
"""Return DNS servers in use."""
|
||||
return self.properties[DBUS_ATTR_DNS]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def dns_ex(self) -> list[tuple[int, DNSAddressFamily, bytes, int, str]] | None:
|
||||
"""Return DNS servers in use including port and server name."""
|
||||
return self.properties[DBUS_ATTR_DNS_EX]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def dns_over_tls(self) -> DNSOverTLSEnabled | None:
|
||||
"""Return DNS over TLS enabled."""
|
||||
return self.properties[DBUS_ATTR_DNS_OVER_TLS]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def dns_stub_listener(self) -> DNSStubListenerEnabled | None:
|
||||
"""Return DNS stub listener enabled on port 53."""
|
||||
return self.properties[DBUS_ATTR_DNS_STUB_LISTENER]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def dnssec(self) -> DNSSECValidation | None:
|
||||
"""Return DNSSEC validation enforced."""
|
||||
return self.properties[DBUS_ATTR_DNSSEC]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def dnssec_negative_trust_anchors(self) -> list[str] | None:
|
||||
"""Return DNSSEC negative trust anchors."""
|
||||
return self.properties[DBUS_ATTR_DNSSEC_NEGATIVE_TRUST_ANCHORS]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def dnssec_statistics(self) -> tuple[int, int, int, int] | None:
|
||||
"""Return Secure, insecure, bogus, and indeterminate DNSSEC validations since last reset."""
|
||||
return self.properties[DBUS_ATTR_DNSSEC_STATISTICS]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def dnssec_supported(self) -> bool | None:
|
||||
"""Return DNSSEC enabled and selected DNS servers support it."""
|
||||
return self.properties[DBUS_ATTR_DNSSEC_SUPPORTED]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def domains(self) -> list[tuple[int, str, bool]] | None:
|
||||
"""Return search and routing domains in use."""
|
||||
return self.properties[DBUS_ATTR_DOMAINS]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def fallback_dns(self) -> list[tuple[int, DNSAddressFamily, bytes]] | None:
|
||||
"""Return fallback DNS servers."""
|
||||
return self.properties[DBUS_ATTR_FALLBACK_DNS]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def fallback_dns_ex(
|
||||
self,
|
||||
) -> list[tuple[int, DNSAddressFamily, bytes, int, str]] | None:
|
||||
"""Return fallback DNS servers including port and server name."""
|
||||
return self.properties[DBUS_ATTR_FALLBACK_DNS_EX]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def llmnr(self) -> MulticastProtocolEnabled | None:
|
||||
"""Return LLMNR enabled."""
|
||||
return self.properties[DBUS_ATTR_LLMNR]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def llmnr_hostname(self) -> str | None:
|
||||
"""Return LLMNR hostname on network."""
|
||||
return self.properties[DBUS_ATTR_LLMNR_HOSTNAME]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def multicast_dns(self) -> MulticastProtocolEnabled | None:
|
||||
"""Return MDNS enabled."""
|
||||
return self.properties[DBUS_ATTR_MULTICAST_DNS]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def resolv_conf_mode(self) -> ResolvConfMode | None:
|
||||
"""Return how /etc/resolv.conf managed on host."""
|
||||
return self.properties[DBUS_ATTR_RESOLV_CONF_MODE]
|
||||
|
||||
@property
|
||||
@dbus_property
|
||||
def transaction_statistics(self) -> tuple[int, int] | None:
|
||||
"""Return transactions processing and processed since last reset."""
|
||||
return self.properties[DBUS_ATTR_TRANSACTION_STATISTICS]
|
||||
|
||||
@dbus_connected
|
||||
async def update(self):
|
||||
"""Update Properties."""
|
||||
self.properties = await self.dbus.get_properties(DBUS_IFACE_RESOLVED_MANAGER)
|
@@ -31,6 +31,7 @@ from ..const import (
|
||||
SYSTEMD_JOURNAL_PERSISTENT,
|
||||
SYSTEMD_JOURNAL_VOLATILE,
|
||||
BusEvent,
|
||||
CpuArch,
|
||||
)
|
||||
from ..coresys import CoreSys
|
||||
from ..exceptions import (
|
||||
@@ -515,7 +516,11 @@ class DockerAddon(DockerInterface):
|
||||
)
|
||||
|
||||
def _install(
|
||||
self, version: AwesomeVersion, image: str | None = None, latest: bool = False
|
||||
self,
|
||||
version: AwesomeVersion,
|
||||
image: str | None = None,
|
||||
latest: bool = False,
|
||||
arch: CpuArch | None = None,
|
||||
) -> None:
|
||||
"""Pull Docker image or build it.
|
||||
|
||||
@@ -524,7 +529,7 @@ class DockerAddon(DockerInterface):
|
||||
if self.addon.need_build:
|
||||
self._build(version)
|
||||
else:
|
||||
super()._install(version, image, latest)
|
||||
super()._install(version, image, latest, arch)
|
||||
|
||||
def _build(self, version: AwesomeVersion) -> None:
|
||||
"""Build a Docker container.
|
||||
@@ -682,7 +687,7 @@ class DockerAddon(DockerInterface):
|
||||
self.sys_security.verify_content(self.addon.codenotary, checksum),
|
||||
self.sys_loop,
|
||||
)
|
||||
job.result(timeout=20)
|
||||
job.result()
|
||||
|
||||
@Job(conditions=[JobCondition.OS_AGENT])
|
||||
async def _hardware_events(self, device: Device) -> None:
|
||||
|
@@ -1,9 +1,11 @@
|
||||
"""Interface class for Supervisor Docker object."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Awaitable, Optional
|
||||
from typing import Any, Awaitable
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
from awesomeversion.strategy import AwesomeVersionStrategy
|
||||
@@ -17,6 +19,7 @@ from ..const import (
|
||||
ATTR_USERNAME,
|
||||
LABEL_ARCH,
|
||||
LABEL_VERSION,
|
||||
CpuArch,
|
||||
)
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..exceptions import (
|
||||
@@ -37,6 +40,14 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
IMAGE_WITH_HOST = re.compile(r"^((?:[a-z0-9]+(?:-[a-z0-9]+)*\.)+[a-z]{2,})\/.+")
|
||||
DOCKER_HUB = "hub.docker.com"
|
||||
|
||||
MAP_ARCH = {
|
||||
CpuArch.ARMV7: "linux/arm/v7",
|
||||
CpuArch.ARMHF: "linux/arm/v6",
|
||||
CpuArch.AARCH64: "linux/arm64",
|
||||
CpuArch.I386: "linux/386",
|
||||
CpuArch.AMD64: "linux/amd64",
|
||||
}
|
||||
|
||||
|
||||
class DockerInterface(CoreSysAttributes):
|
||||
"""Docker Supervisor interface."""
|
||||
@@ -44,7 +55,7 @@ class DockerInterface(CoreSysAttributes):
|
||||
def __init__(self, coresys: CoreSys):
|
||||
"""Initialize Docker base wrapper."""
|
||||
self.coresys: CoreSys = coresys
|
||||
self._meta: Optional[dict[str, Any]] = None
|
||||
self._meta: dict[str, Any] | None = None
|
||||
self.lock: asyncio.Lock = asyncio.Lock()
|
||||
|
||||
@property
|
||||
@@ -53,7 +64,7 @@ class DockerInterface(CoreSysAttributes):
|
||||
return 10
|
||||
|
||||
@property
|
||||
def name(self) -> Optional[str]:
|
||||
def name(self) -> str | None:
|
||||
"""Return name of Docker container."""
|
||||
return None
|
||||
|
||||
@@ -77,7 +88,7 @@ class DockerInterface(CoreSysAttributes):
|
||||
return self.meta_config.get("Labels") or {}
|
||||
|
||||
@property
|
||||
def image(self) -> Optional[str]:
|
||||
def image(self) -> str | None:
|
||||
"""Return name of Docker image."""
|
||||
try:
|
||||
return self.meta_config["Image"].partition(":")[0]
|
||||
@@ -85,14 +96,14 @@ class DockerInterface(CoreSysAttributes):
|
||||
return None
|
||||
|
||||
@property
|
||||
def version(self) -> Optional[AwesomeVersion]:
|
||||
def version(self) -> AwesomeVersion | None:
|
||||
"""Return version of Docker image."""
|
||||
if LABEL_VERSION not in self.meta_labels:
|
||||
return None
|
||||
return AwesomeVersion(self.meta_labels[LABEL_VERSION])
|
||||
|
||||
@property
|
||||
def arch(self) -> Optional[str]:
|
||||
def arch(self) -> str | None:
|
||||
"""Return arch of Docker image."""
|
||||
return self.meta_labels.get(LABEL_ARCH)
|
||||
|
||||
@@ -150,19 +161,28 @@ class DockerInterface(CoreSysAttributes):
|
||||
|
||||
@process_lock
|
||||
def install(
|
||||
self, version: AwesomeVersion, image: Optional[str] = None, latest: bool = False
|
||||
self,
|
||||
version: AwesomeVersion,
|
||||
image: str | None = None,
|
||||
latest: bool = False,
|
||||
arch: CpuArch | None = None,
|
||||
):
|
||||
"""Pull docker image."""
|
||||
return self.sys_run_in_executor(self._install, version, image, latest)
|
||||
return self.sys_run_in_executor(self._install, version, image, latest, arch)
|
||||
|
||||
def _install(
|
||||
self, version: AwesomeVersion, image: Optional[str] = None, latest: bool = False
|
||||
self,
|
||||
version: AwesomeVersion,
|
||||
image: str | None = None,
|
||||
latest: bool = False,
|
||||
arch: CpuArch | None = None,
|
||||
) -> None:
|
||||
"""Pull Docker image.
|
||||
|
||||
Need run inside executor.
|
||||
"""
|
||||
image = image or self.image
|
||||
arch = arch or self.sys_arch.supervisor
|
||||
|
||||
_LOGGER.info("Downloading docker image %s with tag %s.", image, version)
|
||||
try:
|
||||
@@ -171,7 +191,10 @@ class DockerInterface(CoreSysAttributes):
|
||||
self._docker_login(image)
|
||||
|
||||
# Pull new image
|
||||
docker_image = self.sys_docker.images.pull(f"{image}:{version!s}")
|
||||
docker_image = self.sys_docker.images.pull(
|
||||
f"{image}:{version!s}",
|
||||
platform=MAP_ARCH[arch],
|
||||
)
|
||||
|
||||
# Validate content
|
||||
try:
|
||||
@@ -378,13 +401,13 @@ class DockerInterface(CoreSysAttributes):
|
||||
|
||||
@process_lock
|
||||
def update(
|
||||
self, version: AwesomeVersion, image: Optional[str] = None, latest: bool = False
|
||||
self, version: AwesomeVersion, image: str | None = None, latest: bool = False
|
||||
) -> Awaitable[None]:
|
||||
"""Update a Docker image."""
|
||||
return self.sys_run_in_executor(self._update, version, image, latest)
|
||||
|
||||
def _update(
|
||||
self, version: AwesomeVersion, image: Optional[str] = None, latest: bool = False
|
||||
self, version: AwesomeVersion, image: str | None = None, latest: bool = False
|
||||
) -> None:
|
||||
"""Update a docker image.
|
||||
|
||||
@@ -428,11 +451,11 @@ class DockerInterface(CoreSysAttributes):
|
||||
return b""
|
||||
|
||||
@process_lock
|
||||
def cleanup(self, old_image: Optional[str] = None) -> Awaitable[None]:
|
||||
def cleanup(self, old_image: str | None = None) -> Awaitable[None]:
|
||||
"""Check if old version exists and cleanup."""
|
||||
return self.sys_run_in_executor(self._cleanup, old_image)
|
||||
|
||||
def _cleanup(self, old_image: Optional[str] = None) -> None:
|
||||
def _cleanup(self, old_image: str | None = None) -> None:
|
||||
"""Check if old version exists and cleanup.
|
||||
|
||||
Need run inside executor.
|
||||
@@ -635,7 +658,7 @@ class DockerInterface(CoreSysAttributes):
|
||||
job = asyncio.run_coroutine_threadsafe(
|
||||
self.sys_security.verify_own_content(checksum), self.sys_loop
|
||||
)
|
||||
job.result(timeout=20)
|
||||
job.result()
|
||||
|
||||
@process_lock
|
||||
def check_trust(self) -> Awaitable[None]:
|
||||
|
@@ -68,7 +68,7 @@ class HostManager(CoreSysAttributes):
|
||||
"""Return a list of host features."""
|
||||
return self.supported_features()
|
||||
|
||||
@lru_cache
|
||||
@lru_cache(maxsize=128)
|
||||
def supported_features(self) -> list[HostFeature]:
|
||||
"""Return a list of supported host features."""
|
||||
features = []
|
||||
@@ -95,21 +95,29 @@ class HostManager(CoreSysAttributes):
|
||||
|
||||
return features
|
||||
|
||||
async def reload(self):
|
||||
async def reload(
|
||||
self,
|
||||
*,
|
||||
services: bool = True,
|
||||
network: bool = True,
|
||||
agent: bool = True,
|
||||
audio: bool = True,
|
||||
):
|
||||
"""Reload host functions."""
|
||||
await self.info.update()
|
||||
|
||||
if self.sys_dbus.systemd.is_connected:
|
||||
if services and self.sys_dbus.systemd.is_connected:
|
||||
await self.services.update()
|
||||
|
||||
if self.sys_dbus.network.is_connected:
|
||||
if network and self.sys_dbus.network.is_connected:
|
||||
await self.network.update()
|
||||
|
||||
if self.sys_dbus.agent.is_connected:
|
||||
if agent and self.sys_dbus.agent.is_connected:
|
||||
await self.sys_dbus.agent.update()
|
||||
|
||||
with suppress(PulseAudioError):
|
||||
await self.sound.update()
|
||||
if audio:
|
||||
with suppress(PulseAudioError):
|
||||
await self.sound.update()
|
||||
|
||||
_LOGGER.info("Host information reload completed")
|
||||
self.supported_features.cache_clear() # pylint: disable=no-member
|
||||
@@ -117,7 +125,8 @@ class HostManager(CoreSysAttributes):
|
||||
async def load(self):
|
||||
"""Load host information."""
|
||||
with suppress(HassioError):
|
||||
await self.reload()
|
||||
await self.reload(network=False)
|
||||
await self.network.load()
|
||||
|
||||
# Register for events
|
||||
self.sys_bus.register_event(BusEvent.HARDWARE_NEW_DEVICE, self._hardware_events)
|
||||
|
@@ -2,11 +2,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface
|
||||
import logging
|
||||
|
||||
import attr
|
||||
|
||||
from supervisor.jobs.const import JobCondition
|
||||
from supervisor.jobs.decorator import Job
|
||||
|
||||
from ..const import ATTR_HOST_INTERNET
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..dbus.const import (
|
||||
@@ -79,7 +83,6 @@ class NetworkManager(CoreSysAttributes):
|
||||
|
||||
async def check_connectivity(self):
|
||||
"""Check the internet connection."""
|
||||
|
||||
if not self.sys_dbus.network.connectivity_enabled:
|
||||
return
|
||||
|
||||
@@ -100,6 +103,25 @@ class NetworkManager(CoreSysAttributes):
|
||||
self.sys_dbus.network.interfaces[inet_name]
|
||||
)
|
||||
|
||||
@Job(conditions=JobCondition.HOST_NETWORK)
|
||||
async def load(self):
|
||||
"""Load network information and reapply defaults over dbus."""
|
||||
await self.update()
|
||||
|
||||
# Apply current settings on each interface so OS can update any out of date defaults
|
||||
interfaces = [
|
||||
Interface.from_dbus_interface(self.sys_dbus.network.interfaces[i])
|
||||
for i in self.sys_dbus.network.interfaces
|
||||
]
|
||||
with suppress(HostNetworkNotFound):
|
||||
await asyncio.gather(
|
||||
*[
|
||||
self.apply_changes(interface, update_only=True)
|
||||
for interface in interfaces
|
||||
if interface.enabled
|
||||
]
|
||||
)
|
||||
|
||||
async def update(self):
|
||||
"""Update properties over dbus."""
|
||||
_LOGGER.info("Updating local network information")
|
||||
@@ -114,7 +136,9 @@ class NetworkManager(CoreSysAttributes):
|
||||
|
||||
await self.check_connectivity()
|
||||
|
||||
async def apply_changes(self, interface: Interface) -> None:
|
||||
async def apply_changes(
|
||||
self, interface: Interface, *, update_only: bool = False
|
||||
) -> None:
|
||||
"""Apply Interface changes to host."""
|
||||
inet = self.sys_dbus.network.interfaces.get(interface.name)
|
||||
con: NetworkConnection = None
|
||||
@@ -147,6 +171,13 @@ class NetworkManager(CoreSysAttributes):
|
||||
f"Can't update config on {interface.name}: {err}", _LOGGER.error
|
||||
) from err
|
||||
|
||||
# Stop if only updates are allowed as other paths create/delete interfaces
|
||||
elif update_only:
|
||||
raise HostNetworkNotFound(
|
||||
f"Requested to update interface {interface.name} which does not exist or is disabled.",
|
||||
_LOGGER.warning,
|
||||
)
|
||||
|
||||
# Create new configuration and activate interface
|
||||
elif inet and interface.enabled:
|
||||
_LOGGER.debug("Create new configuration for %s", interface.name)
|
||||
|
@@ -19,6 +19,7 @@ class JobCondition(str, Enum):
|
||||
RUNNING = "running"
|
||||
HAOS = "haos"
|
||||
OS_AGENT = "os_agent"
|
||||
HOST_NETWORK = "host_network"
|
||||
|
||||
|
||||
class JobExecutionLimit(str, Enum):
|
||||
|
@@ -181,6 +181,14 @@ class Job(CoreSysAttributes):
|
||||
f"'{self._method.__qualname__}' blocked from execution, no Home Assistant OS-Agent available"
|
||||
)
|
||||
|
||||
if (
|
||||
JobCondition.HOST_NETWORK in self.conditions
|
||||
and not self.sys_dbus.network.is_connected
|
||||
):
|
||||
raise JobConditionException(
|
||||
f"'{self._method.__qualname__}' blocked from execution, host Network Manager not available"
|
||||
)
|
||||
|
||||
async def _acquire_exection_limit(self) -> None:
|
||||
"""Process exection limits."""
|
||||
if self.limit not in (
|
||||
|
@@ -56,4 +56,4 @@ class CheckCoreTrust(CheckBase):
|
||||
@property
|
||||
def states(self) -> list[CoreState]:
|
||||
"""Return a list of valid states when this check can run."""
|
||||
return [CoreState.RUNNING, CoreState.STARTUP]
|
||||
return [CoreState.RUNNING]
|
||||
|
@@ -62,4 +62,4 @@ class CheckPluginTrust(CheckBase):
|
||||
@property
|
||||
def states(self) -> list[CoreState]:
|
||||
"""Return a list of valid states when this check can run."""
|
||||
return [CoreState.RUNNING, CoreState.STARTUP]
|
||||
return [CoreState.RUNNING]
|
||||
|
@@ -41,6 +41,7 @@ class UnsupportedReason(str, Enum):
|
||||
SOFTWARE = "software"
|
||||
SOURCE_MODS = "source_mods"
|
||||
SYSTEMD = "systemd"
|
||||
SYSTEMD_RESOLVED = "systemd_resolved"
|
||||
|
||||
|
||||
class UnhealthyReason(str, Enum):
|
||||
|
@@ -5,7 +5,7 @@ from ...coresys import CoreSys
|
||||
from ..const import UnsupportedReason
|
||||
from .base import EvaluateBase
|
||||
|
||||
SUPPORTED_OS = ["Debian GNU/Linux 10 (buster)", "Debian GNU/Linux 11 (bullseye)"]
|
||||
SUPPORTED_OS = ["Debian GNU/Linux 11 (bullseye)"]
|
||||
|
||||
|
||||
def setup(coresys: CoreSys) -> EvaluateBase:
|
||||
|
34
supervisor/resolution/evaluations/resolved.py
Normal file
34
supervisor/resolution/evaluations/resolved.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Evaluation class for systemd-resolved."""
|
||||
|
||||
from ...const import CoreState
|
||||
from ...coresys import CoreSys
|
||||
from ..const import UnsupportedReason
|
||||
from .base import EvaluateBase
|
||||
|
||||
|
||||
def setup(coresys: CoreSys) -> EvaluateBase:
|
||||
"""Initialize evaluation-setup function."""
|
||||
return EvaluateResolved(coresys)
|
||||
|
||||
|
||||
class EvaluateResolved(EvaluateBase):
|
||||
"""Evaluate systemd-resolved."""
|
||||
|
||||
@property
|
||||
def reason(self) -> UnsupportedReason:
|
||||
"""Return a UnsupportedReason enum."""
|
||||
return UnsupportedReason.SYSTEMD_RESOLVED
|
||||
|
||||
@property
|
||||
def on_failure(self) -> str:
|
||||
"""Return a string that is printed when self.evaluate is False."""
|
||||
return "Systemd-Resolved is required for DNS in Home Assistant."
|
||||
|
||||
@property
|
||||
def states(self) -> list[CoreState]:
|
||||
"""Return a list of valid states when this evaluation can run."""
|
||||
return [CoreState.SETUP]
|
||||
|
||||
async def evaluate(self) -> bool:
|
||||
"""Run evaluation."""
|
||||
return not self.sys_dbus.resolved.is_connected
|
@@ -1,6 +1,5 @@
|
||||
"""Fetch last versions from webserver."""
|
||||
import logging
|
||||
from typing import Awaitable
|
||||
|
||||
from .const import (
|
||||
ATTR_CONTENT_TRUST,
|
||||
@@ -71,9 +70,11 @@ class Security(FileConfiguration, CoreSysAttributes):
|
||||
raise
|
||||
return
|
||||
|
||||
def verify_own_content(self, checksum: str) -> Awaitable[None]:
|
||||
async def verify_own_content(self, checksum: str) -> None:
|
||||
"""Verify content from HA org."""
|
||||
return self.verify_content("notary@home-assistant.io", checksum)
|
||||
return
|
||||
# pylint: disable=unreachable
|
||||
return await self.verify_content("notary@home-assistant.io", checksum)
|
||||
|
||||
async def verify_secret(self, pwned_hash: str) -> None:
|
||||
"""Verify pwned state of a secret."""
|
||||
|
@@ -1,11 +1,13 @@
|
||||
"""Small wrapper for CodeNotary."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
from typing import Final, Union
|
||||
from typing import Final
|
||||
|
||||
import async_timeout
|
||||
from dirhash import dirhash
|
||||
@@ -21,10 +23,11 @@ _CAS_CMD: str = (
|
||||
_CACHE: set[tuple[str, str]] = set()
|
||||
|
||||
|
||||
_ATTR_ERROR: Final = "error"
|
||||
_ATTR_STATUS: Final = "status"
|
||||
|
||||
|
||||
def calc_checksum(data: Union[str, bytes]) -> str:
|
||||
def calc_checksum(data: str | bytes) -> str:
|
||||
"""Generate checksum for CodeNotary."""
|
||||
if isinstance(data, str):
|
||||
return hashlib.sha256(data.encode()).hexdigest()
|
||||
@@ -58,15 +61,15 @@ async def cas_validate(
|
||||
env=clean_env(),
|
||||
)
|
||||
|
||||
async with async_timeout.timeout(30):
|
||||
async with async_timeout.timeout(15):
|
||||
data, error = await proc.communicate()
|
||||
except OSError as err:
|
||||
raise CodeNotaryError(
|
||||
f"CodeNotary fatal error: {err!s}", _LOGGER.critical
|
||||
) from err
|
||||
except asyncio.TimeoutError:
|
||||
raise CodeNotaryError(
|
||||
"Timeout while processing CodeNotary", _LOGGER.error
|
||||
raise CodeNotaryBackendError(
|
||||
"Timeout while processing CodeNotary", _LOGGER.warning
|
||||
) from None
|
||||
|
||||
# Check if Notarized
|
||||
@@ -88,6 +91,9 @@ async def cas_validate(
|
||||
f"Can't parse CodeNotary output: {data!s} - {err!s}", _LOGGER.error
|
||||
) from err
|
||||
|
||||
if _ATTR_ERROR in data_json:
|
||||
raise CodeNotaryBackendError(data_json[_ATTR_ERROR], _LOGGER.warning)
|
||||
|
||||
if data_json[_ATTR_STATUS] == 0:
|
||||
_CACHE.add((checksum, signer))
|
||||
else:
|
||||
|
@@ -13,7 +13,7 @@ from ..exceptions import WhoamiConnectivityError, WhoamiError, WhoamiSSLError
|
||||
from .dt import utc_from_timestamp
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
_API_CALL: str = "whoami.home-assistant.io/v1"
|
||||
_API_CALL: str = "services.home-assistant.io/whoami/v1"
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True)
|
||||
|
@@ -41,3 +41,18 @@ def test_v1_folder_migration():
|
||||
)
|
||||
|
||||
assert data[validate.ATTR_FOLDERS] == [validate.FOLDER_ADDONS]
|
||||
|
||||
|
||||
def test_v1_protected():
|
||||
"""Test v1 protection migration."""
|
||||
data = validate.SCHEMA_BACKUP(
|
||||
{
|
||||
**VALID_DEFAULT,
|
||||
**{
|
||||
validate.ATTR_PROTECTED: "8",
|
||||
validate.ATTR_TYPE: validate.BackupType.FULL,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert data[validate.ATTR_PROTECTED] is True
|
||||
|
@@ -18,8 +18,13 @@ from supervisor.api import RestAPI
|
||||
from supervisor.bootstrap import initialize_coresys
|
||||
from supervisor.const import REQUEST_FROM
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.dbus.agent import OSAgent
|
||||
from supervisor.dbus.const import DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED
|
||||
from supervisor.dbus.hostname import Hostname
|
||||
from supervisor.dbus.interface import DBusInterface
|
||||
from supervisor.dbus.network import NetworkManager
|
||||
from supervisor.dbus.systemd import Systemd
|
||||
from supervisor.dbus.timedate import TimeDate
|
||||
from supervisor.docker import DockerAPI
|
||||
from supervisor.store.addon import AddonStore
|
||||
from supervisor.store.repository import Repository
|
||||
@@ -147,6 +152,37 @@ async def network_manager(dbus) -> NetworkManager:
|
||||
yield nm_obj
|
||||
|
||||
|
||||
async def mock_dbus_interface(dbus: DBus, instance: DBusInterface) -> DBusInterface:
|
||||
"""Mock dbus for a DBusInterface instance."""
|
||||
instance.dbus = dbus
|
||||
await instance.connect()
|
||||
return instance
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def hostname(dbus: DBus) -> Hostname:
|
||||
"""Mock Hostname."""
|
||||
yield await mock_dbus_interface(dbus, Hostname())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def timedate(dbus: DBus) -> TimeDate:
|
||||
"""Mock Timedate."""
|
||||
yield await mock_dbus_interface(dbus, TimeDate())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def systemd(dbus: DBus) -> Systemd:
|
||||
"""Mock Systemd."""
|
||||
yield await mock_dbus_interface(dbus, Systemd())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def os_agent(dbus: DBus) -> Systemd:
|
||||
"""Mock OSAgent."""
|
||||
yield await mock_dbus_interface(dbus, OSAgent())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def coresys(loop, docker, network_manager, aiohttp_client, run_dir) -> CoreSys:
|
||||
"""Create a CoreSys Mock."""
|
||||
|
115
tests/dbus/test_resolved.py
Normal file
115
tests/dbus/test_resolved.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Test systemd-resolved dbus interface."""
|
||||
|
||||
from socket import AF_INET6, inet_aton, inet_pton
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.dbus.const import (
|
||||
DNSOverTLSEnabled,
|
||||
DNSSECValidation,
|
||||
DNSStubListenerEnabled,
|
||||
MulticastProtocolEnabled,
|
||||
ResolvConfMode,
|
||||
)
|
||||
|
||||
DNS_IP_FIELDS = [
|
||||
"DNS",
|
||||
"DNSEx",
|
||||
"FallbackDNS",
|
||||
"FallbackDNSEx",
|
||||
"CurrentDNSServer",
|
||||
"CurrentDNSServerEx",
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(name="coresys_ip_bytes")
|
||||
async def fixture_coresys_ip_bytes(coresys: CoreSys) -> CoreSys:
|
||||
"""Coresys with ip addresses correctly mocked as bytes."""
|
||||
get_properties = coresys.dbus.network.dbus.get_properties
|
||||
|
||||
async def mock_get_properties(dbus_obj, interface):
|
||||
reply = await get_properties(interface)
|
||||
|
||||
for field in DNS_IP_FIELDS:
|
||||
if field in reply and len(reply[field]) > 0:
|
||||
if isinstance(reply[field][0], list):
|
||||
for entry in reply[field]:
|
||||
entry[2] = bytes(entry[2])
|
||||
else:
|
||||
reply[field][2] = bytes(reply[field][2])
|
||||
|
||||
return reply
|
||||
|
||||
with patch("supervisor.utils.dbus.DBus.get_properties", new=mock_get_properties):
|
||||
yield coresys
|
||||
|
||||
|
||||
async def test_dbus_resolved_info(coresys_ip_bytes: CoreSys):
|
||||
"""Test systemd-resolved dbus connection."""
|
||||
coresys = coresys_ip_bytes
|
||||
|
||||
assert coresys.dbus.resolved.dns is None
|
||||
|
||||
await coresys.dbus.resolved.connect()
|
||||
await coresys.dbus.resolved.update()
|
||||
|
||||
assert coresys.dbus.resolved.llmnr_hostname == "homeassistant"
|
||||
assert coresys.dbus.resolved.llmnr == MulticastProtocolEnabled.YES
|
||||
assert coresys.dbus.resolved.multicast_dns == MulticastProtocolEnabled.RESOLVE
|
||||
assert coresys.dbus.resolved.dns_over_tls == DNSOverTLSEnabled.NO
|
||||
|
||||
assert len(coresys.dbus.resolved.dns) == 2
|
||||
assert coresys.dbus.resolved.dns[0] == [0, 2, inet_aton("127.0.0.1")]
|
||||
assert coresys.dbus.resolved.dns[1] == [0, 10, inet_pton(AF_INET6, "::1")]
|
||||
assert len(coresys.dbus.resolved.dns_ex) == 2
|
||||
assert coresys.dbus.resolved.dns_ex[0] == [0, 2, inet_aton("127.0.0.1"), 0, ""]
|
||||
assert coresys.dbus.resolved.dns_ex[1] == [0, 10, inet_pton(AF_INET6, "::1"), 0, ""]
|
||||
|
||||
assert len(coresys.dbus.resolved.fallback_dns) == 2
|
||||
assert coresys.dbus.resolved.fallback_dns[0] == [0, 2, inet_aton("1.1.1.1")]
|
||||
assert coresys.dbus.resolved.fallback_dns[1] == [
|
||||
0,
|
||||
10,
|
||||
inet_pton(AF_INET6, "2606:4700:4700::1111"),
|
||||
]
|
||||
assert len(coresys.dbus.resolved.fallback_dns_ex) == 2
|
||||
assert coresys.dbus.resolved.fallback_dns_ex[0] == [
|
||||
0,
|
||||
2,
|
||||
inet_aton("1.1.1.1"),
|
||||
0,
|
||||
"cloudflare-dns.com",
|
||||
]
|
||||
assert coresys.dbus.resolved.fallback_dns_ex[1] == [
|
||||
0,
|
||||
10,
|
||||
inet_pton(AF_INET6, "2606:4700:4700::1111"),
|
||||
0,
|
||||
"cloudflare-dns.com",
|
||||
]
|
||||
|
||||
assert coresys.dbus.resolved.current_dns_server == [0, 2, inet_aton("127.0.0.1")]
|
||||
assert coresys.dbus.resolved.current_dns_server_ex == [
|
||||
0,
|
||||
2,
|
||||
inet_aton("127.0.0.1"),
|
||||
0,
|
||||
"",
|
||||
]
|
||||
|
||||
assert len(coresys.dbus.resolved.domains) == 1
|
||||
assert coresys.dbus.resolved.domains[0] == [0, "local.hass.io", False]
|
||||
|
||||
assert coresys.dbus.resolved.transaction_statistics == [0, 100000]
|
||||
assert coresys.dbus.resolved.cache_statistics == [10, 50000, 10000]
|
||||
assert coresys.dbus.resolved.dnssec == DNSSECValidation.NO
|
||||
assert coresys.dbus.resolved.dnssec_statistics == [0, 0, 0, 0]
|
||||
assert coresys.dbus.resolved.dnssec_supported is False
|
||||
assert coresys.dbus.resolved.dnssec_negative_trust_anchors == [
|
||||
"168.192.in-addr.arpa",
|
||||
"local",
|
||||
]
|
||||
assert coresys.dbus.resolved.dns_stub_listener == DNSStubListenerEnabled.NO
|
||||
assert coresys.dbus.resolved.resolv_conf_mode == ResolvConfMode.FOREIGN
|
52
tests/docker/test_interface.py
Normal file
52
tests/docker/test_interface.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Test Docker interface."""
|
||||
from unittest.mock import Mock, PropertyMock, call, patch
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
import pytest
|
||||
|
||||
from supervisor.const import CpuArch
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.docker.interface import DockerInterface
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_verify_content(coresys: CoreSys):
|
||||
"""Mock verify_content utility during tests."""
|
||||
with patch.object(
|
||||
coresys.security, "verify_content", return_value=None
|
||||
) as verify_content:
|
||||
yield verify_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cpu_arch, platform",
|
||||
[
|
||||
(CpuArch.ARMV7, "linux/arm/v7"),
|
||||
(CpuArch.ARMHF, "linux/arm/v6"),
|
||||
(CpuArch.AARCH64, "linux/arm64"),
|
||||
(CpuArch.I386, "linux/386"),
|
||||
(CpuArch.AMD64, "linux/amd64"),
|
||||
],
|
||||
)
|
||||
async def test_docker_image_platform(coresys: CoreSys, cpu_arch: str, platform: str):
|
||||
"""Test platform set correctly from arch."""
|
||||
with patch.object(
|
||||
coresys.docker.images, "pull", return_value=Mock(id="test:1.2.3")
|
||||
) as pull:
|
||||
instance = DockerInterface(coresys)
|
||||
await instance.install(AwesomeVersion("1.2.3"), "test", arch=cpu_arch)
|
||||
assert pull.call_count == 1
|
||||
assert pull.call_args == call("test:1.2.3", platform=platform)
|
||||
|
||||
|
||||
async def test_docker_image_default_platform(coresys: CoreSys):
|
||||
"""Test platform set using supervisor arch when omitted."""
|
||||
with patch.object(
|
||||
type(coresys.supervisor), "arch", PropertyMock(return_value="i386")
|
||||
), patch.object(
|
||||
coresys.docker.images, "pull", return_value=Mock(id="test:1.2.3")
|
||||
) as pull:
|
||||
instance = DockerInterface(coresys)
|
||||
await instance.install(AwesomeVersion("1.2.3"), "test")
|
||||
assert pull.call_count == 1
|
||||
assert pull.call_args == call("test:1.2.3", platform="linux/386")
|
@@ -1 +1 @@
|
||||
[]
|
||||
["/org/freedesktop/NetworkManager/ActiveConnection/1"]
|
||||
|
@@ -1 +1,38 @@
|
||||
[{"connection": {"id": "Wired connection 1", "permissions": [], "timestamp": 1598125548, "type": "802-3-ethernet", "uuid": "0c23631e-2118-355c-bbb0-8943229cb0d6"}, "ipv4": {"address-data": [{"address": "192.168.2.148", "prefix": 24}], "addresses": [[2483202240, 24, 16951488]], "dns": [16951488], "dns-search": [], "gateway": "192.168.2.1", "method": "auto", "route-data": [], "routes": []}, "ipv6": {"address-data": [], "addresses": [], "dns": [], "dns-search": [], "method": "auto", "route-data": [], "routes": []}, "proxy": {}, "802-3-ethernet": {"auto-negotiate": false, "mac-address-blacklist": [], "s390-options": {}}, "802-11-wireless": {"ssid": [78, 69, 84, 84]}}]
|
||||
[
|
||||
{
|
||||
"connection": {
|
||||
"id": "Wired connection 1",
|
||||
"interface-name": "eth0",
|
||||
"permissions": [],
|
||||
"timestamp": 1598125548,
|
||||
"type": "802-3-ethernet",
|
||||
"uuid": "0c23631e-2118-355c-bbb0-8943229cb0d6"
|
||||
},
|
||||
"ipv4": {
|
||||
"address-data": [{ "address": "192.168.2.148", "prefix": 24 }],
|
||||
"addresses": [[2483202240, 24, 16951488]],
|
||||
"dns": [16951488],
|
||||
"dns-search": [],
|
||||
"gateway": "192.168.2.1",
|
||||
"method": "auto",
|
||||
"route-data": [],
|
||||
"routes": []
|
||||
},
|
||||
"ipv6": {
|
||||
"address-data": [],
|
||||
"addresses": [],
|
||||
"dns": [],
|
||||
"dns-search": [],
|
||||
"method": "auto",
|
||||
"route-data": [],
|
||||
"routes": []
|
||||
},
|
||||
"proxy": {},
|
||||
"802-3-ethernet": {
|
||||
"auto-negotiate": false,
|
||||
"mac-address-blacklist": [],
|
||||
"s390-options": {}
|
||||
},
|
||||
"802-11-wireless": { "ssid": [78, 69, 84, 84] }
|
||||
}
|
||||
]
|
||||
|
194
tests/fixtures/org_freedesktop_resolve1.xml
vendored
Normal file
194
tests/fixtures/org_freedesktop_resolve1.xml
vendored
Normal file
@@ -0,0 +1,194 @@
|
||||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||
<node>
|
||||
<interface name="org.freedesktop.DBus.Peer">
|
||||
<method name="Ping"/>
|
||||
<method name="GetMachineId">
|
||||
<arg type="s" name="machine_uuid" direction="out"/>
|
||||
</method>
|
||||
</interface>
|
||||
<interface name="org.freedesktop.DBus.Introspectable">
|
||||
<method name="Introspect">
|
||||
<arg name="data" type="s" direction="out"/>
|
||||
</method>
|
||||
</interface>
|
||||
<interface name="org.freedesktop.DBus.Properties">
|
||||
<method name="Get">
|
||||
<arg name="interface" direction="in" type="s"/>
|
||||
<arg name="property" direction="in" type="s"/>
|
||||
<arg name="value" direction="out" type="v"/>
|
||||
</method>
|
||||
<method name="GetAll">
|
||||
<arg name="interface" direction="in" type="s"/>
|
||||
<arg name="properties" direction="out" type="a{sv}"/>
|
||||
</method>
|
||||
<method name="Set">
|
||||
<arg name="interface" direction="in" type="s"/>
|
||||
<arg name="property" direction="in" type="s"/>
|
||||
<arg name="value" direction="in" type="v"/>
|
||||
</method>
|
||||
<signal name="PropertiesChanged">
|
||||
<arg type="s" name="interface"/>
|
||||
<arg type="a{sv}" name="changed_properties"/>
|
||||
<arg type="as" name="invalidated_properties"/>
|
||||
</signal>
|
||||
</interface>
|
||||
<interface name="org.freedesktop.resolve1.Manager">
|
||||
<property name="LLMNRHostname" type="s" access="read">
|
||||
</property>
|
||||
<property name="LLMNR" type="s" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
|
||||
</property>
|
||||
<property name="MulticastDNS" type="s" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
|
||||
</property>
|
||||
<property name="DNSOverTLS" type="s" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
|
||||
</property>
|
||||
<property name="DNS" type="a(iiay)" access="read">
|
||||
</property>
|
||||
<property name="DNSEx" type="a(iiayqs)" access="read">
|
||||
</property>
|
||||
<property name="FallbackDNS" type="a(iiay)" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
|
||||
</property>
|
||||
<property name="FallbackDNSEx" type="a(iiayqs)" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
|
||||
</property>
|
||||
<property name="CurrentDNSServer" type="(iiay)" access="read">
|
||||
</property>
|
||||
<property name="CurrentDNSServerEx" type="(iiayqs)" access="read">
|
||||
</property>
|
||||
<property name="Domains" type="a(isb)" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
|
||||
</property>
|
||||
<property name="TransactionStatistics" type="(tt)" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
|
||||
</property>
|
||||
<property name="CacheStatistics" type="(ttt)" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
|
||||
</property>
|
||||
<property name="DNSSEC" type="s" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
|
||||
</property>
|
||||
<property name="DNSSECStatistics" type="(tttt)" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
|
||||
</property>
|
||||
<property name="DNSSECSupported" type="b" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
|
||||
</property>
|
||||
<property name="DNSSECNegativeTrustAnchors" type="as" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
|
||||
</property>
|
||||
<property name="DNSStubListener" type="s" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
|
||||
</property>
|
||||
<property name="ResolvConfMode" type="s" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
|
||||
</property>
|
||||
<method name="ResolveHostname">
|
||||
<arg type="i" name="ifindex" direction="in"/>
|
||||
<arg type="s" name="name" direction="in"/>
|
||||
<arg type="i" name="family" direction="in"/>
|
||||
<arg type="t" name="flags" direction="in"/>
|
||||
<arg type="a(iiay)" name="addresses" direction="out"/>
|
||||
<arg type="s" name="canonical" direction="out"/>
|
||||
<arg type="t" name="flags" direction="out"/>
|
||||
</method>
|
||||
<method name="ResolveAddress">
|
||||
<arg type="i" name="ifindex" direction="in"/>
|
||||
<arg type="i" name="family" direction="in"/>
|
||||
<arg type="ay" name="address" direction="in"/>
|
||||
<arg type="t" name="flags" direction="in"/>
|
||||
<arg type="a(is)" name="names" direction="out"/>
|
||||
<arg type="t" name="flags" direction="out"/>
|
||||
</method>
|
||||
<method name="ResolveRecord">
|
||||
<arg type="i" name="ifindex" direction="in"/>
|
||||
<arg type="s" name="name" direction="in"/>
|
||||
<arg type="q" name="class" direction="in"/>
|
||||
<arg type="q" name="type" direction="in"/>
|
||||
<arg type="t" name="flags" direction="in"/>
|
||||
<arg type="a(iqqay)" name="records" direction="out"/>
|
||||
<arg type="t" name="flags" direction="out"/>
|
||||
</method>
|
||||
<method name="ResolveService">
|
||||
<arg type="i" name="ifindex" direction="in"/>
|
||||
<arg type="s" name="name" direction="in"/>
|
||||
<arg type="s" name="type" direction="in"/>
|
||||
<arg type="s" name="domain" direction="in"/>
|
||||
<arg type="i" name="family" direction="in"/>
|
||||
<arg type="t" name="flags" direction="in"/>
|
||||
<arg type="a(qqqsa(iiay)s)" name="srv_data" direction="out"/>
|
||||
<arg type="aay" name="txt_data" direction="out"/>
|
||||
<arg type="s" name="canonical_name" direction="out"/>
|
||||
<arg type="s" name="canonical_type" direction="out"/>
|
||||
<arg type="s" name="canonical_domain" direction="out"/>
|
||||
<arg type="t" name="flags" direction="out"/>
|
||||
</method>
|
||||
<method name="GetLink">
|
||||
<arg type="i" name="ifindex" direction="in"/>
|
||||
<arg type="o" name="path" direction="out"/>
|
||||
</method>
|
||||
<method name="SetLinkDNS">
|
||||
<arg type="i" name="ifindex" direction="in"/>
|
||||
<arg type="a(iay)" name="addresses" direction="in"/>
|
||||
</method>
|
||||
<method name="SetLinkDNSEx">
|
||||
<arg type="i" name="ifindex" direction="in"/>
|
||||
<arg type="a(iayqs)" name="addresses" direction="in"/>
|
||||
</method>
|
||||
<method name="SetLinkDomains">
|
||||
<arg type="i" name="ifindex" direction="in"/>
|
||||
<arg type="a(sb)" name="domains" direction="in"/>
|
||||
</method>
|
||||
<method name="SetLinkDefaultRoute">
|
||||
<arg type="i" name="ifindex" direction="in"/>
|
||||
<arg type="b" name="enable" direction="in"/>
|
||||
</method>
|
||||
<method name="SetLinkLLMNR">
|
||||
<arg type="i" name="ifindex" direction="in"/>
|
||||
<arg type="s" name="mode" direction="in"/>
|
||||
</method>
|
||||
<method name="SetLinkMulticastDNS">
|
||||
<arg type="i" name="ifindex" direction="in"/>
|
||||
<arg type="s" name="mode" direction="in"/>
|
||||
</method>
|
||||
<method name="SetLinkDNSOverTLS">
|
||||
<arg type="i" name="ifindex" direction="in"/>
|
||||
<arg type="s" name="mode" direction="in"/>
|
||||
</method>
|
||||
<method name="SetLinkDNSSEC">
|
||||
<arg type="i" name="ifindex" direction="in"/>
|
||||
<arg type="s" name="mode" direction="in"/>
|
||||
</method>
|
||||
<method name="SetLinkDNSSECNegativeTrustAnchors">
|
||||
<arg type="i" name="ifindex" direction="in"/>
|
||||
<arg type="as" name="names" direction="in"/>
|
||||
</method>
|
||||
<method name="RevertLink">
|
||||
<arg type="i" name="ifindex" direction="in"/>
|
||||
</method>
|
||||
<method name="RegisterService">
|
||||
<arg type="s" name="name" direction="in"/>
|
||||
<arg type="s" name="name_template" direction="in"/>
|
||||
<arg type="s" name="type" direction="in"/>
|
||||
<arg type="q" name="service_port" direction="in"/>
|
||||
<arg type="q" name="service_priority" direction="in"/>
|
||||
<arg type="q" name="service_weight" direction="in"/>
|
||||
<arg type="aa{say}" name="txt_datas" direction="in"/>
|
||||
<arg type="o" name="service_path" direction="out"/>
|
||||
</method>
|
||||
<method name="UnregisterService">
|
||||
<arg type="o" name="service_path" direction="in"/>
|
||||
</method>
|
||||
<method name="ResetStatistics">
|
||||
</method>
|
||||
<method name="FlushCaches">
|
||||
</method>
|
||||
<method name="ResetServerFeatures">
|
||||
</method>
|
||||
</interface>
|
||||
<node name="link"/>
|
||||
<node name="dnssd"/>
|
||||
</node>
|
39
tests/fixtures/org_freedesktop_resolve1_Manager.json
vendored
Normal file
39
tests/fixtures/org_freedesktop_resolve1_Manager.json
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"LLMNRHostname": "homeassistant",
|
||||
"LLMNR": "yes",
|
||||
"MulticastDNS": "resolve",
|
||||
"DNSOverTLS": "no",
|
||||
"DNS": [
|
||||
[0, 2, [127, 0, 0, 1]],
|
||||
[0, 10, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]]
|
||||
],
|
||||
"DNSEx": [
|
||||
[0, 2, [127, 0, 0, 1], 0, ""],
|
||||
[0, 10, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], 0, ""]
|
||||
],
|
||||
"FallbackDNS": [
|
||||
[0, 2, [1, 1, 1, 1]],
|
||||
[0, 10, [38, 6, 71, 0, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17]]
|
||||
],
|
||||
"FallbackDNSEx": [
|
||||
[0, 2, [1, 1, 1, 1], 0, "cloudflare-dns.com"],
|
||||
[
|
||||
0,
|
||||
10,
|
||||
[38, 6, 71, 0, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 17],
|
||||
0,
|
||||
"cloudflare-dns.com"
|
||||
]
|
||||
],
|
||||
"CurrentDNSServer": [0, 2, [127, 0, 0, 1]],
|
||||
"CurrentDNSServerEx": [0, 2, [127, 0, 0, 1], 0, ""],
|
||||
"Domains": [[0, "local.hass.io", false]],
|
||||
"TransactionStatistics": [0, 100000],
|
||||
"CacheStatistics": [10, 50000, 10000],
|
||||
"DNSSEC": "no",
|
||||
"DNSSECStatistics": [0, 0, 0, 0],
|
||||
"DNSSECSupported": false,
|
||||
"DNSSECNegativeTrustAnchors": ["168.192.in-addr.arpa", "local"],
|
||||
"DNSStubListener": "no",
|
||||
"ResolvConfMode": "foreign"
|
||||
}
|
50
tests/fixtures/org_freedesktop_systemd1-ListUnits.json
vendored
Normal file
50
tests/fixtures/org_freedesktop_systemd1-ListUnits.json
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
[
|
||||
[
|
||||
"etc-machine\\x2did.mount",
|
||||
"/etc/machine-id",
|
||||
"loaded",
|
||||
"active",
|
||||
"mounted",
|
||||
"",
|
||||
"/org/freedesktop/systemd1/unit/etc_2dmachine_5cx2did_2emount",
|
||||
0,
|
||||
"",
|
||||
"/"
|
||||
],
|
||||
[
|
||||
"firewalld.service",
|
||||
"firewalld.service",
|
||||
"not-found",
|
||||
"inactive",
|
||||
"dead",
|
||||
"",
|
||||
"/org/freedesktop/systemd1/unit/firewalld_2eservice",
|
||||
0,
|
||||
"",
|
||||
"/"
|
||||
],
|
||||
[
|
||||
"sys-devices-virtual-tty-ttypd.device",
|
||||
"/sys/devices/virtual/tty/ttypd",
|
||||
"loaded",
|
||||
"active",
|
||||
"plugged",
|
||||
"",
|
||||
"/org/freedesktop/systemd1/unit/sys_2ddevices_2dvirtual_2dtty_2dttypd_2edevice",
|
||||
0,
|
||||
"",
|
||||
"/"
|
||||
],
|
||||
[
|
||||
"zram-swap.service",
|
||||
"HassOS ZRAM swap",
|
||||
"loaded",
|
||||
"active",
|
||||
"exited",
|
||||
"",
|
||||
"/org/freedesktop/systemd1/unit/zram_2dswap_2eservice",
|
||||
0,
|
||||
"",
|
||||
"/"
|
||||
]
|
||||
]
|
127
tests/fixtures/org_freedesktop_systemd1_Manager.json
vendored
Normal file
127
tests/fixtures/org_freedesktop_systemd1_Manager.json
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"Version": "249",
|
||||
"Features": "+PAM -AUDIT -SELINUX +APPARMOR -IMA -SMACK -SECCOMP +GCRYPT +GNUTLS +OPENSSL -ACL +BLKID +CURL -ELFUTILS -FIDO2 -IDN2 -IDN +IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 -PWQUALITY -P11KIT -QRENCODE -BZIP2 -LZ4 -XZ +ZLIB -ZSTD -XKBCOMMON -UTMP -SYSVINIT default-hierarchy=hybrid",
|
||||
"Virtualization": "",
|
||||
"Architecture": "arm64",
|
||||
"Tainted": "cgroupsv1",
|
||||
"FirmwareTimestamp": 0,
|
||||
"FirmwareTimestampMonotonic": 0,
|
||||
"LoaderTimestamp": 0,
|
||||
"LoaderTimestampMonotonic": 0,
|
||||
"KernelTimestamp": 1646197924245019,
|
||||
"KernelTimestampMonotonic": 0,
|
||||
"InitRDTimestamp": 0,
|
||||
"InitRDTimestampMonotonic": 0,
|
||||
"UserspaceTimestamp": 1646197926126937,
|
||||
"UserspaceTimestampMonotonic": 1881921,
|
||||
"FinishTimestamp": 1646197962613554,
|
||||
"FinishTimestampMonotonic": 38368540,
|
||||
"SecurityStartTimestamp": 1646197926137295,
|
||||
"SecurityStartTimestampMonotonic": 1892280,
|
||||
"SecurityFinishTimestamp": 1646197926139253,
|
||||
"SecurityFinishTimestampMonotonic": 1894237,
|
||||
"GeneratorsStartTimestamp": 1646197926235939,
|
||||
"GeneratorsStartTimestampMonotonic": 1990923,
|
||||
"GeneratorsFinishTimestamp": 1646197926260378,
|
||||
"GeneratorsFinishTimestampMonotonic": 2015363,
|
||||
"UnitsLoadStartTimestamp": 1646197926260388,
|
||||
"UnitsLoadStartTimestampMonotonic": 2015371,
|
||||
"UnitsLoadFinishTimestamp": 1646197926339294,
|
||||
"UnitsLoadFinishTimestampMonotonic": 2094278,
|
||||
"InitRDSecurityStartTimestamp": 0,
|
||||
"InitRDSecurityStartTimestampMonotonic": 0,
|
||||
"InitRDSecurityFinishTimestamp": 0,
|
||||
"InitRDSecurityFinishTimestampMonotonic": 0,
|
||||
"InitRDGeneratorsStartTimestamp": 0,
|
||||
"InitRDGeneratorsStartTimestampMonotonic": 0,
|
||||
"InitRDGeneratorsFinishTimestamp": 0,
|
||||
"InitRDGeneratorsFinishTimestampMonotonic": 0,
|
||||
"InitRDUnitsLoadStartTimestamp": 0,
|
||||
"InitRDUnitsLoadStartTimestampMonotonic": 0,
|
||||
"InitRDUnitsLoadFinishTimestamp": 0,
|
||||
"InitRDUnitsLoadFinishTimestampMonotonic": 0,
|
||||
"LogLevel": "info",
|
||||
"LogTarget": "journal - or - kmsg",
|
||||
"NNames": 377,
|
||||
"NFailedUnits": 0,
|
||||
"NJobs": 0,
|
||||
"NInstalledJobs": 798,
|
||||
"NFailedJobs": 0,
|
||||
"Progress": 1.0,
|
||||
"Environment": [
|
||||
"LANG = C.UTF - 8",
|
||||
"PATH = /usr/local / sbin: /usr/local / bin: /usr/sbin: /usr/bin"
|
||||
],
|
||||
"ConfirmSpawn": false,
|
||||
"ShowStatus": true,
|
||||
"UnitPath": [
|
||||
" / etc / systemd / system.control",
|
||||
" / run / systemd / system.control",
|
||||
" / run / systemd / transient",
|
||||
" / run / systemd / generator.early",
|
||||
" / etc / systemd / system",
|
||||
" / etc / systemd / system.attached",
|
||||
" / run / systemd / system",
|
||||
" / run / systemd / system.attached",
|
||||
" / run / systemd / generator",
|
||||
" / usr / local / lib / systemd / system",
|
||||
" / usr / lib / systemd / system",
|
||||
" / run / systemd / generator.late"
|
||||
],
|
||||
"DefaultStandardOutput": "journal",
|
||||
"DefaultStandardError": "inherit",
|
||||
"RuntimeWatchdogUSec": 0,
|
||||
"RebootWatchdogUSec": 600000000,
|
||||
"KExecWatchdogUSec": 0,
|
||||
"ServiceWatchdogs": true,
|
||||
"ControlGroup": "",
|
||||
"SystemState": "running",
|
||||
"ExitCode": [0, 0, 0, 0],
|
||||
"DefaultTimerAccuracyUSec": 60000000,
|
||||
"DefaultTimeoutStartUSec": 90000000,
|
||||
"DefaultTimeoutStopUSec": 90000000,
|
||||
"DefaultTimeoutAbortUSec": 90000000,
|
||||
"DefaultRestartUSec": 100000,
|
||||
"DefaultStartLimitIntervalUSec": 10000000,
|
||||
"DefaultStartLimitBurst": 5,
|
||||
"DefaultCPUAccounting": false,
|
||||
"DefaultBlockIOAccounting": false,
|
||||
"DefaultMemoryAccounting": true,
|
||||
"DefaultTasksAccounting": true,
|
||||
"DefaultLimitCPU": 18446744073709551615,
|
||||
"DefaultLimitCPUSoft": 18446744073709551615,
|
||||
"DefaultLimitFSIZE": 18446744073709551615,
|
||||
"DefaultLimitFSIZESoft": 18446744073709551615,
|
||||
"DefaultLimitDATA": 18446744073709551615,
|
||||
"DefaultLimitDATASoft": 18446744073709551615,
|
||||
"DefaultLimitSTACK": 18446744073709551615,
|
||||
"DefaultLimitSTACKSoft": 8388608,
|
||||
"DefaultLimitCORE": 18446744073709551615,
|
||||
"DefaultLimitCORESoft": 18446744073709551615,
|
||||
"DefaultLimitRSS": 18446744073709551615,
|
||||
"DefaultLimitRSSSoft": 18446744073709551615,
|
||||
"DefaultLimitNOFILE": 524288,
|
||||
"DefaultLimitNOFILESoft": 1024,
|
||||
"DefaultLimitAS": 18446744073709551615,
|
||||
"DefaultLimitASSoft": 18446744073709551615,
|
||||
"DefaultLimitNPROC": 14236,
|
||||
"DefaultLimitNPROCSoft": 14236,
|
||||
"DefaultLimitMEMLOCK": 65536,
|
||||
"DefaultLimitMEMLOCKSoft": 65536,
|
||||
"DefaultLimitLOCKS": 18446744073709551615,
|
||||
"DefaultLimitLOCKSSoft": 18446744073709551615,
|
||||
"DefaultLimitSIGPENDING": 14236,
|
||||
"DefaultLimitSIGPENDINGSoft": 14236,
|
||||
"DefaultLimitMSGQUEUE": 819200,
|
||||
"DefaultLimitMSGQUEUESoft": 819200,
|
||||
"DefaultLimitNICE": 0,
|
||||
"DefaultLimitNICESoft": 0,
|
||||
"DefaultLimitRTPRIO": 0,
|
||||
"DefaultLimitRTPRIOSoft": 0,
|
||||
"DefaultLimitRTTIME": 18446744073709551615,
|
||||
"DefaultLimitRTTIMESoft": 18446744073709551615,
|
||||
"DefaultTasksMax": 4270,
|
||||
"TimerSlackNSec": 50000,
|
||||
"DefaultOOMPolicy": "stop",
|
||||
"CtrlAltDelBurstAction": "reboot - force"
|
||||
}
|
78
tests/host/test_manager.py
Normal file
78
tests/host/test_manager.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Test host manager."""
|
||||
from unittest.mock import AsyncMock, PropertyMock, patch
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.dbus.agent import OSAgent
|
||||
from supervisor.dbus.hostname import Hostname
|
||||
from supervisor.dbus.systemd import Systemd
|
||||
from supervisor.dbus.timedate import TimeDate
|
||||
|
||||
|
||||
async def test_reload(coresys: CoreSys):
|
||||
"""Test manager reload."""
|
||||
with patch.object(coresys.host.info, "update") as info_update, patch.object(
|
||||
coresys.host.services, "update"
|
||||
) as services_update, patch.object(
|
||||
coresys.host.network, "update"
|
||||
) as network_update, patch.object(
|
||||
coresys.host.sys_dbus.agent, "update", new=AsyncMock()
|
||||
) as agent_update, patch.object(
|
||||
coresys.host.sound, "update"
|
||||
) as sound_update:
|
||||
|
||||
await coresys.host.reload()
|
||||
|
||||
info_update.assert_called_once()
|
||||
services_update.assert_called_once()
|
||||
network_update.assert_called_once()
|
||||
agent_update.assert_called_once()
|
||||
sound_update.assert_called_once()
|
||||
|
||||
info_update.reset_mock()
|
||||
services_update.reset_mock()
|
||||
network_update.reset_mock()
|
||||
agent_update.reset_mock()
|
||||
sound_update.reset_mock()
|
||||
|
||||
await coresys.host.reload(
|
||||
services=False, network=False, agent=False, audio=False
|
||||
)
|
||||
info_update.assert_called_once()
|
||||
services_update.assert_not_called()
|
||||
network_update.assert_not_called()
|
||||
agent_update.assert_not_called()
|
||||
sound_update.assert_not_called()
|
||||
|
||||
|
||||
async def test_load(
|
||||
coresys: CoreSys,
|
||||
hostname: Hostname,
|
||||
systemd: Systemd,
|
||||
timedate: TimeDate,
|
||||
os_agent: OSAgent,
|
||||
):
|
||||
"""Test manager load."""
|
||||
type(coresys.dbus).hostname = PropertyMock(return_value=hostname)
|
||||
type(coresys.dbus).systemd = PropertyMock(return_value=systemd)
|
||||
type(coresys.dbus).timedate = PropertyMock(return_value=timedate)
|
||||
type(coresys.dbus).agent = PropertyMock(return_value=os_agent)
|
||||
|
||||
with patch.object(coresys.host.sound, "update") as sound_update, patch.object(
|
||||
coresys.host.apparmor, "load"
|
||||
) as apparmor_load:
|
||||
# Network is updated on connect for a version check so its not None already
|
||||
assert coresys.dbus.hostname.hostname is None
|
||||
assert coresys.dbus.systemd.boot_timestamp is None
|
||||
assert coresys.dbus.timedate.timezone is None
|
||||
assert coresys.dbus.agent.diagnostics is None
|
||||
|
||||
await coresys.host.load()
|
||||
|
||||
assert coresys.dbus.hostname.hostname == "homeassistant-n2"
|
||||
assert coresys.dbus.systemd.boot_timestamp == 1646197962613554
|
||||
assert coresys.dbus.timedate.timezone == "Etc/UTC"
|
||||
assert coresys.dbus.agent.diagnostics is True
|
||||
assert coresys.dbus.network.connectivity_enabled is True
|
||||
|
||||
sound_update.assert_called_once()
|
||||
apparmor_load.assert_called_once()
|
31
tests/host/test_network.py
Normal file
31
tests/host/test_network.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Test network manager."""
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from supervisor.coresys import CoreSys
|
||||
|
||||
|
||||
async def test_load(coresys: CoreSys):
|
||||
"""Test network manager load."""
|
||||
with patch.object(
|
||||
coresys.host.sys_dbus.network,
|
||||
"activate_connection",
|
||||
new=Mock(wraps=coresys.host.sys_dbus.network.activate_connection),
|
||||
) as activate_connection:
|
||||
await coresys.host.network.load()
|
||||
|
||||
assert coresys.host.network.connectivity is True
|
||||
|
||||
assert len(coresys.host.network.dns_servers) == 1
|
||||
assert str(coresys.host.network.dns_servers[0]) == "192.168.30.1"
|
||||
|
||||
assert len(coresys.host.network.interfaces) == 2
|
||||
assert coresys.host.network.interfaces[0].name == "eth0"
|
||||
assert coresys.host.network.interfaces[0].enabled is True
|
||||
assert coresys.host.network.interfaces[1].name == "wlan0"
|
||||
assert coresys.host.network.interfaces[1].enabled is False
|
||||
|
||||
assert activate_connection.call_count == 1
|
||||
assert activate_connection.call_args.args == (
|
||||
"/org/freedesktop/NetworkManager/Settings/1",
|
||||
"/org/freedesktop/NetworkManager/Devices/1",
|
||||
)
|
49
tests/resolution/evaluation/test_evaluate_resolved.py
Normal file
49
tests/resolution/evaluation/test_evaluate_resolved.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Test evaluate systemd-resolved."""
|
||||
|
||||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
from supervisor.const import CoreState
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.resolution.evaluations.resolved import EvaluateResolved
|
||||
|
||||
|
||||
async def test_evaluation(coresys: CoreSys):
|
||||
"""Test evaluation."""
|
||||
resolved = EvaluateResolved(coresys)
|
||||
coresys.core.state = CoreState.SETUP
|
||||
|
||||
assert resolved.reason not in coresys.resolution.unsupported
|
||||
|
||||
with patch.object(
|
||||
type(coresys.dbus.resolved), "is_connected", PropertyMock(return_value=False)
|
||||
):
|
||||
await resolved()
|
||||
assert resolved.reason in coresys.resolution.unsupported
|
||||
|
||||
await resolved()
|
||||
assert resolved.reason not in coresys.resolution.unsupported
|
||||
|
||||
|
||||
async def test_did_run(coresys: CoreSys):
|
||||
"""Test that the evaluation ran as expected."""
|
||||
resolved = EvaluateResolved(coresys)
|
||||
should_run = resolved.states
|
||||
should_not_run = [state for state in CoreState if state not in should_run]
|
||||
assert len(should_run) != 0
|
||||
assert len(should_not_run) != 0
|
||||
|
||||
with patch(
|
||||
"supervisor.resolution.evaluations.resolved.EvaluateResolved.evaluate",
|
||||
return_value=None,
|
||||
) as evaluate:
|
||||
for state in should_run:
|
||||
coresys.core.state = state
|
||||
await resolved()
|
||||
evaluate.assert_called_once()
|
||||
evaluate.reset_mock()
|
||||
|
||||
for state in should_not_run:
|
||||
coresys.core.state = state
|
||||
await resolved()
|
||||
evaluate.assert_not_called()
|
||||
evaluate.reset_mock()
|
@@ -15,13 +15,6 @@ async def test_content_trust(coresys: CoreSys):
|
||||
assert cas_validate.called
|
||||
cas_validate.assert_called_once_with("test@mail.com", "ffffffffffffff")
|
||||
|
||||
with patch("supervisor.security.cas_validate", AsyncMock()) as cas_validate:
|
||||
await coresys.security.verify_own_content("ffffffffffffff")
|
||||
assert cas_validate.called
|
||||
cas_validate.assert_called_once_with(
|
||||
"notary@home-assistant.io", "ffffffffffffff"
|
||||
)
|
||||
|
||||
|
||||
async def test_disabled_content_trust(coresys: CoreSys):
|
||||
"""Test Content-Trust."""
|
||||
@@ -31,10 +24,6 @@ async def test_disabled_content_trust(coresys: CoreSys):
|
||||
await coresys.security.verify_content("test@mail.com", "ffffffffffffff")
|
||||
assert not cas_validate.called
|
||||
|
||||
with patch("supervisor.security.cas_validate", AsyncMock()) as cas_validate:
|
||||
await coresys.security.verify_own_content("ffffffffffffff")
|
||||
assert not cas_validate.called
|
||||
|
||||
|
||||
async def test_force_content_trust(coresys: CoreSys):
|
||||
"""Force Content-Trust tests."""
|
||||
|
@@ -1,11 +1,51 @@
|
||||
"""Test CodeNotary."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from supervisor.exceptions import CodeNotaryUntrusted
|
||||
from supervisor.exceptions import (
|
||||
CodeNotaryBackendError,
|
||||
CodeNotaryError,
|
||||
CodeNotaryUntrusted,
|
||||
)
|
||||
from supervisor.utils.codenotary import calc_checksum, cas_validate
|
||||
|
||||
|
||||
@dataclass
|
||||
class SubprocessResponse:
|
||||
"""Class for specifying subprocess exec response."""
|
||||
|
||||
returncode: int = 0
|
||||
data: str = ""
|
||||
error: str | None = None
|
||||
exception: Exception | None = None
|
||||
|
||||
|
||||
@pytest.fixture(name="subprocess_exec")
|
||||
def fixture_subprocess_exec(request):
|
||||
"""Mock subprocess exec with specific return."""
|
||||
response = request.param
|
||||
if response.exception:
|
||||
communicate_return = AsyncMock(side_effect=response.exception)
|
||||
else:
|
||||
err_return = None
|
||||
if response.error:
|
||||
err_return = Mock(decode=Mock(return_value=response.error))
|
||||
|
||||
communicate_return = AsyncMock(return_value=(response.data, err_return))
|
||||
|
||||
exec_return = Mock(returncode=response.returncode, communicate=communicate_return)
|
||||
|
||||
with patch(
|
||||
"supervisor.utils.codenotary.asyncio.create_subprocess_exec",
|
||||
return_value=exec_return,
|
||||
) as subprocess_exec:
|
||||
yield subprocess_exec
|
||||
|
||||
|
||||
def test_checksum_calc():
|
||||
"""Calc Checkusm as test."""
|
||||
assert calc_checksum("test") == calc_checksum(b"test")
|
||||
@@ -30,3 +70,46 @@ async def test_invalid_checksum():
|
||||
"notary@home-assistant.io",
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"subprocess_exec",
|
||||
[
|
||||
SubprocessResponse(returncode=1, error="test"),
|
||||
SubprocessResponse(returncode=0, data='{"error":"asn1: structure error"}'),
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_cas_backend_error(subprocess_exec):
|
||||
"""Test backend error executing cas command."""
|
||||
with pytest.raises(CodeNotaryBackendError):
|
||||
await cas_validate(
|
||||
"notary@home-assistant.io",
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"subprocess_exec",
|
||||
[SubprocessResponse(returncode=0, data='{"status":1}')],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_cas_notarized_untrusted(subprocess_exec):
|
||||
"""Test cas found notarized but untrusted content."""
|
||||
with pytest.raises(CodeNotaryUntrusted):
|
||||
await cas_validate(
|
||||
"notary@home-assistant.io",
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"subprocess_exec", [SubprocessResponse(exception=OSError())], indirect=True
|
||||
)
|
||||
async def test_cas_exec_os_error(subprocess_exec):
|
||||
"""Test os error attempting to execute cas command."""
|
||||
with pytest.raises(CodeNotaryError):
|
||||
await cas_validate(
|
||||
"notary@home-assistant.io",
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
)
|
||||
|
Reference in New Issue
Block a user