Compare commits

..

2 Commits

Author SHA1 Message Date
Paulus Schoutsen
ec897081cd Update supervisor/api/__init__.py 2022-01-20 14:58:00 -08:00
Paulus Schoutsen
839361133a Add refresh updates API endpoint 2022-01-20 10:14:37 -08:00
1022 changed files with 12839 additions and 30808 deletions

View File

@@ -10,7 +10,6 @@
"visualstudioexptteam.vscodeintellicode", "visualstudioexptteam.vscodeintellicode",
"esbenp.prettier-vscode" "esbenp.prettier-vscode"
], ],
"mounts": ["type=volume,target=/var/lib/docker"],
"settings": { "settings": {
"terminal.integrated.profiles.linux": { "terminal.integrated.profiles.linux": {
"zsh": { "zsh": {
@@ -26,7 +25,7 @@
"python.linting.pylintEnabled": true, "python.linting.pylintEnabled": true,
"python.linting.enabled": true, "python.linting.enabled": true,
"python.formatting.provider": "black", "python.formatting.provider": "black",
"python.formatting.blackArgs": ["--target-version", "py310"], "python.formatting.blackArgs": ["--target-version", "py39"],
"python.formatting.blackPath": "/usr/local/bin/black", "python.formatting.blackPath": "/usr/local/bin/black",
"python.linting.banditPath": "/usr/local/bin/bandit", "python.linting.banditPath": "/usr/local/bin/bandit",
"python.linting.flake8Path": "/usr/local/bin/flake8", "python.linting.flake8Path": "/usr/local/bin/flake8",

View File

@@ -20,14 +20,22 @@ body:
attributes: attributes:
value: | value: |
## Environment ## Environment
- type: input
validations:
required: true
attributes:
label: What is the used version of the Supervisor?
placeholder: supervisor-
description: >
Can be found in the Supervisor panel -> System tab. Starts with
`supervisor-....`.
- type: dropdown - type: dropdown
validations: validations:
required: true required: true
attributes: attributes:
label: What type of installation are you running? label: What type of installation are you running?
description: > description: >
If you don't know, can be found in [Settings -> System -> Repairs -> System Information](https://my.home-assistant.io/redirect/system_health/). If you don't know, you can find it in: Configuration panel -> Info.
It is listed as the `Installation Type` value.
options: options:
- Home Assistant OS - Home Assistant OS
- Home Assistant Supervised - Home Assistant Supervised
@@ -40,6 +48,22 @@ body:
- Home Assistant Operating System - Home Assistant Operating System
- Debian - Debian
- Other (e.g., Raspbian/Raspberry Pi OS/Fedora) - Other (e.g., Raspbian/Raspberry Pi OS/Fedora)
- type: input
validations:
required: true
attributes:
label: What is the version of your installed operating system?
placeholder: "5.11"
description: Can be found in the Supervisor panel -> System tab.
- type: input
validations:
required: true
attributes:
label: What version of Home Assistant Core is installed?
placeholder: core-
description: >
Can be found in the Supervisor panel -> System tab. Starts with
`core-....`.
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
@@ -63,30 +87,8 @@ body:
attributes: attributes:
label: Anything in the Supervisor logs that might be useful for us? label: Anything in the Supervisor logs that might be useful for us?
description: > description: >
Supervisor Logs can be found in [Settings -> System -> Logs](https://my.home-assistant.io/redirect/logs/) The Supervisor logs can be found in the Supervisor panel -> System tab.
then choose `Supervisor` in the top right.
[![Open your Home Assistant instance and show your Supervisor system logs.](https://my.home-assistant.io/badges/supervisor_logs.svg)](https://my.home-assistant.io/redirect/supervisor_logs/)
render: txt render: txt
- type: textarea
validations:
required: true
attributes:
label: System Health information
description: >
System Health information can be found in the top right menu in [Settings -> System -> Repairs](https://my.home-assistant.io/redirect/repairs/).
Click the copy button at the bottom of the pop-up and paste it here.
[![Open your Home Assistant instance and show health information about your system.](https://my.home-assistant.io/badges/system_health.svg)](https://my.home-assistant.io/redirect/system_health/)
- type: textarea
attributes:
label: Supervisor diagnostics
placeholder: "drag-and-drop the diagnostics data file here (do not copy-and-paste the content)"
description: >-
Supervisor diagnostics can be found in [Settings -> Integrations](https://my.home-assistant.io/redirect/integrations/).
Find the card that says `Home Assistant Supervisor`, open its menu and select 'Download diagnostics'.
**Please drag-and-drop the downloaded file into the textbox below. Do not copy and paste its contents.**
- type: textarea - type: textarea
attributes: attributes:
label: Additional information label: Additional information

View File

@@ -31,7 +31,6 @@ categories:
- title: ":arrow_up: Dependency Updates" - title: ":arrow_up: Dependency Updates"
label: "dependencies" label: "dependencies"
collapse-after: 1
include-labels: include-labels:
- "breaking-change" - "breaking-change"

View File

@@ -33,13 +33,9 @@ on:
- setup.py - setup.py
env: env:
DEFAULT_PYTHON: "3.10"
BUILD_NAME: supervisor BUILD_NAME: supervisor
BUILD_TYPE: supervisor BUILD_TYPE: supervisor
WHEELS_TAG: 3.9-alpine3.14
concurrency:
group: '${{ github.workflow }}-${{ github.ref }}'
cancel-in-progress: true
jobs: jobs:
init: init:
@@ -53,7 +49,7 @@ jobs:
requirements: ${{ steps.requirements.outputs.changed }} requirements: ${{ steps.requirements.outputs.changed }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -88,29 +84,21 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }} arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Write env-file
if: needs.init.outputs.requirements == 'true'
run: |
(
# Fix out of memory issues with rust
echo "CARGO_NET_GIT_FETCH_WITH_CLI=true"
) > .env_file
- name: Build wheels - name: Build wheels
if: needs.init.outputs.requirements == 'true' if: needs.init.outputs.requirements == 'true'
uses: home-assistant/wheels@2022.10.1 uses: home-assistant/wheels@master
with: with:
abi: cp310 tag: ${{ env.WHEELS_TAG }}
tag: musllinux_1_2
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
wheels-host: wheels.hass.io
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
apk: "libffi-dev;openssl-dev" wheels-user: wheels
apk: "build-base;libffi-dev;openssl-dev;cargo"
skip-binary: aiohttp skip-binary: aiohttp
env-file: true
requirements: "requirements.txt" requirements: "requirements.txt"
- name: Set version - name: Set version
@@ -121,14 +109,14 @@ jobs:
- name: Login to DockerHub - name: Login to DockerHub
if: needs.init.outputs.publish == 'true' if: needs.init.outputs.publish == 'true'
uses: docker/login-action@v2.1.0 uses: docker/login-action@v1.12.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
if: needs.init.outputs.publish == 'true' if: needs.init.outputs.publish == 'true'
uses: docker/login-action@v2.1.0 uses: docker/login-action@v1.12.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -139,7 +127,7 @@ jobs:
run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV
- name: Build supervisor - name: Build supervisor
uses: home-assistant/builder@2022.09.0 uses: home-assistant/builder@2021.12.0
with: with:
args: | args: |
$BUILD_ARGS \ $BUILD_ARGS \
@@ -150,43 +138,30 @@ jobs:
CAS_API_KEY: ${{ secrets.CAS_TOKEN }} CAS_API_KEY: ${{ secrets.CAS_TOKEN }}
codenotary: codenotary:
name: CAS signature name: CodeNotary signature
needs: init needs: init
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
if: needs.init.outputs.publish == 'true' if: needs.init.outputs.publish == 'true'
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.publish == 'true'
uses: actions/setup-python@v4.3.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Set version - name: Set version
if: needs.init.outputs.publish == 'true' if: needs.init.outputs.publish == 'true'
uses: home-assistant/actions/helpers/version@master uses: home-assistant/actions/helpers/version@master
with: with:
type: ${{ env.BUILD_TYPE }} type: ${{ env.BUILD_TYPE }}
- name: Install dirhash and calc hash - name: Signing image
if: needs.init.outputs.publish == 'true'
id: dirhash
run: |
pip3 install dirhash
dir_hash="$(dirhash "${{ github.workspace }}/supervisor" -a sha256 --match "*.py")"
echo "::set-output name=dirhash::${dir_hash}"
- name: Signing Source
if: needs.init.outputs.publish == 'true' if: needs.init.outputs.publish == 'true'
uses: home-assistant/actions/helpers/codenotary@master uses: home-assistant/actions/helpers/codenotary@master
with: with:
source: hash://${{ steps.dirhash.outputs.dirhash }} source: dir://${{ github.workspace }}
asset: supervisor-${{ needs.init.outputs.version }} user: ${{ secrets.VCN_USER }}
token: ${{ secrets.CAS_TOKEN }} password: ${{ secrets.VCN_PASSWORD }}
organisation: ${{ secrets.VCN_ORG }}
version: version:
name: Update version name: Update version
@@ -195,7 +170,7 @@ jobs:
steps: steps:
- name: Checkout the repository - name: Checkout the repository
if: needs.init.outputs.publish == 'true' if: needs.init.outputs.publish == 'true'
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
- name: Initialize git - name: Initialize git
if: needs.init.outputs.publish == 'true' if: needs.init.outputs.publish == 'true'
@@ -220,11 +195,11 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
- name: Build the Supervisor - name: Build the Supervisor
if: needs.init.outputs.publish != 'true' if: needs.init.outputs.publish != 'true'
uses: home-assistant/builder@2022.09.0 uses: home-assistant/builder@2021.12.0
with: with:
args: | args: |
--test \ --test \
@@ -271,13 +246,13 @@ jobs:
run: | run: |
echo "Checking supervisor info" echo "Checking supervisor info"
test=$(docker exec hassio_cli ha supervisor info --no-progress --raw-json | jq -r '.result') test=$(docker exec hassio_cli ha supervisor info --no-progress --raw-json | jq -r '.result')
if [ "$test" != "ok" ]; then if [ "$test" != "ok" ];then
exit 1 exit 1
fi fi
echo "Checking supervisor network info" echo "Checking supervisor network info"
test=$(docker exec hassio_cli ha network info --no-progress --raw-json | jq -r '.result') test=$(docker exec hassio_cli ha network info --no-progress --raw-json | jq -r '.result')
if [ "$test" != "ok" ]; then if [ "$test" != "ok" ];then
exit 1 exit 1
fi fi
@@ -285,25 +260,13 @@ jobs:
run: | run: |
echo "Install Core SSH Add-on" echo "Install Core SSH Add-on"
test=$(docker exec hassio_cli ha addons install core_ssh --no-progress --raw-json | jq -r '.result') test=$(docker exec hassio_cli ha addons install core_ssh --no-progress --raw-json | jq -r '.result')
if [ "$test" != "ok" ]; then 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 exit 1
fi fi
echo "Start Core SSH Add-on" echo "Start Core SSH Add-on"
test=$(docker exec hassio_cli ha addons start core_ssh --no-progress --raw-json | jq -r '.result') test=$(docker exec hassio_cli ha addons start core_ssh --no-progress --raw-json | jq -r '.result')
if [ "$test" != "ok" ]; then if [ "$test" != "ok" ];then
exit 1
fi
# Make sure its state is started
test="$(docker exec hassio_cli ha addons info core_ssh --no-progress --raw-json | jq -r '.data.state')"
if [ "$test" != "started" ]; then
exit 1 exit 1
fi fi
@@ -312,83 +275,19 @@ jobs:
run: | run: |
echo "Enable Content-Trust" echo "Enable Content-Trust"
test=$(docker exec hassio_cli ha security options --content-trust=true --no-progress --raw-json | jq -r '.result') test=$(docker exec hassio_cli ha security options --content-trust=true --no-progress --raw-json | jq -r '.result')
if [ "$test" != "ok" ]; then if [ "$test" != "ok" ];then
exit 1 exit 1
fi fi
echo "Run supervisor health check" echo "Run supervisor health check"
test=$(docker exec hassio_cli ha resolution healthcheck --no-progress --raw-json | jq -r '.result') test=$(docker exec hassio_cli ha resolution healthcheck --no-progress --raw-json | jq -r '.result')
if [ "$test" != "ok" ]; then if [ "$test" != "ok" ];then
exit 1 exit 1
fi fi
echo "Check supervisor unhealthy" echo "Check supervisor unhealthy"
test=$(docker exec hassio_cli ha resolution info --no-progress --raw-json | jq -r '.data.unhealthy[]') test=$(docker exec hassio_cli ha resolution info --no-progress --raw-json | jq -r '.data.unhealthy[]')
if [ "$test" != "" ]; then if [ "$test" != "" ];then
exit 1
fi
echo "Check supervisor supported"
test=$(docker exec hassio_cli ha resolution info --no-progress --raw-json | jq -r '.data.unsupported[]')
if [[ "$test" =~ source_mods ]]; then
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
# Make sure its state is started
test="$(docker exec hassio_cli ha addons info core_ssh --no-progress --raw-json | jq -r '.data.state')"
if [ "$test" != "started" ]; then
exit 1
fi
- name: Restore SSL directory from backup
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 exit 1
fi fi

View File

@@ -8,33 +8,30 @@ on:
pull_request: ~ pull_request: ~
env: env:
DEFAULT_PYTHON: "3.10" DEFAULT_PYTHON: 3.9
PRE_COMMIT_HOME: ~/.cache/pre-commit PRE_COMMIT_HOME: ~/.cache/pre-commit
DEFAULT_CAS: v1.0.2 DEFAULT_VCN: v0.9.8
concurrency:
group: '${{ github.workflow }}-${{ github.ref }}'
cancel-in-progress: true
jobs: jobs:
# Separate job to pre-populate the base dependency cache # Separate job to pre-populate the base dependency cache
# This prevent upcoming jobs to do the same individually # This prevent upcoming jobs to do the same individually
prepare: prepare:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: strategy:
python-version: ${{ steps.python.outputs.python-version }} matrix:
name: Prepare Python dependencies python-version: [3.9]
name: Prepare Python ${{ matrix.python-version }} dependencies
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
- name: Set up Python - name: Set up Python ${{ matrix.python-version }}
id: python id: python
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2.3.1
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ matrix.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.0.11 uses: actions/cache@v2.1.7
with: with:
path: venv path: venv
key: | key: |
@@ -48,7 +45,7 @@ jobs:
pip install -r requirements.txt -r requirements_tests.txt pip install -r requirements.txt -r requirements_tests.txt
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v3.0.11 uses: actions/cache@v2.1.7
with: with:
path: ${{ env.PRE_COMMIT_HOME }} path: ${{ env.PRE_COMMIT_HOME }}
key: | key: |
@@ -67,19 +64,19 @@ jobs:
needs: prepare needs: prepare
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2.3.1
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.0.11 uses: actions/cache@v2.1.7
with: with:
path: venv path: venv
key: | key: |
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@@ -96,7 +93,7 @@ jobs:
needs: prepare needs: prepare
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
- name: Register hadolint problem matcher - name: Register hadolint problem matcher
run: | run: |
echo "::add-matcher::.github/workflows/matchers/hadolint.json" echo "::add-matcher::.github/workflows/matchers/hadolint.json"
@@ -111,19 +108,19 @@ jobs:
needs: prepare needs: prepare
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2.3.1
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.0.11 uses: actions/cache@v2.1.7
with: with:
path: venv path: venv
key: | key: |
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@@ -131,7 +128,7 @@ jobs:
exit 1 exit 1
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v3.0.11 uses: actions/cache@v2.1.7
with: with:
path: ${{ env.PRE_COMMIT_HOME }} path: ${{ env.PRE_COMMIT_HOME }}
key: | key: |
@@ -155,19 +152,19 @@ jobs:
needs: prepare needs: prepare
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2.3.1
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.0.11 uses: actions/cache@v2.1.7
with: with:
path: venv path: venv
key: | key: |
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@@ -187,19 +184,19 @@ jobs:
needs: prepare needs: prepare
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2.3.1
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.0.11 uses: actions/cache@v2.1.7
with: with:
path: venv path: venv
key: | key: |
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@@ -207,7 +204,7 @@ jobs:
exit 1 exit 1
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v3.0.11 uses: actions/cache@v2.1.7
with: with:
path: ${{ env.PRE_COMMIT_HOME }} path: ${{ env.PRE_COMMIT_HOME }}
key: | key: |
@@ -228,19 +225,19 @@ jobs:
needs: prepare needs: prepare
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2.3.1
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.0.11 uses: actions/cache@v2.1.7
with: with:
path: venv path: venv
key: | key: |
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@@ -248,7 +245,7 @@ jobs:
exit 1 exit 1
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v3.0.11 uses: actions/cache@v2.1.7
with: with:
path: ${{ env.PRE_COMMIT_HOME }} path: ${{ env.PRE_COMMIT_HOME }}
key: | key: |
@@ -272,19 +269,19 @@ jobs:
needs: prepare needs: prepare
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2.3.1
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.0.11 uses: actions/cache@v2.1.7
with: with:
path: venv path: venv
key: | key: |
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@@ -304,19 +301,19 @@ jobs:
needs: prepare needs: prepare
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2.3.1
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.0.11 uses: actions/cache@v2.1.7
with: with:
path: venv path: venv
key: | key: |
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@@ -324,7 +321,7 @@ jobs:
exit 1 exit 1
- name: Restore pre-commit environment from cache - name: Restore pre-commit environment from cache
id: cache-precommit id: cache-precommit
uses: actions/cache@v3.0.11 uses: actions/cache@v2.1.7
with: with:
path: ${{ env.PRE_COMMIT_HOME }} path: ${{ env.PRE_COMMIT_HOME }}
key: | key: |
@@ -342,26 +339,29 @@ jobs:
pytest: pytest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: prepare needs: prepare
name: Run tests Python ${{ needs.prepare.outputs.python-version }} strategy:
matrix:
python-version: [3.9]
name: Run tests Python ${{ matrix.python-version }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2.3.1
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ matrix.python-version }}
- name: Install CAS tools - name: Install VCN tools
uses: home-assistant/actions/helpers/cas@master uses: home-assistant/actions/helpers/vcn@master
with: with:
version: ${{ env.DEFAULT_CAS }} vcn_version: ${{ env.DEFAULT_VCN }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.0.11 uses: actions/cache@v2.1.7
with: with:
path: venv path: venv
key: | key: |
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
@@ -392,7 +392,7 @@ jobs:
-o console_output_style=count \ -o console_output_style=count \
tests tests
- name: Upload coverage artifact - name: Upload coverage artifact
uses: actions/upload-artifact@v3.1.1 uses: actions/upload-artifact@v2.3.1
with: with:
name: coverage-${{ matrix.python-version }} name: coverage-${{ matrix.python-version }}
path: .coverage path: .coverage
@@ -400,29 +400,29 @@ jobs:
coverage: coverage:
name: Process test coverage name: Process test coverage
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: ["pytest", "prepare"] needs: pytest
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
- name: Set up Python ${{ needs.prepare.outputs.python-version }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v4.3.0 uses: actions/setup-python@v2.3.1
id: python id: python
with: with:
python-version: ${{ needs.prepare.outputs.python-version }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v3.0.11 uses: actions/cache@v2.1.7
with: with:
path: venv path: venv
key: | key: |
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
- name: Fail job if Python cache restore failed - name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true' if: steps.cache-venv.outputs.cache-hit != 'true'
run: | run: |
echo "Failed to restore Python virtual environment from cache" echo "Failed to restore Python virtual environment from cache"
exit 1 exit 1
- name: Download all coverage artifacts - name: Download all coverage artifacts
uses: actions/download-artifact@v3 uses: actions/download-artifact@v2
- name: Combine coverage results - name: Combine coverage results
run: | run: |
. venv/bin/activate . venv/bin/activate
@@ -430,4 +430,4 @@ jobs:
coverage report coverage report
coverage xml coverage xml
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v3.1.1 uses: codecov/codecov-action@v2.1.0

View File

@@ -9,7 +9,7 @@ jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v3.0.0 - uses: dessant/lock-threads@v3
with: with:
github-token: ${{ github.token }} github-token: ${{ github.token }}
issue-inactive-days: "30" issue-inactive-days: "30"

View File

@@ -11,7 +11,7 @@ jobs:
name: Release Drafter name: Release Drafter
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -36,7 +36,7 @@ jobs:
echo "::set-output name=version::$datepre.$newpost" echo "::set-output name=version::$datepre.$newpost"
- name: Run Release Drafter - name: Run Release Drafter
uses: release-drafter/release-drafter@v5.21.1 uses: release-drafter/release-drafter@v5
with: with:
tag: ${{ steps.version.outputs.version }} tag: ${{ steps.version.outputs.version }}
name: ${{ steps.version.outputs.version }} name: ${{ steps.version.outputs.version }}

View File

@@ -10,9 +10,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v3.1.0 uses: actions/checkout@v2.4.0
- name: Sentry Release - name: Sentry Release
uses: getsentry/action-release@v1.2.0 uses: getsentry/action-release@v1.1.6
env: env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_ORG: ${{ secrets.SENTRY_ORG }}

View File

@@ -9,10 +9,10 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v6.0.1 - uses: actions/stale@v4
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 30 days-before-stale: 60
days-before-close: 7 days-before-close: 7
stale-issue-label: "stale" stale-issue-label: "stale"
exempt-issue-labels: "no-stale,Help%20wanted,help-wanted,pinned,rfc,security" exempt-issue-labels: "no-stale,Help%20wanted,help-wanted,pinned,rfc,security"

View File

@@ -1,13 +1,13 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.6.0 rev: 21.12b0
hooks: hooks:
- id: black - id: black
args: args:
- --safe - --safe
- --quiet - --quiet
- --target-version - --target-version
- py310 - py39
files: ^((supervisor|tests)/.+)?[^/]+\.py$ files: ^((supervisor|tests)/.+)?[^/]+\.py$
- repo: https://gitlab.com/pycqa/flake8 - repo: https://gitlab.com/pycqa/flake8
rev: 3.8.3 rev: 3.8.3
@@ -28,7 +28,7 @@ repos:
hooks: hooks:
- id: isort - id: isort
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.32.1 rev: v2.31.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py310-plus] args: [--py39-plus]

View File

@@ -5,11 +5,10 @@ ENV \
S6_SERVICES_GRACETIME=10000 \ S6_SERVICES_GRACETIME=10000 \
SUPERVISOR_API=http://localhost SUPERVISOR_API=http://localhost
ARG \ ARG BUILD_ARCH
CAS_VERSION WORKDIR /usr/src
# Install base # Install base
WORKDIR /usr/src
RUN \ RUN \
set -x \ set -x \
&& apk add --no-cache \ && apk add --no-cache \
@@ -19,27 +18,14 @@ RUN \
libffi \ libffi \
libpulse \ libpulse \
musl \ musl \
openssl \ openssl
&& apk add --no-cache --virtual .build-dependencies \
build-base \
go \
\
&& git clone -b "v${CAS_VERSION}" --depth 1 \
https://github.com/codenotary/cas \
&& cd cas \
&& make cas \
&& mv cas /usr/bin/cas \
\
&& apk del .build-dependencies \
&& rm -rf /root/go /root/.cache \
&& rm -rf /usr/src/cas
# Install requirements # Install requirements
COPY requirements.txt . COPY requirements.txt .
RUN \ RUN \
export MAKEFLAGS="-j$(nproc)" \ export MAKEFLAGS="-j$(nproc)" \
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links \
"https://wheels.home-assistant.io/musllinux/" \ "https://wheels.home-assistant.io/alpine-$(cut -d '.' -f 1-2 < /etc/alpine-release)/${BUILD_ARCH}/" \
-r ./requirements.txt \ -r ./requirements.txt \
&& rm -f requirements.txt && rm -f requirements.txt

View File

@@ -1,16 +1,14 @@
image: homeassistant/{arch}-hassio-supervisor image: homeassistant/{arch}-hassio-supervisor
shadow_repository: ghcr.io/home-assistant shadow_repository: ghcr.io/home-assistant
build_from: build_from:
aarch64: ghcr.io/home-assistant/aarch64-base-python:3.10-alpine3.16 aarch64: ghcr.io/home-assistant/aarch64-base-python:3.9-alpine3.14
armhf: ghcr.io/home-assistant/armhf-base-python:3.10-alpine3.16 armhf: ghcr.io/home-assistant/armhf-base-python:3.9-alpine3.14
armv7: ghcr.io/home-assistant/armv7-base-python:3.10-alpine3.16 armv7: ghcr.io/home-assistant/armv7-base-python:3.9-alpine3.14
amd64: ghcr.io/home-assistant/amd64-base-python:3.10-alpine3.16 amd64: ghcr.io/home-assistant/amd64-base-python:3.9-alpine3.14
i386: ghcr.io/home-assistant/i386-base-python:3.10-alpine3.16 i386: ghcr.io/home-assistant/i386-base-python:3.9-alpine3.14
codenotary: codenotary:
signer: notary@home-assistant.io signer: notary@home-assistant.io
base_image: notary@home-assistant.io base_image: notary@home-assistant.io
args:
CAS_VERSION: 1.0.2
labels: labels:
io.hass.type: supervisor io.hass.type: supervisor
org.opencontainers.image.title: Home Assistant Supervisor org.opencontainers.image.title: Home Assistant Supervisor

View File

@@ -12,19 +12,24 @@ extension-pkg-whitelist=
# locally-disabled - it spams too much # locally-disabled - it spams too much
# duplicate-code - unavoidable # duplicate-code - unavoidable
# cyclic-import - doesn't test if both import on load # cyclic-import - doesn't test if both import on load
# abstract-class-little-used - prevents from setting right foundation
# abstract-class-not-used - is flaky, should not show up but does # abstract-class-not-used - is flaky, should not show up but does
# unused-argument - generic callbacks and setup methods create a lot of warnings # unused-argument - generic callbacks and setup methods create a lot of warnings
# redefined-variable-type - this is Python, we're duck typing!
# too-many-* - are not enforced for the sake of readability # too-many-* - are not enforced for the sake of readability
# too-few-* - same as too-many-* # too-few-* - same as too-many-*
# abstract-method - with intro of async there are always methods missing # abstract-method - with intro of async there are always methods missing
disable= disable=
format, format,
abstract-class-little-used,
abstract-method, abstract-method,
cyclic-import, cyclic-import,
duplicate-code, duplicate-code,
locally-disabled, locally-disabled,
no-else-return, no-else-return,
no-self-use,
not-context-manager, not-context-manager,
redefined-variable-type,
too-few-public-methods, too-few-public-methods,
too-many-arguments, too-many-arguments,
too-many-branches, too-many-branches,

View File

@@ -1,2 +0,0 @@
[pytest]
asyncio_mode = auto

View File

@@ -1,25 +1,22 @@
aiodns==3.0.0 aiohttp==3.8.1
aiohttp==3.8.3
async_timeout==4.0.2 async_timeout==4.0.2
atomicwrites-homeassistant==1.4.1 atomicwrites==1.4.0
attrs==22.1.0 attrs==21.2.0
awesomeversion==22.9.0 awesomeversion==22.1.0
brotli==1.0.9 brotli==1.0.9
cchardet==2.1.7 cchardet==2.1.7
ciso8601==2.2.0 ciso8601==2.2.0
colorlog==6.7.0 colorlog==6.6.0
cpe==1.2.1 cpe==1.2.1
cryptography==38.0.1 cryptography==36.0.1
debugpy==1.6.3 debugpy==1.5.1
deepmerge==1.1.0 deepmerge==1.0.1
dirhash==0.2.1 docker==5.0.3
docker==6.0.0 gitpython==3.1.26
gitpython==3.1.29 jinja2==3.0.3
jinja2==3.1.2 pulsectl==21.10.5
pulsectl==22.3.2 pyudev==0.22.0
pyudev==0.24.0 ruamel.yaml==0.17.17
ruamel.yaml==0.17.21 sentry-sdk==1.5.2
securetar==2022.2.0 voluptuous==0.12.2
sentry-sdk==1.10.1 dbus-next==0.2.3
voluptuous==0.13.1
dbus-fast==1.49.0

View File

@@ -1,15 +1,14 @@
black==22.10.0 black==21.12b0
codecov==2.1.12 codecov==2.1.12
coverage==6.5.0 coverage==6.2
flake8-docstrings==1.6.0 flake8-docstrings==1.6.0
flake8==5.0.4 flake8==4.0.1
pre-commit==2.20.0 pre-commit==2.17.0
pydocstyle==6.1.1 pydocstyle==6.1.1
pylint==2.15.5 pylint==2.12.2
pytest-aiohttp==1.0.4 pytest-aiohttp==0.3.0
pytest-asyncio==0.18.3 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==4.0.0 pytest-cov==3.0.0
pytest-timeout==2.1.0 pytest-timeout==2.0.2
pytest==7.2.0 pytest==6.2.5
pyupgrade==3.1.0 pyupgrade==2.31.0
time-machine==2.8.2

0
rootfs/etc/cont-init.d/udev.sh Executable file → Normal file
View File

11
rootfs/etc/services.d/supervisor/finish Executable file → Normal file
View File

@@ -1,11 +1,8 @@
#!/usr/bin/env bashio #!/usr/bin/execlineb -S1
# ============================================================================== # ==============================================================================
# Take down the S6 supervision tree when Supervisor fails # Take down the S6 supervision tree when Supervisor fails
# ============================================================================== # ==============================================================================
if { s6-test ${1} -ne 100 }
if { s6-test ${1} -ne 256 }
if [[ "$1" -ne 100 ]] && [[ "$1" -ne 256 ]]; then redirfd -w 2 /dev/null s6-svscanctl -t /var/run/s6/services
bashio::log.warning "Halt Supervisor"
/run/s6/basedir/bin/halt
fi
bashio::log.info "Supervisor restart after closing"

1
rootfs/etc/services.d/supervisor/run Executable file → Normal file
View File

@@ -3,6 +3,5 @@
# Start Supervisor service # Start Supervisor service
# ============================================================================== # ==============================================================================
export LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" export LD_PRELOAD="/usr/local/lib/libjemalloc.so.2"
export MALLOC_CONF="background_thread:true,metadata_thp:auto"
exec python3 -m supervisor exec python3 -m supervisor

11
rootfs/etc/services.d/watchdog/finish Executable file → Normal file
View File

@@ -1,11 +1,8 @@
#!/usr/bin/env bashio #!/usr/bin/execlineb -S1
# ============================================================================== # ==============================================================================
# Take down the S6 supervision tree when Watchdog fails # Take down the S6 supervision tree when Watchdog fails
# ============================================================================== # ==============================================================================
if { s6-test ${1} -ne 0 }
if { s6-test ${1} -ne 256 }
if [[ "$1" -ne 0 ]] && [[ "$1" -ne 256 ]]; then s6-svscanctl -t /var/run/s6/services
bashio::log.warning "Halt Supervisor (Wuff)"
/run/s6/basedir/bin/halt
fi
bashio::log.info "Watchdog restart after closing"

2
rootfs/etc/services.d/watchdog/run Executable file → Normal file
View File

@@ -31,4 +31,4 @@ do
done done
bashio::exit.nok "Watchdog detected issue with Supervisor - taking container down!" basio::exit.nok "Watchdog detected issue with Supervisor - taking container down!"

View File

@@ -1,4 +0,0 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE03LvYuz79GTJx4uKp3w6NrSe5JZI
iBtgzzYi0YQYtZO/r+xFpgDJEa0gLHkXtl94fpqrFiN89In83lzaszbZtA==
-----END PUBLIC KEY-----

View File

@@ -1,8 +0,0 @@
{
"currentcontext": {
"LcHost": "cas.codenotary.com",
"LcPort": "443"
},
"schemaversion": 3,
"users": null
}

View File

@@ -49,7 +49,6 @@ setup(
"supervisor.resolution.evaluations", "supervisor.resolution.evaluations",
"supervisor.resolution.fixups", "supervisor.resolution.fixups",
"supervisor.resolution", "supervisor.resolution",
"supervisor.security",
"supervisor.services.modules", "supervisor.services.modules",
"supervisor.services", "supervisor.services",
"supervisor.store", "supervisor.store",

View File

@@ -28,8 +28,7 @@ if __name__ == "__main__":
bootstrap.initialize_logging() bootstrap.initialize_logging()
# Init async event loop # Init async event loop
loop = asyncio.new_event_loop() loop = asyncio.get_event_loop()
asyncio.set_event_loop(loop)
# Check if all information are available to setup Supervisor # Check if all information are available to setup Supervisor
bootstrap.check_environment() bootstrap.check_environment()

View File

@@ -3,7 +3,7 @@ import asyncio
from contextlib import suppress from contextlib import suppress
import logging import logging
import tarfile import tarfile
from typing import Union from typing import Optional, Union
from ..const import AddonBoot, AddonStartup, AddonState from ..const import AddonBoot, AddonStartup, AddonState
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
@@ -24,7 +24,6 @@ from ..resolution.const import ContextType, IssueType, SuggestionType
from ..store.addon import AddonStore from ..store.addon import AddonStore
from ..utils import check_exception_chain from ..utils import check_exception_chain
from .addon import Addon from .addon import Addon
from .const import ADDON_UPDATE_CONDITIONS
from .data import AddonsData from .data import AddonsData
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@@ -53,7 +52,7 @@ class AddonManager(CoreSysAttributes):
"""Return a list of all installed add-ons.""" """Return a list of all installed add-ons."""
return list(self.local.values()) return list(self.local.values())
def get(self, addon_slug: str, local_only: bool = False) -> AnyAddon | None: def get(self, addon_slug: str, local_only: bool = False) -> Optional[AnyAddon]:
"""Return an add-on from slug. """Return an add-on from slug.
Prio: Prio:
@@ -66,7 +65,7 @@ class AddonManager(CoreSysAttributes):
return self.store.get(addon_slug) return self.store.get(addon_slug)
return None return None
def from_token(self, token: str) -> Addon | None: def from_token(self, token: str) -> Optional[Addon]:
"""Return an add-on from Supervisor token.""" """Return an add-on from Supervisor token."""
for addon in self.installed: for addon in self.installed:
if token == addon.supervisor_token: if token == addon.supervisor_token:
@@ -145,7 +144,11 @@ class AddonManager(CoreSysAttributes):
self.sys_capture_exception(err) self.sys_capture_exception(err)
@Job( @Job(
conditions=ADDON_UPDATE_CONDITIONS, conditions=[
JobCondition.FREE_SPACE,
JobCondition.INTERNET_HOST,
JobCondition.HEALTHY,
],
on_condition=AddonsJobError, on_condition=AddonsJobError,
) )
async def install(self, slug: str) -> None: async def install(self, slug: str) -> None:
@@ -164,7 +167,6 @@ class AddonManager(CoreSysAttributes):
self.data.install(store) self.data.install(store)
addon = Addon(self.coresys, slug) addon = Addon(self.coresys, slug)
await addon.load()
if not addon.path_data.is_dir(): if not addon.path_data.is_dir():
_LOGGER.info( _LOGGER.info(
@@ -176,7 +178,7 @@ class AddonManager(CoreSysAttributes):
await addon.install_apparmor() await addon.install_apparmor()
try: try:
await addon.instance.install(store.version, store.image, arch=addon.arch) await addon.instance.install(store.version, store.image)
except DockerError as err: except DockerError as err:
self.data.uninstall(addon) self.data.uninstall(addon)
raise AddonsError() from err raise AddonsError() from err
@@ -203,7 +205,7 @@ class AddonManager(CoreSysAttributes):
else: else:
addon.state = AddonState.UNKNOWN addon.state = AddonState.UNKNOWN
await addon.unload() await addon.remove_data()
# Cleanup audio settings # Cleanup audio settings
if addon.path_pulse.exists(): if addon.path_pulse.exists():
@@ -243,10 +245,14 @@ class AddonManager(CoreSysAttributes):
_LOGGER.info("Add-on '%s' successfully removed", slug) _LOGGER.info("Add-on '%s' successfully removed", slug)
@Job( @Job(
conditions=ADDON_UPDATE_CONDITIONS, conditions=[
JobCondition.FREE_SPACE,
JobCondition.INTERNET_HOST,
JobCondition.HEALTHY,
],
on_condition=AddonsJobError, on_condition=AddonsJobError,
) )
async def update(self, slug: str, backup: bool | None = False) -> None: async def update(self, slug: str, backup: Optional[bool] = False) -> None:
"""Update add-on.""" """Update add-on."""
if slug not in self.local: if slug not in self.local:
raise AddonsError(f"Add-on {slug} is not installed", _LOGGER.error) raise AddonsError(f"Add-on {slug} is not installed", _LOGGER.error)

View File

@@ -10,15 +10,13 @@ import secrets
import shutil import shutil
import tarfile import tarfile
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Any, Awaitable, Final from typing import Any, Awaitable, Final, Optional
import aiohttp import aiohttp
from deepmerge import Merger from deepmerge import Merger
from securetar import atomic_contents_add, secure_path
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
from ..bus import EventListener
from ..const import ( from ..const import (
ATTR_ACCESS_TOKEN, ATTR_ACCESS_TOKEN,
ATTR_AUDIO_INPUT, ATTR_AUDIO_INPUT,
@@ -49,36 +47,26 @@ from ..const import (
AddonBoot, AddonBoot,
AddonStartup, AddonStartup,
AddonState, AddonState,
BusEvent,
) )
from ..coresys import CoreSys from ..coresys import CoreSys
from ..docker.addon import DockerAddon from ..docker.addon import DockerAddon
from ..docker.const import ContainerState
from ..docker.monitor import DockerContainerStateEvent
from ..docker.stats import DockerStats from ..docker.stats import DockerStats
from ..exceptions import ( from ..exceptions import (
AddonConfigurationError, AddonConfigurationError,
AddonsError, AddonsError,
AddonsJobError,
AddonsNotSupportedError, AddonsNotSupportedError,
ConfigurationFileError, ConfigurationFileError,
DockerError, DockerError,
DockerRequestError,
HostAppArmorError, HostAppArmorError,
) )
from ..hardware.data import Device from ..hardware.data import Device
from ..homeassistant.const import WSEvent, WSType from ..homeassistant.const import WSEvent, WSType
from ..jobs.const import JobExecutionLimit
from ..jobs.decorator import Job
from ..utils import check_port from ..utils import check_port
from ..utils.apparmor import adjust_profile from ..utils.apparmor import adjust_profile
from ..utils.json import read_json_file, write_json_file from ..utils.json import read_json_file, write_json_file
from .const import ( from ..utils.tar import atomic_contents_add, secure_path
WATCHDOG_MAX_ATTEMPTS, from .const import AddonBackupMode
WATCHDOG_RETRY_SECONDS,
WATCHDOG_THROTTLE_MAX_CALLS,
WATCHDOG_THROTTLE_PERIOD,
AddonBackupMode,
)
from .model import AddonModel, Data from .model import AddonModel, Data
from .options import AddonOptions from .options import AddonOptions
from .utils import remove_data from .utils import remove_data
@@ -96,6 +84,8 @@ RE_WATCHDOG = re.compile(
r":\/\/\[HOST\]:(?:\[PORT:)?(?P<t_port>\d+)\]?(?P<s_suffix>.*)$" r":\/\/\[HOST\]:(?:\[PORT:)?(?P<t_port>\d+)\]?(?P<s_suffix>.*)$"
) )
RE_OLD_AUDIO = re.compile(r"\d+,\d+")
WATCHDOG_TIMEOUT = aiohttp.ClientTimeout(total=10) WATCHDOG_TIMEOUT = aiohttp.ClientTimeout(total=10)
_OPTIONS_MERGER: Final = Merger( _OPTIONS_MERGER: Final = Merger(
@@ -113,58 +103,6 @@ class Addon(AddonModel):
super().__init__(coresys, slug) super().__init__(coresys, slug)
self.instance: DockerAddon = DockerAddon(coresys, self) self.instance: DockerAddon = DockerAddon(coresys, self)
self._state: AddonState = AddonState.UNKNOWN self._state: AddonState = AddonState.UNKNOWN
self._manual_stop: bool = (
self.sys_hardware.helper.last_boot != self.sys_config.last_boot
)
self._listeners: list[EventListener] = []
@Job(
name=f"addon_{slug}_restart_after_problem",
limit=JobExecutionLimit.THROTTLE_RATE_LIMIT,
throttle_period=WATCHDOG_THROTTLE_PERIOD,
throttle_max_calls=WATCHDOG_THROTTLE_MAX_CALLS,
on_condition=AddonsJobError,
)
async def restart_after_problem(addon: Addon, state: ContainerState):
"""Restart unhealthy or failed addon."""
attempts = 0
while await addon.instance.current_state() == state:
if not addon.in_progress:
_LOGGER.warning(
"Watchdog found addon %s is %s, restarting...",
addon.name,
state.value,
)
try:
if state == ContainerState.FAILED:
# Ensure failed container is removed before attempting reanimation
if attempts == 0:
with suppress(DockerError):
await addon.instance.stop(remove_container=True)
await addon.start()
else:
await addon.restart()
except AddonsError as err:
attempts = attempts + 1
_LOGGER.error(
"Watchdog restart of addon %s failed!", addon.name
)
addon.sys_capture_exception(err)
else:
break
if attempts >= WATCHDOG_MAX_ATTEMPTS:
_LOGGER.critical(
"Watchdog cannot restart addon %s, failed all %s attempts",
addon.name,
attempts,
)
break
await asyncio.sleep(WATCHDOG_RETRY_SECONDS)
self._restart_after_problem = restart_after_problem
def __repr__(self) -> str: def __repr__(self) -> str:
"""Return internal representation.""" """Return internal representation."""
@@ -199,20 +137,15 @@ class Addon(AddonModel):
async def load(self) -> None: async def load(self) -> None:
"""Async initialize of object.""" """Async initialize of object."""
self._listeners.append(
self.sys_bus.register_event(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, self.container_state_changed
)
)
self._listeners.append(
self.sys_bus.register_event(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, self.watchdog_container
)
)
with suppress(DockerError): with suppress(DockerError):
await self.instance.attach(version=self.version) await self.instance.attach(version=self.version)
# Evaluate state
if await self.instance.is_running():
self.state = AddonState.STARTED
else:
self.state = AddonState.STOPPED
@property @property
def ip_address(self) -> IPv4Address: def ip_address(self) -> IPv4Address:
"""Return IP of add-on instance.""" """Return IP of add-on instance."""
@@ -249,7 +182,7 @@ class Addon(AddonModel):
return self._available(self.data_store) return self._available(self.data_store)
@property @property
def version(self) -> str | None: def version(self) -> Optional[str]:
"""Return installed version.""" """Return installed version."""
return self.persist[ATTR_VERSION] return self.persist[ATTR_VERSION]
@@ -273,7 +206,7 @@ class Addon(AddonModel):
) )
@options.setter @options.setter
def options(self, value: dict[str, Any] | None) -> None: def options(self, value: Optional[dict[str, Any]]) -> None:
"""Store user add-on options.""" """Store user add-on options."""
self.persist[ATTR_OPTIONS] = {} if value is None else deepcopy(value) self.persist[ATTR_OPTIONS] = {} if value is None else deepcopy(value)
@@ -318,17 +251,17 @@ class Addon(AddonModel):
return self.persist[ATTR_UUID] return self.persist[ATTR_UUID]
@property @property
def supervisor_token(self) -> str | None: def supervisor_token(self) -> Optional[str]:
"""Return access token for Supervisor API.""" """Return access token for Supervisor API."""
return self.persist.get(ATTR_ACCESS_TOKEN) return self.persist.get(ATTR_ACCESS_TOKEN)
@property @property
def ingress_token(self) -> str | None: def ingress_token(self) -> Optional[str]:
"""Return access token for Supervisor API.""" """Return access token for Supervisor API."""
return self.persist.get(ATTR_INGRESS_TOKEN) return self.persist.get(ATTR_INGRESS_TOKEN)
@property @property
def ingress_entry(self) -> str | None: def ingress_entry(self) -> Optional[str]:
"""Return ingress external URL.""" """Return ingress external URL."""
if self.with_ingress: if self.with_ingress:
return f"/api/hassio_ingress/{self.ingress_token}" return f"/api/hassio_ingress/{self.ingress_token}"
@@ -350,12 +283,12 @@ class Addon(AddonModel):
self.persist[ATTR_PROTECTED] = value self.persist[ATTR_PROTECTED] = value
@property @property
def ports(self) -> dict[str, int | None] | None: def ports(self) -> Optional[dict[str, Optional[int]]]:
"""Return ports of add-on.""" """Return ports of add-on."""
return self.persist.get(ATTR_NETWORK, super().ports) return self.persist.get(ATTR_NETWORK, super().ports)
@ports.setter @ports.setter
def ports(self, value: dict[str, int | None] | None) -> None: def ports(self, value: Optional[dict[str, Optional[int]]]) -> None:
"""Set custom ports of add-on.""" """Set custom ports of add-on."""
if value is None: if value is None:
self.persist.pop(ATTR_NETWORK, None) self.persist.pop(ATTR_NETWORK, None)
@@ -370,7 +303,7 @@ class Addon(AddonModel):
self.persist[ATTR_NETWORK] = new_ports self.persist[ATTR_NETWORK] = new_ports
@property @property
def ingress_url(self) -> str | None: def ingress_url(self) -> Optional[str]:
"""Return URL to ingress url.""" """Return URL to ingress url."""
if not self.with_ingress: if not self.with_ingress:
return None return None
@@ -381,7 +314,7 @@ class Addon(AddonModel):
return url return url
@property @property
def webui(self) -> str | None: def webui(self) -> Optional[str]:
"""Return URL to webui or None.""" """Return URL to webui or None."""
url = super().webui url = super().webui
if not url: if not url:
@@ -409,7 +342,7 @@ class Addon(AddonModel):
return f"{proto}://[HOST]:{port}{s_suffix}" return f"{proto}://[HOST]:{port}{s_suffix}"
@property @property
def ingress_port(self) -> int | None: def ingress_port(self) -> Optional[int]:
"""Return Ingress port.""" """Return Ingress port."""
if not self.with_ingress: if not self.with_ingress:
return None return None
@@ -420,11 +353,8 @@ class Addon(AddonModel):
return port return port
@property @property
def ingress_panel(self) -> bool | None: def ingress_panel(self) -> Optional[bool]:
"""Return True if the add-on access support ingress.""" """Return True if the add-on access support ingress."""
if not self.with_ingress:
return None
return self.persist[ATTR_INGRESS_PANEL] return self.persist[ATTR_INGRESS_PANEL]
@ingress_panel.setter @ingress_panel.setter
@@ -433,32 +363,43 @@ class Addon(AddonModel):
self.persist[ATTR_INGRESS_PANEL] = value self.persist[ATTR_INGRESS_PANEL] = value
@property @property
def audio_output(self) -> str | None: def audio_output(self) -> Optional[str]:
"""Return a pulse profile for output or None.""" """Return a pulse profile for output or None."""
if not self.with_audio: if not self.with_audio:
return None return None
return self.persist.get(ATTR_AUDIO_OUTPUT)
# Fallback with old audio settings
# Remove after 210
output_data = self.persist.get(ATTR_AUDIO_OUTPUT)
if output_data and RE_OLD_AUDIO.fullmatch(output_data):
return None
return output_data
@audio_output.setter @audio_output.setter
def audio_output(self, value: str | None): def audio_output(self, value: Optional[str]):
"""Set audio output profile settings.""" """Set audio output profile settings."""
self.persist[ATTR_AUDIO_OUTPUT] = value self.persist[ATTR_AUDIO_OUTPUT] = value
@property @property
def audio_input(self) -> str | None: def audio_input(self) -> Optional[str]:
"""Return pulse profile for input or None.""" """Return pulse profile for input or None."""
if not self.with_audio: if not self.with_audio:
return None return None
return self.persist.get(ATTR_AUDIO_INPUT) # Fallback with old audio settings
# Remove after 210
input_data = self.persist.get(ATTR_AUDIO_INPUT)
if input_data and RE_OLD_AUDIO.fullmatch(input_data):
return None
return input_data
@audio_input.setter @audio_input.setter
def audio_input(self, value: str | None) -> None: def audio_input(self, value: Optional[str]) -> None:
"""Set audio input settings.""" """Set audio input settings."""
self.persist[ATTR_AUDIO_INPUT] = value self.persist[ATTR_AUDIO_INPUT] = value
@property @property
def image(self) -> str | None: def image(self) -> Optional[str]:
"""Return image name of add-on.""" """Return image name of add-on."""
return self.persist.get(ATTR_IMAGE) return self.persist.get(ATTR_IMAGE)
@@ -510,11 +451,6 @@ class Addon(AddonModel):
return options_schema.pwned return options_schema.pwned
@property
def loaded(self) -> bool:
"""Is add-on loaded."""
return bool(self._listeners)
def save_persist(self) -> None: def save_persist(self) -> None:
"""Save data of add-on.""" """Save data of add-on."""
self.sys_addons.data.save_data() self.sys_addons.data.save_data()
@@ -583,11 +519,8 @@ class Addon(AddonModel):
raise AddonConfigurationError() raise AddonConfigurationError()
async def unload(self) -> None: async def remove_data(self) -> None:
"""Unload add-on and remove data.""" """Remove add-on data."""
for listener in self._listeners:
self.sys_bus.remove_listener(listener)
if not self.path_data.is_dir(): if not self.path_data.is_dir():
return return
@@ -693,18 +626,27 @@ class Addon(AddonModel):
# Start Add-on # Start Add-on
try: try:
await self.instance.run() await self.instance.run()
except DockerRequestError as err:
self.state = AddonState.ERROR
raise AddonsError() from err
except DockerError as err: except DockerError as err:
self.state = AddonState.ERROR self.state = AddonState.ERROR
raise AddonsError() from err raise AddonsError() from err
else:
self.state = AddonState.STARTED
async def stop(self) -> None: async def stop(self) -> None:
"""Stop add-on.""" """Stop add-on."""
self._manual_stop = True
try: try:
await self.instance.stop() await self.instance.stop()
except DockerRequestError as err:
self.state = AddonState.ERROR
raise AddonsError() from err
except DockerError as err: except DockerError as err:
self.state = AddonState.ERROR self.state = AddonState.ERROR
raise AddonsError() from err raise AddonsError() from err
else:
self.state = AddonState.STOPPED
async def restart(self) -> None: async def restart(self) -> None:
"""Restart add-on.""" """Restart add-on."""
@@ -752,18 +694,16 @@ class Addon(AddonModel):
try: try:
command_return = await self.instance.run_inside(command) command_return = await self.instance.run_inside(command)
if command_return.exit_code != 0: if command_return.exit_code != 0:
_LOGGER.debug( _LOGGER.error(
"Pre-/Post backup command failed with: %s", command_return.output "Pre-/Post backup command returned error code: %s",
) command_return.exit_code,
raise AddonsError(
f"Pre-/Post backup command returned error code: {command_return.exit_code}",
_LOGGER.error,
) )
raise AddonsError()
except DockerError as err: except DockerError as err:
raise AddonsError( _LOGGER.error(
f"Failed running pre-/post backup command {command}: {str(err)}", "Failed running pre-/post backup command %s: %s", command, err
_LOGGER.error, )
) from err raise AddonsError() from err
async def backup(self, tar_file: tarfile.TarFile) -> None: async def backup(self, tar_file: tarfile.TarFile) -> None:
"""Backup state of an add-on.""" """Backup state of an add-on."""
@@ -808,7 +748,8 @@ class Addon(AddonModel):
def _write_tarfile(): def _write_tarfile():
"""Write tar inside loop.""" """Write tar inside loop."""
with tar_file as backup: with tar_file as backup:
# Backup metadata # Backup system
backup.add(temp, arcname=".") backup.add(temp, arcname=".")
# Backup data # Backup data
@@ -875,10 +816,12 @@ class Addon(AddonModel):
try: try:
data = SCHEMA_ADDON_BACKUP(data) data = SCHEMA_ADDON_BACKUP(data)
except vol.Invalid as err: except vol.Invalid as err:
raise AddonsError( _LOGGER.error(
f"Can't validate {self.slug}, backup data: {humanize_error(data, err)}", "Can't validate %s, backup data: %s",
_LOGGER.error, self.slug,
) from err humanize_error(data, err),
)
raise AddonsError() from err
# If available # If available
if not self._available(data[ATTR_SYSTEM]): if not self._available(data[ATTR_SYSTEM]):
@@ -945,52 +888,8 @@ class Addon(AddonModel):
) )
raise AddonsError() from err raise AddonsError() from err
# Is add-on loaded
if not self.loaded:
await self.load()
# Run add-on # Run add-on
if data[ATTR_STATE] == AddonState.STARTED: if data[ATTR_STATE] == AddonState.STARTED:
return await self.start() return await self.start()
_LOGGER.info("Finished restore for add-on %s", self.slug) _LOGGER.info("Finished restore for add-on %s", self.slug)
def check_trust(self) -> Awaitable[None]:
"""Calculate Addon docker content trust.
Return Coroutine.
"""
return self.instance.check_trust()
async def container_state_changed(self, event: DockerContainerStateEvent) -> None:
"""Set addon state from container state."""
if event.name != self.instance.name:
return
if event.state in [
ContainerState.RUNNING,
ContainerState.HEALTHY,
ContainerState.UNHEALTHY,
]:
self._manual_stop = False
self.state = AddonState.STARTED
elif event.state == ContainerState.STOPPED:
self.state = AddonState.STOPPED
elif event.state == ContainerState.FAILED:
self.state = AddonState.ERROR
async def watchdog_container(self, event: DockerContainerStateEvent) -> None:
"""Process state changes in addon container and restart if necessary."""
if event.name != self.instance.name:
return
# Skip watchdog if not enabled or manual stopped
if not self.watchdog or self._manual_stop:
return
if event.state in [
ContainerState.FAILED,
ContainerState.STOPPED,
ContainerState.UNHEALTHY,
]:
await self._restart_after_problem(self, event.state)

View File

@@ -15,7 +15,6 @@ from ..const import (
META_ADDON, META_ADDON,
) )
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..docker.interface import MAP_ARCH
from ..exceptions import ConfigurationFileError from ..exceptions import ConfigurationFileError
from ..utils.common import FileConfiguration, find_one_filetype from ..utils.common import FileConfiguration, find_one_filetype
from .validate import SCHEMA_BUILD_CONFIG from .validate import SCHEMA_BUILD_CONFIG
@@ -51,9 +50,6 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
if not self._data[ATTR_BUILD_FROM]: if not self._data[ATTR_BUILD_FROM]:
return f"ghcr.io/home-assistant/{self.sys_arch.default}-base:latest" return f"ghcr.io/home-assistant/{self.sys_arch.default}-base:latest"
if isinstance(self._data[ATTR_BUILD_FROM], str):
return self._data[ATTR_BUILD_FROM]
# Evaluate correct base image # Evaluate correct base image
arch = self.sys_arch.match(list(self._data[ATTR_BUILD_FROM].keys())) arch = self.sys_arch.match(list(self._data[ATTR_BUILD_FROM].keys()))
return self._data[ATTR_BUILD_FROM][arch] return self._data[ATTR_BUILD_FROM][arch]
@@ -91,7 +87,6 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
"pull": True, "pull": True,
"forcerm": not self.sys_dev, "forcerm": not self.sys_dev,
"squash": self.squash, "squash": self.squash,
"platform": MAP_ARCH[self.sys_arch.match(self.addon.arch)],
"labels": { "labels": {
"io.hass.version": version, "io.hass.version": version,
"io.hass.arch": self.sys_arch.default, "io.hass.arch": self.sys_arch.default,

View File

@@ -1,9 +1,6 @@
"""Add-on static data.""" """Add-on static data."""
from datetime import timedelta
from enum import Enum from enum import Enum
from ..jobs.const import JobCondition
class AddonBackupMode(str, Enum): class AddonBackupMode(str, Enum):
"""Backup mode of an Add-on.""" """Backup mode of an Add-on."""
@@ -13,16 +10,3 @@ class AddonBackupMode(str, Enum):
ATTR_BACKUP = "backup" ATTR_BACKUP = "backup"
ATTR_CODENOTARY = "codenotary"
WATCHDOG_RETRY_SECONDS = 10
WATCHDOG_MAX_ATTEMPTS = 5
WATCHDOG_THROTTLE_PERIOD = timedelta(minutes=30)
WATCHDOG_THROTTLE_MAX_CALLS = 10
ADDON_UPDATE_CONDITIONS = [
JobCondition.FREE_SPACE,
JobCondition.HEALTHY,
JobCondition.INTERNET_HOST,
JobCondition.PLUGINS_UPDATED,
JobCondition.SUPERVISOR_UPDATED,
]

View File

@@ -1,10 +1,12 @@
"""Init file for Supervisor add-ons.""" """Init file for Supervisor add-ons."""
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pathlib import Path from pathlib import Path
from typing import Any, Awaitable from typing import Any, Awaitable, Optional
from awesomeversion import AwesomeVersion, AwesomeVersionException from awesomeversion import AwesomeVersion, AwesomeVersionException
from supervisor.addons.const import AddonBackupMode
from ..const import ( from ..const import (
ATTR_ADVANCED, ATTR_ADVANCED,
ATTR_APPARMOR, ATTR_APPARMOR,
@@ -77,7 +79,7 @@ from ..const import (
) )
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..docker.const import Capabilities from ..docker.const import Capabilities
from .const import ATTR_BACKUP, ATTR_CODENOTARY, AddonBackupMode from .const import ATTR_BACKUP
from .options import AddonOptions, UiOptions from .options import AddonOptions, UiOptions
from .validate import RE_SERVICE, RE_VOLUME from .validate import RE_SERVICE, RE_VOLUME
@@ -123,7 +125,7 @@ class AddonModel(CoreSysAttributes, ABC):
return self.data[ATTR_BOOT] return self.data[ATTR_BOOT]
@property @property
def auto_update(self) -> bool | None: def auto_update(self) -> Optional[bool]:
"""Return if auto update is enable.""" """Return if auto update is enable."""
return None return None
@@ -148,22 +150,22 @@ class AddonModel(CoreSysAttributes, ABC):
return self.data[ATTR_TIMEOUT] return self.data[ATTR_TIMEOUT]
@property @property
def uuid(self) -> str | None: def uuid(self) -> Optional[str]:
"""Return an API token for this add-on.""" """Return an API token for this add-on."""
return None return None
@property @property
def supervisor_token(self) -> str | None: def supervisor_token(self) -> Optional[str]:
"""Return access token for Supervisor API.""" """Return access token for Supervisor API."""
return None return None
@property @property
def ingress_token(self) -> str | None: def ingress_token(self) -> Optional[str]:
"""Return access token for Supervisor API.""" """Return access token for Supervisor API."""
return None return None
@property @property
def ingress_entry(self) -> str | None: def ingress_entry(self) -> Optional[str]:
"""Return ingress external URL.""" """Return ingress external URL."""
return None return None
@@ -173,7 +175,7 @@ class AddonModel(CoreSysAttributes, ABC):
return self.data[ATTR_DESCRIPTON] return self.data[ATTR_DESCRIPTON]
@property @property
def long_description(self) -> str | None: def long_description(self) -> Optional[str]:
"""Return README.md as long_description.""" """Return README.md as long_description."""
readme = Path(self.path_location, "README.md") readme = Path(self.path_location, "README.md")
@@ -243,32 +245,32 @@ class AddonModel(CoreSysAttributes, ABC):
return self.data.get(ATTR_DISCOVERY, []) return self.data.get(ATTR_DISCOVERY, [])
@property @property
def ports_description(self) -> dict[str, str] | None: def ports_description(self) -> Optional[dict[str, str]]:
"""Return descriptions of ports.""" """Return descriptions of ports."""
return self.data.get(ATTR_PORTS_DESCRIPTION) return self.data.get(ATTR_PORTS_DESCRIPTION)
@property @property
def ports(self) -> dict[str, int | None] | None: def ports(self) -> Optional[dict[str, Optional[int]]]:
"""Return ports of add-on.""" """Return ports of add-on."""
return self.data.get(ATTR_PORTS) return self.data.get(ATTR_PORTS)
@property @property
def ingress_url(self) -> str | None: def ingress_url(self) -> Optional[str]:
"""Return URL to ingress url.""" """Return URL to ingress url."""
return None return None
@property @property
def webui(self) -> str | None: def webui(self) -> Optional[str]:
"""Return URL to webui or None.""" """Return URL to webui or None."""
return self.data.get(ATTR_WEBUI) return self.data.get(ATTR_WEBUI)
@property @property
def watchdog(self) -> str | None: def watchdog(self) -> Optional[str]:
"""Return URL to for watchdog or None.""" """Return URL to for watchdog or None."""
return self.data.get(ATTR_WATCHDOG) return self.data.get(ATTR_WATCHDOG)
@property @property
def ingress_port(self) -> int | None: def ingress_port(self) -> Optional[int]:
"""Return Ingress port.""" """Return Ingress port."""
return None return None
@@ -313,7 +315,7 @@ class AddonModel(CoreSysAttributes, ABC):
return [Path(node) for node in self.data.get(ATTR_DEVICES, [])] return [Path(node) for node in self.data.get(ATTR_DEVICES, [])]
@property @property
def environment(self) -> dict[str, str] | None: def environment(self) -> Optional[dict[str, str]]:
"""Return environment of add-on.""" """Return environment of add-on."""
return self.data.get(ATTR_ENVIRONMENT) return self.data.get(ATTR_ENVIRONMENT)
@@ -362,12 +364,12 @@ class AddonModel(CoreSysAttributes, ABC):
return self.data.get(ATTR_BACKUP_EXCLUDE, []) return self.data.get(ATTR_BACKUP_EXCLUDE, [])
@property @property
def backup_pre(self) -> str | None: def backup_pre(self) -> Optional[str]:
"""Return pre-backup command.""" """Return pre-backup command."""
return self.data.get(ATTR_BACKUP_PRE) return self.data.get(ATTR_BACKUP_PRE)
@property @property
def backup_post(self) -> str | None: def backup_post(self) -> Optional[str]:
"""Return post-backup command.""" """Return post-backup command."""
return self.data.get(ATTR_BACKUP_POST) return self.data.get(ATTR_BACKUP_POST)
@@ -392,7 +394,7 @@ class AddonModel(CoreSysAttributes, ABC):
return self.data[ATTR_INGRESS] return self.data[ATTR_INGRESS]
@property @property
def ingress_panel(self) -> bool | None: def ingress_panel(self) -> Optional[bool]:
"""Return True if the add-on access support ingress.""" """Return True if the add-on access support ingress."""
return None return None
@@ -442,7 +444,7 @@ class AddonModel(CoreSysAttributes, ABC):
return self.data[ATTR_DEVICETREE] return self.data[ATTR_DEVICETREE]
@property @property
def with_tmpfs(self) -> str | None: def with_tmpfs(self) -> Optional[str]:
"""Return if tmp is in memory of add-on.""" """Return if tmp is in memory of add-on."""
return self.data[ATTR_TMPFS] return self.data[ATTR_TMPFS]
@@ -462,12 +464,12 @@ class AddonModel(CoreSysAttributes, ABC):
return self.data[ATTR_VIDEO] return self.data[ATTR_VIDEO]
@property @property
def homeassistant_version(self) -> str | None: def homeassistant_version(self) -> Optional[str]:
"""Return min Home Assistant version they needed by Add-on.""" """Return min Home Assistant version they needed by Add-on."""
return self.data.get(ATTR_HOMEASSISTANT) return self.data.get(ATTR_HOMEASSISTANT)
@property @property
def url(self) -> str | None: def url(self) -> Optional[str]:
"""Return URL of add-on.""" """Return URL of add-on."""
return self.data.get(ATTR_URL) return self.data.get(ATTR_URL)
@@ -502,15 +504,7 @@ class AddonModel(CoreSysAttributes, ABC):
return self.data.get(ATTR_MACHINE, []) return self.data.get(ATTR_MACHINE, [])
@property @property
def arch(self) -> str: def image(self) -> Optional[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) -> str | None:
"""Generate image name from data.""" """Generate image name from data."""
return self._image(self.data) return self._image(self.data)
@@ -571,7 +565,7 @@ class AddonModel(CoreSysAttributes, ABC):
return AddonOptions(self.coresys, raw_schema, self.name, self.slug) return AddonOptions(self.coresys, raw_schema, self.name, self.slug)
@property @property
def schema_ui(self) -> list[dict[any, any]] | None: def schema_ui(self) -> Optional[list[dict[any, any]]]:
"""Create a UI schema for add-on options.""" """Create a UI schema for add-on options."""
raw_schema = self.data[ATTR_SCHEMA] raw_schema = self.data[ATTR_SCHEMA]
@@ -584,16 +578,6 @@ class AddonModel(CoreSysAttributes, ABC):
"""Return True if the add-on accesses the system journal.""" """Return True if the add-on accesses the system journal."""
return self.data[ATTR_JOURNALD] return self.data[ATTR_JOURNALD]
@property
def signed(self) -> bool:
"""Return True if the image is signed."""
return ATTR_CODENOTARY in self.data
@property
def codenotary(self) -> str | None:
"""Return Signer email address for CAS."""
return self.data.get(ATTR_CODENOTARY)
def __eq__(self, other): def __eq__(self, other):
"""Compaired add-on objects.""" """Compaired add-on objects."""
if not isinstance(other, AddonModel): if not isinstance(other, AddonModel):
@@ -614,7 +598,7 @@ class AddonModel(CoreSysAttributes, ABC):
return False return False
# Home Assistant # Home Assistant
version: AwesomeVersion | None = config.get(ATTR_HOMEASSISTANT) version: Optional[AwesomeVersion] = config.get(ATTR_HOMEASSISTANT)
try: try:
return self.sys_homeassistant.version >= version return self.sys_homeassistant.version >= version
except (AwesomeVersionException, TypeError): except (AwesomeVersionException, TypeError):
@@ -638,7 +622,7 @@ class AddonModel(CoreSysAttributes, ABC):
"""Uninstall this add-on.""" """Uninstall this add-on."""
return self.sys_addons.uninstall(self.slug) return self.sys_addons.uninstall(self.slug)
def update(self, backup: bool | None = False) -> Awaitable[None]: def update(self, backup: Optional[bool] = False) -> Awaitable[None]:
"""Update this add-on.""" """Update this add-on."""
return self.sys_addons.update(self.slug, backup=backup) return self.sys_addons.update(self.slug, backup=backup)

View File

@@ -3,7 +3,7 @@ import hashlib
import logging import logging
from pathlib import Path from pathlib import Path
import re import re
from typing import Any from typing import Any, Union
import voluptuous as vol import voluptuous as vol
@@ -293,7 +293,7 @@ class UiOptions(CoreSysAttributes):
multiple: bool = False, multiple: bool = False,
) -> None: ) -> None:
"""Validate a single element.""" """Validate a single element."""
ui_node: dict[str, str | bool | float | list[str]] = {"name": key} ui_node: dict[str, Union[str, bool, float, list[str]]] = {"name": key}
# If multiple # If multiple
if multiple: if multiple:

View File

@@ -16,10 +16,10 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
def rating_security(addon: AddonModel) -> int: def rating_security(addon: AddonModel) -> int:
"""Return 1-8 for security rating. """Return 1-6 for security rating.
1 = not secure 1 = not secure
8 = high secure 6 = high secure
""" """
rating = 5 rating = 5
@@ -35,10 +35,6 @@ def rating_security(addon: AddonModel) -> int:
elif addon.access_auth_api: elif addon.access_auth_api:
rating += 1 rating += 1
# Signed
if addon.signed:
rating += 1
# Privileged options # Privileged options
if ( if (
any( any(
@@ -74,7 +70,7 @@ def rating_security(addon: AddonModel) -> int:
if addon.access_docker_api or addon.with_full_access: if addon.access_docker_api or addon.with_full_access:
rating = 1 rating = 1
return max(min(8, rating), 1) return max(min(6, rating), 1)
async def remove_data(folder: Path) -> None: async def remove_data(folder: Path) -> None:

View File

@@ -7,6 +7,8 @@ import uuid
import voluptuous as vol import voluptuous as vol
from supervisor.addons.const import AddonBackupMode
from ..const import ( from ..const import (
ARCH_ALL, ARCH_ALL,
ATTR_ACCESS_TOKEN, ATTR_ACCESS_TOKEN,
@@ -108,7 +110,7 @@ from ..validate import (
uuid_match, uuid_match,
version_tag, version_tag,
) )
from .const import ATTR_BACKUP, ATTR_CODENOTARY, AddonBackupMode from .const import ATTR_BACKUP
from .options import RE_SCHEMA_ELEMENT from .options import RE_SCHEMA_ELEMENT
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@@ -315,7 +317,6 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
vol.Optional(ATTR_BACKUP, default=AddonBackupMode.HOT): vol.Coerce( vol.Optional(ATTR_BACKUP, default=AddonBackupMode.HOT): vol.Coerce(
AddonBackupMode AddonBackupMode
), ),
vol.Optional(ATTR_CODENOTARY): vol.Email(),
vol.Optional(ATTR_OPTIONS, default={}): dict, vol.Optional(ATTR_OPTIONS, default={}): dict,
vol.Optional(ATTR_SCHEMA, default={}): vol.Any( vol.Optional(ATTR_SCHEMA, default={}): vol.Any(
vol.Schema( vol.Schema(
@@ -351,9 +352,8 @@ SCHEMA_ADDON_CONFIG = vol.All(
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
SCHEMA_BUILD_CONFIG = vol.Schema( SCHEMA_BUILD_CONFIG = vol.Schema(
{ {
vol.Optional(ATTR_BUILD_FROM, default=dict): vol.Any( vol.Optional(ATTR_BUILD_FROM, default=dict): vol.Schema(
vol.Match(RE_DOCKER_IMAGE_BUILD), {vol.In(ARCH_ALL): vol.Match(RE_DOCKER_IMAGE_BUILD)}
vol.Schema({vol.In(ARCH_ALL): vol.Match(RE_DOCKER_IMAGE_BUILD)}),
), ),
vol.Optional(ATTR_SQUASH, default=False): vol.Boolean(), vol.Optional(ATTR_SQUASH, default=False): vol.Boolean(),
vol.Optional(ATTR_ARGS, default=dict): vol.Schema({str: str}), vol.Optional(ATTR_ARGS, default=dict): vol.Schema({str: str}),

View File

@@ -1,14 +1,11 @@
"""Init file for Supervisor RESTful API.""" """Init file for Supervisor RESTful API."""
from functools import partial
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Any from typing import Optional
from aiohttp import web from aiohttp import web
from ..const import AddonState
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import APIAddonNotInstalled
from .addons import APIAddons from .addons import APIAddons
from .audio import APIAudio from .audio import APIAudio
from .auth import APIAuth from .auth import APIAuth
@@ -20,6 +17,7 @@ from .docker import APIDocker
from .hardware import APIHardware from .hardware import APIHardware
from .homeassistant import APIHomeAssistant from .homeassistant import APIHomeAssistant
from .host import APIHost from .host import APIHost
from .info import APIInfo
from .ingress import APIIngress from .ingress import APIIngress
from .jobs import APIJobs from .jobs import APIJobs
from .middleware.security import SecurityMiddleware from .middleware.security import SecurityMiddleware
@@ -29,18 +27,15 @@ from .observer import APIObserver
from .os import APIOS from .os import APIOS
from .proxy import APIProxy from .proxy import APIProxy
from .resolution import APIResoulution from .resolution import APIResoulution
from .root import APIRoot
from .security import APISecurity from .security import APISecurity
from .services import APIServices from .services import APIServices
from .store import APIStore from .store import APIStore
from .supervisor import APISupervisor from .supervisor import APISupervisor
from .utils import api_process
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
MAX_CLIENT_SIZE: int = 1024**2 * 16 MAX_CLIENT_SIZE: int = 1024 ** 2 * 16
MAX_LINE_SIZE: int = 24570
class RestAPI(CoreSysAttributes): class RestAPI(CoreSysAttributes):
@@ -56,15 +51,11 @@ class RestAPI(CoreSysAttributes):
self.security.system_validation, self.security.system_validation,
self.security.token_validation, self.security.token_validation,
], ],
handler_args={
"max_line_size": MAX_LINE_SIZE,
"max_field_size": MAX_LINE_SIZE,
},
) )
# service stuff # service stuff
self._runner: web.AppRunner = web.AppRunner(self.webapp) self._runner: web.AppRunner = web.AppRunner(self.webapp)
self._site: web.TCPSite | None = None self._site: Optional[web.TCPSite] = None
async def load(self) -> None: async def load(self) -> None:
"""Register REST API Calls.""" """Register REST API Calls."""
@@ -79,7 +70,7 @@ class RestAPI(CoreSysAttributes):
self._register_hardware() self._register_hardware()
self._register_homeassistant() self._register_homeassistant()
self._register_host() self._register_host()
self._register_root() self._register_info()
self._register_ingress() self._register_ingress()
self._register_multicast() self._register_multicast()
self._register_network() self._register_network()
@@ -104,36 +95,16 @@ class RestAPI(CoreSysAttributes):
self.webapp.add_routes( self.webapp.add_routes(
[ [
web.get("/host/info", api_host.info), web.get("/host/info", api_host.info),
web.get("/host/logs", api_host.advanced_logs), web.get("/host/logs", api_host.logs),
web.get(
"/host/logs/follow",
partial(api_host.advanced_logs, follow=True),
),
web.get("/host/logs/identifiers", api_host.list_identifiers),
web.get("/host/logs/identifiers/{identifier}", api_host.advanced_logs),
web.get(
"/host/logs/identifiers/{identifier}/follow",
partial(api_host.advanced_logs, follow=True),
),
web.get("/host/logs/boots", api_host.list_boots),
web.get("/host/logs/boots/{bootid}", api_host.advanced_logs),
web.get(
"/host/logs/boots/{bootid}/follow",
partial(api_host.advanced_logs, follow=True),
),
web.get(
"/host/logs/boots/{bootid}/identifiers/{identifier}",
api_host.advanced_logs,
),
web.get(
"/host/logs/boots/{bootid}/identifiers/{identifier}/follow",
partial(api_host.advanced_logs, follow=True),
),
web.post("/host/reboot", api_host.reboot), web.post("/host/reboot", api_host.reboot),
web.post("/host/shutdown", api_host.shutdown), web.post("/host/shutdown", api_host.shutdown),
web.post("/host/reload", api_host.reload), web.post("/host/reload", api_host.reload),
web.post("/host/options", api_host.options), web.post("/host/options", api_host.options),
web.get("/host/services", api_host.services), web.get("/host/services", api_host.services),
web.post("/host/services/{service}/stop", api_host.service_stop),
web.post("/host/services/{service}/start", api_host.service_start),
web.post("/host/services/{service}/restart", api_host.service_restart),
web.post("/host/services/{service}/reload", api_host.service_reload),
] ]
) )
@@ -188,7 +159,6 @@ class RestAPI(CoreSysAttributes):
[ [
web.get("/security/info", api_security.info), web.get("/security/info", api_security.info),
web.post("/security/options", api_security.options), web.post("/security/options", api_security.options),
web.post("/security/integrity", api_security.integrity_check),
] ]
) )
@@ -258,21 +228,12 @@ class RestAPI(CoreSysAttributes):
] ]
) )
def _register_root(self) -> None: def _register_info(self) -> None:
"""Register root functions.""" """Register info functions."""
api_root = APIRoot() api_info = APIInfo()
api_root.coresys = self.coresys api_info.coresys = self.coresys
self.webapp.add_routes([web.get("/info", api_root.info)]) self.webapp.add_routes([web.get("/info", api_info.info)])
self.webapp.add_routes([web.post("/refresh_updates", api_root.refresh_updates)])
self.webapp.add_routes(
[web.get("/available_updates", api_root.available_updates)]
)
# Remove: 2023
self.webapp.add_routes(
[web.get("/supervisor/available_updates", api_root.available_updates)]
)
def _register_resolution(self) -> None: def _register_resolution(self) -> None:
"""Register info functions.""" """Register info functions."""
@@ -298,10 +259,6 @@ class RestAPI(CoreSysAttributes):
"/resolution/issue/{issue}", "/resolution/issue/{issue}",
api_resolution.dismiss_issue, api_resolution.dismiss_issue,
), ),
web.get(
"/resolution/issue/{issue}/suggestions",
api_resolution.suggestions_for_issue,
),
web.post("/resolution/healthcheck", api_resolution.healthcheck), web.post("/resolution/healthcheck", api_resolution.healthcheck),
] ]
) )
@@ -327,6 +284,10 @@ class RestAPI(CoreSysAttributes):
self.webapp.add_routes( self.webapp.add_routes(
[ [
web.get(
"/supervisor/available_updates", api_supervisor.available_updates
),
web.post("/refresh_updates", api_supervisor.reload),
web.get("/supervisor/ping", api_supervisor.ping), web.get("/supervisor/ping", api_supervisor.ping),
web.get("/supervisor/info", api_supervisor.info), web.get("/supervisor/info", api_supervisor.info),
web.get("/supervisor/stats", api_supervisor.stats), web.get("/supervisor/stats", api_supervisor.stats),
@@ -356,22 +317,17 @@ class RestAPI(CoreSysAttributes):
web.post("/core/start", api_hass.start), web.post("/core/start", api_hass.start),
web.post("/core/check", api_hass.check), web.post("/core/check", api_hass.check),
web.post("/core/rebuild", api_hass.rebuild), web.post("/core/rebuild", api_hass.rebuild),
] # Remove with old Supervisor fallback
)
# Reroute from legacy
self.webapp.add_routes(
[
web.get("/homeassistant/info", api_hass.info), web.get("/homeassistant/info", api_hass.info),
web.get("/homeassistant/logs", api_hass.logs), web.get("/homeassistant/logs", api_hass.logs),
web.get("/homeassistant/stats", api_hass.stats), web.get("/homeassistant/stats", api_hass.stats),
web.post("/homeassistant/options", api_hass.options), web.post("/homeassistant/options", api_hass.options),
web.post("/homeassistant/update", api_hass.update),
web.post("/homeassistant/restart", api_hass.restart), web.post("/homeassistant/restart", api_hass.restart),
web.post("/homeassistant/stop", api_hass.stop), web.post("/homeassistant/stop", api_hass.stop),
web.post("/homeassistant/start", api_hass.start), web.post("/homeassistant/start", api_hass.start),
web.post("/homeassistant/update", api_hass.update),
web.post("/homeassistant/rebuild", api_hass.rebuild),
web.post("/homeassistant/check", api_hass.check), web.post("/homeassistant/check", api_hass.check),
web.post("/homeassistant/rebuild", api_hass.rebuild),
] ]
) )
@@ -388,12 +344,7 @@ class RestAPI(CoreSysAttributes):
web.post("/core/api/{path:.+}", api_proxy.api), web.post("/core/api/{path:.+}", api_proxy.api),
web.get("/core/api/{path:.+}", api_proxy.api), web.get("/core/api/{path:.+}", api_proxy.api),
web.get("/core/api/", api_proxy.api), web.get("/core/api/", api_proxy.api),
] # Remove with old Supervisor fallback
)
# Reroute from legacy
self.webapp.add_routes(
[
web.get("/homeassistant/api/websocket", api_proxy.websocket), web.get("/homeassistant/api/websocket", api_proxy.websocket),
web.get("/homeassistant/websocket", api_proxy.websocket), web.get("/homeassistant/websocket", api_proxy.websocket),
web.get("/homeassistant/api/stream", api_proxy.stream), web.get("/homeassistant/api/stream", api_proxy.stream),
@@ -411,6 +362,8 @@ class RestAPI(CoreSysAttributes):
self.webapp.add_routes( self.webapp.add_routes(
[ [
web.get("/addons", api_addons.list), web.get("/addons", api_addons.list),
web.post("/addons/reload", api_addons.reload),
web.get("/addons/{addon}/info", api_addons.info),
web.post("/addons/{addon}/uninstall", api_addons.uninstall), web.post("/addons/{addon}/uninstall", api_addons.uninstall),
web.post("/addons/{addon}/start", api_addons.start), web.post("/addons/{addon}/start", api_addons.start),
web.post("/addons/{addon}/stop", api_addons.stop), web.post("/addons/{addon}/stop", api_addons.stop),
@@ -422,31 +375,16 @@ class RestAPI(CoreSysAttributes):
web.get("/addons/{addon}/options/config", api_addons.options_config), web.get("/addons/{addon}/options/config", api_addons.options_config),
web.post("/addons/{addon}/rebuild", api_addons.rebuild), web.post("/addons/{addon}/rebuild", api_addons.rebuild),
web.get("/addons/{addon}/logs", api_addons.logs), web.get("/addons/{addon}/logs", api_addons.logs),
web.get("/addons/{addon}/icon", api_addons.icon),
web.get("/addons/{addon}/logo", api_addons.logo),
web.get("/addons/{addon}/changelog", api_addons.changelog),
web.get("/addons/{addon}/documentation", api_addons.documentation),
web.post("/addons/{addon}/stdin", api_addons.stdin), web.post("/addons/{addon}/stdin", api_addons.stdin),
web.post("/addons/{addon}/security", api_addons.security), web.post("/addons/{addon}/security", api_addons.security),
web.get("/addons/{addon}/stats", api_addons.stats), web.get("/addons/{addon}/stats", api_addons.stats),
] ]
) )
# Legacy routing to support requests for not installed addons
api_store = APIStore()
api_store.coresys = self.coresys
@api_process
async def addons_addon_info(request: web.Request) -> dict[str, Any]:
"""Route to store if info requested for not installed addon."""
try:
return await api_addons.info(request)
except APIAddonNotInstalled:
# Route to store/{addon}/info but add missing fields
return dict(
await api_store.addons_addon_info_wrapped(request),
state=AddonState.UNKNOWN,
options=self.sys_addons.store[request.match_info["addon"]].options,
)
self.webapp.add_routes([web.get("/addons/{addon}/info", addons_addon_info)])
def _register_ingress(self) -> None: def _register_ingress(self) -> None:
"""Register Ingress functions.""" """Register Ingress functions."""
api_ingress = APIIngress() api_ingress = APIIngress()
@@ -468,14 +406,27 @@ class RestAPI(CoreSysAttributes):
self.webapp.add_routes( self.webapp.add_routes(
[ [
web.get("/snapshots", api_backups.list),
web.post("/snapshots/reload", api_backups.reload),
web.post("/snapshots/new/full", api_backups.backup_full),
web.post("/snapshots/new/partial", api_backups.backup_partial),
web.post("/snapshots/new/upload", api_backups.upload),
web.get("/snapshots/{slug}/info", api_backups.info),
web.delete("/snapshots/{slug}", api_backups.remove),
web.post("/snapshots/{slug}/restore/full", api_backups.restore_full),
web.post(
"/snapshots/{slug}/restore/partial",
api_backups.restore_partial,
),
web.get("/snapshots/{slug}/download", api_backups.download),
web.post("/snapshots/{slug}/remove", api_backups.remove),
# June 2021: /snapshots was renamed to /backups
web.get("/backups", api_backups.list), web.get("/backups", api_backups.list),
web.get("/backups/info", api_backups.info),
web.post("/backups/options", api_backups.options),
web.post("/backups/reload", api_backups.reload), web.post("/backups/reload", api_backups.reload),
web.post("/backups/new/full", api_backups.backup_full), web.post("/backups/new/full", api_backups.backup_full),
web.post("/backups/new/partial", api_backups.backup_partial), web.post("/backups/new/partial", api_backups.backup_partial),
web.post("/backups/new/upload", api_backups.upload), web.post("/backups/new/upload", api_backups.upload),
web.get("/backups/{slug}/info", api_backups.backup_info), web.get("/backups/{slug}/info", api_backups.info),
web.delete("/backups/{slug}", api_backups.remove), web.delete("/backups/{slug}", api_backups.remove),
web.post("/backups/{slug}/restore/full", api_backups.restore_full), web.post("/backups/{slug}/restore/full", api_backups.restore_full),
web.post( web.post(
@@ -535,8 +486,6 @@ class RestAPI(CoreSysAttributes):
"""Register Audio functions.""" """Register Audio functions."""
api_audio = APIAudio() api_audio = APIAudio()
api_audio.coresys = self.coresys api_audio.coresys = self.coresys
api_host = APIHost()
api_host.coresys = self.coresys
self.webapp.add_routes( self.webapp.add_routes(
[ [
@@ -566,15 +515,6 @@ class RestAPI(CoreSysAttributes):
web.get("/store/addons", api_store.addons_list), web.get("/store/addons", api_store.addons_list),
web.get("/store/addons/{addon}", api_store.addons_addon_info), web.get("/store/addons/{addon}", api_store.addons_addon_info),
web.get("/store/addons/{addon}/{version}", api_store.addons_addon_info), web.get("/store/addons/{addon}/{version}", api_store.addons_addon_info),
web.get("/store/addons/{addon}/icon", api_store.addons_addon_icon),
web.get("/store/addons/{addon}/logo", api_store.addons_addon_logo),
web.get(
"/store/addons/{addon}/changelog", api_store.addons_addon_changelog
),
web.get(
"/store/addons/{addon}/documentation",
api_store.addons_addon_documentation,
),
web.post( web.post(
"/store/addons/{addon}/install", api_store.addons_addon_install "/store/addons/{addon}/install", api_store.addons_addon_install
), ),
@@ -593,26 +533,14 @@ class RestAPI(CoreSysAttributes):
"/store/repositories/{repository}", "/store/repositories/{repository}",
api_store.repositories_repository_info, api_store.repositories_repository_info,
), ),
web.post("/store/repositories", api_store.add_repository),
web.delete(
"/store/repositories/{repository}", api_store.remove_repository
),
] ]
) )
# Reroute from legacy # Reroute from legacy
self.webapp.add_routes( self.webapp.add_routes(
[ [
web.post("/addons/reload", api_store.reload),
web.post("/addons/{addon}/install", api_store.addons_addon_install), web.post("/addons/{addon}/install", api_store.addons_addon_install),
web.post("/addons/{addon}/update", api_store.addons_addon_update), web.post("/addons/{addon}/update", api_store.addons_addon_update),
web.get("/addons/{addon}/icon", api_store.addons_addon_icon),
web.get("/addons/{addon}/logo", api_store.addons_addon_logo),
web.get("/addons/{addon}/changelog", api_store.addons_addon_changelog),
web.get(
"/addons/{addon}/documentation",
api_store.addons_addon_documentation,
),
] ]
) )

View File

@@ -52,11 +52,13 @@ from ..const import (
ATTR_INGRESS_PANEL, ATTR_INGRESS_PANEL,
ATTR_INGRESS_PORT, ATTR_INGRESS_PORT,
ATTR_INGRESS_URL, ATTR_INGRESS_URL,
ATTR_INSTALLED,
ATTR_IP_ADDRESS, ATTR_IP_ADDRESS,
ATTR_KERNEL_MODULES, ATTR_KERNEL_MODULES,
ATTR_LOGO, ATTR_LOGO,
ATTR_LONG_DESCRIPTION, ATTR_LONG_DESCRIPTION,
ATTR_MACHINE, ATTR_MACHINE,
ATTR_MAINTAINER,
ATTR_MEMORY_LIMIT, ATTR_MEMORY_LIMIT,
ATTR_MEMORY_PERCENT, ATTR_MEMORY_PERCENT,
ATTR_MEMORY_USAGE, ATTR_MEMORY_USAGE,
@@ -71,10 +73,12 @@ from ..const import (
ATTR_PROTECTED, ATTR_PROTECTED,
ATTR_PWNED, ATTR_PWNED,
ATTR_RATING, ATTR_RATING,
ATTR_REPOSITORIES,
ATTR_REPOSITORY, ATTR_REPOSITORY,
ATTR_SCHEMA, ATTR_SCHEMA,
ATTR_SERVICES, ATTR_SERVICES,
ATTR_SLUG, ATTR_SLUG,
ATTR_SOURCE,
ATTR_STAGE, ATTR_STAGE,
ATTR_STARTUP, ATTR_STARTUP,
ATTR_STATE, ATTR_STATE,
@@ -91,20 +95,17 @@ from ..const import (
ATTR_VIDEO, ATTR_VIDEO,
ATTR_WATCHDOG, ATTR_WATCHDOG,
ATTR_WEBUI, ATTR_WEBUI,
CONTENT_TYPE_BINARY,
CONTENT_TYPE_PNG,
CONTENT_TYPE_TEXT,
REQUEST_FROM, REQUEST_FROM,
AddonBoot, AddonBoot,
AddonState,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..docker.stats import DockerStats from ..docker.stats import DockerStats
from ..exceptions import ( from ..exceptions import APIError, APIForbidden, PwnedError, PwnedSecret
APIAddonNotInstalled,
APIError,
APIForbidden,
PwnedError,
PwnedSecret,
)
from ..validate import docker_ports from ..validate import docker_ports
from .const import ATTR_SIGNED, CONTENT_TYPE_BINARY
from .utils import api_process, api_process_raw, api_validate, json_loads from .utils import api_process, api_process_raw, api_validate, json_loads
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@@ -131,7 +132,7 @@ SCHEMA_SECURITY = vol.Schema({vol.Optional(ATTR_PROTECTED): vol.Boolean()})
class APIAddons(CoreSysAttributes): class APIAddons(CoreSysAttributes):
"""Handle RESTful API for add-on functions.""" """Handle RESTful API for add-on functions."""
def _extract_addon(self, request: web.Request) -> Addon: def _extract_addon(self, request: web.Request) -> AnyAddon:
"""Return addon, throw an exception it it doesn't exist.""" """Return addon, throw an exception it it doesn't exist."""
addon_slug: str = request.match_info.get("addon") addon_slug: str = request.match_info.get("addon")
@@ -145,11 +146,15 @@ class APIAddons(CoreSysAttributes):
addon = self.sys_addons.get(addon_slug) addon = self.sys_addons.get(addon_slug)
if not addon: if not addon:
raise APIError(f"Addon {addon_slug} does not exist") raise APIError(f"Addon {addon_slug} does not exist")
if not isinstance(addon, Addon) or not addon.is_installed:
raise APIAddonNotInstalled("Addon is not installed")
return addon return addon
def _extract_addon_installed(self, request: web.Request) -> Addon:
addon = self._extract_addon(request)
if not isinstance(addon, Addon) or not addon.is_installed:
raise APIError("Addon is not installed")
return addon
@api_process @api_process
async def list(self, request: web.Request) -> dict[str, Any]: async def list(self, request: web.Request) -> dict[str, Any]:
"""Return all add-ons or repositories.""" """Return all add-ons or repositories."""
@@ -160,29 +165,42 @@ class APIAddons(CoreSysAttributes):
ATTR_DESCRIPTON: addon.description, ATTR_DESCRIPTON: addon.description,
ATTR_ADVANCED: addon.advanced, ATTR_ADVANCED: addon.advanced,
ATTR_STAGE: addon.stage, ATTR_STAGE: addon.stage,
ATTR_VERSION: addon.version, ATTR_VERSION: addon.version if addon.is_installed else None,
ATTR_VERSION_LATEST: addon.latest_version, ATTR_VERSION_LATEST: addon.latest_version,
ATTR_UPDATE_AVAILABLE: addon.need_update, ATTR_UPDATE_AVAILABLE: addon.need_update
if addon.is_installed
else False,
ATTR_INSTALLED: addon.is_installed,
ATTR_AVAILABLE: addon.available, ATTR_AVAILABLE: addon.available,
ATTR_DETACHED: addon.is_detached, ATTR_DETACHED: addon.is_detached,
ATTR_HOMEASSISTANT: addon.homeassistant_version, ATTR_HOMEASSISTANT: addon.homeassistant_version,
ATTR_STATE: addon.state,
ATTR_REPOSITORY: addon.repository, ATTR_REPOSITORY: addon.repository,
ATTR_BUILD: addon.need_build, ATTR_BUILD: addon.need_build,
ATTR_URL: addon.url, ATTR_URL: addon.url,
ATTR_ICON: addon.with_icon, ATTR_ICON: addon.with_icon,
ATTR_LOGO: addon.with_logo, ATTR_LOGO: addon.with_logo,
} }
for addon in self.sys_addons.installed for addon in self.sys_addons.all
] ]
return {ATTR_ADDONS: data_addons} data_repositories = [
{
ATTR_SLUG: repository.slug,
ATTR_NAME: repository.name,
ATTR_SOURCE: repository.source,
ATTR_URL: repository.url,
ATTR_MAINTAINER: repository.maintainer,
}
for repository in self.sys_store.all
]
return {ATTR_ADDONS: data_addons, ATTR_REPOSITORIES: data_repositories}
@api_process @api_process
async def reload(self, request: web.Request) -> None: async def reload(self, request: web.Request) -> None:
"""Reload all add-on data from store.""" """Reload all add-on data from store."""
await asyncio.shield(self.sys_store.reload()) await asyncio.shield(self.sys_store.reload())
@api_process
async def info(self, request: web.Request) -> dict[str, Any]: async def info(self, request: web.Request) -> dict[str, Any]:
"""Return add-on information.""" """Return add-on information."""
addon: AnyAddon = self._extract_addon(request) addon: AnyAddon = self._extract_addon(request)
@@ -196,8 +214,11 @@ class APIAddons(CoreSysAttributes):
ATTR_LONG_DESCRIPTION: addon.long_description, ATTR_LONG_DESCRIPTION: addon.long_description,
ATTR_ADVANCED: addon.advanced, ATTR_ADVANCED: addon.advanced,
ATTR_STAGE: addon.stage, ATTR_STAGE: addon.stage,
ATTR_AUTO_UPDATE: None,
ATTR_REPOSITORY: addon.repository, ATTR_REPOSITORY: addon.repository,
ATTR_VERSION: None,
ATTR_VERSION_LATEST: addon.latest_version, ATTR_VERSION_LATEST: addon.latest_version,
ATTR_UPDATE_AVAILABLE: False,
ATTR_PROTECTED: addon.protected, ATTR_PROTECTED: addon.protected,
ATTR_RATING: rating_security(addon), ATTR_RATING: rating_security(addon),
ATTR_BOOT: addon.boot, ATTR_BOOT: addon.boot,
@@ -207,6 +228,7 @@ class APIAddons(CoreSysAttributes):
ATTR_MACHINE: addon.supported_machine, ATTR_MACHINE: addon.supported_machine,
ATTR_HOMEASSISTANT: addon.homeassistant_version, ATTR_HOMEASSISTANT: addon.homeassistant_version,
ATTR_URL: addon.url, ATTR_URL: addon.url,
ATTR_STATE: AddonState.UNKNOWN,
ATTR_DETACHED: addon.is_detached, ATTR_DETACHED: addon.is_detached,
ATTR_AVAILABLE: addon.available, ATTR_AVAILABLE: addon.available,
ATTR_BUILD: addon.need_build, ATTR_BUILD: addon.need_build,
@@ -219,11 +241,13 @@ class APIAddons(CoreSysAttributes):
ATTR_PRIVILEGED: addon.privileged, ATTR_PRIVILEGED: addon.privileged,
ATTR_FULL_ACCESS: addon.with_full_access, ATTR_FULL_ACCESS: addon.with_full_access,
ATTR_APPARMOR: addon.apparmor, ATTR_APPARMOR: addon.apparmor,
ATTR_DEVICES: addon.static_devices,
ATTR_ICON: addon.with_icon, ATTR_ICON: addon.with_icon,
ATTR_LOGO: addon.with_logo, ATTR_LOGO: addon.with_logo,
ATTR_CHANGELOG: addon.with_changelog, ATTR_CHANGELOG: addon.with_changelog,
ATTR_DOCUMENTATION: addon.with_documentation, ATTR_DOCUMENTATION: addon.with_documentation,
ATTR_STDIN: addon.with_stdin, ATTR_STDIN: addon.with_stdin,
ATTR_WEBUI: None,
ATTR_HASSIO_API: addon.access_hassio_api, ATTR_HASSIO_API: addon.access_hassio_api,
ATTR_HASSIO_ROLE: addon.hassio_role, ATTR_HASSIO_ROLE: addon.hassio_role,
ATTR_AUTH_API: addon.access_auth_api, ATTR_AUTH_API: addon.access_auth_api,
@@ -237,35 +261,48 @@ class APIAddons(CoreSysAttributes):
ATTR_DOCKER_API: addon.access_docker_api, ATTR_DOCKER_API: addon.access_docker_api,
ATTR_VIDEO: addon.with_video, ATTR_VIDEO: addon.with_video,
ATTR_AUDIO: addon.with_audio, ATTR_AUDIO: addon.with_audio,
ATTR_AUDIO_INPUT: None,
ATTR_AUDIO_OUTPUT: None,
ATTR_STARTUP: addon.startup, ATTR_STARTUP: addon.startup,
ATTR_SERVICES: _pretty_services(addon), ATTR_SERVICES: _pretty_services(addon),
ATTR_DISCOVERY: addon.discovery, ATTR_DISCOVERY: addon.discovery,
ATTR_IP_ADDRESS: None,
ATTR_TRANSLATIONS: addon.translations, ATTR_TRANSLATIONS: addon.translations,
ATTR_INGRESS: addon.with_ingress, ATTR_INGRESS: addon.with_ingress,
ATTR_SIGNED: addon.signed, ATTR_INGRESS_ENTRY: None,
ATTR_STATE: addon.state, ATTR_INGRESS_URL: None,
ATTR_WEBUI: addon.webui, ATTR_INGRESS_PORT: None,
ATTR_INGRESS_ENTRY: addon.ingress_entry, ATTR_INGRESS_PANEL: None,
ATTR_INGRESS_URL: addon.ingress_url, ATTR_WATCHDOG: None,
ATTR_INGRESS_PORT: addon.ingress_port,
ATTR_INGRESS_PANEL: addon.ingress_panel,
ATTR_AUDIO_INPUT: addon.audio_input,
ATTR_AUDIO_OUTPUT: addon.audio_output,
ATTR_AUTO_UPDATE: addon.auto_update,
ATTR_IP_ADDRESS: str(addon.ip_address),
ATTR_VERSION: addon.version,
ATTR_UPDATE_AVAILABLE: addon.need_update,
ATTR_WATCHDOG: addon.watchdog,
ATTR_DEVICES: addon.static_devices
+ [device.path for device in addon.devices],
} }
if isinstance(addon, Addon) and addon.is_installed:
data.update(
{
ATTR_STATE: addon.state,
ATTR_WEBUI: addon.webui,
ATTR_INGRESS_ENTRY: addon.ingress_entry,
ATTR_INGRESS_URL: addon.ingress_url,
ATTR_INGRESS_PORT: addon.ingress_port,
ATTR_INGRESS_PANEL: addon.ingress_panel,
ATTR_AUDIO_INPUT: addon.audio_input,
ATTR_AUDIO_OUTPUT: addon.audio_output,
ATTR_AUTO_UPDATE: addon.auto_update,
ATTR_IP_ADDRESS: str(addon.ip_address),
ATTR_VERSION: addon.version,
ATTR_UPDATE_AVAILABLE: addon.need_update,
ATTR_WATCHDOG: addon.watchdog,
ATTR_DEVICES: addon.static_devices
+ [device.path for device in addon.devices],
}
)
return data return data
@api_process @api_process
async def options(self, request: web.Request) -> None: async def options(self, request: web.Request) -> None:
"""Store user options for add-on.""" """Store user options for add-on."""
addon = self._extract_addon(request) addon = self._extract_addon_installed(request)
# Update secrets for validation # Update secrets for validation
await self.sys_homeassistant.secrets.reload() await self.sys_homeassistant.secrets.reload()
@@ -300,7 +337,7 @@ class APIAddons(CoreSysAttributes):
@api_process @api_process
async def options_validate(self, request: web.Request) -> None: async def options_validate(self, request: web.Request) -> None:
"""Validate user options for add-on.""" """Validate user options for add-on."""
addon = self._extract_addon(request) addon = self._extract_addon_installed(request)
data = {ATTR_MESSAGE: "", ATTR_VALID: True, ATTR_PWNED: False} data = {ATTR_MESSAGE: "", ATTR_VALID: True, ATTR_PWNED: False}
options = await request.json(loads=json_loads) or addon.options options = await request.json(loads=json_loads) or addon.options
@@ -342,7 +379,7 @@ class APIAddons(CoreSysAttributes):
slug: str = request.match_info.get("addon") slug: str = request.match_info.get("addon")
if slug != "self": if slug != "self":
raise APIForbidden("This can be only read by the Add-on itself!") raise APIForbidden("This can be only read by the Add-on itself!")
addon = self._extract_addon(request) addon = self._extract_addon_installed(request)
# Lookup/reload secrets # Lookup/reload secrets
await self.sys_homeassistant.secrets.reload() await self.sys_homeassistant.secrets.reload()
@@ -354,7 +391,7 @@ class APIAddons(CoreSysAttributes):
@api_process @api_process
async def security(self, request: web.Request) -> None: async def security(self, request: web.Request) -> None:
"""Store security options for add-on.""" """Store security options for add-on."""
addon = self._extract_addon(request) addon = self._extract_addon_installed(request)
body: dict[str, Any] = await api_validate(SCHEMA_SECURITY, request) body: dict[str, Any] = await api_validate(SCHEMA_SECURITY, request)
if ATTR_PROTECTED in body: if ATTR_PROTECTED in body:
@@ -366,7 +403,7 @@ class APIAddons(CoreSysAttributes):
@api_process @api_process
async def stats(self, request: web.Request) -> dict[str, Any]: async def stats(self, request: web.Request) -> dict[str, Any]:
"""Return resource information.""" """Return resource information."""
addon = self._extract_addon(request) addon = self._extract_addon_installed(request)
stats: DockerStats = await addon.stats() stats: DockerStats = await addon.stats()
@@ -384,43 +421,83 @@ class APIAddons(CoreSysAttributes):
@api_process @api_process
def uninstall(self, request: web.Request) -> Awaitable[None]: def uninstall(self, request: web.Request) -> Awaitable[None]:
"""Uninstall add-on.""" """Uninstall add-on."""
addon = self._extract_addon(request) addon = self._extract_addon_installed(request)
return asyncio.shield(addon.uninstall()) return asyncio.shield(addon.uninstall())
@api_process @api_process
def start(self, request: web.Request) -> Awaitable[None]: def start(self, request: web.Request) -> Awaitable[None]:
"""Start add-on.""" """Start add-on."""
addon = self._extract_addon(request) addon = self._extract_addon_installed(request)
return asyncio.shield(addon.start()) return asyncio.shield(addon.start())
@api_process @api_process
def stop(self, request: web.Request) -> Awaitable[None]: def stop(self, request: web.Request) -> Awaitable[None]:
"""Stop add-on.""" """Stop add-on."""
addon = self._extract_addon(request) addon = self._extract_addon_installed(request)
return asyncio.shield(addon.stop()) return asyncio.shield(addon.stop())
@api_process @api_process
def restart(self, request: web.Request) -> Awaitable[None]: def restart(self, request: web.Request) -> Awaitable[None]:
"""Restart add-on.""" """Restart add-on."""
addon: Addon = self._extract_addon(request) addon: Addon = self._extract_addon_installed(request)
return asyncio.shield(addon.restart()) return asyncio.shield(addon.restart())
@api_process @api_process
def rebuild(self, request: web.Request) -> Awaitable[None]: def rebuild(self, request: web.Request) -> Awaitable[None]:
"""Rebuild local build add-on.""" """Rebuild local build add-on."""
addon = self._extract_addon(request) addon = self._extract_addon_installed(request)
return asyncio.shield(addon.rebuild()) return asyncio.shield(addon.rebuild())
@api_process_raw(CONTENT_TYPE_BINARY) @api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request: web.Request) -> Awaitable[bytes]: def logs(self, request: web.Request) -> Awaitable[bytes]:
"""Return logs from add-on.""" """Return logs from add-on."""
addon = self._extract_addon(request) addon = self._extract_addon_installed(request)
return addon.logs() return addon.logs()
@api_process_raw(CONTENT_TYPE_PNG)
async def icon(self, request: web.Request) -> bytes:
"""Return icon from add-on."""
addon = self._extract_addon(request)
if not addon.with_icon:
raise APIError(f"No icon found for add-on {addon.slug}!")
with addon.path_icon.open("rb") as png:
return png.read()
@api_process_raw(CONTENT_TYPE_PNG)
async def logo(self, request: web.Request) -> bytes:
"""Return logo from add-on."""
addon = self._extract_addon(request)
if not addon.with_logo:
raise APIError(f"No logo found for add-on {addon.slug}!")
with addon.path_logo.open("rb") as png:
return png.read()
@api_process_raw(CONTENT_TYPE_TEXT)
async def changelog(self, request: web.Request) -> str:
"""Return changelog from add-on."""
addon = self._extract_addon(request)
if not addon.with_changelog:
raise APIError(f"No changelog found for add-on {addon.slug}!")
with addon.path_changelog.open("r") as changelog:
return changelog.read()
@api_process_raw(CONTENT_TYPE_TEXT)
async def documentation(self, request: web.Request) -> str:
"""Return documentation from add-on."""
addon = self._extract_addon(request)
if not addon.with_documentation:
raise APIError(f"No documentation found for add-on {addon.slug}!")
with addon.path_documentation.open("r") as documentation:
return documentation.read()
@api_process @api_process
async def stdin(self, request: web.Request) -> None: async def stdin(self, request: web.Request) -> None:
"""Write to stdin of add-on.""" """Write to stdin of add-on."""
addon = self._extract_addon(request) addon = self._extract_addon_installed(request)
if not addon.with_stdin: if not addon.with_stdin:
raise APIError(f"STDIN not supported the {addon.slug} add-on") raise APIError(f"STDIN not supported the {addon.slug} add-on")
@@ -428,6 +505,6 @@ class APIAddons(CoreSysAttributes):
await asyncio.shield(addon.write_stdin(data)) await asyncio.shield(addon.write_stdin(data))
def _pretty_services(addon: Addon) -> list[str]: def _pretty_services(addon: AnyAddon) -> list[str]:
"""Return a simplified services role list.""" """Return a simplified services role list."""
return [f"{name}:{access}" for name, access in addon.services_role.items()] return [f"{name}:{access}" for name, access in addon.services_role.items()]

View File

@@ -29,12 +29,12 @@ from ..const import (
ATTR_VERSION, ATTR_VERSION,
ATTR_VERSION_LATEST, ATTR_VERSION_LATEST,
ATTR_VOLUME, ATTR_VOLUME,
CONTENT_TYPE_BINARY,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIError from ..exceptions import APIError
from ..host.sound import StreamType from ..host.sound import StreamType
from ..validate import version_tag from ..validate import version_tag
from .const import CONTENT_TYPE_BINARY
from .utils import api_process, api_process_raw, api_validate from .utils import api_process, api_process_raw, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)

View File

@@ -8,10 +8,15 @@ from aiohttp.web_exceptions import HTTPUnauthorized
import voluptuous as vol import voluptuous as vol
from ..addons.addon import Addon from ..addons.addon import Addon
from ..const import ATTR_PASSWORD, ATTR_USERNAME, REQUEST_FROM from ..const import (
ATTR_PASSWORD,
ATTR_USERNAME,
CONTENT_TYPE_JSON,
CONTENT_TYPE_URL,
REQUEST_FROM,
)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIForbidden from ..exceptions import APIForbidden
from .const import CONTENT_TYPE_JSON, CONTENT_TYPE_URL
from .utils import api_process, api_validate from .utils import api_process, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)

View File

@@ -9,14 +9,12 @@ from aiohttp import web
from aiohttp.hdrs import CONTENT_DISPOSITION from aiohttp.hdrs import CONTENT_DISPOSITION
import voluptuous as vol import voluptuous as vol
from ..backups.validate import ALL_FOLDERS, FOLDER_HOMEASSISTANT, days_until_stale from ..backups.validate import ALL_FOLDERS
from ..const import ( from ..const import (
ATTR_ADDONS, ATTR_ADDONS,
ATTR_BACKUPS, ATTR_BACKUPS,
ATTR_COMPRESSED,
ATTR_CONTENT, ATTR_CONTENT,
ATTR_DATE, ATTR_DATE,
ATTR_DAYS_UNTIL_STALE,
ATTR_FOLDERS, ATTR_FOLDERS,
ATTR_HOMEASSISTANT, ATTR_HOMEASSISTANT,
ATTR_NAME, ATTR_NAME,
@@ -25,30 +23,25 @@ from ..const import (
ATTR_REPOSITORIES, ATTR_REPOSITORIES,
ATTR_SIZE, ATTR_SIZE,
ATTR_SLUG, ATTR_SLUG,
ATTR_SUPERVISOR_VERSION,
ATTR_TYPE, ATTR_TYPE,
ATTR_VERSION, ATTR_VERSION,
CONTENT_TYPE_TAR,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIError from ..exceptions import APIError
from .const import CONTENT_TYPE_TAR
from .utils import api_process, api_validate from .utils import api_process, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
RE_SLUGIFY_NAME = re.compile(r"[^A-Za-z0-9]+") 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 # pylint: disable=no-value-for-parameter
SCHEMA_RESTORE_PARTIAL = vol.Schema( SCHEMA_RESTORE_PARTIAL = vol.Schema(
{ {
vol.Optional(ATTR_PASSWORD): vol.Maybe(str), vol.Optional(ATTR_PASSWORD): vol.Maybe(str),
vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(), vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(),
vol.Optional(ATTR_ADDONS): vol.All([str], vol.Unique()), 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()),
} }
) )
@@ -58,24 +51,17 @@ SCHEMA_BACKUP_FULL = vol.Schema(
{ {
vol.Optional(ATTR_NAME): str, vol.Optional(ATTR_NAME): str,
vol.Optional(ATTR_PASSWORD): vol.Maybe(str), vol.Optional(ATTR_PASSWORD): vol.Maybe(str),
vol.Optional(ATTR_COMPRESSED): vol.Maybe(vol.Boolean()),
} }
) )
SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend( SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend(
{ {
vol.Optional(ATTR_ADDONS): vol.All([str], vol.Unique()), 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(), vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(),
} }
) )
SCHEMA_OPTIONS = vol.Schema(
{
vol.Optional(ATTR_DAYS_UNTIL_STALE): days_until_stale,
}
)
class APIBackups(CoreSysAttributes): class APIBackups(CoreSysAttributes):
"""Handle RESTful API for backups functions.""" """Handle RESTful API for backups functions."""
@@ -87,30 +73,26 @@ class APIBackups(CoreSysAttributes):
raise APIError("Backup does not exist") raise APIError("Backup does not exist")
return backup return backup
def _list_backups(self):
"""Return list of backups."""
return [
{
ATTR_SLUG: backup.slug,
ATTR_NAME: backup.name,
ATTR_DATE: backup.date,
ATTR_TYPE: backup.sys_type,
ATTR_SIZE: backup.size,
ATTR_PROTECTED: backup.protected,
ATTR_COMPRESSED: backup.compressed,
ATTR_CONTENT: {
ATTR_HOMEASSISTANT: backup.homeassistant_version is not None,
ATTR_ADDONS: backup.addon_list,
ATTR_FOLDERS: backup.folders,
},
}
for backup in self.sys_backups.list_backups
]
@api_process @api_process
async def list(self, request): async def list(self, request):
"""Return backup list.""" """Return backup list."""
data_backups = self._list_backups() data_backups = []
for backup in self.sys_backups.list_backups:
data_backups.append(
{
ATTR_SLUG: backup.slug,
ATTR_NAME: backup.name,
ATTR_DATE: backup.date,
ATTR_TYPE: backup.sys_type,
ATTR_SIZE: backup.size,
ATTR_PROTECTED: backup.protected,
ATTR_CONTENT: {
ATTR_HOMEASSISTANT: backup.homeassistant_version is not None,
ATTR_ADDONS: backup.addon_list,
ATTR_FOLDERS: backup.folders,
},
}
)
if request.path == "/snapshots": if request.path == "/snapshots":
# Kept for backwards compability # Kept for backwards compability
@@ -118,24 +100,6 @@ class APIBackups(CoreSysAttributes):
return {ATTR_BACKUPS: data_backups} return {ATTR_BACKUPS: data_backups}
@api_process
async def info(self, request):
"""Return backup list and manager info."""
return {
ATTR_BACKUPS: self._list_backups(),
ATTR_DAYS_UNTIL_STALE: self.sys_backups.days_until_stale,
}
@api_process
async def options(self, request):
"""Set backup manager options."""
body = await api_validate(SCHEMA_OPTIONS, request)
if ATTR_DAYS_UNTIL_STALE in body:
self.sys_backups.days_until_stale = body[ATTR_DAYS_UNTIL_STALE]
self.sys_backups.save_data()
@api_process @api_process
async def reload(self, request): async def reload(self, request):
"""Reload backup list.""" """Reload backup list."""
@@ -143,7 +107,7 @@ class APIBackups(CoreSysAttributes):
return True return True
@api_process @api_process
async def backup_info(self, request): async def info(self, request):
"""Return backup info.""" """Return backup info."""
backup = self._extract_slug(request) backup = self._extract_slug(request)
@@ -164,9 +128,7 @@ class APIBackups(CoreSysAttributes):
ATTR_NAME: backup.name, ATTR_NAME: backup.name,
ATTR_DATE: backup.date, ATTR_DATE: backup.date,
ATTR_SIZE: backup.size, ATTR_SIZE: backup.size,
ATTR_COMPRESSED: backup.compressed,
ATTR_PROTECTED: backup.protected, ATTR_PROTECTED: backup.protected,
ATTR_SUPERVISOR_VERSION: backup.supervisor_version,
ATTR_HOMEASSISTANT: backup.homeassistant_version, ATTR_HOMEASSISTANT: backup.homeassistant_version,
ATTR_ADDONS: data_addons, ATTR_ADDONS: data_addons,
ATTR_REPOSITORIES: backup.repositories, ATTR_REPOSITORIES: backup.repositories,

View File

@@ -1,41 +1,15 @@
"""Const for API.""" """Const for API."""
CONTENT_TYPE_BINARY = "application/octet-stream"
CONTENT_TYPE_JSON = "application/json"
CONTENT_TYPE_PNG = "image/png"
CONTENT_TYPE_TAR = "application/tar"
CONTENT_TYPE_TEXT = "text/plain"
CONTENT_TYPE_URL = "application/x-www-form-urlencoded"
COOKIE_INGRESS = "ingress_session"
HEADER_TOKEN_OLD = "X-Hassio-Key"
HEADER_TOKEN = "X-Supervisor-Token"
ATTR_APPARMOR_VERSION = "apparmor_version"
ATTR_AGENT_VERSION = "agent_version" ATTR_AGENT_VERSION = "agent_version"
ATTR_AVAILABLE_UPDATES = "available_updates"
ATTR_BOOT_TIMESTAMP = "boot_timestamp" ATTR_BOOT_TIMESTAMP = "boot_timestamp"
ATTR_BOOTS = "boots"
ATTR_BROADCAST_LLMNR = "broadcast_llmnr"
ATTR_BROADCAST_MDNS = "broadcast_mdns"
ATTR_DATA_DISK = "data_disk" ATTR_DATA_DISK = "data_disk"
ATTR_DEVICE = "device" ATTR_DEVICE = "device"
ATTR_DT_SYNCHRONIZED = "dt_synchronized" ATTR_DT_SYNCHRONIZED = "dt_synchronized"
ATTR_DT_UTC = "dt_utc" ATTR_DT_UTC = "dt_utc"
ATTR_FALLBACK = "fallback"
ATTR_IDENTIFIERS = "identifiers"
ATTR_LLMNR = "llmnr"
ATTR_LLMNR_HOSTNAME = "llmnr_hostname"
ATTR_MDNS = "mdns"
ATTR_PANEL_PATH = "panel_path"
ATTR_SIGNED = "signed"
ATTR_STARTUP_TIME = "startup_time" ATTR_STARTUP_TIME = "startup_time"
ATTR_UPDATE_TYPE = "update_type"
ATTR_USE_NTP = "use_ntp" ATTR_USE_NTP = "use_ntp"
ATTR_BY_ID = "by_id" ATTR_USE_RTC = "use_rtc"
ATTR_SUBSYSTEM = "subsystem" ATTR_APPARMOR_VERSION = "apparmor_version"
ATTR_SYSFS = "sysfs" ATTR_PANEL_PATH = "panel_path"
ATTR_DEV_PATH = "dev_path" ATTR_UPDATE_TYPE = "update_type"
ATTR_ATTRIBUTES = "attributes" ATTR_AVAILABLE_UPDATES = "available_updates"
ATTR_CHILDREN = "children"

View File

@@ -21,22 +21,17 @@ from ..const import (
ATTR_UPDATE_AVAILABLE, ATTR_UPDATE_AVAILABLE,
ATTR_VERSION, ATTR_VERSION,
ATTR_VERSION_LATEST, ATTR_VERSION_LATEST,
CONTENT_TYPE_BINARY,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIError from ..exceptions import APIError
from ..validate import dns_server_list, version_tag from ..validate import dns_server_list, version_tag
from .const import ATTR_FALLBACK, ATTR_LLMNR, ATTR_MDNS, CONTENT_TYPE_BINARY
from .utils import api_process, api_process_raw, api_validate from .utils import api_process, api_process_raw, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
SCHEMA_OPTIONS = vol.Schema( SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_SERVERS): dns_server_list})
{
vol.Optional(ATTR_SERVERS): dns_server_list,
vol.Optional(ATTR_FALLBACK): vol.Boolean(),
}
)
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag}) SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag})
@@ -54,26 +49,15 @@ class APICoreDNS(CoreSysAttributes):
ATTR_HOST: str(self.sys_docker.network.dns), ATTR_HOST: str(self.sys_docker.network.dns),
ATTR_SERVERS: self.sys_plugins.dns.servers, ATTR_SERVERS: self.sys_plugins.dns.servers,
ATTR_LOCALS: self.sys_plugins.dns.locals, ATTR_LOCALS: self.sys_plugins.dns.locals,
ATTR_MDNS: self.sys_plugins.dns.mdns,
ATTR_LLMNR: self.sys_plugins.dns.llmnr,
ATTR_FALLBACK: self.sys_plugins.dns.fallback,
} }
@api_process @api_process
async def options(self, request: web.Request) -> None: async def options(self, request: web.Request) -> None:
"""Set DNS options.""" """Set DNS options."""
body = await api_validate(SCHEMA_OPTIONS, request) body = await api_validate(SCHEMA_OPTIONS, request)
restart_required = False
if ATTR_SERVERS in body: if ATTR_SERVERS in body:
self.sys_plugins.dns.servers = body[ATTR_SERVERS] self.sys_plugins.dns.servers = body[ATTR_SERVERS]
restart_required = True
if ATTR_FALLBACK in body:
self.sys_plugins.dns.fallback = body[ATTR_FALLBACK]
restart_required = True
if restart_required:
self.sys_create_task(self.sys_plugins.dns.restart()) self.sys_create_task(self.sys_plugins.dns.restart())
self.sys_plugins.dns.save_data() self.sys_plugins.dns.save_data()

View File

@@ -6,15 +6,14 @@ from aiohttp import web
from ..const import ATTR_AUDIO, ATTR_DEVICES, ATTR_INPUT, ATTR_NAME, ATTR_OUTPUT from ..const import ATTR_AUDIO, ATTR_DEVICES, ATTR_INPUT, ATTR_NAME, ATTR_OUTPUT
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..hardware.data import Device from ..hardware.const import (
from .const import (
ATTR_ATTRIBUTES, ATTR_ATTRIBUTES,
ATTR_BY_ID, ATTR_BY_ID,
ATTR_CHILDREN,
ATTR_DEV_PATH, ATTR_DEV_PATH,
ATTR_SUBSYSTEM, ATTR_SUBSYSTEM,
ATTR_SYSFS, ATTR_SYSFS,
) )
from ..hardware.data import Device
from .utils import api_process from .utils import api_process
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@@ -29,7 +28,6 @@ def device_struct(device: Device) -> dict[str, Any]:
ATTR_SUBSYSTEM: device.subsystem, ATTR_SUBSYSTEM: device.subsystem,
ATTR_BY_ID: device.by_id, ATTR_BY_ID: device.by_id,
ATTR_ATTRIBUTES: device.attributes, ATTR_ATTRIBUTES: device.attributes,
ATTR_CHILDREN: device.children,
} }

View File

@@ -29,12 +29,13 @@ from ..const import (
ATTR_UPDATE_AVAILABLE, ATTR_UPDATE_AVAILABLE,
ATTR_VERSION, ATTR_VERSION,
ATTR_VERSION_LATEST, ATTR_VERSION_LATEST,
ATTR_WAIT_BOOT,
ATTR_WATCHDOG, ATTR_WATCHDOG,
CONTENT_TYPE_BINARY,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIError from ..exceptions import APIError
from ..validate import docker_image, network_port, version_tag from ..validate import docker_image, network_port, version_tag
from .const import CONTENT_TYPE_BINARY
from .utils import api_process, api_process_raw, api_validate from .utils import api_process, api_process_raw, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@@ -47,6 +48,7 @@ SCHEMA_OPTIONS = vol.Schema(
vol.Optional(ATTR_PORT): network_port, vol.Optional(ATTR_PORT): network_port,
vol.Optional(ATTR_SSL): vol.Boolean(), vol.Optional(ATTR_SSL): vol.Boolean(),
vol.Optional(ATTR_WATCHDOG): vol.Boolean(), vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
vol.Optional(ATTR_WAIT_BOOT): vol.All(vol.Coerce(int), vol.Range(min=60)),
vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(str), vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(str),
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(str), vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(str),
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(str), vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(str),
@@ -79,8 +81,11 @@ class APIHomeAssistant(CoreSysAttributes):
ATTR_PORT: self.sys_homeassistant.api_port, ATTR_PORT: self.sys_homeassistant.api_port,
ATTR_SSL: self.sys_homeassistant.api_ssl, ATTR_SSL: self.sys_homeassistant.api_ssl,
ATTR_WATCHDOG: self.sys_homeassistant.watchdog, ATTR_WATCHDOG: self.sys_homeassistant.watchdog,
ATTR_WAIT_BOOT: self.sys_homeassistant.wait_boot,
ATTR_AUDIO_INPUT: self.sys_homeassistant.audio_input, ATTR_AUDIO_INPUT: self.sys_homeassistant.audio_input,
ATTR_AUDIO_OUTPUT: self.sys_homeassistant.audio_output, ATTR_AUDIO_OUTPUT: self.sys_homeassistant.audio_output,
# Remove end of Q3 2020
"last_version": self.sys_homeassistant.latest_version,
} }
@api_process @api_process
@@ -103,6 +108,9 @@ class APIHomeAssistant(CoreSysAttributes):
if ATTR_WATCHDOG in body: if ATTR_WATCHDOG in body:
self.sys_homeassistant.watchdog = body[ATTR_WATCHDOG] self.sys_homeassistant.watchdog = body[ATTR_WATCHDOG]
if ATTR_WAIT_BOOT in body:
self.sys_homeassistant.wait_boot = body[ATTR_WAIT_BOOT]
if ATTR_REFRESH_TOKEN in body: if ATTR_REFRESH_TOKEN in body:
self.sys_homeassistant.refresh_token = body[ATTR_REFRESH_TOKEN] self.sys_homeassistant.refresh_token = body[ATTR_REFRESH_TOKEN]

View File

@@ -1,12 +1,9 @@
"""Init file for Supervisor host RESTful API.""" """Init file for Supervisor host RESTful API."""
import asyncio import asyncio
from contextlib import suppress from typing import Awaitable
import logging
from aiohttp import web from aiohttp import web
from aiohttp.hdrs import ACCEPT, RANGE
import voluptuous as vol import voluptuous as vol
from voluptuous.error import CoerceInvalid
from ..const import ( from ..const import (
ATTR_CHASSIS, ATTR_CHASSIS,
@@ -25,32 +22,22 @@ from ..const import (
ATTR_SERVICES, ATTR_SERVICES,
ATTR_STATE, ATTR_STATE,
ATTR_TIMEZONE, ATTR_TIMEZONE,
CONTENT_TYPE_BINARY,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIError, HostLogError
from ..host.const import PARAM_BOOT_ID, PARAM_FOLLOW, PARAM_SYSLOG_IDENTIFIER
from .const import ( from .const import (
ATTR_AGENT_VERSION, ATTR_AGENT_VERSION,
ATTR_APPARMOR_VERSION, ATTR_APPARMOR_VERSION,
ATTR_BOOT_TIMESTAMP, ATTR_BOOT_TIMESTAMP,
ATTR_BOOTS,
ATTR_BROADCAST_LLMNR,
ATTR_BROADCAST_MDNS,
ATTR_DT_SYNCHRONIZED, ATTR_DT_SYNCHRONIZED,
ATTR_DT_UTC, ATTR_DT_UTC,
ATTR_IDENTIFIERS,
ATTR_LLMNR_HOSTNAME,
ATTR_STARTUP_TIME, ATTR_STARTUP_TIME,
ATTR_USE_NTP, ATTR_USE_NTP,
CONTENT_TYPE_TEXT, ATTR_USE_RTC,
) )
from .utils import api_process, api_validate from .utils import api_process, api_process_raw, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__) SERVICE = "service"
IDENTIFIER = "identifier"
BOOTID = "bootid"
DEFAULT_RANGE = 100
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): str}) SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): str})
@@ -73,17 +60,15 @@ class APIHost(CoreSysAttributes):
ATTR_DISK_LIFE_TIME: self.sys_host.info.disk_life_time, ATTR_DISK_LIFE_TIME: self.sys_host.info.disk_life_time,
ATTR_FEATURES: self.sys_host.features, ATTR_FEATURES: self.sys_host.features,
ATTR_HOSTNAME: self.sys_host.info.hostname, ATTR_HOSTNAME: self.sys_host.info.hostname,
ATTR_LLMNR_HOSTNAME: self.sys_host.info.llmnr_hostname,
ATTR_KERNEL: self.sys_host.info.kernel, ATTR_KERNEL: self.sys_host.info.kernel,
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system, ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
ATTR_TIMEZONE: self.sys_host.info.timezone, ATTR_TIMEZONE: self.sys_host.info.timezone,
ATTR_DT_UTC: self.sys_host.info.dt_utc, ATTR_DT_UTC: self.sys_host.info.dt_utc,
ATTR_DT_SYNCHRONIZED: self.sys_host.info.dt_synchronized, ATTR_DT_SYNCHRONIZED: self.sys_host.info.dt_synchronized,
ATTR_USE_NTP: self.sys_host.info.use_ntp, ATTR_USE_NTP: self.sys_host.info.use_ntp,
ATTR_USE_RTC: self.sys_host.info.use_rtc,
ATTR_STARTUP_TIME: self.sys_host.info.startup_time, ATTR_STARTUP_TIME: self.sys_host.info.startup_time,
ATTR_BOOT_TIMESTAMP: self.sys_host.info.boot_timestamp, ATTR_BOOT_TIMESTAMP: self.sys_host.info.boot_timestamp,
ATTR_BROADCAST_LLMNR: self.sys_host.info.broadcast_llmnr,
ATTR_BROADCAST_MDNS: self.sys_host.info.broadcast_mdns,
} }
@api_process @api_process
@@ -110,7 +95,11 @@ class APIHost(CoreSysAttributes):
@api_process @api_process
def reload(self, request): def reload(self, request):
"""Reload host data.""" """Reload host data."""
return asyncio.shield(self.sys_host.reload()) return asyncio.shield(
asyncio.wait(
[self.sys_host.reload(), self.sys_resolution.evaluate.evaluate_system()]
)
)
@api_process @api_process
async def services(self, request): async def services(self, request):
@@ -128,75 +117,30 @@ class APIHost(CoreSysAttributes):
return {ATTR_SERVICES: services} return {ATTR_SERVICES: services}
@api_process @api_process
async def list_boots(self, _: web.Request): def service_start(self, request):
"""Return a list of boot IDs.""" """Start a service."""
boot_ids = await self.sys_host.logs.get_boot_ids() unit = request.match_info.get(SERVICE)
return { return asyncio.shield(self.sys_host.services.start(unit))
ATTR_BOOTS: {
str(1 + i - len(boot_ids)): boot_id
for i, boot_id in enumerate(boot_ids)
}
}
@api_process @api_process
async def list_identifiers(self, _: web.Request): def service_stop(self, request):
"""Return a list of syslog identifiers.""" """Stop a service."""
return {ATTR_IDENTIFIERS: await self.sys_host.logs.get_identifiers()} unit = request.match_info.get(SERVICE)
return asyncio.shield(self.sys_host.services.stop(unit))
async def _get_boot_id(self, possible_offset: str) -> str:
"""Convert offset into boot ID if required."""
with suppress(CoerceInvalid):
offset = vol.Coerce(int)(possible_offset)
try:
return await self.sys_host.logs.get_boot_id(offset)
except (ValueError, HostLogError) as err:
raise APIError() from err
return possible_offset
@api_process @api_process
async def advanced_logs( def service_reload(self, request):
self, request: web.Request, identifier: str | None = None, follow: bool = False """Reload a service."""
) -> web.StreamResponse: unit = request.match_info.get(SERVICE)
"""Return systemd-journald logs.""" return asyncio.shield(self.sys_host.services.reload(unit))
params = {}
if identifier:
params[PARAM_SYSLOG_IDENTIFIER] = identifier
elif IDENTIFIER in request.match_info:
params[PARAM_SYSLOG_IDENTIFIER] = request.match_info.get(IDENTIFIER)
else:
params[PARAM_SYSLOG_IDENTIFIER] = self.sys_host.logs.default_identifiers
if BOOTID in request.match_info: @api_process
params[PARAM_BOOT_ID] = await self._get_boot_id( def service_restart(self, request):
request.match_info.get(BOOTID) """Restart a service."""
) unit = request.match_info.get(SERVICE)
if follow: return asyncio.shield(self.sys_host.services.restart(unit))
params[PARAM_FOLLOW] = ""
if ACCEPT in request.headers and request.headers[ACCEPT] not in [ @api_process_raw(CONTENT_TYPE_BINARY)
CONTENT_TYPE_TEXT, def logs(self, request: web.Request) -> Awaitable[bytes]:
"*/*", """Return host kernel logs."""
]: return self.sys_host.info.get_dmesg()
raise APIError(
"Invalid content type requested. Only text/plain supported for now."
)
if RANGE in request.headers:
range_header = request.headers.get(RANGE)
else:
range_header = f"entries=:-{DEFAULT_RANGE}:"
async with self.sys_host.logs.journald_logs(
params=params, range_header=range_header
) as resp:
try:
response = web.StreamResponse()
response.content_type = CONTENT_TYPE_TEXT
await response.prepare(request)
async for data in resp.content:
await response.write(data)
except ConnectionResetError as ex:
raise APIError(
"Connection reset when trying to fetch data from systemd-journald."
) from ex
return response

