Compare commits

..

4 Commits

Author SHA1 Message Date
Pascal Vizeli
2470d16d85 Cleanup 2022-08-09 14:36:56 +00:00
Pascal Vizeli
5c0172440a change name 2022-08-09 11:18:33 +00:00
Pascal Vizeli
006db94cc0 fix gateway 2022-08-09 08:32:16 +00:00
Pascal Vizeli
9f1ab99265 Support basic IPv6 2022-08-09 08:07:52 +00:00
1630 changed files with 21033 additions and 26752 deletions

View File

@@ -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",

View File

@@ -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.
[![Open your Home Assistant instance and show your Supervisor system logs.](https://my.home-assistant.io/badges/supervisor_logs.svg)](https://my.home-assistant.io/redirect/supervisor_logs/)
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.
[![Open your Home Assistant instance and show health information about your system.](https://my.home-assistant.io/badges/system_health.svg)](https://my.home-assistant.io/redirect/system_health/)
- type: textarea
attributes:
label: Supervisor diagnostics
placeholder: "drag-and-drop the diagnostics data file here (do not copy-and-paste the content)"
description: >-
Supervisor diagnostics can be found in [Settings -> Integrations](https://my.home-assistant.io/redirect/integrations/).
Find the card that says `Home Assistant Supervisor`, open its menu and select 'Download diagnostics'.
**Please drag-and-drop the downloaded file into the textbox below. Do not copy and paste its contents.**
- type: textarea
attributes:
label: Additional information

View File

@@ -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')

View File

@@ -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

View File

@@ -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"

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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"

View File

@@ -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
View File

@@ -13,13 +13,6 @@
"remoteRoot": "/usr/src/supervisor"
}
]
},
{
"name": "Debug Tests",
"type": "python",
"request": "test",
"console": "internalConsole",
"justMyCode": false
}
]
}

View File

@@ -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

View File

@@ -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

View File

@@ -38,7 +38,7 @@ disable=
consider-using-with
[EXCEPTIONS]
overgeneral-exceptions=builtins.Exception
overgeneral-exceptions=Exception
[TYPECHECK]

View File

@@ -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

View File

@@ -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

View File

@@ -27,5 +27,3 @@ ignore =
E203,
D202,
W504
per-file-ignores =
tests/dbus_service_mocks/*.py: F821,F722

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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"),

View File

@@ -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]+"

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

View File

@@ -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}),

View File

@@ -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()

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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 = {}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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(

View File

@@ -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 {}

View File

@@ -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();
}

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View 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()}();

Binary file not shown.

File diff suppressed because one or more lines are too long

View 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.
***************************************************************************** */

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */

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

View File

@@ -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

View File

@@ -1 +0,0 @@
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
"use strict";(self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[]).push([[1639],{71639:function(s){s.exports=[]}}]);

View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
{"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

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

View File

@@ -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
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,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
*/

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