mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-08-12 10:39:21 +00:00
Compare commits
4 Commits
add-networ
...
ipv6
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2470d16d85 | ||
![]() |
5c0172440a | ||
![]() |
006db94cc0 | ||
![]() |
9f1ab99265 |
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"name": "Supervisor dev",
|
||||
"image": "ghcr.io/home-assistant/devcontainer:supervisor",
|
||||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace/supervisor/,type=bind",
|
||||
"workspaceFolder": "/workspace/supervisor/",
|
||||
"appPort": ["9123:8123", "7357:4357"],
|
||||
"postCreateCommand": "bash devcontainer_bootstrap",
|
||||
"runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
|
||||
@@ -12,7 +10,7 @@
|
||||
"visualstudioexptteam.vscodeintellicode",
|
||||
"esbenp.prettier-vscode"
|
||||
],
|
||||
"mounts": ["type=volume,target=/var/lib/docker"],
|
||||
"mounts": [ "type=volume,target=/var/lib/docker" ],
|
||||
"settings": {
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"zsh": {
|
||||
@@ -28,7 +26,7 @@
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"python.formatting.provider": "black",
|
||||
"python.formatting.blackArgs": ["--target-version", "py310"],
|
||||
"python.formatting.blackArgs": ["--target-version", "py39"],
|
||||
"python.formatting.blackPath": "/usr/local/bin/black",
|
||||
"python.linting.banditPath": "/usr/local/bin/bandit",
|
||||
"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:
|
||||
value: |
|
||||
## 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
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: What type of installation are you running?
|
||||
description: >
|
||||
If you don't know, can be found in [Settings -> System -> Repairs -> System Information](https://my.home-assistant.io/redirect/system_health/).
|
||||
It is listed as the `Installation Type` value.
|
||||
If you don't know, you can find it in: Configuration panel -> Info.
|
||||
options:
|
||||
- Home Assistant OS
|
||||
- Home Assistant Supervised
|
||||
@@ -40,6 +48,22 @@ body:
|
||||
- Home Assistant Operating System
|
||||
- Debian
|
||||
- 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
|
||||
attributes:
|
||||
value: |
|
||||
@@ -63,30 +87,8 @@ body:
|
||||
attributes:
|
||||
label: Anything in the Supervisor logs that might be useful for us?
|
||||
description: >
|
||||
Supervisor Logs can be found in [Settings -> System -> Logs](https://my.home-assistant.io/redirect/logs/)
|
||||
then choose `Supervisor` in the top right.
|
||||
|
||||
[](https://my.home-assistant.io/redirect/supervisor_logs/)
|
||||
The Supervisor logs can be found in the Supervisor panel -> System tab.
|
||||
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
|
||||
attributes:
|
||||
label: Additional information
|
||||
|
57
.github/workflows/builder.yml
vendored
57
.github/workflows/builder.yml
vendored
@@ -33,13 +33,10 @@ on:
|
||||
- setup.py
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.10"
|
||||
DEFAULT_PYTHON: 3.9
|
||||
BUILD_NAME: supervisor
|
||||
BUILD_TYPE: supervisor
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.ref }}'
|
||||
cancel-in-progress: true
|
||||
WHEELS_TAG: 3.9-alpine3.14
|
||||
|
||||
jobs:
|
||||
init:
|
||||
@@ -53,7 +50,7 @@ jobs:
|
||||
requirements: ${{ steps.requirements.outputs.changed }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -88,29 +85,21 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
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
|
||||
if: needs.init.outputs.requirements == 'true'
|
||||
uses: home-assistant/wheels@2023.04.0
|
||||
uses: home-assistant/wheels@2022.01.2
|
||||
with:
|
||||
abi: cp310
|
||||
tag: musllinux_1_2
|
||||
tag: ${{ env.WHEELS_TAG }}
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-host: wheels.hass.io
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
apk: "libffi-dev;openssl-dev"
|
||||
wheels-user: wheels
|
||||
apk: "build-base;libffi-dev;openssl-dev;cargo"
|
||||
skip-binary: aiohttp
|
||||
env-file: true
|
||||
requirements: "requirements.txt"
|
||||
|
||||
- name: Set version
|
||||
@@ -121,14 +110,14 @@ jobs:
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: docker/login-action@v2.1.0
|
||||
uses: docker/login-action@v2.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: docker/login-action@v2.1.0
|
||||
uses: docker/login-action@v2.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -139,7 +128,7 @@ jobs:
|
||||
run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV
|
||||
|
||||
- name: Build supervisor
|
||||
uses: home-assistant/builder@2023.03.0
|
||||
uses: home-assistant/builder@2022.06.2
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@@ -156,13 +145,13 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -195,7 +184,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.0.2
|
||||
|
||||
- name: Initialize git
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
@@ -220,11 +209,11 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.0.2
|
||||
|
||||
- name: Build the Supervisor
|
||||
if: needs.init.outputs.publish != 'true'
|
||||
uses: home-assistant/builder@2023.03.0
|
||||
uses: home-assistant/builder@2022.06.2
|
||||
with:
|
||||
args: |
|
||||
--test \
|
||||
@@ -301,12 +290,6 @@ jobs:
|
||||
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: Check the Supervisor code sign
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
run: |
|
||||
@@ -379,12 +362,6 @@ jobs:
|
||||
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')
|
||||
|
156
.github/workflows/ci.yaml
vendored
156
.github/workflows/ci.yaml
vendored
@@ -8,33 +8,30 @@ on:
|
||||
pull_request: ~
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.10"
|
||||
DEFAULT_PYTHON: 3.9
|
||||
PRE_COMMIT_HOME: ~/.cache/pre-commit
|
||||
DEFAULT_CAS: v1.0.2
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.ref }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Separate job to pre-populate the base dependency cache
|
||||
# This prevent upcoming jobs to do the same individually
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
python-version: ${{ steps.python.outputs.python-version }}
|
||||
name: Prepare Python dependencies
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
name: Prepare Python ${{ matrix.python-version }} dependencies
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.2.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.0.6
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -48,7 +45,7 @@ jobs:
|
||||
pip install -r requirements.txt -r requirements_tests.txt
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.0.6
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -67,19 +64,19 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.0.6
|
||||
with:
|
||||
path: venv
|
||||
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
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@@ -96,7 +93,7 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Register hadolint problem matcher
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
||||
@@ -111,19 +108,19 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.0.6
|
||||
with:
|
||||
path: venv
|
||||
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
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@@ -131,7 +128,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.0.6
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -155,19 +152,19 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.0.6
|
||||
with:
|
||||
path: venv
|
||||
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
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@@ -187,19 +184,19 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.0.6
|
||||
with:
|
||||
path: venv
|
||||
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
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@@ -207,7 +204,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.0.6
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -228,19 +225,19 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.0.6
|
||||
with:
|
||||
path: venv
|
||||
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
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@@ -248,7 +245,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.0.6
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -272,19 +269,19 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.0.6
|
||||
with:
|
||||
path: venv
|
||||
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
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@@ -304,19 +301,19 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.0.6
|
||||
with:
|
||||
path: venv
|
||||
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
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@@ -324,7 +321,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.0.6
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -342,26 +339,29 @@ jobs:
|
||||
pytest:
|
||||
runs-on: ubuntu-latest
|
||||
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:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install CAS tools
|
||||
uses: home-assistant/actions/helpers/cas@master
|
||||
with:
|
||||
version: ${{ env.DEFAULT_CAS }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.0.6
|
||||
with:
|
||||
path: venv
|
||||
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
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@@ -370,7 +370,7 @@ jobs:
|
||||
- name: Install additional system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends libpulse0 libudev1 dbus dbus-x11
|
||||
sudo apt-get install -y --no-install-recommends libpulse0 libudev1
|
||||
- name: Register Python problem matcher
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/python.json"
|
||||
@@ -392,7 +392,7 @@ jobs:
|
||||
-o console_output_style=count \
|
||||
tests
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v3.1.2
|
||||
uses: actions/upload-artifact@v3.1.0
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}
|
||||
path: .coverage
|
||||
@@ -400,22 +400,22 @@ jobs:
|
||||
coverage:
|
||||
name: Process test coverage
|
||||
runs-on: ubuntu-latest
|
||||
needs: ["pytest", "prepare"]
|
||||
needs: pytest
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.2.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v3.3.1
|
||||
uses: actions/cache@v3.0.6
|
||||
with:
|
||||
path: venv
|
||||
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
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@@ -430,4 +430,4 @@ jobs:
|
||||
coverage report
|
||||
coverage xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3.1.3
|
||||
uses: codecov/codecov-action@v3.1.0
|
||||
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4.0.0
|
||||
- uses: dessant/lock-threads@v3.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: "30"
|
||||
|
4
.github/workflows/release-drafter.yml
vendored
4
.github/workflows/release-drafter.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
name: Release Drafter
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
echo "::set-output name=version::$datepre.$newpost"
|
||||
|
||||
- name: Run Release Drafter
|
||||
uses: release-drafter/release-drafter@v5.23.0
|
||||
uses: release-drafter/release-drafter@v5.20.0
|
||||
with:
|
||||
tag: ${{ 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
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Sentry Release
|
||||
uses: getsentry/action-release@v1.4.1
|
||||
uses: getsentry/action-release@v1.2.0
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
|
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
@@ -9,10 +9,10 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v8.0.0
|
||||
- uses: actions/stale@v5.1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 30
|
||||
days-before-stale: 60
|
||||
days-before-close: 7
|
||||
stale-issue-label: "stale"
|
||||
exempt-issue-labels: "no-stale,Help%20wanted,help-wanted,pinned,rfc,security"
|
||||
|
@@ -1,34 +1,34 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.1.0
|
||||
rev: 22.6.0
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
- --safe
|
||||
- --quiet
|
||||
- --target-version
|
||||
- py310
|
||||
- py39
|
||||
files: ^((supervisor|tests)/.+)?[^/]+\.py$
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.8.3
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
- flake8-docstrings==1.7.0
|
||||
- pydocstyle==6.3.0
|
||||
- flake8-docstrings==1.5.0
|
||||
- pydocstyle==5.0.2
|
||||
files: ^(supervisor|script|tests)/.+\.py$
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
rev: v3.1.0
|
||||
hooks:
|
||||
- id: check-executables-have-shebangs
|
||||
stages: [manual]
|
||||
- id: check-json
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
rev: 5.9.3
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.3.1
|
||||
rev: v2.32.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py310-plus]
|
||||
args: [--py39-plus]
|
||||
|
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -13,13 +13,6 @@
|
||||
"remoteRoot": "/usr/src/supervisor"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Debug Tests",
|
||||
"type": "python",
|
||||
"request": "test",
|
||||
"console": "internalConsole",
|
||||
"justMyCode": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ ENV \
|
||||
SUPERVISOR_API=http://localhost
|
||||
|
||||
ARG \
|
||||
BUILD_ARCH \
|
||||
CAS_VERSION
|
||||
|
||||
# Install base
|
||||
@@ -39,7 +40,7 @@ COPY requirements.txt .
|
||||
RUN \
|
||||
export MAKEFLAGS="-j$(nproc)" \
|
||||
&& 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 \
|
||||
&& rm -f requirements.txt
|
||||
|
||||
|
10
build.yaml
10
build.yaml
@@ -1,11 +1,11 @@
|
||||
image: homeassistant/{arch}-hassio-supervisor
|
||||
shadow_repository: ghcr.io/home-assistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-base-python:3.10-alpine3.16
|
||||
armhf: ghcr.io/home-assistant/armhf-base-python:3.10-alpine3.16
|
||||
armv7: ghcr.io/home-assistant/armv7-base-python:3.10-alpine3.16
|
||||
amd64: ghcr.io/home-assistant/amd64-base-python:3.10-alpine3.16
|
||||
i386: ghcr.io/home-assistant/i386-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.9-alpine3.14
|
||||
armv7: ghcr.io/home-assistant/armv7-base-python:3.9-alpine3.14
|
||||
amd64: ghcr.io/home-assistant/amd64-base-python:3.9-alpine3.14
|
||||
i386: ghcr.io/home-assistant/i386-base-python:3.9-alpine3.14
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
Submodule home-assistant-polymer updated: 3a1fff81b8...ca28feca80
2
pylintrc
2
pylintrc
@@ -38,7 +38,7 @@ disable=
|
||||
consider-using-with
|
||||
|
||||
[EXCEPTIONS]
|
||||
overgeneral-exceptions=builtins.Exception
|
||||
overgeneral-exceptions=Exception
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
@@ -1,26 +1,25 @@
|
||||
aiodns==3.0.0
|
||||
aiohttp==3.8.4
|
||||
aiohttp==3.8.1
|
||||
async_timeout==4.0.2
|
||||
atomicwrites-homeassistant==1.4.1
|
||||
attrs==23.1.0
|
||||
awesomeversion==22.9.0
|
||||
attrs==22.1.0
|
||||
awesomeversion==22.6.0
|
||||
brotli==1.0.9
|
||||
cchardet==2.1.7
|
||||
ciso8601==2.3.0
|
||||
colorlog==6.7.0
|
||||
ciso8601==2.2.0
|
||||
colorlog==6.6.0
|
||||
cpe==1.2.1
|
||||
cryptography==40.0.2
|
||||
debugpy==1.6.7
|
||||
deepmerge==1.1.0
|
||||
cryptography==37.0.4
|
||||
debugpy==1.6.2
|
||||
deepmerge==1.0.1
|
||||
dirhash==0.2.1
|
||||
docker==6.0.1
|
||||
gitpython==3.1.31
|
||||
docker==5.0.3
|
||||
gitpython==3.1.27
|
||||
jinja2==3.1.2
|
||||
pulsectl==22.3.2
|
||||
pyudev==0.24.0
|
||||
ruamel.yaml==0.17.21
|
||||
securetar==2023.3.0
|
||||
sentry-sdk==1.21.0
|
||||
pyudev==0.23.2
|
||||
ruamel.yaml==0.17.17
|
||||
securetar==2022.2.0
|
||||
sentry-sdk==1.9.2
|
||||
voluptuous==0.13.1
|
||||
dbus-fast==1.84.2
|
||||
typing_extensions==4.5.0
|
||||
dbus-next==0.2.3
|
||||
|
@@ -1,16 +1,15 @@
|
||||
black==23.3.0
|
||||
coverage==7.2.3
|
||||
flake8-docstrings==1.7.0
|
||||
flake8==6.0.0
|
||||
pre-commit==3.2.2
|
||||
pydocstyle==6.3.0
|
||||
pylint==2.17.3
|
||||
black==22.6.0
|
||||
codecov==2.1.12
|
||||
coverage==6.4.3
|
||||
flake8-docstrings==1.6.0
|
||||
flake8==5.0.4
|
||||
pre-commit==2.20.0
|
||||
pydocstyle==6.1.1
|
||||
pylint==2.14.5
|
||||
pytest-aiohttp==1.0.4
|
||||
pytest-asyncio==0.18.3
|
||||
pytest-cov==4.0.0
|
||||
pytest-cov==3.0.0
|
||||
pytest-timeout==2.1.0
|
||||
pytest==7.3.1
|
||||
pyupgrade==3.3.1
|
||||
time-machine==2.9.0
|
||||
typing_extensions==4.5.0
|
||||
urllib3==1.26.15
|
||||
pytest==7.1.2
|
||||
pyupgrade==2.37.3
|
||||
time-machine==2.7.1
|
||||
|
@@ -27,5 +27,3 @@ ignore =
|
||||
E203,
|
||||
D202,
|
||||
W504
|
||||
per-file-ignores =
|
||||
tests/dbus_service_mocks/*.py: F821,F722
|
||||
|
@@ -28,8 +28,7 @@ if __name__ == "__main__":
|
||||
bootstrap.initialize_logging()
|
||||
|
||||
# Init async event loop
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# Check if all information are available to setup Supervisor
|
||||
bootstrap.check_environment()
|
||||
|
@@ -3,7 +3,7 @@ import asyncio
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
import tarfile
|
||||
from typing import Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from ..const import AddonBoot, AddonStartup, AddonState
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
@@ -23,9 +23,7 @@ from ..jobs.decorator import Job, JobCondition
|
||||
from ..resolution.const import ContextType, IssueType, SuggestionType
|
||||
from ..store.addon import AddonStore
|
||||
from ..utils import check_exception_chain
|
||||
from ..utils.sentry import capture_exception
|
||||
from .addon import Addon
|
||||
from .const import ADDON_UPDATE_CONDITIONS
|
||||
from .data import AddonsData
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@@ -54,7 +52,7 @@ class AddonManager(CoreSysAttributes):
|
||||
"""Return a list of all installed add-ons."""
|
||||
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.
|
||||
|
||||
Prio:
|
||||
@@ -67,7 +65,7 @@ class AddonManager(CoreSysAttributes):
|
||||
return self.store.get(addon_slug)
|
||||
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."""
|
||||
for addon in self.installed:
|
||||
if token == addon.supervisor_token:
|
||||
@@ -115,7 +113,7 @@ class AddonManager(CoreSysAttributes):
|
||||
addon.boot = AddonBoot.MANUAL
|
||||
addon.save_persist()
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
capture_exception(err)
|
||||
self.sys_capture_exception(err)
|
||||
else:
|
||||
continue
|
||||
|
||||
@@ -143,10 +141,14 @@ class AddonManager(CoreSysAttributes):
|
||||
await addon.stop()
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.warning("Can't stop Add-on %s: %s", addon.slug, err)
|
||||
capture_exception(err)
|
||||
self.sys_capture_exception(err)
|
||||
|
||||
@Job(
|
||||
conditions=ADDON_UPDATE_CONDITIONS,
|
||||
conditions=[
|
||||
JobCondition.FREE_SPACE,
|
||||
JobCondition.INTERNET_HOST,
|
||||
JobCondition.HEALTHY,
|
||||
],
|
||||
on_condition=AddonsJobError,
|
||||
)
|
||||
async def install(self, slug: str) -> None:
|
||||
@@ -158,7 +160,10 @@ class AddonManager(CoreSysAttributes):
|
||||
if not store:
|
||||
raise AddonsError(f"Add-on {slug} does not exist", _LOGGER.error)
|
||||
|
||||
store.validate_availability()
|
||||
if not store.available:
|
||||
raise AddonsNotSupportedError(
|
||||
f"Add-on {slug} not supported on this platform", _LOGGER.error
|
||||
)
|
||||
|
||||
self.data.install(store)
|
||||
addon = Addon(self.coresys, slug)
|
||||
@@ -178,8 +183,8 @@ class AddonManager(CoreSysAttributes):
|
||||
except DockerError as err:
|
||||
self.data.uninstall(addon)
|
||||
raise AddonsError() from err
|
||||
|
||||
self.local[slug] = addon
|
||||
else:
|
||||
self.local[slug] = addon
|
||||
|
||||
# Reload ingress tokens
|
||||
if addon.with_ingress:
|
||||
@@ -198,10 +203,10 @@ class AddonManager(CoreSysAttributes):
|
||||
await addon.instance.remove()
|
||||
except DockerError as err:
|
||||
raise AddonsError() from err
|
||||
else:
|
||||
addon.state = AddonState.UNKNOWN
|
||||
|
||||
addon.state = AddonState.UNKNOWN
|
||||
|
||||
await addon.unload()
|
||||
await addon.remove_data()
|
||||
|
||||
# Cleanup audio settings
|
||||
if addon.path_pulse.exists():
|
||||
@@ -241,10 +246,14 @@ class AddonManager(CoreSysAttributes):
|
||||
_LOGGER.info("Add-on '%s' successfully removed", slug)
|
||||
|
||||
@Job(
|
||||
conditions=ADDON_UPDATE_CONDITIONS,
|
||||
conditions=[
|
||||
JobCondition.FREE_SPACE,
|
||||
JobCondition.INTERNET_HOST,
|
||||
JobCondition.HEALTHY,
|
||||
],
|
||||
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."""
|
||||
if slug not in self.local:
|
||||
raise AddonsError(f"Add-on {slug} is not installed", _LOGGER.error)
|
||||
@@ -260,7 +269,10 @@ class AddonManager(CoreSysAttributes):
|
||||
raise AddonsError(f"No update available for add-on {slug}", _LOGGER.warning)
|
||||
|
||||
# Check if available, Maybe something have changed
|
||||
store.validate_availability()
|
||||
if not store.available:
|
||||
raise AddonsNotSupportedError(
|
||||
f"Add-on {slug} not supported on that platform", _LOGGER.error
|
||||
)
|
||||
|
||||
if backup:
|
||||
await self.sys_backups.do_backup_partial(
|
||||
@@ -328,9 +340,9 @@ class AddonManager(CoreSysAttributes):
|
||||
await addon.instance.install(addon.version)
|
||||
except DockerError as err:
|
||||
raise AddonsError() from err
|
||||
|
||||
self.data.update(store)
|
||||
_LOGGER.info("Add-on '%s' successfully rebuilt", slug)
|
||||
else:
|
||||
self.data.update(store)
|
||||
_LOGGER.info("Add-on '%s' successfully rebuilt", slug)
|
||||
|
||||
# restore state
|
||||
if last_state == AddonState.STARTED:
|
||||
@@ -416,7 +428,7 @@ class AddonManager(CoreSysAttributes):
|
||||
reference=addon.slug,
|
||||
suggestions=[SuggestionType.EXECUTE_REPAIR],
|
||||
)
|
||||
capture_exception(err)
|
||||
self.sys_capture_exception(err)
|
||||
else:
|
||||
self.sys_plugins.dns.add_host(
|
||||
ipv4=addon.ip_address, names=[addon.hostname], write=False
|
||||
|
@@ -1,6 +1,5 @@
|
||||
"""Init file for Supervisor add-ons."""
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
from contextlib import suppress
|
||||
from copy import deepcopy
|
||||
from ipaddress import IPv4Address
|
||||
@@ -11,7 +10,7 @@ import secrets
|
||||
import shutil
|
||||
import tarfile
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Any, Final
|
||||
from typing import Any, Awaitable, Final, Optional
|
||||
|
||||
import aiohttp
|
||||
from deepmerge import Merger
|
||||
@@ -19,7 +18,6 @@ from securetar import atomic_contents_add, secure_path
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from ..bus import EventListener
|
||||
from ..const import (
|
||||
ATTR_ACCESS_TOKEN,
|
||||
ATTR_AUDIO_INPUT,
|
||||
@@ -60,7 +58,6 @@ from ..docker.stats import DockerStats
|
||||
from ..exceptions import (
|
||||
AddonConfigurationError,
|
||||
AddonsError,
|
||||
AddonsJobError,
|
||||
AddonsNotSupportedError,
|
||||
ConfigurationFileError,
|
||||
DockerError,
|
||||
@@ -68,19 +65,10 @@ from ..exceptions import (
|
||||
)
|
||||
from ..hardware.data import Device
|
||||
from ..homeassistant.const import WSEvent, WSType
|
||||
from ..jobs.const import JobExecutionLimit
|
||||
from ..jobs.decorator import Job
|
||||
from ..utils import check_port
|
||||
from ..utils.apparmor import adjust_profile
|
||||
from ..utils.json import read_json_file, write_json_file
|
||||
from ..utils.sentry import capture_exception
|
||||
from .const import (
|
||||
WATCHDOG_MAX_ATTEMPTS,
|
||||
WATCHDOG_RETRY_SECONDS,
|
||||
WATCHDOG_THROTTLE_MAX_CALLS,
|
||||
WATCHDOG_THROTTLE_PERIOD,
|
||||
AddonBackupMode,
|
||||
)
|
||||
from .const import WATCHDOG_RETRY_SECONDS, AddonBackupMode
|
||||
from .model import AddonModel, Data
|
||||
from .options import AddonOptions
|
||||
from .utils import remove_data
|
||||
@@ -115,58 +103,6 @@ class Addon(AddonModel):
|
||||
super().__init__(coresys, slug)
|
||||
self.instance: DockerAddon = DockerAddon(coresys, self)
|
||||
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
|
||||
)
|
||||
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:
|
||||
"""Return internal representation."""
|
||||
@@ -201,15 +137,11 @@ class Addon(AddonModel):
|
||||
|
||||
async def load(self) -> None:
|
||||
"""Async initialize of object."""
|
||||
self._listeners.append(
|
||||
self.sys_bus.register_event(
|
||||
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, self.container_state_changed
|
||||
)
|
||||
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
|
||||
)
|
||||
self.sys_bus.register_event(
|
||||
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, self.watchdog_container
|
||||
)
|
||||
|
||||
with suppress(DockerError):
|
||||
@@ -251,7 +183,7 @@ class Addon(AddonModel):
|
||||
return self._available(self.data_store)
|
||||
|
||||
@property
|
||||
def version(self) -> str | None:
|
||||
def version(self) -> Optional[str]:
|
||||
"""Return installed version."""
|
||||
return self.persist[ATTR_VERSION]
|
||||
|
||||
@@ -275,7 +207,7 @@ class Addon(AddonModel):
|
||||
)
|
||||
|
||||
@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."""
|
||||
self.persist[ATTR_OPTIONS] = {} if value is None else deepcopy(value)
|
||||
|
||||
@@ -320,17 +252,17 @@ class Addon(AddonModel):
|
||||
return self.persist[ATTR_UUID]
|
||||
|
||||
@property
|
||||
def supervisor_token(self) -> str | None:
|
||||
def supervisor_token(self) -> Optional[str]:
|
||||
"""Return access token for Supervisor API."""
|
||||
return self.persist.get(ATTR_ACCESS_TOKEN)
|
||||
|
||||
@property
|
||||
def ingress_token(self) -> str | None:
|
||||
def ingress_token(self) -> Optional[str]:
|
||||
"""Return access token for Supervisor API."""
|
||||
return self.persist.get(ATTR_INGRESS_TOKEN)
|
||||
|
||||
@property
|
||||
def ingress_entry(self) -> str | None:
|
||||
def ingress_entry(self) -> Optional[str]:
|
||||
"""Return ingress external URL."""
|
||||
if self.with_ingress:
|
||||
return f"/api/hassio_ingress/{self.ingress_token}"
|
||||
@@ -352,12 +284,12 @@ class Addon(AddonModel):
|
||||
self.persist[ATTR_PROTECTED] = value
|
||||
|
||||
@property
|
||||
def ports(self) -> dict[str, int | None] | None:
|
||||
def ports(self) -> Optional[dict[str, Optional[int]]]:
|
||||
"""Return ports of add-on."""
|
||||
return self.persist.get(ATTR_NETWORK, super().ports)
|
||||
|
||||
@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."""
|
||||
if value is None:
|
||||
self.persist.pop(ATTR_NETWORK, None)
|
||||
@@ -372,7 +304,7 @@ class Addon(AddonModel):
|
||||
self.persist[ATTR_NETWORK] = new_ports
|
||||
|
||||
@property
|
||||
def ingress_url(self) -> str | None:
|
||||
def ingress_url(self) -> Optional[str]:
|
||||
"""Return URL to ingress url."""
|
||||
if not self.with_ingress:
|
||||
return None
|
||||
@@ -383,7 +315,7 @@ class Addon(AddonModel):
|
||||
return url
|
||||
|
||||
@property
|
||||
def webui(self) -> str | None:
|
||||
def webui(self) -> Optional[str]:
|
||||
"""Return URL to webui or None."""
|
||||
url = super().webui
|
||||
if not url:
|
||||
@@ -411,7 +343,7 @@ class Addon(AddonModel):
|
||||
return f"{proto}://[HOST]:{port}{s_suffix}"
|
||||
|
||||
@property
|
||||
def ingress_port(self) -> int | None:
|
||||
def ingress_port(self) -> Optional[int]:
|
||||
"""Return Ingress port."""
|
||||
if not self.with_ingress:
|
||||
return None
|
||||
@@ -422,7 +354,7 @@ class Addon(AddonModel):
|
||||
return port
|
||||
|
||||
@property
|
||||
def ingress_panel(self) -> bool | None:
|
||||
def ingress_panel(self) -> Optional[bool]:
|
||||
"""Return True if the add-on access support ingress."""
|
||||
if not self.with_ingress:
|
||||
return None
|
||||
@@ -435,19 +367,19 @@ class Addon(AddonModel):
|
||||
self.persist[ATTR_INGRESS_PANEL] = value
|
||||
|
||||
@property
|
||||
def audio_output(self) -> str | None:
|
||||
def audio_output(self) -> Optional[str]:
|
||||
"""Return a pulse profile for output or None."""
|
||||
if not self.with_audio:
|
||||
return None
|
||||
return self.persist.get(ATTR_AUDIO_OUTPUT)
|
||||
|
||||
@audio_output.setter
|
||||
def audio_output(self, value: str | None):
|
||||
def audio_output(self, value: Optional[str]):
|
||||
"""Set audio output profile settings."""
|
||||
self.persist[ATTR_AUDIO_OUTPUT] = value
|
||||
|
||||
@property
|
||||
def audio_input(self) -> str | None:
|
||||
def audio_input(self) -> Optional[str]:
|
||||
"""Return pulse profile for input or None."""
|
||||
if not self.with_audio:
|
||||
return None
|
||||
@@ -455,12 +387,12 @@ class Addon(AddonModel):
|
||||
return self.persist.get(ATTR_AUDIO_INPUT)
|
||||
|
||||
@audio_input.setter
|
||||
def audio_input(self, value: str | None) -> None:
|
||||
def audio_input(self, value: Optional[str]) -> None:
|
||||
"""Set audio input settings."""
|
||||
self.persist[ATTR_AUDIO_INPUT] = value
|
||||
|
||||
@property
|
||||
def image(self) -> str | None:
|
||||
def image(self) -> Optional[str]:
|
||||
"""Return image name of add-on."""
|
||||
return self.persist.get(ATTR_IMAGE)
|
||||
|
||||
@@ -469,11 +401,6 @@ class Addon(AddonModel):
|
||||
"""Return True if this add-on need a local build."""
|
||||
return ATTR_IMAGE not in self.data
|
||||
|
||||
@property
|
||||
def latest_need_build(self) -> bool:
|
||||
"""Return True if the latest version of the addon needs a local build."""
|
||||
return ATTR_IMAGE not in self.data_store
|
||||
|
||||
@property
|
||||
def path_data(self) -> Path:
|
||||
"""Return add-on data path inside Supervisor."""
|
||||
@@ -517,11 +444,6 @@ class Addon(AddonModel):
|
||||
|
||||
return options_schema.pwned
|
||||
|
||||
@property
|
||||
def loaded(self) -> bool:
|
||||
"""Is add-on loaded."""
|
||||
return bool(self._listeners)
|
||||
|
||||
def save_persist(self) -> None:
|
||||
"""Save data of add-on."""
|
||||
self.sys_addons.data.save_data()
|
||||
@@ -590,11 +512,8 @@ class Addon(AddonModel):
|
||||
|
||||
raise AddonConfigurationError()
|
||||
|
||||
async def unload(self) -> None:
|
||||
"""Unload add-on and remove data."""
|
||||
for listener in self._listeners:
|
||||
self.sys_bus.remove_listener(listener)
|
||||
|
||||
async def remove_data(self) -> None:
|
||||
"""Remove add-on data."""
|
||||
if not self.path_data.is_dir():
|
||||
return
|
||||
|
||||
@@ -706,7 +625,6 @@ class Addon(AddonModel):
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop add-on."""
|
||||
self._manual_stop = True
|
||||
try:
|
||||
await self.instance.stop()
|
||||
except DockerError as err:
|
||||
@@ -952,10 +870,6 @@ class Addon(AddonModel):
|
||||
)
|
||||
raise AddonsError() from err
|
||||
|
||||
# Is add-on loaded
|
||||
if not self.loaded:
|
||||
await self.load()
|
||||
|
||||
# Run add-on
|
||||
if data[ATTR_STATE] == AddonState.STARTED:
|
||||
return await self.start()
|
||||
@@ -979,7 +893,6 @@ class Addon(AddonModel):
|
||||
ContainerState.HEALTHY,
|
||||
ContainerState.UNHEALTHY,
|
||||
]:
|
||||
self._manual_stop = False
|
||||
self.state = AddonState.STARTED
|
||||
elif event.state == ContainerState.STOPPED:
|
||||
self.state = AddonState.STOPPED
|
||||
@@ -988,16 +901,43 @@ class Addon(AddonModel):
|
||||
|
||||
async def watchdog_container(self, event: DockerContainerStateEvent) -> None:
|
||||
"""Process state changes in addon container and restart if necessary."""
|
||||
if event.name != self.instance.name:
|
||||
if not (event.name == self.instance.name and self.watchdog):
|
||||
return
|
||||
|
||||
# Skip watchdog if not enabled or manual stopped
|
||||
if not self.watchdog or self._manual_stop:
|
||||
return
|
||||
if event.state == ContainerState.UNHEALTHY:
|
||||
while await self.instance.current_state() == event.state:
|
||||
if not self.in_progress:
|
||||
_LOGGER.warning(
|
||||
"Watchdog found addon %s is unhealthy, restarting...", self.name
|
||||
)
|
||||
try:
|
||||
await self.restart()
|
||||
except AddonsError as err:
|
||||
_LOGGER.error("Watchdog restart of addon %s failed!", self.name)
|
||||
self.sys_capture_exception(err)
|
||||
else:
|
||||
break
|
||||
|
||||
if event.state in [
|
||||
ContainerState.FAILED,
|
||||
ContainerState.STOPPED,
|
||||
ContainerState.UNHEALTHY,
|
||||
]:
|
||||
await self._restart_after_problem(self, event.state)
|
||||
await asyncio.sleep(WATCHDOG_RETRY_SECONDS)
|
||||
|
||||
elif event.state == ContainerState.FAILED:
|
||||
# Ensure failed container is removed before attempting reanimation
|
||||
with suppress(DockerError):
|
||||
await self.instance.stop(remove_container=True)
|
||||
|
||||
while await self.instance.current_state() == event.state:
|
||||
if not self.in_progress:
|
||||
_LOGGER.warning(
|
||||
"Watchdog found addon %s failed, restarting...", self.name
|
||||
)
|
||||
try:
|
||||
await self.start()
|
||||
except AddonsError as err:
|
||||
_LOGGER.error(
|
||||
"Watchdog reanimation of addon %s failed!", self.name
|
||||
)
|
||||
self.sys_capture_exception(err)
|
||||
else:
|
||||
break
|
||||
|
||||
await asyncio.sleep(WATCHDOG_RETRY_SECONDS)
|
||||
|
@@ -1,7 +1,6 @@
|
||||
"""Supervisor add-on build environment."""
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -16,8 +15,7 @@ from ..const import (
|
||||
META_ADDON,
|
||||
)
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..docker.interface import MAP_ARCH
|
||||
from ..exceptions import ConfigurationFileError, HassioArchNotFound
|
||||
from ..exceptions import ConfigurationFileError
|
||||
from ..utils.common import FileConfiguration, find_one_filetype
|
||||
from .validate import SCHEMA_BUILD_CONFIG
|
||||
|
||||
@@ -46,33 +44,15 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
|
||||
"""Ignore save function."""
|
||||
raise RuntimeError()
|
||||
|
||||
@cached_property
|
||||
def arch(self) -> str:
|
||||
"""Return arch of the add-on."""
|
||||
return self.sys_arch.match(self.addon.arch)
|
||||
|
||||
@property
|
||||
def base_image(self) -> str:
|
||||
"""Return base image for this add-on."""
|
||||
if not self._data[ATTR_BUILD_FROM]:
|
||||
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
|
||||
if self.arch not in self._data[ATTR_BUILD_FROM]:
|
||||
raise HassioArchNotFound(
|
||||
f"Add-on {self.addon.slug} is not supported on {self.arch}"
|
||||
)
|
||||
return self._data[ATTR_BUILD_FROM][self.arch]
|
||||
|
||||
@property
|
||||
def dockerfile(self) -> Path:
|
||||
"""Return Dockerfile path."""
|
||||
if self.addon.path_location.joinpath(f"Dockerfile.{self.arch}").exists():
|
||||
return self.addon.path_location.joinpath(f"Dockerfile.{self.arch}")
|
||||
return self.addon.path_location.joinpath("Dockerfile")
|
||||
arch = self.sys_arch.match(list(self._data[ATTR_BUILD_FROM].keys()))
|
||||
return self._data[ATTR_BUILD_FROM][arch]
|
||||
|
||||
@property
|
||||
def squash(self) -> bool:
|
||||
@@ -92,29 +72,24 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
"""Return true if the build env is valid."""
|
||||
try:
|
||||
return all(
|
||||
[
|
||||
self.addon.path_location.is_dir(),
|
||||
self.dockerfile.is_file(),
|
||||
]
|
||||
)
|
||||
except HassioArchNotFound:
|
||||
return False
|
||||
return all(
|
||||
[
|
||||
self.addon.path_location.is_dir(),
|
||||
Path(self.addon.path_location, "Dockerfile").is_file(),
|
||||
]
|
||||
)
|
||||
|
||||
def get_docker_args(self, version: AwesomeVersion):
|
||||
"""Create a dict with Docker build arguments."""
|
||||
args = {
|
||||
"path": str(self.addon.path_location),
|
||||
"tag": f"{self.addon.image}:{version!s}",
|
||||
"dockerfile": str(self.dockerfile),
|
||||
"pull": True,
|
||||
"forcerm": not self.sys_dev,
|
||||
"squash": self.squash,
|
||||
"platform": MAP_ARCH[self.arch],
|
||||
"labels": {
|
||||
"io.hass.version": version,
|
||||
"io.hass.arch": self.arch,
|
||||
"io.hass.arch": self.sys_arch.default,
|
||||
"io.hass.type": META_ADDON,
|
||||
"io.hass.name": self._fix_label("name"),
|
||||
"io.hass.description": self._fix_label("description"),
|
||||
|
@@ -1,9 +1,6 @@
|
||||
"""Add-on static data."""
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
|
||||
from ..jobs.const import JobCondition
|
||||
|
||||
|
||||
class AddonBackupMode(str, Enum):
|
||||
"""Backup mode of an Add-on."""
|
||||
@@ -15,16 +12,3 @@ class AddonBackupMode(str, Enum):
|
||||
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,
|
||||
]
|
||||
|
||||
RE_SLUG = r"[-_.A-Za-z0-9]+"
|
||||
|
@@ -1,13 +1,12 @@
|
||||
"""Init file for Supervisor add-ons."""
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Awaitable, Callable
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, Awaitable, Optional
|
||||
|
||||
from awesomeversion import AwesomeVersion, AwesomeVersionException
|
||||
|
||||
from supervisor.addons.const import AddonBackupMode
|
||||
|
||||
from ..const import (
|
||||
ATTR_ADVANCED,
|
||||
ATTR_APPARMOR,
|
||||
@@ -34,7 +33,6 @@ from ..const import (
|
||||
ATTR_HOST_IPC,
|
||||
ATTR_HOST_NETWORK,
|
||||
ATTR_HOST_PID,
|
||||
ATTR_HOST_UTS,
|
||||
ATTR_IMAGE,
|
||||
ATTR_INGRESS,
|
||||
ATTR_INGRESS_STREAM,
|
||||
@@ -81,13 +79,10 @@ from ..const import (
|
||||
)
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..docker.const import Capabilities
|
||||
from ..exceptions import AddonsNotSupportedError
|
||||
from .const import ATTR_BACKUP, ATTR_CODENOTARY, AddonBackupMode
|
||||
from .const import ATTR_BACKUP, ATTR_CODENOTARY
|
||||
from .options import AddonOptions, UiOptions
|
||||
from .validate import RE_SERVICE, RE_VOLUME
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
Data = dict[str, Any]
|
||||
|
||||
|
||||
@@ -130,7 +125,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return self.data[ATTR_BOOT]
|
||||
|
||||
@property
|
||||
def auto_update(self) -> bool | None:
|
||||
def auto_update(self) -> Optional[bool]:
|
||||
"""Return if auto update is enable."""
|
||||
return None
|
||||
|
||||
@@ -155,22 +150,22 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return self.data[ATTR_TIMEOUT]
|
||||
|
||||
@property
|
||||
def uuid(self) -> str | None:
|
||||
def uuid(self) -> Optional[str]:
|
||||
"""Return an API token for this add-on."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def supervisor_token(self) -> str | None:
|
||||
def supervisor_token(self) -> Optional[str]:
|
||||
"""Return access token for Supervisor API."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def ingress_token(self) -> str | None:
|
||||
def ingress_token(self) -> Optional[str]:
|
||||
"""Return access token for Supervisor API."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def ingress_entry(self) -> str | None:
|
||||
def ingress_entry(self) -> Optional[str]:
|
||||
"""Return ingress external URL."""
|
||||
return None
|
||||
|
||||
@@ -180,7 +175,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return self.data[ATTR_DESCRIPTON]
|
||||
|
||||
@property
|
||||
def long_description(self) -> str | None:
|
||||
def long_description(self) -> Optional[str]:
|
||||
"""Return README.md as long_description."""
|
||||
readme = Path(self.path_location, "README.md")
|
||||
|
||||
@@ -250,32 +245,32 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return self.data.get(ATTR_DISCOVERY, [])
|
||||
|
||||
@property
|
||||
def ports_description(self) -> dict[str, str] | None:
|
||||
def ports_description(self) -> Optional[dict[str, str]]:
|
||||
"""Return descriptions of ports."""
|
||||
return self.data.get(ATTR_PORTS_DESCRIPTION)
|
||||
|
||||
@property
|
||||
def ports(self) -> dict[str, int | None] | None:
|
||||
def ports(self) -> Optional[dict[str, Optional[int]]]:
|
||||
"""Return ports of add-on."""
|
||||
return self.data.get(ATTR_PORTS)
|
||||
|
||||
@property
|
||||
def ingress_url(self) -> str | None:
|
||||
def ingress_url(self) -> Optional[str]:
|
||||
"""Return URL to ingress url."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def webui(self) -> str | None:
|
||||
def webui(self) -> Optional[str]:
|
||||
"""Return URL to webui or None."""
|
||||
return self.data.get(ATTR_WEBUI)
|
||||
|
||||
@property
|
||||
def watchdog(self) -> str | None:
|
||||
def watchdog(self) -> Optional[str]:
|
||||
"""Return URL to for watchdog or None."""
|
||||
return self.data.get(ATTR_WATCHDOG)
|
||||
|
||||
@property
|
||||
def ingress_port(self) -> int | None:
|
||||
def ingress_port(self) -> Optional[int]:
|
||||
"""Return Ingress port."""
|
||||
return None
|
||||
|
||||
@@ -309,11 +304,6 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
"""Return True if add-on run on host IPC namespace."""
|
||||
return self.data[ATTR_HOST_IPC]
|
||||
|
||||
@property
|
||||
def host_uts(self) -> bool:
|
||||
"""Return True if add-on run on host UTS namespace."""
|
||||
return self.data[ATTR_HOST_UTS]
|
||||
|
||||
@property
|
||||
def host_dbus(self) -> bool:
|
||||
"""Return True if add-on run on host D-BUS."""
|
||||
@@ -325,7 +315,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return [Path(node) for node in self.data.get(ATTR_DEVICES, [])]
|
||||
|
||||
@property
|
||||
def environment(self) -> dict[str, str] | None:
|
||||
def environment(self) -> Optional[dict[str, str]]:
|
||||
"""Return environment of add-on."""
|
||||
return self.data.get(ATTR_ENVIRONMENT)
|
||||
|
||||
@@ -374,12 +364,12 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return self.data.get(ATTR_BACKUP_EXCLUDE, [])
|
||||
|
||||
@property
|
||||
def backup_pre(self) -> str | None:
|
||||
def backup_pre(self) -> Optional[str]:
|
||||
"""Return pre-backup command."""
|
||||
return self.data.get(ATTR_BACKUP_PRE)
|
||||
|
||||
@property
|
||||
def backup_post(self) -> str | None:
|
||||
def backup_post(self) -> Optional[str]:
|
||||
"""Return post-backup command."""
|
||||
return self.data.get(ATTR_BACKUP_POST)
|
||||
|
||||
@@ -404,7 +394,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return self.data[ATTR_INGRESS]
|
||||
|
||||
@property
|
||||
def ingress_panel(self) -> bool | None:
|
||||
def ingress_panel(self) -> Optional[bool]:
|
||||
"""Return True if the add-on access support ingress."""
|
||||
return None
|
||||
|
||||
@@ -454,7 +444,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return self.data[ATTR_DEVICETREE]
|
||||
|
||||
@property
|
||||
def with_tmpfs(self) -> str | None:
|
||||
def with_tmpfs(self) -> Optional[str]:
|
||||
"""Return if tmp is in memory of add-on."""
|
||||
return self.data[ATTR_TMPFS]
|
||||
|
||||
@@ -474,12 +464,12 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return self.data[ATTR_VIDEO]
|
||||
|
||||
@property
|
||||
def homeassistant_version(self) -> str | None:
|
||||
def homeassistant_version(self) -> Optional[str]:
|
||||
"""Return min Home Assistant version they needed by Add-on."""
|
||||
return self.data.get(ATTR_HOMEASSISTANT)
|
||||
|
||||
@property
|
||||
def url(self) -> str | None:
|
||||
def url(self) -> Optional[str]:
|
||||
"""Return URL of add-on."""
|
||||
return self.data.get(ATTR_URL)
|
||||
|
||||
@@ -522,7 +512,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return self.sys_arch.default
|
||||
|
||||
@property
|
||||
def image(self) -> str | None:
|
||||
def image(self) -> Optional[str]:
|
||||
"""Generate image name from data."""
|
||||
return self._image(self.data)
|
||||
|
||||
@@ -583,7 +573,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return AddonOptions(self.coresys, raw_schema, self.name, self.slug)
|
||||
|
||||
@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."""
|
||||
raw_schema = self.data[ATTR_SCHEMA]
|
||||
|
||||
@@ -602,58 +592,35 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return ATTR_CODENOTARY in self.data
|
||||
|
||||
@property
|
||||
def codenotary(self) -> str | None:
|
||||
def codenotary(self) -> Optional[str]:
|
||||
"""Return Signer email address for CAS."""
|
||||
return self.data.get(ATTR_CODENOTARY)
|
||||
|
||||
def validate_availability(self) -> None:
|
||||
"""Validate if addon is available for current system."""
|
||||
return self._validate_availability(self.data, logger=_LOGGER.error)
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Compaired add-on objects."""
|
||||
if not isinstance(other, AddonModel):
|
||||
return False
|
||||
return self.slug == other.slug
|
||||
|
||||
def _validate_availability(
|
||||
self, config, *, logger: Callable[..., None] | None = None
|
||||
) -> None:
|
||||
"""Validate if addon is available for current system."""
|
||||
def _available(self, config) -> bool:
|
||||
"""Return True if this add-on is available on this platform."""
|
||||
# Architecture
|
||||
if not self.sys_arch.is_supported(config[ATTR_ARCH]):
|
||||
raise AddonsNotSupportedError(
|
||||
f"Add-on {self.slug} not supported on this platform, supported architectures: {', '.join(config[ATTR_ARCH])}",
|
||||
logger,
|
||||
)
|
||||
return False
|
||||
|
||||
# Machine / Hardware
|
||||
machine = config.get(ATTR_MACHINE)
|
||||
if machine and (
|
||||
f"!{self.sys_machine}" in machine or self.sys_machine not in machine
|
||||
):
|
||||
raise AddonsNotSupportedError(
|
||||
f"Add-on {self.slug} not supported on this machine, supported machine types: {', '.join(machine)}",
|
||||
logger,
|
||||
)
|
||||
|
||||
# Home Assistant
|
||||
version: AwesomeVersion | None = config.get(ATTR_HOMEASSISTANT)
|
||||
with suppress(AwesomeVersionException, TypeError):
|
||||
if self.sys_homeassistant.version < version:
|
||||
raise AddonsNotSupportedError(
|
||||
f"Add-on {self.slug} not supported on this system, requires Home Assistant version {version} or greater",
|
||||
logger,
|
||||
)
|
||||
|
||||
def _available(self, config) -> bool:
|
||||
"""Return True if this add-on is available on this platform."""
|
||||
try:
|
||||
self._validate_availability(config)
|
||||
except AddonsNotSupportedError:
|
||||
if machine and f"!{self.sys_machine}" in machine:
|
||||
return False
|
||||
elif machine and self.sys_machine not in machine:
|
||||
return False
|
||||
|
||||
return True
|
||||
# Home Assistant
|
||||
version: Optional[AwesomeVersion] = config.get(ATTR_HOMEASSISTANT)
|
||||
try:
|
||||
return self.sys_homeassistant.version >= version
|
||||
except (AwesomeVersionException, TypeError):
|
||||
return True
|
||||
|
||||
def _image(self, config) -> str:
|
||||
"""Generate image name from data."""
|
||||
@@ -673,7 +640,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
"""Uninstall this add-on."""
|
||||
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."""
|
||||
return self.sys_addons.update(self.slug, backup=backup)
|
||||
|
||||
|
@@ -3,7 +3,7 @@ import hashlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import Any, Union
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -293,7 +293,7 @@ class UiOptions(CoreSysAttributes):
|
||||
multiple: bool = False,
|
||||
) -> None:
|
||||
"""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:
|
||||
|
@@ -45,7 +45,6 @@ def rating_security(addon: AddonModel) -> int:
|
||||
privilege in addon.privileged
|
||||
for privilege in (
|
||||
Capabilities.NET_ADMIN,
|
||||
Capabilities.NET_RAW,
|
||||
Capabilities.SYS_ADMIN,
|
||||
Capabilities.SYS_RAWIO,
|
||||
Capabilities.SYS_PTRACE,
|
||||
@@ -71,10 +70,6 @@ def rating_security(addon: AddonModel) -> int:
|
||||
if addon.host_pid:
|
||||
rating += -2
|
||||
|
||||
# UTS host namespace allows to set hostname only with SYS_ADMIN
|
||||
if addon.host_uts and Capabilities.SYS_ADMIN in addon.privileged:
|
||||
rating += -1
|
||||
|
||||
# Docker Access & full Access
|
||||
if addon.access_docker_api or addon.with_full_access:
|
||||
rating = 1
|
||||
|
@@ -7,6 +7,8 @@ import uuid
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from supervisor.addons.const import AddonBackupMode
|
||||
|
||||
from ..const import (
|
||||
ARCH_ALL,
|
||||
ATTR_ACCESS_TOKEN,
|
||||
@@ -41,7 +43,6 @@ from ..const import (
|
||||
ATTR_HOST_IPC,
|
||||
ATTR_HOST_NETWORK,
|
||||
ATTR_HOST_PID,
|
||||
ATTR_HOST_UTS,
|
||||
ATTR_IMAGE,
|
||||
ATTR_INGRESS,
|
||||
ATTR_INGRESS_ENTRY,
|
||||
@@ -109,7 +110,7 @@ from ..validate import (
|
||||
uuid_match,
|
||||
version_tag,
|
||||
)
|
||||
from .const import ATTR_BACKUP, ATTR_CODENOTARY, RE_SLUG, AddonBackupMode
|
||||
from .const import ATTR_BACKUP, ATTR_CODENOTARY
|
||||
from .options import RE_SCHEMA_ELEMENT
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@@ -130,7 +131,6 @@ RE_MACHINE = re.compile(
|
||||
r"|generic-x86-64"
|
||||
r"|odroid-c2"
|
||||
r"|odroid-c4"
|
||||
r"|odroid-m1"
|
||||
r"|odroid-n2"
|
||||
r"|odroid-xu"
|
||||
r"|qemuarm-64"
|
||||
@@ -147,8 +147,6 @@ RE_MACHINE = re.compile(
|
||||
r")$"
|
||||
)
|
||||
|
||||
RE_SLUG_FIELD = re.compile(r"^" + RE_SLUG + r"$")
|
||||
|
||||
|
||||
def _warn_addon_config(config: dict[str, Any]):
|
||||
"""Warn about miss configs."""
|
||||
@@ -254,7 +252,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_NAME): str,
|
||||
vol.Required(ATTR_VERSION): version_tag,
|
||||
vol.Required(ATTR_SLUG): vol.Match(RE_SLUG_FIELD),
|
||||
vol.Required(ATTR_SLUG): str,
|
||||
vol.Required(ATTR_DESCRIPTON): str,
|
||||
vol.Required(ATTR_ARCH): [vol.In(ARCH_ALL)],
|
||||
vol.Optional(ATTR_MACHINE): vol.All([vol.Match(RE_MACHINE)], vol.Unique()),
|
||||
@@ -287,7 +285,6 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
vol.Optional(ATTR_HOST_NETWORK, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_HOST_PID, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_HOST_IPC, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_HOST_UTS, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_HOST_DBUS, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_DEVICES): [str],
|
||||
vol.Optional(ATTR_UDEV, default=False): vol.Boolean(),
|
||||
@@ -356,9 +353,8 @@ SCHEMA_ADDON_CONFIG = vol.All(
|
||||
# pylint: disable=no-value-for-parameter
|
||||
SCHEMA_BUILD_CONFIG = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_BUILD_FROM, default=dict): vol.Any(
|
||||
vol.Match(RE_DOCKER_IMAGE_BUILD),
|
||||
vol.Schema({vol.In(ARCH_ALL): vol.Match(RE_DOCKER_IMAGE_BUILD)}),
|
||||
vol.Optional(ATTR_BUILD_FROM, default=dict): vol.Schema(
|
||||
{vol.In(ARCH_ALL): vol.Match(RE_DOCKER_IMAGE_BUILD)}
|
||||
),
|
||||
vol.Optional(ATTR_SQUASH, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_ARGS, default=dict): vol.Schema({str: str}),
|
||||
|
@@ -1,14 +1,15 @@
|
||||
"""Init file for Supervisor RESTful API."""
|
||||
from functools import partial
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, Optional
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from ..const import AddonState
|
||||
from supervisor.api.utils import api_process
|
||||
from supervisor.const import AddonState
|
||||
from supervisor.exceptions import APIAddonNotInstalled
|
||||
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..exceptions import APIAddonNotInstalled
|
||||
from .addons import APIAddons
|
||||
from .audio import APIAudio
|
||||
from .auth import APIAuth
|
||||
@@ -23,7 +24,6 @@ from .host import APIHost
|
||||
from .ingress import APIIngress
|
||||
from .jobs import APIJobs
|
||||
from .middleware.security import SecurityMiddleware
|
||||
from .mounts import APIMounts
|
||||
from .multicast import APIMulticast
|
||||
from .network import APINetwork
|
||||
from .observer import APIObserver
|
||||
@@ -35,7 +35,6 @@ from .security import APISecurity
|
||||
from .services import APIServices
|
||||
from .store import APIStore
|
||||
from .supervisor import APISupervisor
|
||||
from .utils import api_process
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,10 +53,8 @@ class RestAPI(CoreSysAttributes):
|
||||
self.webapp: web.Application = web.Application(
|
||||
client_max_size=MAX_CLIENT_SIZE,
|
||||
middlewares=[
|
||||
self.security.block_bad_requests,
|
||||
self.security.system_validation,
|
||||
self.security.token_validation,
|
||||
self.security.core_proxy,
|
||||
],
|
||||
handler_args={
|
||||
"max_line_size": MAX_LINE_SIZE,
|
||||
@@ -67,7 +64,7 @@ class RestAPI(CoreSysAttributes):
|
||||
|
||||
# service stuff
|
||||
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:
|
||||
"""Register REST API Calls."""
|
||||
@@ -82,21 +79,20 @@ class RestAPI(CoreSysAttributes):
|
||||
self._register_hardware()
|
||||
self._register_homeassistant()
|
||||
self._register_host()
|
||||
self._register_jobs()
|
||||
self._register_root()
|
||||
self._register_ingress()
|
||||
self._register_mounts()
|
||||
self._register_multicast()
|
||||
self._register_network()
|
||||
self._register_observer()
|
||||
self._register_os()
|
||||
self._register_jobs()
|
||||
self._register_panel()
|
||||
self._register_proxy()
|
||||
self._register_resolution()
|
||||
self._register_root()
|
||||
self._register_security()
|
||||
self._register_services()
|
||||
self._register_store()
|
||||
self._register_supervisor()
|
||||
self._register_store()
|
||||
self._register_security()
|
||||
|
||||
await self.start()
|
||||
|
||||
@@ -108,36 +104,16 @@ class RestAPI(CoreSysAttributes):
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.get("/host/info", api_host.info),
|
||||
web.get("/host/logs", api_host.advanced_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.get("/host/logs", api_host.logs),
|
||||
web.post("/host/reboot", api_host.reboot),
|
||||
web.post("/host/shutdown", api_host.shutdown),
|
||||
web.post("/host/reload", api_host.reload),
|
||||
web.post("/host/options", api_host.options),
|
||||
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),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -183,15 +159,6 @@ class RestAPI(CoreSysAttributes):
|
||||
]
|
||||
)
|
||||
|
||||
# Boards endpoints
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.get("/os/boards/yellow", api_os.boards_yellow_info),
|
||||
web.post("/os/boards/yellow", api_os.boards_yellow_options),
|
||||
web.get("/os/boards/{board}", api_os.boards_other_info),
|
||||
]
|
||||
)
|
||||
|
||||
def _register_security(self) -> None:
|
||||
"""Register Security functions."""
|
||||
api_security = APISecurity()
|
||||
@@ -311,10 +278,6 @@ class RestAPI(CoreSysAttributes):
|
||||
"/resolution/issue/{issue}",
|
||||
api_resolution.dismiss_issue,
|
||||
),
|
||||
web.get(
|
||||
"/resolution/issue/{issue}/suggestions",
|
||||
api_resolution.suggestions_for_issue,
|
||||
),
|
||||
web.post("/resolution/healthcheck", api_resolution.healthcheck),
|
||||
]
|
||||
)
|
||||
@@ -482,13 +445,11 @@ class RestAPI(CoreSysAttributes):
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
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/new/full", api_backups.backup_full),
|
||||
web.post("/backups/new/partial", api_backups.backup_partial),
|
||||
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.post("/backups/{slug}/restore/full", api_backups.restore_full),
|
||||
web.post(
|
||||
@@ -548,8 +509,6 @@ class RestAPI(CoreSysAttributes):
|
||||
"""Register Audio functions."""
|
||||
api_audio = APIAudio()
|
||||
api_audio.coresys = self.coresys
|
||||
api_host = APIHost()
|
||||
api_host.coresys = self.coresys
|
||||
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
@@ -568,21 +527,6 @@ class RestAPI(CoreSysAttributes):
|
||||
]
|
||||
)
|
||||
|
||||
def _register_mounts(self) -> None:
|
||||
"""Register mounts endpoints."""
|
||||
api_mounts = APIMounts()
|
||||
api_mounts.coresys = self.coresys
|
||||
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.get("/mounts", api_mounts.info),
|
||||
web.post("/mounts", api_mounts.create_mount),
|
||||
web.put("/mounts/{mount}", api_mounts.update_mount),
|
||||
web.delete("/mounts/{mount}", api_mounts.delete_mount),
|
||||
web.post("/mounts/{mount}/reload", api_mounts.reload_mount),
|
||||
]
|
||||
)
|
||||
|
||||
def _register_store(self) -> None:
|
||||
"""Register store endpoints."""
|
||||
api_store = APIStore()
|
||||
|
@@ -1,8 +1,7 @@
|
||||
"""Init file for Supervisor Home Assistant RESTful API."""
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, Awaitable
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
@@ -46,7 +45,6 @@ from ..const import (
|
||||
ATTR_HOST_IPC,
|
||||
ATTR_HOST_NETWORK,
|
||||
ATTR_HOST_PID,
|
||||
ATTR_HOST_UTS,
|
||||
ATTR_HOSTNAME,
|
||||
ATTR_ICON,
|
||||
ATTR_INGRESS,
|
||||
@@ -217,7 +215,6 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_HOST_NETWORK: addon.host_network,
|
||||
ATTR_HOST_PID: addon.host_pid,
|
||||
ATTR_HOST_IPC: addon.host_ipc,
|
||||
ATTR_HOST_UTS: addon.host_uts,
|
||||
ATTR_HOST_DBUS: addon.host_dbus,
|
||||
ATTR_PRIVILEGED: addon.privileged,
|
||||
ATTR_FULL_ACCESS: addon.with_full_access,
|
||||
|
@@ -1,8 +1,7 @@
|
||||
"""Init file for Supervisor Audio RESTful API."""
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, Awaitable
|
||||
|
||||
from aiohttp import web
|
||||
import attr
|
||||
|
@@ -9,14 +9,13 @@ from aiohttp import web
|
||||
from aiohttp.hdrs import CONTENT_DISPOSITION
|
||||
import voluptuous as vol
|
||||
|
||||
from ..backups.validate import ALL_FOLDERS, FOLDER_HOMEASSISTANT, days_until_stale
|
||||
from ..backups.validate import ALL_FOLDERS, FOLDER_HOMEASSISTANT
|
||||
from ..const import (
|
||||
ATTR_ADDONS,
|
||||
ATTR_BACKUPS,
|
||||
ATTR_COMPRESSED,
|
||||
ATTR_CONTENT,
|
||||
ATTR_DATE,
|
||||
ATTR_DAYS_UNTIL_STALE,
|
||||
ATTR_FOLDERS,
|
||||
ATTR_HOMEASSISTANT,
|
||||
ATTR_NAME,
|
||||
@@ -25,7 +24,6 @@ from ..const import (
|
||||
ATTR_REPOSITORIES,
|
||||
ATTR_SIZE,
|
||||
ATTR_SLUG,
|
||||
ATTR_SUPERVISOR_VERSION,
|
||||
ATTR_TYPE,
|
||||
ATTR_VERSION,
|
||||
)
|
||||
@@ -70,12 +68,6 @@ SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend(
|
||||
}
|
||||
)
|
||||
|
||||
SCHEMA_OPTIONS = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_DAYS_UNTIL_STALE): days_until_stale,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class APIBackups(CoreSysAttributes):
|
||||
"""Handle RESTful API for backups functions."""
|
||||
@@ -87,30 +79,27 @@ class APIBackups(CoreSysAttributes):
|
||||
raise APIError("Backup does not exist")
|
||||
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
|
||||
async def list(self, request):
|
||||
"""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_COMPRESSED: backup.compressed,
|
||||
ATTR_CONTENT: {
|
||||
ATTR_HOMEASSISTANT: backup.homeassistant_version is not None,
|
||||
ATTR_ADDONS: backup.addon_list,
|
||||
ATTR_FOLDERS: backup.folders,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if request.path == "/snapshots":
|
||||
# Kept for backwards compability
|
||||
@@ -118,24 +107,6 @@ class APIBackups(CoreSysAttributes):
|
||||
|
||||
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
|
||||
async def reload(self, request):
|
||||
"""Reload backup list."""
|
||||
@@ -143,7 +114,7 @@ class APIBackups(CoreSysAttributes):
|
||||
return True
|
||||
|
||||
@api_process
|
||||
async def backup_info(self, request):
|
||||
async def info(self, request):
|
||||
"""Return backup info."""
|
||||
backup = self._extract_slug(request)
|
||||
|
||||
@@ -166,7 +137,6 @@ class APIBackups(CoreSysAttributes):
|
||||
ATTR_SIZE: backup.size,
|
||||
ATTR_COMPRESSED: backup.compressed,
|
||||
ATTR_PROTECTED: backup.protected,
|
||||
ATTR_SUPERVISOR_VERSION: backup.supervisor_version,
|
||||
ATTR_HOMEASSISTANT: backup.homeassistant_version,
|
||||
ATTR_ADDONS: data_addons,
|
||||
ATTR_REPOSITORIES: backup.repositories,
|
||||
|
@@ -9,47 +9,31 @@ CONTENT_TYPE_URL = "application/x-www-form-urlencoded"
|
||||
|
||||
COOKIE_INGRESS = "ingress_session"
|
||||
|
||||
ATTR_AGENT_VERSION = "agent_version"
|
||||
HEADER_TOKEN_OLD = "X-Hassio-Key"
|
||||
HEADER_TOKEN = "X-Supervisor-Token"
|
||||
|
||||
ATTR_APPARMOR_VERSION = "apparmor_version"
|
||||
ATTR_ATTRIBUTES = "attributes"
|
||||
ATTR_AGENT_VERSION = "agent_version"
|
||||
ATTR_AVAILABLE_UPDATES = "available_updates"
|
||||
ATTR_BOOT_TIMESTAMP = "boot_timestamp"
|
||||
ATTR_BOOTS = "boots"
|
||||
ATTR_BROADCAST_LLMNR = "broadcast_llmnr"
|
||||
ATTR_BROADCAST_MDNS = "broadcast_mdns"
|
||||
ATTR_BY_ID = "by_id"
|
||||
ATTR_CHILDREN = "children"
|
||||
ATTR_CONNECTION_BUS = "connection_bus"
|
||||
ATTR_DATA_DISK = "data_disk"
|
||||
ATTR_DEVICE = "device"
|
||||
ATTR_DEV_PATH = "dev_path"
|
||||
ATTR_DISK_LED = "disk_led"
|
||||
ATTR_DISKS = "disks"
|
||||
ATTR_DRIVES = "drives"
|
||||
ATTR_DT_SYNCHRONIZED = "dt_synchronized"
|
||||
ATTR_DT_UTC = "dt_utc"
|
||||
ATTR_EJECTABLE = "ejectable"
|
||||
ATTR_FALLBACK = "fallback"
|
||||
ATTR_FILESYSTEMS = "filesystems"
|
||||
ATTR_HEARTBEAT_LED = "heartbeat_led"
|
||||
ATTR_IDENTIFIERS = "identifiers"
|
||||
ATTR_LLMNR = "llmnr"
|
||||
ATTR_LLMNR_HOSTNAME = "llmnr_hostname"
|
||||
ATTR_MDNS = "mdns"
|
||||
ATTR_MODEL = "model"
|
||||
ATTR_MOUNTS = "mounts"
|
||||
ATTR_MOUNT_POINTS = "mount_points"
|
||||
ATTR_PANEL_PATH = "panel_path"
|
||||
ATTR_POWER_LED = "power_led"
|
||||
ATTR_REMOVABLE = "removable"
|
||||
ATTR_REVISION = "revision"
|
||||
ATTR_SEAT = "seat"
|
||||
ATTR_SIGNED = "signed"
|
||||
ATTR_STARTUP_TIME = "startup_time"
|
||||
ATTR_SUBSYSTEM = "subsystem"
|
||||
ATTR_SYSFS = "sysfs"
|
||||
ATTR_TIME_DETECTED = "time_detected"
|
||||
ATTR_UPDATE_TYPE = "update_type"
|
||||
ATTR_USE_NTP = "use_ntp"
|
||||
ATTR_USAGE = "usage"
|
||||
ATTR_VENDOR = "vendor"
|
||||
ATTR_BY_ID = "by_id"
|
||||
ATTR_SUBSYSTEM = "subsystem"
|
||||
ATTR_SYSFS = "sysfs"
|
||||
ATTR_DEV_PATH = "dev_path"
|
||||
ATTR_ATTRIBUTES = "attributes"
|
||||
ATTR_CHILDREN = "children"
|
||||
|
@@ -1,8 +1,7 @@
|
||||
"""Init file for Supervisor DNS RESTful API."""
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, Awaitable
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
@@ -4,41 +4,16 @@ from typing import Any
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from ..const import (
|
||||
ATTR_AUDIO,
|
||||
ATTR_DEVICES,
|
||||
ATTR_ID,
|
||||
ATTR_INPUT,
|
||||
ATTR_NAME,
|
||||
ATTR_OUTPUT,
|
||||
ATTR_SERIAL,
|
||||
ATTR_SIZE,
|
||||
ATTR_SYSTEM,
|
||||
)
|
||||
from ..const import ATTR_AUDIO, ATTR_DEVICES, ATTR_INPUT, ATTR_NAME, ATTR_OUTPUT
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..dbus.udisks2 import UDisks2
|
||||
from ..dbus.udisks2.block import UDisks2Block
|
||||
from ..dbus.udisks2.drive import UDisks2Drive
|
||||
from ..hardware.data import Device
|
||||
from .const import (
|
||||
ATTR_ATTRIBUTES,
|
||||
ATTR_BY_ID,
|
||||
ATTR_CHILDREN,
|
||||
ATTR_CONNECTION_BUS,
|
||||
ATTR_DEV_PATH,
|
||||
ATTR_DEVICE,
|
||||
ATTR_DRIVES,
|
||||
ATTR_EJECTABLE,
|
||||
ATTR_FILESYSTEMS,
|
||||
ATTR_MODEL,
|
||||
ATTR_MOUNT_POINTS,
|
||||
ATTR_REMOVABLE,
|
||||
ATTR_REVISION,
|
||||
ATTR_SEAT,
|
||||
ATTR_SUBSYSTEM,
|
||||
ATTR_SYSFS,
|
||||
ATTR_TIME_DETECTED,
|
||||
ATTR_VENDOR,
|
||||
)
|
||||
from .utils import api_process
|
||||
|
||||
@@ -46,7 +21,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def device_struct(device: Device) -> dict[str, Any]:
|
||||
"""Return a dict with information of a interface to be used in the API."""
|
||||
"""Return a dict with information of a interface to be used in th API."""
|
||||
return {
|
||||
ATTR_NAME: device.name,
|
||||
ATTR_SYSFS: device.sysfs,
|
||||
@@ -58,42 +33,6 @@ def device_struct(device: Device) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def filesystem_struct(fs_block: UDisks2Block) -> dict[str, Any]:
|
||||
"""Return a dict with information of a filesystem block device to be used in the API."""
|
||||
return {
|
||||
ATTR_DEVICE: str(fs_block.device),
|
||||
ATTR_ID: fs_block.id,
|
||||
ATTR_SIZE: fs_block.size,
|
||||
ATTR_NAME: fs_block.id_label,
|
||||
ATTR_SYSTEM: fs_block.hint_system,
|
||||
ATTR_MOUNT_POINTS: [
|
||||
str(mount_point) for mount_point in fs_block.filesystem.mount_points
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def drive_struct(udisks2: UDisks2, drive: UDisks2Drive) -> dict[str, Any]:
|
||||
"""Return a dict with information of a disk to be used in the API."""
|
||||
return {
|
||||
ATTR_VENDOR: drive.vendor,
|
||||
ATTR_MODEL: drive.model,
|
||||
ATTR_REVISION: drive.revision,
|
||||
ATTR_SERIAL: drive.serial,
|
||||
ATTR_ID: drive.id,
|
||||
ATTR_SIZE: drive.size,
|
||||
ATTR_TIME_DETECTED: drive.time_detected.isoformat(),
|
||||
ATTR_CONNECTION_BUS: drive.connection_bus,
|
||||
ATTR_SEAT: drive.seat,
|
||||
ATTR_REMOVABLE: drive.removable,
|
||||
ATTR_EJECTABLE: drive.ejectable,
|
||||
ATTR_FILESYSTEMS: [
|
||||
filesystem_struct(block)
|
||||
for block in udisks2.block_devices
|
||||
if block.filesystem and block.drive == drive.object_path
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class APIHardware(CoreSysAttributes):
|
||||
"""Handle RESTful API for hardware functions."""
|
||||
|
||||
@@ -103,11 +42,7 @@ class APIHardware(CoreSysAttributes):
|
||||
return {
|
||||
ATTR_DEVICES: [
|
||||
device_struct(device) for device in self.sys_hardware.devices
|
||||
],
|
||||
ATTR_DRIVES: [
|
||||
drive_struct(self.sys_dbus.udisks2, drive)
|
||||
for drive in self.sys_dbus.udisks2.drives
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
@api_process
|
||||
|
@@ -1,8 +1,7 @@
|
||||
"""Init file for Supervisor Home Assistant RESTful API."""
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, Awaitable
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
@@ -1,12 +1,9 @@
|
||||
"""Init file for Supervisor host RESTful API."""
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
from typing import Awaitable
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.hdrs import ACCEPT, RANGE
|
||||
import voluptuous as vol
|
||||
from voluptuous.error import CoerceInvalid
|
||||
|
||||
from ..const import (
|
||||
ATTR_CHASSIS,
|
||||
@@ -27,30 +24,22 @@ from ..const import (
|
||||
ATTR_TIMEZONE,
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import APIError, HostLogError
|
||||
from ..host.const import PARAM_BOOT_ID, PARAM_FOLLOW, PARAM_SYSLOG_IDENTIFIER
|
||||
from .const import (
|
||||
ATTR_AGENT_VERSION,
|
||||
ATTR_APPARMOR_VERSION,
|
||||
ATTR_BOOT_TIMESTAMP,
|
||||
ATTR_BOOTS,
|
||||
ATTR_BROADCAST_LLMNR,
|
||||
ATTR_BROADCAST_MDNS,
|
||||
ATTR_DT_SYNCHRONIZED,
|
||||
ATTR_DT_UTC,
|
||||
ATTR_IDENTIFIERS,
|
||||
ATTR_LLMNR_HOSTNAME,
|
||||
ATTR_STARTUP_TIME,
|
||||
ATTR_USE_NTP,
|
||||
CONTENT_TYPE_TEXT,
|
||||
CONTENT_TYPE_BINARY,
|
||||
)
|
||||
from .utils import api_process, api_validate
|
||||
from .utils import api_process, api_process_raw, api_validate
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
IDENTIFIER = "identifier"
|
||||
BOOTID = "bootid"
|
||||
DEFAULT_RANGE = 100
|
||||
SERVICE = "service"
|
||||
|
||||
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): str})
|
||||
|
||||
@@ -128,75 +117,30 @@ class APIHost(CoreSysAttributes):
|
||||
return {ATTR_SERVICES: services}
|
||||
|
||||
@api_process
|
||||
async def list_boots(self, _: web.Request):
|
||||
"""Return a list of boot IDs."""
|
||||
boot_ids = await self.sys_host.logs.get_boot_ids()
|
||||
return {
|
||||
ATTR_BOOTS: {
|
||||
str(1 + i - len(boot_ids)): boot_id
|
||||
for i, boot_id in enumerate(boot_ids)
|
||||
}
|
||||
}
|
||||
def service_start(self, request):
|
||||
"""Start a service."""
|
||||
unit = request.match_info.get(SERVICE)
|
||||
return asyncio.shield(self.sys_host.services.start(unit))
|
||||
|
||||
@api_process
|
||||
async def list_identifiers(self, _: web.Request):
|
||||
"""Return a list of syslog identifiers."""
|
||||
return {ATTR_IDENTIFIERS: await self.sys_host.logs.get_identifiers()}
|
||||
|
||||
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
|
||||
def service_stop(self, request):
|
||||
"""Stop a service."""
|
||||
unit = request.match_info.get(SERVICE)
|
||||
return asyncio.shield(self.sys_host.services.stop(unit))
|
||||
|
||||
@api_process
|
||||
async def advanced_logs(
|
||||
self, request: web.Request, identifier: str | None = None, follow: bool = False
|
||||
) -> web.StreamResponse:
|
||||
"""Return systemd-journald logs."""
|
||||
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
|
||||
def service_reload(self, request):
|
||||
"""Reload a service."""
|
||||
unit = request.match_info.get(SERVICE)
|
||||
return asyncio.shield(self.sys_host.services.reload(unit))
|
||||
|
||||
if BOOTID in request.match_info:
|
||||
params[PARAM_BOOT_ID] = await self._get_boot_id(
|
||||
request.match_info.get(BOOTID)
|
||||
)
|
||||
if follow:
|
||||
params[PARAM_FOLLOW] = ""
|
||||
@api_process
|
||||
def service_restart(self, request):
|
||||
"""Restart a service."""
|
||||
unit = request.match_info.get(SERVICE)
|
||||
return asyncio.shield(self.sys_host.services.restart(unit))
|
||||
|
||||
if ACCEPT in request.headers and request.headers[ACCEPT] not in [
|
||||
CONTENT_TYPE_TEXT,
|
||||
"*/*",
|
||||
]:
|
||||
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
|
||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
||||
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
||||
"""Return host kernel logs."""
|
||||
return self.sys_host.info.get_dmesg()
|
||||
|
@@ -2,7 +2,7 @@
|
||||
import asyncio
|
||||
from ipaddress import ip_address
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, Union
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import ClientTimeout, hdrs, web
|
||||
@@ -22,11 +22,9 @@ from ..const import (
|
||||
ATTR_PANELS,
|
||||
ATTR_SESSION,
|
||||
ATTR_TITLE,
|
||||
HEADER_TOKEN,
|
||||
HEADER_TOKEN_OLD,
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from .const import COOKIE_INGRESS
|
||||
from .const import COOKIE_INGRESS, HEADER_TOKEN, HEADER_TOKEN_OLD
|
||||
from .utils import api_process, api_validate, require_home_assistant
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@@ -85,9 +83,10 @@ class APIIngress(CoreSysAttributes):
|
||||
_LOGGER.warning("No valid ingress session %s", data[ATTR_SESSION])
|
||||
raise HTTPUnauthorized()
|
||||
|
||||
@require_home_assistant
|
||||
async def handler(
|
||||
self, request: web.Request
|
||||
) -> web.Response | web.StreamResponse | web.WebSocketResponse:
|
||||
) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]:
|
||||
"""Route data to Supervisor ingress service."""
|
||||
|
||||
# Check Ingress Session
|
||||
@@ -158,7 +157,7 @@ class APIIngress(CoreSysAttributes):
|
||||
|
||||
async def _handle_request(
|
||||
self, request: web.Request, addon: Addon, path: str
|
||||
) -> web.Response | web.StreamResponse:
|
||||
) -> Union[web.Response, web.StreamResponse]:
|
||||
"""Ingress route for request."""
|
||||
url = self._create_url(addon, path)
|
||||
source_header = _init_header(request, addon)
|
||||
@@ -181,7 +180,6 @@ class APIIngress(CoreSysAttributes):
|
||||
allow_redirects=False,
|
||||
data=data,
|
||||
timeout=ClientTimeout(total=None),
|
||||
skip_auto_headers={hdrs.CONTENT_TYPE},
|
||||
) as result:
|
||||
headers = _response_header(result)
|
||||
|
||||
@@ -218,7 +216,9 @@ class APIIngress(CoreSysAttributes):
|
||||
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."""
|
||||
headers = {}
|
||||
|
||||
|
@@ -1,14 +1,10 @@
|
||||
"""Handle security part of this API."""
|
||||
import logging
|
||||
import re
|
||||
from typing import Final
|
||||
from urllib.parse import unquote
|
||||
|
||||
from aiohttp.web import Request, RequestHandler, Response, middleware
|
||||
from aiohttp.web_exceptions import HTTPBadRequest, HTTPForbidden, HTTPUnauthorized
|
||||
from awesomeversion import AwesomeVersion
|
||||
from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized
|
||||
|
||||
from ...addons.const import RE_SLUG
|
||||
from ...const import (
|
||||
REQUEST_FROM,
|
||||
ROLE_ADMIN,
|
||||
@@ -22,22 +18,11 @@ from ...coresys import CoreSys, CoreSysAttributes
|
||||
from ..utils import api_return_error, excract_supervisor_token
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
_CORE_VERSION: Final = AwesomeVersion("2023.3.4")
|
||||
|
||||
# fmt: off
|
||||
|
||||
_CORE_FRONTEND_PATHS: Final = (
|
||||
r"|/app/.*\.(?:js|gz|json|map)"
|
||||
r"|/(store/)?addons/" + RE_SLUG + r"/(logo|icon)"
|
||||
)
|
||||
|
||||
CORE_FRONTEND: Final = re.compile(
|
||||
r"^(?:" + _CORE_FRONTEND_PATHS + r")$"
|
||||
)
|
||||
|
||||
|
||||
# Block Anytime
|
||||
BLACKLIST: Final = re.compile(
|
||||
BLACKLIST = re.compile(
|
||||
r"^(?:"
|
||||
r"|/homeassistant/api/hassio/.*"
|
||||
r"|/core/api/hassio/.*"
|
||||
@@ -45,27 +30,25 @@ BLACKLIST: Final = re.compile(
|
||||
)
|
||||
|
||||
# Free to call or have own security concepts
|
||||
NO_SECURITY_CHECK: Final = re.compile(
|
||||
NO_SECURITY_CHECK = re.compile(
|
||||
r"^(?:"
|
||||
r"|/homeassistant/api/.*"
|
||||
r"|/homeassistant/websocket"
|
||||
r"|/core/api/.*"
|
||||
r"|/core/websocket"
|
||||
r"|/supervisor/ping"
|
||||
r"|/ingress/[-_A-Za-z0-9]+/.*"
|
||||
+ _CORE_FRONTEND_PATHS
|
||||
+ r")$"
|
||||
r")$"
|
||||
)
|
||||
|
||||
# Observer allow API calls
|
||||
OBSERVER_CHECK: Final = re.compile(
|
||||
OBSERVER_CHECK = re.compile(
|
||||
r"^(?:"
|
||||
r"|/.+/info"
|
||||
r")$"
|
||||
)
|
||||
|
||||
# Can called by every add-on
|
||||
ADDONS_API_BYPASS: Final = re.compile(
|
||||
ADDONS_API_BYPASS = re.compile(
|
||||
r"^(?:"
|
||||
r"|/addons/self/(?!security|update)[^/]+"
|
||||
r"|/addons/self/options/config"
|
||||
@@ -77,7 +60,7 @@ ADDONS_API_BYPASS: Final = re.compile(
|
||||
)
|
||||
|
||||
# Policy role add-on API access
|
||||
ADDONS_ROLE_ACCESS: dict[str, re.Pattern] = {
|
||||
ADDONS_ROLE_ACCESS = {
|
||||
ROLE_DEFAULT: re.compile(
|
||||
r"^(?:"
|
||||
r"|/.+/info"
|
||||
@@ -99,7 +82,7 @@ ADDONS_ROLE_ACCESS: dict[str, re.Pattern] = {
|
||||
ROLE_MANAGER: re.compile(
|
||||
r"^(?:"
|
||||
r"|/.+/info"
|
||||
r"|/addons(?:/" + RE_SLUG + r"/(?!security).+|/reload)?"
|
||||
r"|/addons(?:/[^/]+/(?!security).+|/reload)?"
|
||||
r"|/audio/.+"
|
||||
r"|/auth/cache"
|
||||
r"|/cli/.+"
|
||||
@@ -128,26 +111,6 @@ ADDONS_ROLE_ACCESS: dict[str, re.Pattern] = {
|
||||
),
|
||||
}
|
||||
|
||||
FILTERS: Final = re.compile(
|
||||
r"(?:"
|
||||
|
||||
# Common exploits
|
||||
r"proc/self/environ"
|
||||
r"|(<|%3C).*script.*(>|%3E)"
|
||||
|
||||
# File Injections
|
||||
r"|(\.\.//?)+" # ../../anywhere
|
||||
r"|[a-zA-Z0-9_]=/([a-z0-9_.]//?)+" # .html?v=/.//test
|
||||
|
||||
# SQL Injections
|
||||
r"|union.*select.*\("
|
||||
r"|union.*all.*select.*"
|
||||
r"|concat.*\("
|
||||
|
||||
r")",
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
@@ -158,32 +121,6 @@ class SecurityMiddleware(CoreSysAttributes):
|
||||
"""Initialize security middleware."""
|
||||
self.coresys: CoreSys = coresys
|
||||
|
||||
def _recursive_unquote(self, value: str) -> str:
|
||||
"""Handle values that are encoded multiple times."""
|
||||
if (unquoted := unquote(value)) != value:
|
||||
unquoted = self._recursive_unquote(unquoted)
|
||||
return unquoted
|
||||
|
||||
@middleware
|
||||
async def block_bad_requests(
|
||||
self, request: Request, handler: RequestHandler
|
||||
) -> Response:
|
||||
"""Process request and tblock commonly known exploit attempts."""
|
||||
if FILTERS.search(self._recursive_unquote(request.path)):
|
||||
_LOGGER.warning(
|
||||
"Filtered a potential harmful request to: %s", request.raw_path
|
||||
)
|
||||
raise HTTPBadRequest
|
||||
|
||||
if FILTERS.search(self._recursive_unquote(request.query_string)):
|
||||
_LOGGER.warning(
|
||||
"Filtered a request with a potential harmful query string: %s",
|
||||
request.raw_path,
|
||||
)
|
||||
raise HTTPBadRequest
|
||||
|
||||
return await handler(request)
|
||||
|
||||
@middleware
|
||||
async def system_validation(
|
||||
self, request: Request, handler: RequestHandler
|
||||
@@ -216,7 +153,6 @@ class SecurityMiddleware(CoreSysAttributes):
|
||||
# Ignore security check
|
||||
if NO_SECURITY_CHECK.match(request.path):
|
||||
_LOGGER.debug("Passthrough %s", request.path)
|
||||
request[REQUEST_FROM] = None
|
||||
return await handler(request)
|
||||
|
||||
# Not token
|
||||
@@ -269,45 +205,3 @@ class SecurityMiddleware(CoreSysAttributes):
|
||||
|
||||
_LOGGER.error("Invalid token for access %s", request.path)
|
||||
raise HTTPForbidden()
|
||||
|
||||
@middleware
|
||||
async def core_proxy(self, request: Request, handler: RequestHandler) -> Response:
|
||||
"""Validate user from Core API proxy."""
|
||||
if (
|
||||
request[REQUEST_FROM] != self.sys_homeassistant
|
||||
or self.sys_homeassistant.version >= _CORE_VERSION
|
||||
):
|
||||
return await handler(request)
|
||||
|
||||
authorization_index: int | None = None
|
||||
content_type_index: int | None = None
|
||||
user_request: bool = False
|
||||
admin_request: bool = False
|
||||
ingress_request: bool = False
|
||||
|
||||
for idx, (key, value) in enumerate(request.raw_headers):
|
||||
if key in (b"Authorization", b"X-Hassio-Key"):
|
||||
authorization_index = idx
|
||||
elif key == b"Content-Type":
|
||||
content_type_index = idx
|
||||
elif key == b"X-Hass-User-ID":
|
||||
user_request = True
|
||||
elif key == b"X-Hass-Is-Admin":
|
||||
admin_request = value == b"1"
|
||||
elif key == b"X-Ingress-Path":
|
||||
ingress_request = True
|
||||
|
||||
if (user_request or admin_request) and not ingress_request:
|
||||
return await handler(request)
|
||||
|
||||
is_proxy_request = (
|
||||
authorization_index is not None
|
||||
and content_type_index is not None
|
||||
and content_type_index - authorization_index == 1
|
||||
)
|
||||
|
||||
if (
|
||||
not CORE_FRONTEND.match(request.path) and is_proxy_request
|
||||
) or ingress_request:
|
||||
raise HTTPBadRequest()
|
||||
return await handler(request)
|
||||
|
@@ -1,75 +0,0 @@
|
||||
"""Inits file for supervisor mounts REST API."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from ..const import ATTR_NAME, ATTR_STATE
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import APIError
|
||||
from ..mounts.mount import Mount
|
||||
from ..mounts.validate import SCHEMA_MOUNT_CONFIG
|
||||
from .const import ATTR_MOUNTS
|
||||
from .utils import api_process, api_validate
|
||||
|
||||
|
||||
class APIMounts(CoreSysAttributes):
|
||||
"""Handle REST API for mounting options."""
|
||||
|
||||
@api_process
|
||||
async def info(self, request: web.Request) -> dict[str, Any]:
|
||||
"""Return MountManager info."""
|
||||
return {
|
||||
ATTR_MOUNTS: [
|
||||
mount.to_dict() | {ATTR_STATE: mount.state}
|
||||
for mount in self.sys_mounts.mounts
|
||||
]
|
||||
}
|
||||
|
||||
@api_process
|
||||
async def create_mount(self, request: web.Request) -> None:
|
||||
"""Create a new mount in supervisor."""
|
||||
body = await api_validate(SCHEMA_MOUNT_CONFIG, request)
|
||||
|
||||
if body[ATTR_NAME] in self.sys_mounts:
|
||||
raise APIError(f"A mount already exists with name {body[ATTR_NAME]}")
|
||||
|
||||
await self.sys_mounts.create_mount(Mount.from_dict(self.coresys, body))
|
||||
self.sys_mounts.save_data()
|
||||
|
||||
@api_process
|
||||
async def update_mount(self, request: web.Request) -> None:
|
||||
"""Update an existing mount in supervisor."""
|
||||
mount = request.match_info.get("mount")
|
||||
name_schema = vol.Schema(
|
||||
{vol.Optional(ATTR_NAME, default=mount): mount}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
body = await api_validate(vol.All(name_schema, SCHEMA_MOUNT_CONFIG), request)
|
||||
|
||||
if mount not in self.sys_mounts:
|
||||
raise APIError(f"No mount exists with name {mount}")
|
||||
|
||||
await self.sys_mounts.create_mount(Mount.from_dict(self.coresys, body))
|
||||
self.sys_mounts.save_data()
|
||||
|
||||
@api_process
|
||||
async def delete_mount(self, request: web.Request) -> None:
|
||||
"""Delete an existing mount in supervisor."""
|
||||
mount = request.match_info.get("mount")
|
||||
|
||||
if mount not in self.sys_mounts:
|
||||
raise APIError(f"No mount exists with name {mount}")
|
||||
|
||||
await self.sys_mounts.remove_mount(mount)
|
||||
self.sys_mounts.save_data()
|
||||
|
||||
@api_process
|
||||
async def reload_mount(self, request: web.Request) -> None:
|
||||
"""Reload an existing mount in supervisor."""
|
||||
mount = request.match_info.get("mount")
|
||||
|
||||
if mount not in self.sys_mounts:
|
||||
raise APIError(f"No mount exists with name {mount}")
|
||||
|
||||
await self.sys_mounts.reload_mount(mount)
|
@@ -1,8 +1,7 @@
|
||||
"""Init file for Supervisor Multicast RESTful API."""
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, Awaitable
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
@@ -1,8 +1,7 @@
|
||||
"""REST API for network."""
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
from ipaddress import ip_address, ip_interface
|
||||
from typing import Any
|
||||
from typing import Any, Awaitable
|
||||
|
||||
from aiohttp import web
|
||||
import attr
|
||||
@@ -31,7 +30,6 @@ from ..const import (
|
||||
ATTR_PARENT,
|
||||
ATTR_PRIMARY,
|
||||
ATTR_PSK,
|
||||
ATTR_READY,
|
||||
ATTR_SIGNAL,
|
||||
ATTR_SSID,
|
||||
ATTR_SUPERVISOR_INTERNET,
|
||||
@@ -91,7 +89,6 @@ def ipconfig_struct(config: IpConfig) -> dict[str, Any]:
|
||||
ATTR_ADDRESS: [address.with_prefixlen for address in config.address],
|
||||
ATTR_NAMESERVERS: [str(address) for address in config.nameservers],
|
||||
ATTR_GATEWAY: str(config.gateway) if config.gateway else None,
|
||||
ATTR_READY: config.ready,
|
||||
}
|
||||
|
||||
|
||||
@@ -197,14 +194,12 @@ class APINetwork(CoreSysAttributes):
|
||||
for key, config in body.items():
|
||||
if key == ATTR_IPV4:
|
||||
interface.ipv4 = attr.evolve(
|
||||
interface.ipv4
|
||||
or IpConfig(InterfaceMethod.STATIC, [], None, [], None),
|
||||
interface.ipv4 or IpConfig(InterfaceMethod.STATIC, [], None, []),
|
||||
**config,
|
||||
)
|
||||
elif key == ATTR_IPV6:
|
||||
interface.ipv6 = attr.evolve(
|
||||
interface.ipv6
|
||||
or IpConfig(InterfaceMethod.STATIC, [], None, [], None),
|
||||
interface.ipv6 or IpConfig(InterfaceMethod.STATIC, [], None, []),
|
||||
**config,
|
||||
)
|
||||
elif key == ATTR_WIFI:
|
||||
@@ -223,9 +218,7 @@ class APINetwork(CoreSysAttributes):
|
||||
@api_process
|
||||
def reload(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Reload network data."""
|
||||
return asyncio.shield(
|
||||
self.sys_host.network.update(force_connectivity_check=True)
|
||||
)
|
||||
return asyncio.shield(self.sys_host.network.update())
|
||||
|
||||
@api_process
|
||||
async def scan_accesspoints(self, request: web.Request) -> dict[str, Any]:
|
||||
@@ -262,7 +255,6 @@ class APINetwork(CoreSysAttributes):
|
||||
body[ATTR_IPV4].get(ATTR_ADDRESS, []),
|
||||
body[ATTR_IPV4].get(ATTR_GATEWAY, None),
|
||||
body[ATTR_IPV4].get(ATTR_NAMESERVERS, []),
|
||||
None,
|
||||
)
|
||||
|
||||
ipv6_config = None
|
||||
@@ -272,7 +264,6 @@ class APINetwork(CoreSysAttributes):
|
||||
body[ATTR_IPV6].get(ATTR_ADDRESS, []),
|
||||
body[ATTR_IPV6].get(ATTR_GATEWAY, None),
|
||||
body[ATTR_IPV6].get(ATTR_NAMESERVERS, []),
|
||||
None,
|
||||
)
|
||||
|
||||
vlan_interface = Interface(
|
||||
|
@@ -1,8 +1,8 @@
|
||||
"""Init file for Supervisor HassOS RESTful API."""
|
||||
import asyncio
|
||||
from collections.abc import Awaitable
|
||||
import logging
|
||||
from typing import Any
|
||||
from pathlib import Path
|
||||
from typing import Any, Awaitable
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
@@ -11,44 +11,19 @@ from ..const import (
|
||||
ATTR_BOARD,
|
||||
ATTR_BOOT,
|
||||
ATTR_DEVICES,
|
||||
ATTR_ID,
|
||||
ATTR_NAME,
|
||||
ATTR_SERIAL,
|
||||
ATTR_SIZE,
|
||||
ATTR_UPDATE_AVAILABLE,
|
||||
ATTR_VERSION,
|
||||
ATTR_VERSION_LATEST,
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import BoardInvalidError
|
||||
from ..resolution.const import ContextType, IssueType, SuggestionType
|
||||
from ..validate import version_tag
|
||||
from .const import (
|
||||
ATTR_DATA_DISK,
|
||||
ATTR_DEV_PATH,
|
||||
ATTR_DEVICE,
|
||||
ATTR_DISK_LED,
|
||||
ATTR_DISKS,
|
||||
ATTR_HEARTBEAT_LED,
|
||||
ATTR_MODEL,
|
||||
ATTR_POWER_LED,
|
||||
ATTR_VENDOR,
|
||||
)
|
||||
from .const import ATTR_DATA_DISK, ATTR_DEVICE
|
||||
from .utils import api_process, api_validate
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag})
|
||||
SCHEMA_DISK = vol.Schema({vol.Required(ATTR_DEVICE): str})
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
SCHEMA_YELLOW_OPTIONS = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_DISK_LED): vol.Boolean(),
|
||||
vol.Optional(ATTR_HEARTBEAT_LED): vol.Boolean(),
|
||||
vol.Optional(ATTR_POWER_LED): vol.Boolean(),
|
||||
}
|
||||
)
|
||||
SCHEMA_DISK = vol.Schema({vol.Required(ATTR_DEVICE): vol.All(str, vol.Coerce(Path))})
|
||||
|
||||
|
||||
class APIOS(CoreSysAttributes):
|
||||
@@ -63,7 +38,7 @@ class APIOS(CoreSysAttributes):
|
||||
ATTR_UPDATE_AVAILABLE: self.sys_os.need_update,
|
||||
ATTR_BOARD: self.sys_os.board,
|
||||
ATTR_BOOT: self.sys_dbus.rauc.boot_slot,
|
||||
ATTR_DATA_DISK: self.sys_os.datadisk.disk_used_id,
|
||||
ATTR_DATA_DISK: self.sys_os.datadisk.disk_used,
|
||||
}
|
||||
|
||||
@api_process
|
||||
@@ -90,56 +65,5 @@ class APIOS(CoreSysAttributes):
|
||||
async def list_data(self, request: web.Request) -> dict[str, Any]:
|
||||
"""Return possible data targets."""
|
||||
return {
|
||||
ATTR_DEVICES: [disk.id for disk in self.sys_os.datadisk.available_disks],
|
||||
ATTR_DISKS: [
|
||||
{
|
||||
ATTR_NAME: disk.name,
|
||||
ATTR_VENDOR: disk.vendor,
|
||||
ATTR_MODEL: disk.model,
|
||||
ATTR_SERIAL: disk.serial,
|
||||
ATTR_SIZE: disk.size,
|
||||
ATTR_ID: disk.id,
|
||||
ATTR_DEV_PATH: disk.device_path.as_posix(),
|
||||
}
|
||||
for disk in self.sys_os.datadisk.available_disks
|
||||
],
|
||||
ATTR_DEVICES: self.sys_os.datadisk.available_disks,
|
||||
}
|
||||
|
||||
@api_process
|
||||
async def boards_yellow_info(self, request: web.Request) -> dict[str, Any]:
|
||||
"""Get yellow board settings."""
|
||||
return {
|
||||
ATTR_DISK_LED: self.sys_dbus.agent.board.yellow.disk_led,
|
||||
ATTR_HEARTBEAT_LED: self.sys_dbus.agent.board.yellow.heartbeat_led,
|
||||
ATTR_POWER_LED: self.sys_dbus.agent.board.yellow.power_led,
|
||||
}
|
||||
|
||||
@api_process
|
||||
async def boards_yellow_options(self, request: web.Request) -> None:
|
||||
"""Update yellow board settings."""
|
||||
body = await api_validate(SCHEMA_YELLOW_OPTIONS, request)
|
||||
|
||||
if ATTR_DISK_LED in body:
|
||||
self.sys_dbus.agent.board.yellow.disk_led = body[ATTR_DISK_LED]
|
||||
|
||||
if ATTR_HEARTBEAT_LED in body:
|
||||
self.sys_dbus.agent.board.yellow.heartbeat_led = body[ATTR_HEARTBEAT_LED]
|
||||
|
||||
if ATTR_POWER_LED in body:
|
||||
self.sys_dbus.agent.board.yellow.power_led = body[ATTR_POWER_LED]
|
||||
|
||||
self.sys_resolution.create_issue(
|
||||
IssueType.REBOOT_REQUIRED,
|
||||
ContextType.SYSTEM,
|
||||
suggestions=[SuggestionType.EXECUTE_REBOOT],
|
||||
)
|
||||
|
||||
@api_process
|
||||
async def boards_other_info(self, request: web.Request) -> dict[str, Any]:
|
||||
"""Empty success return if board is in use, error otherwise."""
|
||||
if request.match_info["board"] != self.sys_os.board:
|
||||
raise BoardInvalidError(
|
||||
f"{request.match_info['board']} board is not in use", _LOGGER.error
|
||||
)
|
||||
|
||||
return {}
|
||||
|
@@ -1,14 +1,14 @@
|
||||
|
||||
function loadES5() {
|
||||
var el = document.createElement('script');
|
||||
el.src = '/api/hassio/app/frontend_es5/entrypoint--6PDbD45dS8.js';
|
||||
el.src = '/api/hassio/app/frontend_es5/entrypoint.75b60951.js';
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
if (/.*Version\/(?:11|12)(?:\.\d+)*.*Safari\//.test(navigator.userAgent)) {
|
||||
loadES5();
|
||||
} else {
|
||||
try {
|
||||
new Function("import('/api/hassio/app/frontend_latest/entrypoint-Rzm-3XAKFKI.js')")();
|
||||
new Function("import('/api/hassio/app/frontend_latest/entrypoint.f358ba39.js')")();
|
||||
} catch (err) {
|
||||
loadES5();
|
||||
}
|
||||
|
Binary file not shown.
1
supervisor/api/panel/frontend_es5/0c8a1a3d.js
Normal file
1
supervisor/api/panel/frontend_es5/0c8a1a3d.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/0c8a1a3d.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/0c8a1a3d.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/0ce034b1.js
Normal file
1
supervisor/api/panel/frontend_es5/0ce034b1.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/0ce034b1.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/0ce034b1.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/0d47fc56.js
Normal file
1
supervisor/api/panel/frontend_es5/0d47fc56.js
Normal file
@@ -0,0 +1 @@
|
||||
!function(){"use strict";var t,n,e={14971:function(t,n,e){var r,i,o=e(93217),u=e(69330),a=(e(58556),e(62173)),c=function(t,n,e){if("input"===t){if("type"===n&&"checkbox"===e||"checked"===n||"disabled"===n)return;return""}},f={renderMarkdown:function(t,n){var e,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return r||(r=Object.assign({},(0,a.getDefaultWhiteList)(),{input:["type","disabled","checked"],"ha-icon":["icon"],"ha-svg-icon":["path"],"ha-alert":["alert-type","title"]})),o.allowSvg?(i||(i=Object.assign({},r,{svg:["xmlns","height","width"],path:["transform","stroke","d"],img:["src"]})),e=i):e=r,(0,a.filterXSS)((0,u.TU)(t,n),{whiteList:e,onTagAttr:c})}};(0,o.Jj)(f)}},r={};function i(t){var n=r[t];if(void 0!==n)return n.exports;var o=r[t]={exports:{}};return e[t](o,o.exports,i),o.exports}i.m=e,i.x=function(){var t=i.O(void 0,[191,752],(function(){return i(14971)}));return t=i.O(t)},t=[],i.O=function(n,e,r,o){if(!e){var u=1/0;for(s=0;s<t.length;s++){e=t[s][0],r=t[s][1],o=t[s][2];for(var a=!0,c=0;c<e.length;c++)(!1&o||u>=o)&&Object.keys(i.O).every((function(t){return i.O[t](e[c])}))?e.splice(c--,1):(a=!1,o<u&&(u=o));if(a){t.splice(s--,1);var f=r();void 0!==f&&(n=f)}}return n}o=o||0;for(var s=t.length;s>0&&t[s-1][2]>o;s--)t[s]=t[s-1];t[s]=[e,r,o]},i.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return i.d(n,{a:n}),n},i.d=function(t,n){for(var e in n)i.o(n,e)&&!i.o(t,e)&&Object.defineProperty(t,e,{enumerable:!0,get:n[e]})},i.f={},i.e=function(t){return Promise.all(Object.keys(i.f).reduce((function(n,e){return i.f[e](t,n),n}),[]))},i.u=function(t){return{191:"2dbdaab4",752:"829db8ac"}[t]+".js"},i.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},i.p="/api/hassio/app/frontend_es5/",function(){var t={971:1};i.f.i=function(n,e){t[n]||importScripts(i.p+i.u(n))};var n=self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[],e=n.push.bind(n);n.push=function(n){var r=n[0],o=n[1],u=n[2];for(var a in o)i.o(o,a)&&(i.m[a]=o[a]);for(u&&u(i);r.length;)t[r.pop()]=1;e(n)}}(),n=i.x,i.x=function(){return Promise.all([i.e(191),i.e(752)]).then(n)};i.x()}();
|
BIN
supervisor/api/panel/frontend_es5/0d47fc56.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/0d47fc56.js.gz
Normal file
Binary file not shown.
2
supervisor/api/panel/frontend_es5/0ef95294.js
Normal file
2
supervisor/api/panel/frontend_es5/0ef95294.js
Normal file
File diff suppressed because one or more lines are too long
14
supervisor/api/panel/frontend_es5/0ef95294.js.LICENSE.txt
Normal file
14
supervisor/api/panel/frontend_es5/0ef95294.js.LICENSE.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
/*! *****************************************************************************
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||
this file except in compliance with the License. You may obtain a copy of the
|
||||
License at http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
|
||||
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
|
||||
MERCHANTABLITY OR NON-INFRINGEMENT.
|
||||
|
||||
See the Apache Version 2.0 License for specific language governing permissions
|
||||
and limitations under the License.
|
||||
***************************************************************************** */
|
BIN
supervisor/api/panel/frontend_es5/0ef95294.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/0ef95294.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/0f16c741.js
Normal file
1
supervisor/api/panel/frontend_es5/0f16c741.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/0f16c741.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/0f16c741.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
Binary file not shown.
File diff suppressed because one or more lines are too long
1
supervisor/api/panel/frontend_es5/11665104.js
Normal file
1
supervisor/api/panel/frontend_es5/11665104.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/11665104.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/11665104.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"1267-9DmGGrZiikM.js","mappings":"qyQAKA,IACaA,E,mxBAAgBC,CAAA,EAD5BC,EAAAA,EAAAA,IAAc,uCAAoC,SAAAC,EAAAC,GAAA,IACtCJ,EAAgB,SAAAK,I,qRAAAC,CAAAN,EAAAK,GAAA,I,MAAAE,EAAAC,EAAAR,GAAA,SAAAA,IAAA,IAAAS,G,4FAAAC,CAAA,KAAAV,GAAA,QAAAW,EAAAC,UAAAC,OAAAC,EAAA,IAAAC,MAAAJ,GAAAK,EAAA,EAAAA,EAAAL,EAAAK,IAAAF,EAAAE,GAAAJ,UAAAI,GAAA,OAAAP,EAAAF,EAAAU,KAAAC,MAAAX,EAAA,OAAAY,OAAAL,IAAAX,EAAAiB,EAAAX,IAAAA,CAAA,Q,EAAAT,E,kFAAA,EAAAI,GAAA,OAAAiB,EAAhBrB,EAAgBsB,EAAA,EAAAC,KAAA,QAAAC,WAAA,EAC1BC,EAAAA,EAAAA,IAAS,CAAEC,WAAW,KAAQC,IAAA,SAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAE9BC,EAAAA,EAAAA,IAAS,CAAEC,WAAW,KAAQC,IAAA,OAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAE9BC,EAAAA,EAAAA,OAAUE,IAAA,QAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAEVC,EAAAA,EAAAA,IAAS,CAAEI,KAAMC,WAAUH,IAAA,WAAAC,MAAA,kBAAmB,CAAK,IAAAL,KAAA,QAAAC,WAAA,EAEnDO,EAAAA,EAAAA,IAAM,iBAAiB,IAAKJ,IAAA,SAAAC,WAAA,IAAAL,KAAA,SAAAI,IAAA,QAAAC,MAE7B,WACMI,KAAKC,QACPD,KAAKC,OAAOC,OAEhB,GAAC,CAAAX,KAAA,SAAAI,IAAA,SAAAC,MAED,WACE,OAAOO,EAAAA,EAAAA,IAAIC,I,EAAA,uG,kBAAAA,E,0EAEEJ,KAAKK,MACFL,KAAKM,OAAOC,SAChBP,KAAKQ,KACDR,KAAKS,U,OAGvB,IAAC,GA1BmCC,EAAAA,G","sources":["https://raw.githubusercontent.com/home-assistant/frontend/20230411.0/src/components/ha-form/ha-form-positive_time_period_dict.ts"],"names":["HaFormTimePeriod","_decorate","customElement","_initialize","_LitElement","_LitElement2","_inherits","_super","_createSuper","_this","_classCallCheck","_len","arguments","length","args","Array","_key","call","apply","concat","_assertThisInitialized","F","d","kind","decorators","property","attribute","key","value","type","Boolean","query","this","_input","focus","html","_templateObject","label","schema","required","data","disabled","LitElement"],"sourceRoot":""}
|
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
"use strict";(self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[]).push([[1639],{71639:function(s){s.exports=[]}}]);
|
Binary file not shown.
1
supervisor/api/panel/frontend_es5/16aa06d8.js
Normal file
1
supervisor/api/panel/frontend_es5/16aa06d8.js
Normal file
@@ -0,0 +1 @@
|
||||
"use strict";(self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[]).push([[639],{71639:function(s){s.exports=[]}}]);
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"1927-LLhYEyKUsLw.js","mappings":"4lSAIqBA,CAAA,EADpBC,E,SAAAA,IAAc,0BAAuB,SAAAC,EAAAC,GAAA,IAChCC,EAAe,SAAAC,I,qRAAAC,CAAAF,EAAAC,GAAA,I,MAAAE,EAAAC,EAAAJ,GAAA,SAAAA,IAAA,IAAAK,G,4FAAAC,CAAA,KAAAN,GAAA,QAAAO,EAAAC,UAAAC,OAAAC,EAAA,IAAAC,MAAAJ,GAAAK,EAAA,EAAAA,EAAAL,EAAAK,IAAAF,EAAAE,GAAAJ,UAAAI,GAAA,OAAAP,EAAAF,EAAAU,KAAAC,MAAAX,EAAA,OAAAY,OAAAL,IAAAZ,EAAAkB,EAAAX,IAAAA,CAAA,Q,EAAAL,E,kFAAA,EAAAD,GAAA,OAAAkB,EAAfjB,EAAekB,EAAA,EAAAC,KAAA,SAAAC,IAAA,SAAAC,MACnB,WACE,OAAOC,EAAAA,EAAAA,IAAIC,IAAAA,EAAAC,EAAA,oBACb,GAAC,CAAAL,KAAA,kBAAAC,IAAA,SAAAC,MAAA,kBAEeI,EAAAA,EAAAA,IAAGC,IAAAA,EAAAF,EAAA,mJALSG,EAAAA,G,w0QCI9B,IACaC,E,mxBAAiBhC,CAAA,EAD7BC,EAAAA,EAAAA,IAAc,yBAAsB,SAAAC,EAAAC,GAAA,IACxB6B,EAAiB,SAAA3B,I,qRAAAC,CAAA0B,EAAA3B,GAAA,I,MAAAE,EAAAC,EAAAwB,GAAA,SAAAA,IAAA,IAAAvB,G,4FAAAC,CAAA,KAAAsB,GAAA,QAAArB,EAAAC,UAAAC,OAAAC,EAAA,IAAAC,MAAAJ,GAAAK,EAAA,EAAAA,EAAAL,EAAAK,IAAAF,EAAAE,GAAAJ,UAAAI,GAAA,OAAAP,EAAAF,EAAAU,KAAAC,MAAAX,EAAA,OAAAY,OAAAL,IAAAZ,EAAAkB,EAAAX,IAAAA,CAAA,Q,EAAAuB,E,kFAAA,EAAA7B,GAAA,OAAAkB,EAAjBW,EAAiBV,EAAA,EAAAC,KAAA,QAAAU,WAAA,EAC3BC,EAAAA,EAAAA,OAAUV,IAAA,OAAAC,WAAA,IAAAF,KAAA,QAAAU,WAAA,EAEVC,EAAAA,EAAAA,OAAUV,IAAA,QAAAC,WAAA,IAAAF,KAAA,QAAAU,WAAA,EAEVC,EAAAA,EAAAA,OAAUV,IAAA,QAAAC,WAAA,IAAAF,KAAA,QAAAU,WAAA,EAEVC,EAAAA,EAAAA,OAAUV,IAAA,SAAAC,WAAA,IAAAF,KAAA,QAAAU,WAAA,EAEVC,EAAAA,EAAAA,IAAS,CAAEC,KAAMC,WAAUZ,IAAA,WAAAC,MAAA,kBAAmB,CAAK,IAAAF,KAAA,SAAAC,IAAA,SAAAC,MAEpD,WACE,OAAOC,EAAAA,EAAAA,IAAIC,IAAAA,EAAAC,EAAA,mJACoCS,KAAKC,MAEnCD,KAAKZ,MACNY,KAAKE,cACHF,KAAKG,SAGnBH,KAAKI,QACHf,EAAAA,EAAAA,IAAII,IAAAA,EAAAF,EAAA,uDAAyBS,KAAKI,QAClC,GAER,GAAC,CAAAlB,KAAA,SAAAC,IAAA,gBAAAC,MAED,SAAsBiB,GACpB,IAAMjB,EAAQiB,EAAGC,OAAOC,QACpBP,KAAKZ,QAAUA,IAGnBoB,EAAAA,EAAAA,GAAUR,KAAM,gBAAiB,CAAEZ,MAAAA,GACrC,GAAC,CAAAF,KAAA,gBAAAC,IAAA,SAAAC,MAED,WACE,OAAOI,EAAAA,EAAAA,IAAGiB,IAAAA,EAAAlB,EAAA,qGAQZ,IAAC,GA3CoCG,EAAAA,G","sources":["https://raw.githubusercontent.com/home-assistant/frontend/20230411.0/src/components/ha-input-helper-text.ts","https://raw.githubusercontent.com/home-assistant/frontend/20230411.0/src/components/ha-selector/ha-selector-boolean.ts"],"names":["_decorate","customElement","_initialize","_LitElement","InputHelperText","_LitElement2","_inherits","_super","_createSuper","_this","_classCallCheck","_len","arguments","length","args","Array","_key","call","apply","concat","_assertThisInitialized","F","d","kind","key","value","html","_templateObject","_taggedTemplateLiteral","css","_templateObject2","LitElement","HaBooleanSelector","decorators","property","type","Boolean","this","label","_handleChange","disabled","helper","ev","target","checked","fireEvent","_templateObject3"],"sourceRoot":""}
|
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
supervisor/api/panel/frontend_es5/19929d68.js
Normal file
1
supervisor/api/panel/frontend_es5/19929d68.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/19929d68.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/19929d68.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,4 +0,0 @@
|
||||
/* @preserve
|
||||
* Leaflet 1.9.3, a JS library for interactive maps. https://leafletjs.com
|
||||
* (c) 2010-2022 Vladimir Agafonkin, (c) 2010-2011 CloudMade
|
||||
*/
|
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Google LLC
|
||||
* SPDX-LIcense-Identifier: Apache-2.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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user