52
supervisor/api/info.py Normal file
View File

@@ -0,0 +1,52 @@
"""Init file for Supervisor info RESTful API."""
import logging
from typing import Any
from aiohttp import web
from ..const import (
ATTR_ARCH,
ATTR_CHANNEL,
ATTR_DOCKER,
ATTR_FEATURES,
ATTR_HASSOS,
ATTR_HOMEASSISTANT,
ATTR_HOSTNAME,
ATTR_LOGGING,
ATTR_MACHINE,
ATTR_OPERATING_SYSTEM,
ATTR_STATE,
ATTR_SUPERVISOR,
ATTR_SUPPORTED,
ATTR_SUPPORTED_ARCH,
ATTR_TIMEZONE,
)
from ..coresys import CoreSysAttributes
from .utils import api_process
_LOGGER: logging.Logger = logging.getLogger(__name__)
class APIInfo(CoreSysAttributes):
"""Handle RESTful API for info functions."""
@api_process
async def info(self, request: web.Request) -> dict[str, Any]:
"""Show system info."""
return {
ATTR_SUPERVISOR: self.sys_supervisor.version,
ATTR_HOMEASSISTANT: self.sys_homeassistant.version,
ATTR_HASSOS: self.sys_os.version,
ATTR_DOCKER: self.sys_docker.info.version,
ATTR_HOSTNAME: self.sys_host.info.hostname,
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
ATTR_FEATURES: self.sys_host.features,
ATTR_MACHINE: self.sys_machine,
ATTR_ARCH: self.sys_arch.default,
ATTR_STATE: self.sys_core.state,
ATTR_SUPPORTED_ARCH: self.sys_arch.supported,
ATTR_SUPPORTED: self.sys_core.supported,
ATTR_CHANNEL: self.sys_updater.channel,
ATTR_LOGGING: self.sys_config.logging,
ATTR_TIMEZONE: self.sys_timezone,
}

