mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-08-29 19:09:20 +00:00
Compare commits
2 Commits
2022.10.2
...
refresh-up
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ec897081cd | ||
![]() |
839361133a |
@@ -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",
|
||||||
|
52
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
52
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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.
|
|
||||||
|
|
||||||
[](https://my.home-assistant.io/redirect/supervisor_logs/)
|
|
||||||
render: txt
|
render: txt
|
||||||
- type: textarea
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
attributes:
|
|
||||||
label: System Health information
|
|
||||||
description: >
|
|
||||||
System Health information can be found in the top right menu in [Settings -> System -> Repairs](https://my.home-assistant.io/redirect/repairs/).
|
|
||||||
Click the copy button at the bottom of the pop-up and paste it here.
|
|
||||||
|
|
||||||
[](https://my.home-assistant.io/redirect/system_health/)
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Supervisor diagnostics
|
|
||||||
placeholder: "drag-and-drop the diagnostics data file here (do not copy-and-paste the content)"
|
|
||||||
description: >-
|
|
||||||
Supervisor diagnostics can be found in [Settings -> Integrations](https://my.home-assistant.io/redirect/integrations/).
|
|
||||||
Find the card that says `Home Assistant Supervisor`, open its menu and select 'Download diagnostics'.
|
|
||||||
|
|
||||||
**Please drag-and-drop the downloaded file into the textbox below. Do not copy and paste its contents.**
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional information
|
label: Additional information
|
||||||
|
1
.github/release-drafter.yml
vendored
1
.github/release-drafter.yml
vendored
@@ -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"
|
||||||
|
157
.github/workflows/builder.yml
vendored
157
.github/workflows/builder.yml
vendored
@@ -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
|
||||||
|
|
||||||
|
164
.github/workflows/ci.yaml
vendored
164
.github/workflows/ci.yaml
vendored
@@ -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
|
||||||
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v3.0.0
|
- uses: dessant/lock-threads@v3
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-inactive-days: "30"
|
issue-inactive-days: "30"
|
||||||
|
4
.github/workflows/release-drafter.yml
vendored
4
.github/workflows/release-drafter.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
name: Release Drafter
|
name: Release Drafter
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v3.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 }}
|
||||||
|
4
.github/workflows/sentry.yaml
vendored
4
.github/workflows/sentry.yaml
vendored
@@ -10,9 +10,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v3.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 }}
|
||||||
|
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
@@ -9,10 +9,10 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@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"
|
||||||
|
@@ -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]
|
||||||
|
22
Dockerfile
22
Dockerfile
@@ -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
|
||||||
|
|
||||||
|
12
build.yaml
12
build.yaml
@@ -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
|
||||||
|
Submodule home-assistant-polymer updated: 5bb9538861...2f9c088091
5
pylintrc
5
pylintrc
@@ -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,
|
||||||
|
@@ -1,2 +0,0 @@
|
|||||||
[pytest]
|
|
||||||
asyncio_mode = auto
|
|
@@ -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
|
|
||||||
|
@@ -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
0
rootfs/etc/cont-init.d/udev.sh
Executable file → Normal file
11
rootfs/etc/services.d/supervisor/finish
Executable file → Normal file
11
rootfs/etc/services.d/supervisor/finish
Executable file → Normal 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
1
rootfs/etc/services.d/supervisor/run
Executable file → Normal 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
11
rootfs/etc/services.d/watchdog/finish
Executable file → Normal 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
2
rootfs/etc/services.d/watchdog/run
Executable file → Normal 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!"
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE03LvYuz79GTJx4uKp3w6NrSe5JZI
|
|
||||||
iBtgzzYi0YQYtZO/r+xFpgDJEa0gLHkXtl94fpqrFiN89In83lzaszbZtA==
|
|
||||||
-----END PUBLIC KEY-----
|
|
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"currentcontext": {
|
|
||||||
"LcHost": "cas.codenotary.com",
|
|
||||||
"LcPort": "443"
|
|
||||||
},
|
|
||||||
"schemaversion": 3,
|
|
||||||
"users": null
|
|
||||||
}
|
|
1
setup.py
1
setup.py
@@ -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",
|
||||||
|
@@ -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()
|
||||||
|
@@ -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)
|
||||||
|
@@ -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)
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -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,
|
|
||||||
]
|
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
@@ -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:
|
||||||
|
@@ -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:
|
||||||
|
@@ -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}),
|
||||||
|
@@ -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,
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -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()]
|
||||||
|
@@ -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__)
|
||||||
|
@@ -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__)
|
||||||
|
@@ -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,
|
||||||
|
@@ -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"
|
|
||||||
|
@@ -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()
|
||||||
|
@@ -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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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]
|
||||||
|
|
||||||
|
@@ -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
52
supervisor/api/info.py
Normal 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,
|
||||||
|
}
|
@@ -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 = {}
|
||||||
|
|
||||||
|
@@ -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(
|
||||||
|
@@ -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__)
|
||||||
|
@@ -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(
|
||||||
|
@@ -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();
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
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
Binary file not shown.
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
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
!function(){"use strict";var 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()}();
|
|
Binary file not shown.
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
Binary file not shown.
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
Binary file not shown.
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
Binary file not shown.
1
supervisor/api/panel/frontend_es5/12dbe34e.js
Normal file
1
supervisor/api/panel/frontend_es5/12dbe34e.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/12dbe34e.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/12dbe34e.js.gz
Normal file
Binary file not shown.
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
Binary file not shown.
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
Binary file not shown.
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
@@ -1,5 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2021 Google LLC
|
|
||||||
* SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
*/
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
1
supervisor/api/panel/frontend_es5/2dbdaab4.js
Normal file
1
supervisor/api/panel/frontend_es5/2dbdaab4.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/2dbdaab4.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/2dbdaab4.js.gz
Normal file
Binary file not shown.
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
Binary file not shown.
2
supervisor/api/panel/frontend_es5/33b00c5f.js
Normal file
2
supervisor/api/panel/frontend_es5/33b00c5f.js
Normal file
File diff suppressed because one or more lines are too long
10
supervisor/api/panel/frontend_es5/33b00c5f.js.LICENSE.txt
Normal file
10
supervisor/api/panel/frontend_es5/33b00c5f.js.LICENSE.txt
Normal 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
Reference in New Issue
Block a user