View File

@@ -2,7 +2,7 @@
import asyncio import asyncio
from ipaddress import ip_address from ipaddress import ip_address
import logging import logging
from typing import Any from typing import Any, Union
import aiohttp import aiohttp
from aiohttp import ClientTimeout, hdrs, web from aiohttp import ClientTimeout, hdrs, web
@@ -22,9 +22,11 @@ from ..const import (
ATTR_PANELS, ATTR_PANELS,
ATTR_SESSION, ATTR_SESSION,
ATTR_TITLE, ATTR_TITLE,
COOKIE_INGRESS,
HEADER_TOKEN,
HEADER_TOKEN_OLD,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from .const import COOKIE_INGRESS, HEADER_TOKEN, HEADER_TOKEN_OLD
from .utils import api_process, api_validate, require_home_assistant from .utils import api_process, api_validate, require_home_assistant
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@@ -86,7 +88,7 @@ class APIIngress(CoreSysAttributes):
@require_home_assistant @require_home_assistant
async def handler( async def handler(
self, request: web.Request self, request: web.Request
) -> web.Response | web.StreamResponse | web.WebSocketResponse: ) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]:
"""Route data to Supervisor ingress service.""" """Route data to Supervisor ingress service."""
# Check Ingress Session # Check Ingress Session
@@ -157,7 +159,7 @@ class APIIngress(CoreSysAttributes):
async def _handle_request( async def _handle_request(
self, request: web.Request, addon: Addon, path: str self, request: web.Request, addon: Addon, path: str
) -> web.Response | web.StreamResponse: ) -> Union[web.Response, web.StreamResponse]:
"""Ingress route for request.""" """Ingress route for request."""
url = self._create_url(addon, path) url = self._create_url(addon, path)
source_header = _init_header(request, addon) source_header = _init_header(request, addon)
@@ -216,7 +218,9 @@ class APIIngress(CoreSysAttributes):
return response return response
def _init_header(request: web.Request, addon: str) -> CIMultiDict | dict[str, str]: def _init_header(
request: web.Request, addon: str
) -> Union[CIMultiDict, dict[str, str]]:
"""Create initial header.""" """Create initial header."""
headers = {} headers = {}

View File

@@ -77,6 +77,7 @@ ADDONS_ROLE_ACCESS = {
r"^(?:" r"^(?:"
r"|/.+/info" r"|/.+/info"
r"|/backups.*" r"|/backups.*"
r"|/snapshots.*"
r")$" r")$"
), ),
ROLE_MANAGER: re.compile( ROLE_MANAGER: re.compile(

View File

@@ -18,11 +18,11 @@ from ..const import (
ATTR_UPDATE_AVAILABLE, ATTR_UPDATE_AVAILABLE,
ATTR_VERSION, ATTR_VERSION,
ATTR_VERSION_LATEST, ATTR_VERSION_LATEST,
CONTENT_TYPE_BINARY,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIError from ..exceptions import APIError
from ..validate import version_tag from ..validate import version_tag
from .const import CONTENT_TYPE_BINARY
from .utils import api_process, api_process_raw, api_validate from .utils import api_process, api_process_raw, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)

View File

@@ -30,7 +30,6 @@ from ..const import (
ATTR_PARENT, ATTR_PARENT,
ATTR_PRIMARY, ATTR_PRIMARY,
ATTR_PSK, ATTR_PSK,
ATTR_READY,
ATTR_SIGNAL, ATTR_SIGNAL,
ATTR_SSID, ATTR_SSID,
ATTR_SUPERVISOR_INTERNET, ATTR_SUPERVISOR_INTERNET,
@@ -90,7 +89,6 @@ def ipconfig_struct(config: IpConfig) -> dict[str, Any]:
ATTR_ADDRESS: [address.with_prefixlen for address in config.address], ATTR_ADDRESS: [address.with_prefixlen for address in config.address],
ATTR_NAMESERVERS: [str(address) for address in config.nameservers], ATTR_NAMESERVERS: [str(address) for address in config.nameservers],
ATTR_GATEWAY: str(config.gateway) if config.gateway else None, ATTR_GATEWAY: str(config.gateway) if config.gateway else None,
ATTR_READY: config.ready,
} }
@@ -143,7 +141,9 @@ class APINetwork(CoreSysAttributes):
def _get_interface(self, name: str) -> Interface: def _get_interface(self, name: str) -> Interface:
"""Get Interface by name or default.""" """Get Interface by name or default."""
if name.lower() == "default": name = name.lower()
if name == "default":
for interface in self.sys_host.network.interfaces: for interface in self.sys_host.network.interfaces:
if not interface.primary: if not interface.primary:
continue continue
@@ -196,14 +196,12 @@ class APINetwork(CoreSysAttributes):
for key, config in body.items(): for key, config in body.items():
if key == ATTR_IPV4: if key == ATTR_IPV4:
interface.ipv4 = attr.evolve( interface.ipv4 = attr.evolve(
interface.ipv4 interface.ipv4 or IpConfig(InterfaceMethod.STATIC, [], None, []),
or IpConfig(InterfaceMethod.STATIC, [], None, [], None),
**config, **config,
) )
elif key == ATTR_IPV6: elif key == ATTR_IPV6:
interface.ipv6 = attr.evolve( interface.ipv6 = attr.evolve(
interface.ipv6 interface.ipv6 or IpConfig(InterfaceMethod.STATIC, [], None, []),
or IpConfig(InterfaceMethod.STATIC, [], None, [], None),
**config, **config,
) )
elif key == ATTR_WIFI: elif key == ATTR_WIFI:
@@ -222,9 +220,7 @@ class APINetwork(CoreSysAttributes):
@api_process @api_process
def reload(self, request: web.Request) -> Awaitable[None]: def reload(self, request: web.Request) -> Awaitable[None]:
"""Reload network data.""" """Reload network data."""
return asyncio.shield( return asyncio.shield(self.sys_host.network.update())
self.sys_host.network.update(force_connectivity_check=True)
)
@api_process @api_process
async def scan_accesspoints(self, request: web.Request) -> dict[str, Any]: async def scan_accesspoints(self, request: web.Request) -> dict[str, Any]:
@@ -261,7 +257,6 @@ class APINetwork(CoreSysAttributes):
body[ATTR_IPV4].get(ATTR_ADDRESS, []), body[ATTR_IPV4].get(ATTR_ADDRESS, []),
body[ATTR_IPV4].get(ATTR_GATEWAY, None), body[ATTR_IPV4].get(ATTR_GATEWAY, None),
body[ATTR_IPV4].get(ATTR_NAMESERVERS, []), body[ATTR_IPV4].get(ATTR_NAMESERVERS, []),
None,
) )
ipv6_config = None ipv6_config = None
@@ -271,7 +266,6 @@ class APINetwork(CoreSysAttributes):
body[ATTR_IPV6].get(ATTR_ADDRESS, []), body[ATTR_IPV6].get(ATTR_ADDRESS, []),
body[ATTR_IPV6].get(ATTR_GATEWAY, None), body[ATTR_IPV6].get(ATTR_GATEWAY, None),
body[ATTR_IPV6].get(ATTR_NAMESERVERS, []), body[ATTR_IPV6].get(ATTR_NAMESERVERS, []),
None,
) )
vlan_interface = Interface( vlan_interface = Interface(

View File

@@ -1,14 +1,14 @@
function loadES5() { function loadES5() {
var el = document.createElement('script'); var el = document.createElement('script');
el.src = '/api/hassio/app/frontend_es5/entrypoint.4dd28383.js'; el.src = '/api/hassio/app/frontend_es5/entrypoint.5d40ff8b.js';
document.body.appendChild(el); document.body.appendChild(el);
} }
if (/.*Version\/(?:11|12)(?:\.\d+)*.*Safari\//.test(navigator.userAgent)) { if (/.*Version\/(?:11|12)(?:\.\d+)*.*Safari\//.test(navigator.userAgent)) {
loadES5(); loadES5();
} else { } else {
try { try {
new Function("import('/api/hassio/app/frontend_latest/entrypoint.962b867f.js')")(); new Function("import('/api/hassio/app/frontend_latest/entrypoint.f09e9f8e.js')")();
} catch (err) { } catch (err) {
loadES5(); loadES5();
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
!function(){"use strict";var r,t,e={5425:function(r,t,e){var n=e(93217);e(58556);function o(r,t){return function(r){if(Array.isArray(r))return r}(r)||function(r,t){var e=null==r?null:"undefined"!=typeof Symbol&&r[Symbol.iterator]||r["@@iterator"];if(null==e)return;var n,o,u=[],i=!0,a=!1;try{for(e=e.call(r);!(i=(n=e.next()).done)&&(u.push(n.value),!t||u.length!==t);i=!0);}catch(f){a=!0,o=f}finally{try{i||null==e.return||e.return()}finally{if(a)throw o}}return u}(r,t)||function(r,t){if(!r)return;if("string"==typeof r)return u(r,t);var e=Object.prototype.toString.call(r).slice(8,-1);"Object"===e&&r.constructor&&(e=r.constructor.name);if("Map"===e||"Set"===e)return Array.from(r);if("Arguments"===e||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e))return u(r,t)}(r,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function u(r,t){(null==t||t>r.length)&&(t=r.length);for(var e=0,n=new Array(t);e<t;e++)n[e]=r[e];return n}var i={filterData:function(r,t,e){return e=e.toUpperCase(),r.filter((function(r){return Object.entries(t).some((function(t){var n=o(t,2),u=n[0],i=n[1];return!(!i.filterable||!String(i.filterKey?r[i.valueColumn||u][i.filterKey]:r[i.valueColumn||u]).toUpperCase().includes(e))}))}))},sortData:function(r,t,e,n){return r.sort((function(r,o){var u=1;"desc"===e&&(u=-1);var i=t.filterKey?r[t.valueColumn||n][t.filterKey]:r[t.valueColumn||n],a=t.filterKey?o[t.valueColumn||n][t.filterKey]:o[t.valueColumn||n];return"string"==typeof i&&(i=i.toUpperCase()),"string"==typeof a&&(a=a.toUpperCase()),void 0===i&&void 0!==a?1:void 0===a&&void 0!==i?-1:i<a?-1*u:i>a?1*u:0}))}};(0,n.Jj)(i)}},n={};function o(r){var t=n[r];if(void 0!==t)return t.exports;var u=n[r]={exports:{}};return e[r](u,u.exports,o),u.exports}o.m=e,o.x=function(){var r=o.O(void 0,[9191],(function(){return o(5425)}));return r=o.O(r)},r=[],o.O=function(t,e,n,u){if(!e){var i=1/0;for(c=0;c<r.length;c++){e=r[c][0],n=r[c][1],u=r[c][2];for(var a=!0,f=0;f<e.length;f++)(!1&u||i>=u)&&Object.keys(o.O).every((function(r){return o.O[r](e[f])}))?e.splice(f--,1):(a=!1,u<i&&(i=u));if(a){r.splice(c--,1);var l=n();void 0!==l&&(t=l)}}return t}u=u||0;for(var c=r.length;c>0&&r[c-1][2]>u;c--)r[c]=r[c-1];r[c]=[e,n,u]},o.n=function(r){var t=r&&r.__esModule?function(){return r.default}:function(){return r};return o.d(t,{a:t}),t},o.d=function(r,t){for(var e in t)o.o(t,e)&&!o.o(r,e)&&Object.defineProperty(r,e,{enumerable:!0,get:t[e]})},o.f={},o.e=function(r){return Promise.all(Object.keys(o.f).reduce((function(t,e){return o.f[e](r,t),t}),[]))},o.u=function(r){return"ed6e5677.js"},o.o=function(r,t){return Object.prototype.hasOwnProperty.call(r,t)},o.p="/api/hassio/app/frontend_es5/",function(){var r={5425:1,5477:1};o.f.i=function(t,e){r[t]||importScripts(o.p+o.u(t))};var t=self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[],e=t.push.bind(t);t.push=function(t){var n=t[0],u=t[1],i=t[2];for(var a in u)o.o(u,a)&&(o.m[a]=u[a]);for(i&&i(o);n.length;)r[n.pop()]=1;e(t)}}(),t=o.x,o.x=function(){return o.e(9191).then(t)};o.x()}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +0,0 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
/**
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
part of the polymer project is also subject to an additional IP rights grant
found at http://polymer.github.io/PATENTS.txt
*/

Some files were not shown because too many files have changed in this diff Show More