mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-09-09 04:59:27 +00:00
Compare commits
363 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b2abe37d72 | ||
![]() |
9bf8d15b01 | ||
![]() |
70acbffc23 | ||
![]() |
c9b1eb751e | ||
![]() |
ad8d850ed7 | ||
![]() |
1b0eb9397d | ||
![]() |
8572f8c4e5 | ||
![]() |
66565dde87 | ||
![]() |
d54c23952f | ||
![]() |
62b364ea29 | ||
![]() |
e6f00144f2 | ||
![]() |
8894984c12 | ||
![]() |
49fbdedf6b | ||
![]() |
0899c16895 | ||
![]() |
0747a7e4b2 | ||
![]() |
0123d7935d | ||
![]() |
7a1009446b | ||
![]() |
034606cd0f | ||
![]() |
ddc30cfd7d | ||
![]() |
f10fccaff8 | ||
![]() |
31001280c8 | ||
![]() |
9638775944 | ||
![]() |
fbec0befde | ||
![]() |
81e7fac848 | ||
![]() |
97599b3e70 | ||
![]() |
c94b23a3fd | ||
![]() |
9758980ae0 | ||
![]() |
6ab3fbaab3 | ||
![]() |
4933ff83df | ||
![]() |
71e12ecb2b | ||
![]() |
36687530e0 | ||
![]() |
e7b5864c03 | ||
![]() |
9497f85db9 | ||
![]() |
419f603571 | ||
![]() |
f4f1fc524d | ||
![]() |
6d9f44a900 | ||
![]() |
aeb9b26d44 | ||
![]() |
631f78f468 | ||
![]() |
13cedb308e | ||
![]() |
82c183e1a8 | ||
![]() |
25cf1e7394 | ||
![]() |
91509a4205 | ||
![]() |
d93ebd15a2 | ||
![]() |
85e7f817e6 | ||
![]() |
772cadb435 | ||
![]() |
853aeef583 | ||
![]() |
cd07bde307 | ||
![]() |
3057df3181 | ||
![]() |
fe785622ec | ||
![]() |
2b6829a786 | ||
![]() |
7c6c982414 | ||
![]() |
07eeb2eaf2 | ||
![]() |
223f5b7bb1 | ||
![]() |
8a9657c452 | ||
![]() |
564e9811d0 | ||
![]() |
b944b52b21 | ||
![]() |
24aecdddf3 | ||
![]() |
7bbfb60039 | ||
![]() |
ece40008c7 | ||
![]() |
0177b38ded | ||
![]() |
16f2f63081 | ||
![]() |
5f376c2a27 | ||
![]() |
90a6f109ee | ||
![]() |
e6fd0ef5dc | ||
![]() |
d46ab56901 | ||
![]() |
3b1ad5c0cd | ||
![]() |
de8a241e72 | ||
![]() |
a4a0b43d91 | ||
![]() |
adf355e54f | ||
![]() |
4f9e646b4c | ||
![]() |
ce57d384ca | ||
![]() |
d53d526673 | ||
![]() |
cd8fc16bcb | ||
![]() |
6b58970354 | ||
![]() |
b70ed9a60d | ||
![]() |
22c8ff1314 | ||
![]() |
ba2cf8078e | ||
![]() |
ef138b619b | ||
![]() |
9252af5ddb | ||
![]() |
bcef34012d | ||
![]() |
2f18c177ae | ||
![]() |
12487fb69d | ||
![]() |
e629bab8ee | ||
![]() |
e85d7c3d2e | ||
![]() |
64c59d0fe9 | ||
![]() |
522cbe3295 | ||
![]() |
fd185fc326 | ||
![]() |
6ddc135266 | ||
![]() |
f8d5279d9c | ||
![]() |
2acae9af57 | ||
![]() |
b425d21d05 | ||
![]() |
4c7ba20a58 | ||
![]() |
a4f325dd2e | ||
![]() |
a99bfa2926 | ||
![]() |
bb127a614b | ||
![]() |
6f2f005897 | ||
![]() |
e22a20c165 | ||
![]() |
20c2121e5f | ||
![]() |
8a5831d6b2 | ||
![]() |
fb81946240 | ||
![]() |
4bec86c58c | ||
![]() |
7034b79991 | ||
![]() |
7b9a09dc4b | ||
![]() |
0746c4dec5 | ||
![]() |
6dadb933bd | ||
![]() |
07197e6a50 | ||
![]() |
6c79fb8325 | ||
![]() |
7488750ee4 | ||
![]() |
c9574254aa | ||
![]() |
f466721ffa | ||
![]() |
3834cead07 | ||
![]() |
75975de201 | ||
![]() |
cb9f998ef1 | ||
![]() |
eb9ce8ea1f | ||
![]() |
a5ed68b641 | ||
![]() |
1ef46424ea | ||
![]() |
53c99547d0 | ||
![]() |
a34e7622d2 | ||
![]() |
b234c18664 | ||
![]() |
d8d594c728 | ||
![]() |
1cd35841e8 | ||
![]() |
d05b7edd87 | ||
![]() |
95ef7d4508 | ||
![]() |
9812e5be6a | ||
![]() |
183182943d | ||
![]() |
a0189d65de | ||
![]() |
b59f741162 | ||
![]() |
efc2e826a1 | ||
![]() |
a3ad23e262 | ||
![]() |
5e3bcbfaac | ||
![]() |
7f3e4558b9 | ||
![]() |
567a01c2ed | ||
![]() |
2236cf146e | ||
![]() |
8e2f33ba1e | ||
![]() |
8190883a71 | ||
![]() |
c01218a97a | ||
![]() |
2437817a41 | ||
![]() |
682ee4529e | ||
![]() |
cee520f0b5 | ||
![]() |
0d915a3efc | ||
![]() |
f3a562006a | ||
![]() |
d78091cc60 | ||
![]() |
f785c4e909 | ||
![]() |
cda66ba737 | ||
![]() |
ea68ffc5a4 | ||
![]() |
31b0b721c8 | ||
![]() |
b97e33f5d5 | ||
![]() |
29e55d3664 | ||
![]() |
9112f27dc0 | ||
![]() |
9e67df26b3 | ||
![]() |
37d1a577ef | ||
![]() |
1eebb31004 | ||
![]() |
885764ea1c | ||
![]() |
b3d184b5c7 | ||
![]() |
96d04ec17e | ||
![]() |
e0bb3ad609 | ||
![]() |
1a8842cb81 | ||
![]() |
092d526749 | ||
![]() |
9db95c188a | ||
![]() |
0e45fc7d66 | ||
![]() |
4d1ddbfa2b | ||
![]() |
caa1c6f1bd | ||
![]() |
10d686b415 | ||
![]() |
29fae90da5 | ||
![]() |
e27337da85 | ||
![]() |
8f22316869 | ||
![]() |
dd10d3e037 | ||
![]() |
4a53c62af8 | ||
![]() |
1ebbf2b693 | ||
![]() |
62d198111c | ||
![]() |
1fc0ab71aa | ||
![]() |
f4402a1633 | ||
![]() |
13a17bcb34 | ||
![]() |
e1b49d90c2 | ||
![]() |
85ab25ea16 | ||
![]() |
80131ddfa8 | ||
![]() |
e9c123459f | ||
![]() |
d3e4bb7219 | ||
![]() |
fd98d38125 | ||
![]() |
3237611034 | ||
![]() |
ce2bffda15 | ||
![]() |
977e7b7adc | ||
![]() |
5082078527 | ||
![]() |
3615091c93 | ||
![]() |
fb1eb44d82 | ||
![]() |
13910d44bf | ||
![]() |
cda1d15070 | ||
![]() |
d0a1de23a6 | ||
![]() |
44fd75220f | ||
![]() |
ed594d653f | ||
![]() |
40bb3a7581 | ||
![]() |
df7f0345e8 | ||
![]() |
f7ab76bb9a | ||
![]() |
45e24bfa65 | ||
![]() |
8cd149783c | ||
![]() |
8e8e6e48a9 | ||
![]() |
816e0d503a | ||
![]() |
c43acd50f4 | ||
![]() |
16ce4296a2 | ||
![]() |
65386b753f | ||
![]() |
2be1529cb8 | ||
![]() |
98f8e032e3 | ||
![]() |
900b785789 | ||
![]() |
9194088947 | ||
![]() |
58c40cbef6 | ||
![]() |
e6c57dfc80 | ||
![]() |
82f76f60bd | ||
![]() |
b9af4aec6b | ||
![]() |
f71ce27248 | ||
![]() |
5b2b1765bc | ||
![]() |
2a892544c2 | ||
![]() |
bedb37ca6b | ||
![]() |
a456cd645f | ||
![]() |
9c68094cf6 | ||
![]() |
379cef9e35 | ||
![]() |
cb3e2dab71 | ||
![]() |
3e89f83e0b | ||
![]() |
af0bdd890a | ||
![]() |
f93f5d0e71 | ||
![]() |
667672a20b | ||
![]() |
9e1f899274 | ||
![]() |
75e0741665 | ||
![]() |
392d0e929b | ||
![]() |
b342073ba9 | ||
![]() |
ff4e550ba3 | ||
![]() |
17aa544be5 | ||
![]() |
390676dbc4 | ||
![]() |
d423252bc7 | ||
![]() |
790e887b70 | ||
![]() |
47e377683e | ||
![]() |
b1232c0d8d | ||
![]() |
059233c111 | ||
![]() |
55382d000b | ||
![]() |
75ab6eec43 | ||
![]() |
e30171746b | ||
![]() |
73849b7468 | ||
![]() |
a52713611c | ||
![]() |
85a66c663c | ||
![]() |
e478e68b70 | ||
![]() |
16095c319a | ||
![]() |
f4a6100fba | ||
![]() |
82060dd242 | ||
![]() |
a58cfb797c | ||
![]() |
c8256a50f4 | ||
![]() |
3ae974e9e2 | ||
![]() |
ac5e74a375 | ||
![]() |
05e3d3b779 | ||
![]() |
681a1ecff5 | ||
![]() |
2b411b0bf9 | ||
![]() |
fee16847d3 | ||
![]() |
501a52a3c6 | ||
![]() |
2bb014fda5 | ||
![]() |
09203f67b2 | ||
![]() |
169c7ec004 | ||
![]() |
202e94615e | ||
![]() |
5fe2a815ad | ||
![]() |
a13a0b4770 | ||
![]() |
455bbc457b | ||
![]() |
d50fd3b580 | ||
![]() |
455e80b07c | ||
![]() |
291becbdf9 | ||
![]() |
33385b46a7 | ||
![]() |
df17668369 | ||
![]() |
43449c85bb | ||
![]() |
9e86eda05a | ||
![]() |
b288554d9c | ||
![]() |
bee55d08fb | ||
![]() |
7a542aeb38 | ||
![]() |
8d42513ba8 | ||
![]() |
89b7247aa2 | ||
![]() |
29132e7f4c | ||
![]() |
3fd9baf78e | ||
![]() |
f3aa3757ce | ||
![]() |
3760967f59 | ||
![]() |
f7ab8e0f7f | ||
![]() |
0e46ea12b2 | ||
![]() |
be226b2b01 | ||
![]() |
9e1239e192 | ||
![]() |
2eba3d85b0 | ||
![]() |
9b569268ab | ||
![]() |
31f5033dca | ||
![]() |
78d9c60be5 | ||
![]() |
baa86f09e5 | ||
![]() |
a4c4b39ba8 | ||
![]() |
752068bb56 | ||
![]() |
739cfbb273 | ||
![]() |
115af4cadf | ||
![]() |
ae3274e559 | ||
![]() |
c61f096dbd | ||
![]() |
ee7b5c42fd | ||
![]() |
85d527bfbc | ||
![]() |
dd561da819 | ||
![]() |
cb5932cb8b | ||
![]() |
8630adc54a | ||
![]() |
90d8832cd2 | ||
![]() |
3802b97bb6 | ||
![]() |
2de175e181 | ||
![]() |
6b7d437b00 | ||
![]() |
e2faf906de | ||
![]() |
bb44ce5cd2 | ||
![]() |
15544ae589 | ||
![]() |
e421284471 | ||
![]() |
785dc64787 | ||
![]() |
7e7e3a7876 | ||
![]() |
2b45c059e0 | ||
![]() |
14ec61f9bd | ||
![]() |
5cc72756f8 | ||
![]() |
44785ef3e2 | ||
![]() |
e60d858feb | ||
![]() |
b31ecfefcd | ||
![]() |
c342231052 | ||
![]() |
673666837e | ||
![]() |
c8f74d6c0d | ||
![]() |
7ed9de8014 | ||
![]() |
8650947f04 | ||
![]() |
a0ac8ced31 | ||
![]() |
2145bbea81 | ||
![]() |
480000ee7f | ||
![]() |
9ec2ad022e | ||
![]() |
43e40816dc | ||
![]() |
941ea3ee68 | ||
![]() |
a6e4b5159e | ||
![]() |
6f542d58d5 | ||
![]() |
b2b5fcee7d | ||
![]() |
59a82345a9 | ||
![]() |
b61a747876 | ||
![]() |
72e5d800d5 | ||
![]() |
c7aa6d4804 | ||
![]() |
b31063449d | ||
![]() |
477672459d | ||
![]() |
9c33897296 | ||
![]() |
100cfb57c5 | ||
![]() |
40b34071e7 | ||
![]() |
341833fd8f | ||
![]() |
f647fd6fea | ||
![]() |
53642f2389 | ||
![]() |
b9bdd655ab | ||
![]() |
e9e1b5b54f | ||
![]() |
be2163d635 | ||
![]() |
7f6dde3a5f | ||
![]() |
334aafee23 | ||
![]() |
1a20c18b19 | ||
![]() |
6e655b165c | ||
![]() |
d768b2fa1e | ||
![]() |
85bce1cfba | ||
![]() |
a798a2466f | ||
![]() |
2a5d8a5c82 | ||
![]() |
ea62171d98 | ||
![]() |
196389d5ee | ||
![]() |
1776021620 | ||
![]() |
c42a9124d3 | ||
![]() |
a44647b4cd | ||
![]() |
e0c3fd87c5 | ||
![]() |
ed8f2a85b7 | ||
![]() |
48f8553c75 | ||
![]() |
af4517fd1e | ||
![]() |
78e6a46318 | ||
![]() |
49ca923e51 | ||
![]() |
7ad22e0399 | ||
![]() |
bb8acc6065 | ||
![]() |
c0fa4a19e9 | ||
![]() |
3f1741dd18 | ||
![]() |
9ef02e4110 |
@@ -1,4 +1,10 @@
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.8
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.9
|
||||
|
||||
ENV \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
VCN_VERSION=0.9.8
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
WORKDIR /workspaces
|
||||
|
||||
@@ -8,12 +14,13 @@ RUN \
|
||||
&& echo '{"storage-driver": "vfs"}' > /etc/docker/daemon.json
|
||||
|
||||
# Install Node/Yarn for Frontent
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||
&& apt-get update \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
git \
|
||||
apt-utils \
|
||||
apt-transport-https \
|
||||
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||
nodejs \
|
||||
@@ -43,7 +50,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
jq \
|
||||
dbus \
|
||||
network-manager \
|
||||
apparmor-utils \
|
||||
libpulse0 \
|
||||
&& curl -Lo /bin/vcn https://github.com/codenotary/vcn/releases/download/${VCN_VERSION}/vcn-${VCN_VERSION}-linux-amd64-static \
|
||||
&& chmod a+x /bin/vcn \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies from requirements.txt if it exists
|
||||
|
@@ -5,7 +5,7 @@
|
||||
"appPort": "9123:8123",
|
||||
"postCreateCommand": "pre-commit install",
|
||||
"runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
|
||||
"containerEnv": {"NVM_DIR":"/usr/local/share/nvm"},
|
||||
"containerEnv": { "NVM_DIR": "/usr/local/share/nvm" },
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
@@ -22,7 +22,7 @@
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.enabled": true,
|
||||
"python.formatting.provider": "black",
|
||||
"python.formatting.blackArgs": ["--target-version", "py38"],
|
||||
"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",
|
||||
|
@@ -1,64 +1,78 @@
|
||||
name: Bug Report Form
|
||||
about: Report an issue related to the Home Assistant Supervisor.
|
||||
description: Report an issue related to the Home Assistant Supervisor.
|
||||
labels: bug
|
||||
title: ""
|
||||
issue_body: true
|
||||
inputs:
|
||||
- type: description
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
This issue form is for reporting bugs with **supported** setups only!
|
||||
|
||||
If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr].
|
||||
|
||||
[fr]: https://community.home-assistant.io/c/feature-requests
|
||||
- type: input
|
||||
attributes:
|
||||
label: What is the version of the Supervisor used?
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Describe the issue you are experiencing
|
||||
description: Provide a clear and concise description of what the bug is.
|
||||
- type: markdown
|
||||
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?
|
||||
required: true
|
||||
description: >
|
||||
If you don't know, you can find it in: Configuration panel -> Info.
|
||||
choices:
|
||||
options:
|
||||
- Home Assistant OS
|
||||
- Home Assistant Supervised
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Which operating system are you running on?
|
||||
required: true
|
||||
choices:
|
||||
options:
|
||||
- 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?
|
||||
required: true
|
||||
placeholder: 5.10
|
||||
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?
|
||||
required: true
|
||||
placeholder: core-
|
||||
description: >
|
||||
Can be found in the Supervisor panel -> System tab. Starts with
|
||||
`core-....`.
|
||||
- type: textarea
|
||||
- type: markdown
|
||||
attributes:
|
||||
label: Describe the issue you are experiencing
|
||||
required: true
|
||||
description: Provide a clear and concise description of what the bug is.
|
||||
value: |
|
||||
# Details
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Steps to reproduce the issue
|
||||
required: true
|
||||
description: |
|
||||
Please tell us exactly how to reproduce your issue.
|
||||
Provide clear and concise step by step instructions and add code snippets if needed.
|
||||
@@ -68,13 +82,17 @@ inputs:
|
||||
3.
|
||||
...
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Anything in the Supervisor logs that might be useful for us?
|
||||
required: false
|
||||
description: >
|
||||
The Supervisor logs can be found in the Supervisor panel -> System tab.
|
||||
- type: description
|
||||
render: txt
|
||||
- type: textarea
|
||||
attributes:
|
||||
value: |
|
||||
label: Additional information
|
||||
description: >
|
||||
If you have any additional information for us, use the field below.
|
||||
Please note, you can attach screenshots or screen recordings here.
|
||||
Please note, you can attach screenshots or screen recordings here, by
|
||||
dragging and dropping files in the field below.
|
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -37,6 +37,7 @@
|
||||
- This PR fixes or closes issue: fixes #
|
||||
- This PR is related to issue:
|
||||
- Link to documentation pull request:
|
||||
- Link to cli pull request:
|
||||
|
||||
## Checklist
|
||||
|
||||
|
114
.github/workflows/builder.yml
vendored
114
.github/workflows/builder.yml
vendored
@@ -35,7 +35,7 @@ on:
|
||||
env:
|
||||
BUILD_NAME: supervisor
|
||||
BUILD_TYPE: supervisor
|
||||
WHEELS_TAG: 3.8-alpine3.12
|
||||
WHEELS_TAG: 3.9-alpine3.14
|
||||
|
||||
jobs:
|
||||
init:
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
requirements: ${{ steps.requirements.outputs.changed }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Check if requirements files changed
|
||||
id: requirements
|
||||
run: |
|
||||
if [[ "${{ steps.changed_files.outputs.all }}" =~ requirements.txt ]]; then
|
||||
if [[ "${{ steps.changed_files.outputs.all }}" =~ (requirements.txt|build.json) ]]; then
|
||||
echo "::set-output name=changed::true"
|
||||
fi
|
||||
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -94,10 +94,10 @@ jobs:
|
||||
with:
|
||||
tag: ${{ env.WHEELS_TAG }}
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-host: ${{ secrets.WHEELS_HOST }}
|
||||
wheels-host: wheels.hass.io
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
wheels-user: wheels
|
||||
apk: "build-base;libffi-dev;openssl-dev"
|
||||
apk: "build-base;libffi-dev;openssl-dev;cargo"
|
||||
skip-binary: aiohttp
|
||||
requirements: "requirements.txt"
|
||||
|
||||
@@ -109,24 +109,60 @@ jobs:
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v1.10.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@v1.10.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ secrets.GIT_USER }}
|
||||
password: ${{ secrets.GIT_TOKEN }}
|
||||
|
||||
- name: Set build arguments
|
||||
if: needs.init.outputs.publish == 'false'
|
||||
run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV
|
||||
|
||||
- name: Build supervisor
|
||||
uses: home-assistant/builder@2021.01.1
|
||||
uses: home-assistant/builder@2021.07.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
--${{ matrix.arch }} \
|
||||
--target /data \
|
||||
--with-codenotary "${{ secrets.VCN_USER }}" "${{ secrets.VCN_PASSWORD }}" "${{ secrets.VCN_ORG }}" \
|
||||
--validate-from "${{ secrets.VCN_ORG }}" \
|
||||
--generic ${{ needs.init.outputs.version }}
|
||||
|
||||
codenotary:
|
||||
name: CodeNotary signature
|
||||
needs: init
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set version
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: home-assistant/actions/helpers/version@master
|
||||
with:
|
||||
type: ${{ env.BUILD_TYPE }}
|
||||
|
||||
- name: Signing image
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: home-assistant/actions/helpers/codenotary@master
|
||||
with:
|
||||
source: dir://${{ github.workspace }}
|
||||
user: ${{ secrets.VCN_USER }}
|
||||
password: ${{ secrets.VCN_PASSWORD }}
|
||||
organisation: ${{ secrets.VCN_ORG }}
|
||||
|
||||
version:
|
||||
name: Update version
|
||||
needs: ["init", "run_supervisor"]
|
||||
@@ -134,7 +170,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: Initialize git
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
@@ -155,13 +191,14 @@ jobs:
|
||||
run_supervisor:
|
||||
runs-on: ubuntu-latest
|
||||
name: Run the Supervisor
|
||||
needs: ["build"]
|
||||
needs: ["build", "codenotary", "init"]
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
- name: Build the Supervisor
|
||||
uses: home-assistant/builder@2021.01.1
|
||||
if: needs.init.outputs.publish != 'true'
|
||||
uses: home-assistant/builder@2021.07.0
|
||||
with:
|
||||
args: |
|
||||
--test \
|
||||
@@ -169,6 +206,12 @@ jobs:
|
||||
--target /data \
|
||||
--generic runner
|
||||
|
||||
- name: Pull Supervisor
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
run: |
|
||||
docker pull homeassistant/amd64-hassio-supervisor:${{ needs.init.outputs.version }}
|
||||
docker tag homeassistant/amd64-hassio-supervisor:${{ needs.init.outputs.version }} homeassistant/amd64-hassio-supervisor:runner
|
||||
|
||||
- name: Create the Supervisor
|
||||
run: |
|
||||
mkdir -p /tmp/supervisor/data
|
||||
@@ -194,22 +237,63 @@ jobs:
|
||||
SUPERVISOR=$(docker inspect --format='{{.NetworkSettings.IPAddress}}' hassio_supervisor)
|
||||
ping="error"
|
||||
while [ "$ping" != "ok" ]; do
|
||||
ping=$(curl -sSL "http://$SUPERVISOR/supervisor/ping" | jq -r .result)
|
||||
ping=$(curl -sSL "http://$SUPERVISOR/supervisor/ping" | jq -r '.result')
|
||||
sleep 5
|
||||
done
|
||||
docker logs hassio_supervisor
|
||||
|
||||
- name: Check the Supervisor
|
||||
run: |
|
||||
echo "Checking supervisor info"
|
||||
test=$(docker exec hassio_cli ha supervisor info --no-progress --raw-json | jq -r .result)
|
||||
test=$(docker exec hassio_cli ha supervisor info --no-progress --raw-json | jq -r '.result')
|
||||
if [ "$test" != "ok" ];then
|
||||
docker logs hassio_supervisor
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Checking supervisor network info"
|
||||
test=$(docker exec hassio_cli ha network info --no-progress --raw-json | jq -r .result)
|
||||
test=$(docker exec hassio_cli ha network info --no-progress --raw-json | jq -r '.result')
|
||||
if [ "$test" != "ok" ];then
|
||||
docker logs hassio_supervisor
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check the Store / Addon
|
||||
run: |
|
||||
echo "Install Core SSH Add-on"
|
||||
test=$(docker exec hassio_cli ha addons install core_ssh --no-progress --raw-json | jq -r '.result')
|
||||
if [ "$test" != "ok" ];then
|
||||
docker logs hassio_supervisor
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Start Core SSH Add-on"
|
||||
test=$(docker exec hassio_cli ha addons start core_ssh --no-progress --raw-json | jq -r '.result')
|
||||
if [ "$test" != "ok" ];then
|
||||
docker logs hassio_supervisor
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check the Supervisor code sign
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
run: |
|
||||
echo "Enable Content-Trust"
|
||||
test=$(docker exec hassio_cli ha security options --content-trust=true --no-progress --raw-json | jq -r '.result')
|
||||
if [ "$test" != "ok" ];then
|
||||
docker logs hassio_supervisor
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Run supervisor health check"
|
||||
test=$(docker exec hassio_cli ha resolution healthcheck --no-progress --raw-json | jq -r '.result')
|
||||
if [ "$test" != "ok" ];then
|
||||
docker logs hassio_supervisor
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Check supervisor unhealthy"
|
||||
test=$(docker exec hassio_cli ha resolution info --no-progress --raw-json | jq -r '.data.unhealthy[]')
|
||||
if [ "$test" != "" ];then
|
||||
docker logs hassio_supervisor
|
||||
exit 1
|
||||
fi
|
||||
|
87
.github/workflows/ci.yaml
vendored
87
.github/workflows/ci.yaml
vendored
@@ -8,8 +8,9 @@ on:
|
||||
pull_request: ~
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: 3.8
|
||||
DEFAULT_PYTHON: 3.9
|
||||
PRE_COMMIT_HOME: ~/.cache/pre-commit
|
||||
DEFAULT_VCN: v0.9.8
|
||||
|
||||
jobs:
|
||||
# Separate job to pre-populate the base dependency cache
|
||||
@@ -18,19 +19,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.8]
|
||||
python-version: [3.9]
|
||||
name: Prepare Python ${{ matrix.python-version }} dependencies
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v2.2.1
|
||||
uses: actions/setup-python@v2.2.2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -47,7 +48,7 @@ jobs:
|
||||
pip install -r requirements.txt -r requirements_tests.txt
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -66,15 +67,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.1
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -95,7 +96,7 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Register hadolint problem matcher
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
||||
@@ -110,15 +111,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.1
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -130,7 +131,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -154,15 +155,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.1
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -186,15 +187,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.1
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -206,7 +207,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -227,15 +228,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.1
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -247,7 +248,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -271,15 +272,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.1
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -303,15 +304,15 @@ jobs:
|
||||
needs: prepare
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.1
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -323,7 +324,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -343,19 +344,23 @@ jobs:
|
||||
needs: prepare
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.8]
|
||||
python-version: [3.9]
|
||||
name: Run tests Python ${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2.2.1
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install VCN tools
|
||||
uses: home-assistant/actions/helpers/vcn@master
|
||||
with:
|
||||
vnc_version: ${{ env.DEFAULT_VCN }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -390,7 +395,7 @@ jobs:
|
||||
-o console_output_style=count \
|
||||
tests
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v2.2.2
|
||||
uses: actions/upload-artifact@v2.2.4
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}
|
||||
path: .coverage
|
||||
@@ -401,15 +406,15 @@ jobs:
|
||||
needs: pytest
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.2.1
|
||||
uses: actions/setup-python@v2.2.2
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -428,4 +433,4 @@ jobs:
|
||||
coverage report
|
||||
coverage xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1.2.1
|
||||
uses: codecov/codecov-action@v2.0.3
|
||||
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v2.0.3
|
||||
- uses: dessant/lock-threads@v2.1.2
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-lock-inactive-days: "30"
|
||||
|
2
.github/workflows/release-drafter.yml
vendored
2
.github/workflows/release-drafter.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
name: Release Drafter
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
4
.github/workflows/sentry.yaml
vendored
4
.github/workflows/sentry.yaml
vendored
@@ -10,9 +10,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v2.3.4
|
||||
- name: Sentry Release
|
||||
uses: getsentry/action-release@v1.1
|
||||
uses: getsentry/action-release@v1.1.6
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v3.0.15
|
||||
- uses: actions/stale@v4
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 60
|
||||
|
@@ -1,5 +1,6 @@
|
||||
ignored:
|
||||
- DL3018
|
||||
- DL3003
|
||||
- DL3006
|
||||
- DL3013
|
||||
- DL3018
|
||||
- SC2155
|
||||
|
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 20.8b1
|
||||
rev: 21.8b0
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
|
21
.vcnignore
Normal file
21
.vcnignore
Normal file
@@ -0,0 +1,21 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Distribution / packaging
|
||||
*.egg-info/
|
||||
|
||||
# General files
|
||||
.git
|
||||
.github
|
||||
.devcontainer
|
||||
.vscode
|
||||
.tox
|
||||
|
||||
# Data
|
||||
home-assistant-polymer/
|
||||
script/
|
||||
tests/
|
||||
data/
|
||||
venv/
|
16
.vscode/tasks.json
vendored
16
.vscode/tasks.json
vendored
@@ -4,7 +4,7 @@
|
||||
{
|
||||
"label": "Run Supervisor",
|
||||
"type": "shell",
|
||||
"command": "./scripts/run-supervisor.sh",
|
||||
"command": "./scripts/supervisor.sh",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
@@ -15,20 +15,6 @@
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Build Supervisor",
|
||||
"type": "shell",
|
||||
"command": "./scripts/build-supervisor.sh",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Run Supervisor CLI",
|
||||
"type": "shell",
|
||||
|
41
Dockerfile
41
Dockerfile
@@ -1,13 +1,18 @@
|
||||
ARG BUILD_FROM
|
||||
FROM $BUILD_FROM
|
||||
FROM ${BUILD_FROM}
|
||||
|
||||
ENV \
|
||||
S6_SERVICES_GRACETIME=10000 \
|
||||
SUPERVISOR_API=http://localhost
|
||||
|
||||
ARG BUILD_ARCH
|
||||
ARG VCN_VERSION
|
||||
WORKDIR /usr/src
|
||||
|
||||
# Install base
|
||||
RUN \
|
||||
apk add --no-cache \
|
||||
set -x \
|
||||
&& apk add --no-cache \
|
||||
eudev \
|
||||
eudev-libs \
|
||||
git \
|
||||
@@ -15,10 +20,34 @@ RUN \
|
||||
libffi \
|
||||
libpulse \
|
||||
musl \
|
||||
openssl
|
||||
|
||||
ARG BUILD_ARCH
|
||||
WORKDIR /usr/src
|
||||
openssl \
|
||||
&& apk add --no-cache --virtual .build-dependencies \
|
||||
build-base \
|
||||
go \
|
||||
\
|
||||
&& git clone -b v${VCN_VERSION} --depth 1 \
|
||||
https://github.com/codenotary/vcn \
|
||||
&& cd vcn \
|
||||
\
|
||||
&& if [ "${BUILD_ARCH}" = "armhf" ]; then \
|
||||
GOARM=6 GOARCH=arm go build -o vcn -ldflags="-s -w" ./cmd/vcn; \
|
||||
elif [ "${BUILD_ARCH}" = "armv7" ]; then \
|
||||
GOARM=7 GOARCH=arm go build -o vcn -ldflags="-s -w" ./cmd/vcn; \
|
||||
elif [ "${BUILD_ARCH}" = "aarch64" ]; then \
|
||||
GOARCH=arm64 go build -o vcn -ldflags="-s -w" ./cmd/vcn; \
|
||||
elif [ "${BUILD_ARCH}" = "i386" ]; then \
|
||||
GOARCH=386 go build -o vcn -ldflags="-s -w" ./cmd/vcn; \
|
||||
elif [ "${BUILD_ARCH}" = "amd64" ]; then \
|
||||
GOARCH=amd64 go build -o vcn -ldflags="-s -w" ./cmd/vcn; \
|
||||
else \
|
||||
exit 1; \
|
||||
fi \
|
||||
\
|
||||
&& rm -rf /root/go /root/.cache \
|
||||
&& mv vcn /usr/bin/vcn \
|
||||
\
|
||||
&& apk del .build-dependencies \
|
||||
&& rm -rf /usr/src/vcn
|
||||
|
||||
# Install requirements
|
||||
COPY requirements.txt .
|
||||
|
23
build.json
23
build.json
@@ -1,13 +1,24 @@
|
||||
{
|
||||
"image": "homeassistant/{arch}-hassio-supervisor",
|
||||
"shadow_repository": "ghcr.io/home-assistant",
|
||||
"build_from": {
|
||||
"aarch64": "homeassistant/aarch64-base-python:3.8-alpine3.12",
|
||||
"armhf": "homeassistant/armhf-base-python:3.8-alpine3.12",
|
||||
"armv7": "homeassistant/armv7-base-python:3.8-alpine3.12",
|
||||
"amd64": "homeassistant/amd64-base-python:3.8-alpine3.12",
|
||||
"i386": "homeassistant/i386-base-python:3.8-alpine3.12"
|
||||
"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"
|
||||
},
|
||||
"args": {
|
||||
"VCN_VERSION": "0.9.8"
|
||||
},
|
||||
"labels": {
|
||||
"io.hass.type": "supervisor"
|
||||
"io.hass.type": "supervisor",
|
||||
"org.opencontainers.image.title": "Home Assistant Supervisor",
|
||||
"org.opencontainers.image.description": "Container-based system for managing Home Assistant Core installation",
|
||||
"org.opencontainers.image.source": "https://github.com/home-assistant/supervisor",
|
||||
"org.opencontainers.image.authors": "The Home Assistant Authors",
|
||||
"org.opencontainers.image.url": "https://www.home-assistant.io/",
|
||||
"org.opencontainers.image.documentation": "https://www.home-assistant.io/docs/",
|
||||
"org.opencontainers.image.licenses": "Apache License 2.0"
|
||||
}
|
||||
}
|
||||
|
Submodule home-assistant-polymer updated: a9192ae2e1...67ab63f00e
4
pylintrc
4
pylintrc
@@ -4,6 +4,9 @@ jobs=2
|
||||
|
||||
good-names=id,i,j,k,ex,Run,_,fp,T
|
||||
|
||||
extension-pkg-whitelist=
|
||||
ciso8601
|
||||
|
||||
# Reasons disabled:
|
||||
# format - handled by black
|
||||
# locally-disabled - it spams too much
|
||||
@@ -37,6 +40,7 @@ disable=
|
||||
too-many-return-statements,
|
||||
too-many-statements,
|
||||
unused-argument,
|
||||
consider-using-with
|
||||
|
||||
[EXCEPTIONS]
|
||||
overgeneral-exceptions=Exception
|
||||
|
@@ -1,20 +1,20 @@
|
||||
aiohttp==3.7.3
|
||||
aiohttp==3.7.4.post0
|
||||
async_timeout==3.0.1
|
||||
atomicwrites==1.4.0
|
||||
attrs==20.3.0
|
||||
awesomeversion==21.2.0
|
||||
brotli==1.0.9
|
||||
attrs==21.2.0
|
||||
awesomeversion==21.8.1
|
||||
brotlipy==0.7.0
|
||||
cchardet==2.1.7
|
||||
colorlog==4.7.2
|
||||
ciso8601==2.2.0
|
||||
colorlog==6.4.1
|
||||
cpe==1.2.1
|
||||
cryptography==3.3.1
|
||||
debugpy==1.2.1
|
||||
docker==4.4.1
|
||||
gitpython==3.1.12
|
||||
jinja2==2.11.3
|
||||
pulsectl==20.5.1
|
||||
pytz==2021.1
|
||||
cryptography==3.4.6
|
||||
debugpy==1.4.1
|
||||
docker==5.0.0
|
||||
gitpython==3.1.18
|
||||
jinja2==3.0.1
|
||||
pulsectl==21.5.18
|
||||
pyudev==0.22.0
|
||||
ruamel.yaml==0.15.100
|
||||
sentry-sdk==0.19.5
|
||||
sentry-sdk==1.3.1
|
||||
voluptuous==0.12.1
|
||||
|
@@ -1,14 +1,14 @@
|
||||
black==20.8b1
|
||||
codecov==2.1.11
|
||||
coverage==5.4
|
||||
flake8-docstrings==1.5.0
|
||||
flake8==3.8.4
|
||||
pre-commit==2.10.0
|
||||
pydocstyle==5.1.1
|
||||
pylint==2.6.0
|
||||
black==21.8b0
|
||||
codecov==2.1.12
|
||||
coverage==5.5
|
||||
flake8-docstrings==1.6.0
|
||||
flake8==3.9.2
|
||||
pre-commit==2.14.1
|
||||
pydocstyle==6.1.1
|
||||
pylint==2.10.2
|
||||
pytest-aiohttp==0.3.0
|
||||
pytest-asyncio==0.12.0 # NB!: Versions over 0.12.0 breaks pytest-aiohttp (https://github.com/aio-libs/pytest-aiohttp/issues/16)
|
||||
pytest-cov==2.11.1
|
||||
pytest-cov==2.12.1
|
||||
pytest-timeout==1.4.2
|
||||
pytest==6.2.2
|
||||
pyupgrade==2.9.0
|
||||
pytest==6.2.4
|
||||
pyupgrade==2.25.0
|
||||
|
@@ -3,15 +3,16 @@
|
||||
# Start udev service
|
||||
# ==============================================================================
|
||||
|
||||
if bashio::fs.directory_exists /run/udev; then
|
||||
if bashio::fs.directory_exists /run/udev && ! bashio::fs.file_exists /run/.old_udev; then
|
||||
bashio::log.info "Using udev information from host"
|
||||
bashio::exit.ok
|
||||
fi
|
||||
|
||||
|
||||
bashio::log.info "Setup udev backend inside container"
|
||||
udevd --daemon
|
||||
|
||||
bashio::log.info "Update udev information"
|
||||
touch /run/.old_udev
|
||||
if udevadm trigger; then
|
||||
udevadm settle || true
|
||||
else
|
||||
|
@@ -1,28 +0,0 @@
|
||||
#!/bin/bash
|
||||
source "${BASH_SOURCE[0]%/*}/common.sh"
|
||||
|
||||
set -eE
|
||||
|
||||
DOCKER_TIMEOUT=30
|
||||
DOCKER_PID=0
|
||||
|
||||
function build_supervisor() {
|
||||
docker pull homeassistant/amd64-builder:dev
|
||||
|
||||
docker run --rm \
|
||||
--privileged \
|
||||
-v /run/docker.sock:/run/docker.sock \
|
||||
-v "$(pwd):/data" \
|
||||
homeassistant/amd64-builder:dev \
|
||||
--generic latest \
|
||||
--target /data \
|
||||
--test \
|
||||
--amd64 \
|
||||
--no-cache
|
||||
}
|
||||
|
||||
echo "Build Supervisor"
|
||||
start_docker
|
||||
trap "stop_docker" ERR
|
||||
|
||||
build_supervisor
|
@@ -4,6 +4,9 @@ function start_docker() {
|
||||
local starttime
|
||||
local endtime
|
||||
|
||||
update-alternatives --set iptables /usr/sbin/iptables-legacy || echo "Fails adjust iptables"
|
||||
update-alternatives --set ip6tables /usr/sbin/iptables-legacy || echo "Fails adjust ip6tables"
|
||||
|
||||
echo "Starting docker."
|
||||
dockerd 2> /dev/null &
|
||||
DOCKER_PID=$!
|
||||
|
@@ -1,12 +1,26 @@
|
||||
#!/bin/bash
|
||||
source "${BASH_SOURCE[0]%/*}/common.sh"
|
||||
source "${BASH_SOURCE[0]%/*}/build-supervisor.sh"
|
||||
|
||||
set -eE
|
||||
|
||||
DOCKER_TIMEOUT=30
|
||||
DOCKER_PID=0
|
||||
|
||||
function build_supervisor() {
|
||||
docker pull homeassistant/amd64-builder:dev
|
||||
|
||||
docker run --rm \
|
||||
--privileged \
|
||||
-v /run/docker.sock:/run/docker.sock \
|
||||
-v "$(pwd):/data" \
|
||||
homeassistant/amd64-builder:dev \
|
||||
--generic latest \
|
||||
--target /data \
|
||||
--test \
|
||||
--amd64 \
|
||||
--no-cache
|
||||
}
|
||||
|
||||
|
||||
function cleanup_docker() {
|
||||
echo "Cleaning up stopped containers..."
|
||||
@@ -74,6 +88,21 @@ function init_udev() {
|
||||
udevadm trigger && udevadm settle
|
||||
}
|
||||
|
||||
function init_os-agent() {
|
||||
if pgrep os-agent; then
|
||||
echo "os-agent is running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -f /usr/sbin/os-agent ]; then
|
||||
curl -Lo /usr/sbin/os-agent https://github.com/home-assistant/os-agent/releases/latest/download/os-agent-debian-amd64.bin
|
||||
curl -Lo /etc/dbus-1/system.d/io.hass.conf https://raw.githubusercontent.com/home-assistant/os-agent/main/contrib/io.hass.conf
|
||||
chmod a+x /usr/sbin/os-agent
|
||||
fi
|
||||
|
||||
/usr/sbin/os-agent &
|
||||
}
|
||||
|
||||
echo "Run Supervisor"
|
||||
|
||||
start_docker
|
||||
@@ -85,6 +114,7 @@ if [ "$( docker container inspect -f '{{.State.Status}}' hassio_supervisor )" ==
|
||||
docker rm -f hassio_supervisor
|
||||
init_dbus
|
||||
init_udev
|
||||
init_os-agent
|
||||
cleanup_lastboot
|
||||
run_supervisor
|
||||
stop_docker
|
||||
@@ -97,6 +127,7 @@ else
|
||||
cleanup_docker
|
||||
init_dbus
|
||||
init_udev
|
||||
init_os-agent
|
||||
run_supervisor
|
||||
stop_docker
|
||||
fi
|
||||
fi
|
4
setup.py
4
setup.py
@@ -33,6 +33,7 @@ setup(
|
||||
packages=[
|
||||
"supervisor.addons",
|
||||
"supervisor.api",
|
||||
"supervisor.backups",
|
||||
"supervisor.dbus.network",
|
||||
"supervisor.dbus.payloads",
|
||||
"supervisor.dbus",
|
||||
@@ -44,11 +45,12 @@ setup(
|
||||
"supervisor.jobs",
|
||||
"supervisor.misc",
|
||||
"supervisor.plugins",
|
||||
"supervisor.resolution.checks",
|
||||
"supervisor.resolution.evaluations",
|
||||
"supervisor.resolution.fixups",
|
||||
"supervisor.resolution",
|
||||
"supervisor.services.modules",
|
||||
"supervisor.services",
|
||||
"supervisor.snapshots",
|
||||
"supervisor.store",
|
||||
"supervisor.utils",
|
||||
"supervisor",
|
||||
|
@@ -154,17 +154,16 @@ class AddonManager(CoreSysAttributes):
|
||||
async def install(self, slug: str) -> None:
|
||||
"""Install an add-on."""
|
||||
if slug in self.local:
|
||||
_LOGGER.warning("Add-on %s is already installed", slug)
|
||||
return
|
||||
raise AddonsError(f"Add-on {slug} is already installed", _LOGGER.warning)
|
||||
store = self.store.get(slug)
|
||||
|
||||
if not store:
|
||||
_LOGGER.error("Add-on %s not exists", slug)
|
||||
raise AddonsError()
|
||||
raise AddonsError(f"Add-on {slug} not exists", _LOGGER.error)
|
||||
|
||||
if not store.available:
|
||||
_LOGGER.error("Add-on %s not supported on that platform", slug)
|
||||
raise AddonsNotSupportedError()
|
||||
raise AddonsNotSupportedError(
|
||||
f"Add-on {slug} not supported on that platform", _LOGGER.error
|
||||
)
|
||||
|
||||
self.data.install(store)
|
||||
addon = Addon(self.coresys, slug)
|
||||
@@ -256,37 +255,38 @@ class AddonManager(CoreSysAttributes):
|
||||
async def update(self, slug: str) -> None:
|
||||
"""Update add-on."""
|
||||
if slug not in self.local:
|
||||
_LOGGER.error("Add-on %s is not installed", slug)
|
||||
raise AddonsError()
|
||||
raise AddonsError(f"Add-on {slug} is not installed", _LOGGER.error)
|
||||
addon = self.local[slug]
|
||||
|
||||
if addon.is_detached:
|
||||
_LOGGER.error("Add-on %s is not available inside store", slug)
|
||||
raise AddonsError()
|
||||
raise AddonsError(
|
||||
f"Add-on {slug} is not available inside store", _LOGGER.error
|
||||
)
|
||||
store = self.store[slug]
|
||||
|
||||
if addon.version == store.version:
|
||||
_LOGGER.warning("No update available for add-on %s", slug)
|
||||
return
|
||||
raise AddonsError(f"No update available for add-on {slug}", _LOGGER.warning)
|
||||
|
||||
# Check if available, Maybe something have changed
|
||||
if not store.available:
|
||||
_LOGGER.error("Add-on %s not supported on that platform", slug)
|
||||
raise AddonsNotSupportedError()
|
||||
raise AddonsNotSupportedError(
|
||||
f"Add-on {slug} not supported on that platform", _LOGGER.error
|
||||
)
|
||||
|
||||
# Update instance
|
||||
last_state: AddonState = addon.state
|
||||
old_image = addon.image
|
||||
try:
|
||||
await addon.instance.update(store.version, store.image)
|
||||
|
||||
# Cleanup
|
||||
with suppress(DockerError):
|
||||
await addon.instance.cleanup()
|
||||
except DockerError as err:
|
||||
raise AddonsError() from err
|
||||
else:
|
||||
self.data.update(store)
|
||||
_LOGGER.info("Add-on '%s' successfully updated", slug)
|
||||
|
||||
_LOGGER.info("Add-on '%s' successfully updated", slug)
|
||||
self.data.update(store)
|
||||
|
||||
# Cleanup
|
||||
with suppress(DockerError):
|
||||
await addon.instance.cleanup(old_image=old_image)
|
||||
|
||||
# Setup/Fix AppArmor profile
|
||||
await addon.install_apparmor()
|
||||
|
@@ -22,6 +22,8 @@ from ..const import (
|
||||
ATTR_AUDIO_OUTPUT,
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_BOOT,
|
||||
ATTR_DATA,
|
||||
ATTR_EVENT,
|
||||
ATTR_IMAGE,
|
||||
ATTR_INGRESS_ENTRY,
|
||||
ATTR_INGRESS_PANEL,
|
||||
@@ -32,8 +34,10 @@ from ..const import (
|
||||
ATTR_PORTS,
|
||||
ATTR_PROTECTED,
|
||||
ATTR_SCHEMA,
|
||||
ATTR_SLUG,
|
||||
ATTR_STATE,
|
||||
ATTR_SYSTEM,
|
||||
ATTR_TYPE,
|
||||
ATTR_USER,
|
||||
ATTR_UUID,
|
||||
ATTR_VERSION,
|
||||
@@ -50,20 +54,22 @@ from ..exceptions import (
|
||||
AddonConfigurationError,
|
||||
AddonsError,
|
||||
AddonsNotSupportedError,
|
||||
ConfigurationFileError,
|
||||
DockerError,
|
||||
DockerRequestError,
|
||||
HostAppArmorError,
|
||||
JsonFileError,
|
||||
)
|
||||
from ..hardware.data import Device
|
||||
from ..homeassistant.const import WSEvent, WSType
|
||||
from ..utils import check_port
|
||||
from ..utils.apparmor import adjust_profile
|
||||
from ..utils.json import read_json_file, write_json_file
|
||||
from ..utils.tar import atomic_contents_add, secure_path
|
||||
from .const import AddonBackupMode
|
||||
from .model import AddonModel, Data
|
||||
from .options import AddonOptions
|
||||
from .utils import remove_data
|
||||
from .validate import SCHEMA_ADDON_SNAPSHOT
|
||||
from .validate import SCHEMA_ADDON_BACKUP
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -74,7 +80,7 @@ RE_WEBUI = re.compile(
|
||||
|
||||
RE_WATCHDOG = re.compile(
|
||||
r"^(?:(?P<s_prefix>https?|tcp)|\[PROTO:(?P<t_proto>\w+)\])"
|
||||
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$"
|
||||
r":\/\/\[HOST\]:(?:\[PORT:)?(?P<t_port>\d+)\]?(?P<s_suffix>.*)$"
|
||||
)
|
||||
|
||||
RE_OLD_AUDIO = re.compile(r"\d+,\d+")
|
||||
@@ -89,12 +95,34 @@ class Addon(AddonModel):
|
||||
"""Initialize data holder."""
|
||||
super().__init__(coresys, slug)
|
||||
self.instance: DockerAddon = DockerAddon(coresys, self)
|
||||
self.state: AddonState = AddonState.UNKNOWN
|
||||
self._state: AddonState = AddonState.UNKNOWN
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return internal representation."""
|
||||
return f"<Addon: {self.slug}>"
|
||||
|
||||
@property
|
||||
def state(self) -> AddonState:
|
||||
"""Return state of the add-on."""
|
||||
return self._state
|
||||
|
||||
@state.setter
|
||||
def state(self, new_state: AddonState) -> None:
|
||||
"""Set the add-on into new state."""
|
||||
if self._state == new_state:
|
||||
return
|
||||
self._state = new_state
|
||||
self.sys_homeassistant.websocket.send_command(
|
||||
{
|
||||
ATTR_TYPE: WSType.SUPERVISOR_EVENT,
|
||||
ATTR_DATA: {
|
||||
ATTR_EVENT: WSEvent.ADDON,
|
||||
ATTR_SLUG: self.slug,
|
||||
ATTR_STATE: new_state,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def in_progress(self) -> bool:
|
||||
"""Return True if a task is in progress."""
|
||||
@@ -398,17 +426,21 @@ class Addon(AddonModel):
|
||||
|
||||
@property
|
||||
def devices(self) -> Set[Device]:
|
||||
"""Create a schema for add-on options."""
|
||||
raw_schema = self.data[ATTR_SCHEMA]
|
||||
if isinstance(raw_schema, bool) or not raw_schema:
|
||||
return set()
|
||||
|
||||
# Validate devices
|
||||
options_validator = AddonOptions(self.coresys, raw_schema)
|
||||
"""Extract devices from add-on options."""
|
||||
options_schema = self.schema
|
||||
with suppress(vol.Invalid):
|
||||
options_validator(self.options)
|
||||
options_schema.validate(self.options)
|
||||
|
||||
return options_validator.devices
|
||||
return options_schema.devices
|
||||
|
||||
@property
|
||||
def pwned(self) -> Set[str]:
|
||||
"""Extract pwned data for add-on options."""
|
||||
options_schema = self.schema
|
||||
with suppress(vol.Invalid):
|
||||
options_schema.validate(self.options)
|
||||
|
||||
return options_schema.pwned
|
||||
|
||||
def save_persist(self) -> None:
|
||||
"""Save data of add-on."""
|
||||
@@ -422,7 +454,7 @@ class Addon(AddonModel):
|
||||
application = RE_WATCHDOG.match(url)
|
||||
|
||||
# extract arguments
|
||||
t_port = application.group("t_port")
|
||||
t_port = int(application.group("t_port"))
|
||||
t_proto = application.group("t_proto")
|
||||
s_prefix = application.group("s_prefix") or ""
|
||||
s_suffix = application.group("s_suffix") or ""
|
||||
@@ -446,8 +478,8 @@ class Addon(AddonModel):
|
||||
# Make HTTP request
|
||||
try:
|
||||
url = f"{proto}://{self.ip_address}:{port}{s_suffix}"
|
||||
async with self.sys_websession_ssl.get(
|
||||
url, timeout=WATCHDOG_TIMEOUT
|
||||
async with self.sys_websession.get(
|
||||
url, timeout=WATCHDOG_TIMEOUT, ssl=False
|
||||
) as req:
|
||||
if req.status < 300:
|
||||
return True
|
||||
@@ -462,7 +494,7 @@ class Addon(AddonModel):
|
||||
await self.sys_homeassistant.secrets.reload()
|
||||
|
||||
try:
|
||||
options = self.schema(self.options)
|
||||
options = self.schema.validate(self.options)
|
||||
write_json_file(self.path_options, options)
|
||||
except vol.Invalid as ex:
|
||||
_LOGGER.error(
|
||||
@@ -470,7 +502,7 @@ class Addon(AddonModel):
|
||||
self.slug,
|
||||
humanize_error(self.options, ex),
|
||||
)
|
||||
except JsonFileError:
|
||||
except ConfigurationFileError:
|
||||
_LOGGER.error("Add-on %s can't write options", self.slug)
|
||||
else:
|
||||
_LOGGER.debug("Add-on %s write options: %s", self.slug, options)
|
||||
@@ -551,7 +583,9 @@ class Addon(AddonModel):
|
||||
|
||||
# create voluptuous
|
||||
new_schema = vol.Schema(
|
||||
vol.All(dict, AddonOptions(self.coresys, new_raw_schema))
|
||||
vol.All(
|
||||
dict, AddonOptions(self.coresys, new_raw_schema, self.name, self.slug)
|
||||
)
|
||||
)
|
||||
|
||||
# validate
|
||||
@@ -583,7 +617,7 @@ class Addon(AddonModel):
|
||||
try:
|
||||
await self.instance.run()
|
||||
except DockerRequestError as err:
|
||||
self.state = AddonState.STOPPED
|
||||
self.state = AddonState.ERROR
|
||||
raise AddonsError() from err
|
||||
except DockerError as err:
|
||||
self.state = AddonState.ERROR
|
||||
@@ -594,8 +628,9 @@ class Addon(AddonModel):
|
||||
async def stop(self) -> None:
|
||||
"""Stop add-on."""
|
||||
try:
|
||||
return await self.instance.stop()
|
||||
await self.instance.stop()
|
||||
except DockerRequestError as err:
|
||||
self.state = AddonState.ERROR
|
||||
raise AddonsError() from err
|
||||
except DockerError as err:
|
||||
self.state = AddonState.ERROR
|
||||
@@ -644,8 +679,25 @@ class Addon(AddonModel):
|
||||
except DockerError as err:
|
||||
raise AddonsError() from err
|
||||
|
||||
async def snapshot(self, tar_file: tarfile.TarFile) -> None:
|
||||
"""Snapshot state of an add-on."""
|
||||
async def _backup_command(self, command: str) -> None:
|
||||
try:
|
||||
command_return = await self.instance.run_inside(command)
|
||||
if command_return.exit_code != 0:
|
||||
_LOGGER.error(
|
||||
"Pre-/Post backup command returned error code: %s",
|
||||
command_return.exit_code,
|
||||
)
|
||||
raise AddonsError()
|
||||
except DockerError as err:
|
||||
_LOGGER.error(
|
||||
"Failed running pre-/post backup command %s: %s", command, err
|
||||
)
|
||||
raise AddonsError() from err
|
||||
|
||||
async def backup(self, tar_file: tarfile.TarFile) -> None:
|
||||
"""Backup state of an add-on."""
|
||||
is_running = await self.is_running()
|
||||
|
||||
with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp:
|
||||
temp_path = Path(temp)
|
||||
|
||||
@@ -666,7 +718,7 @@ class Addon(AddonModel):
|
||||
# Store local configs/state
|
||||
try:
|
||||
write_json_file(temp_path.joinpath("addon.json"), data)
|
||||
except JsonFileError as err:
|
||||
except ConfigurationFileError as err:
|
||||
_LOGGER.error("Can't save meta for %s", self.slug)
|
||||
raise AddonsError() from err
|
||||
|
||||
@@ -682,36 +734,56 @@ class Addon(AddonModel):
|
||||
# write into tarfile
|
||||
def _write_tarfile():
|
||||
"""Write tar inside loop."""
|
||||
with tar_file as snapshot:
|
||||
# Snapshot system
|
||||
with tar_file as backup:
|
||||
# Backup system
|
||||
|
||||
snapshot.add(temp, arcname=".")
|
||||
backup.add(temp, arcname=".")
|
||||
|
||||
# Snapshot data
|
||||
# Backup data
|
||||
atomic_contents_add(
|
||||
snapshot,
|
||||
backup,
|
||||
self.path_data,
|
||||
excludes=self.snapshot_exclude,
|
||||
excludes=self.backup_exclude,
|
||||
arcname="data",
|
||||
)
|
||||
|
||||
if (
|
||||
is_running
|
||||
and self.backup_mode == AddonBackupMode.HOT
|
||||
and self.backup_pre is not None
|
||||
):
|
||||
await self._backup_command(self.backup_pre)
|
||||
elif is_running and self.backup_mode == AddonBackupMode.COLD:
|
||||
_LOGGER.info("Shutdown add-on %s for cold backup", self.slug)
|
||||
await self.instance.stop()
|
||||
|
||||
try:
|
||||
_LOGGER.info("Building snapshot for add-on %s", self.slug)
|
||||
_LOGGER.info("Building backup for add-on %s", self.slug)
|
||||
await self.sys_run_in_executor(_write_tarfile)
|
||||
except (tarfile.TarError, OSError) as err:
|
||||
_LOGGER.error("Can't write tarfile %s: %s", tar_file, err)
|
||||
raise AddonsError() from err
|
||||
finally:
|
||||
if (
|
||||
is_running
|
||||
and self.backup_mode == AddonBackupMode.HOT
|
||||
and self.backup_post is not None
|
||||
):
|
||||
await self._backup_command(self.backup_post)
|
||||
elif is_running and self.backup_mode is AddonBackupMode.COLD:
|
||||
_LOGGER.info("Starting add-on %s again", self.slug)
|
||||
await self.start()
|
||||
|
||||
_LOGGER.info("Finish snapshot for addon %s", self.slug)
|
||||
_LOGGER.info("Finish backup for addon %s", self.slug)
|
||||
|
||||
async def restore(self, tar_file: tarfile.TarFile) -> None:
|
||||
"""Restore state of an add-on."""
|
||||
with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp:
|
||||
# extract snapshot
|
||||
# extract backup
|
||||
def _extract_tarfile():
|
||||
"""Extract tar snapshot."""
|
||||
with tar_file as snapshot:
|
||||
snapshot.extractall(path=Path(temp), members=secure_path(snapshot))
|
||||
"""Extract tar backup."""
|
||||
with tar_file as backup:
|
||||
backup.extractall(path=Path(temp), members=secure_path(backup))
|
||||
|
||||
try:
|
||||
await self.sys_run_in_executor(_extract_tarfile)
|
||||
@@ -719,18 +791,18 @@ class Addon(AddonModel):
|
||||
_LOGGER.error("Can't read tarfile %s: %s", tar_file, err)
|
||||
raise AddonsError() from err
|
||||
|
||||
# Read snapshot data
|
||||
# Read backup data
|
||||
try:
|
||||
data = read_json_file(Path(temp, "addon.json"))
|
||||
except JsonFileError as err:
|
||||
except ConfigurationFileError as err:
|
||||
raise AddonsError() from err
|
||||
|
||||
# Validate
|
||||
try:
|
||||
data = SCHEMA_ADDON_SNAPSHOT(data)
|
||||
data = SCHEMA_ADDON_BACKUP(data)
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error(
|
||||
"Can't validate %s, snapshot data: %s",
|
||||
"Can't validate %s, backup data: %s",
|
||||
self.slug,
|
||||
humanize_error(data, err),
|
||||
)
|
||||
|
@@ -6,16 +6,24 @@ from typing import TYPE_CHECKING, Dict
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
|
||||
from ..const import ATTR_ARGS, ATTR_BUILD_FROM, ATTR_SQUASH, META_ADDON
|
||||
from ..const import (
|
||||
ATTR_ARGS,
|
||||
ATTR_BUILD_FROM,
|
||||
ATTR_LABELS,
|
||||
ATTR_SQUASH,
|
||||
FILE_SUFFIX_CONFIGURATION,
|
||||
META_ADDON,
|
||||
)
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..utils.json import JsonConfig
|
||||
from ..exceptions import ConfigurationFileError
|
||||
from ..utils.common import FileConfiguration, find_one_filetype
|
||||
from .validate import SCHEMA_BUILD_CONFIG
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AnyAddon
|
||||
|
||||
|
||||
class AddonBuild(JsonConfig, CoreSysAttributes):
|
||||
class AddonBuild(FileConfiguration, CoreSysAttributes):
|
||||
"""Handle build options for add-ons."""
|
||||
|
||||
def __init__(self, coresys: CoreSys, addon: AnyAddon) -> None:
|
||||
@@ -23,9 +31,14 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
||||
self.coresys: CoreSys = coresys
|
||||
self.addon = addon
|
||||
|
||||
super().__init__(
|
||||
Path(self.addon.path_location, "build.json"), SCHEMA_BUILD_CONFIG
|
||||
)
|
||||
try:
|
||||
build_file = find_one_filetype(
|
||||
self.addon.path_location, "build", FILE_SUFFIX_CONFIGURATION
|
||||
)
|
||||
except ConfigurationFileError:
|
||||
build_file = self.addon.path_location / "build.json"
|
||||
|
||||
super().__init__(build_file, SCHEMA_BUILD_CONFIG)
|
||||
|
||||
def save_data(self):
|
||||
"""Ignore save function."""
|
||||
@@ -34,9 +47,12 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
||||
@property
|
||||
def base_image(self) -> str:
|
||||
"""Return base image for this add-on."""
|
||||
return self._data[ATTR_BUILD_FROM].get(
|
||||
self.sys_arch.default, f"homeassistant/{self.sys_arch.default}-base:latest"
|
||||
)
|
||||
if not self._data[ATTR_BUILD_FROM]:
|
||||
return f"ghcr.io/home-assistant/{self.sys_arch.default}-base:latest"
|
||||
|
||||
# Evaluate correct base image
|
||||
arch = self.sys_arch.match(list(self._data[ATTR_BUILD_FROM].keys()))
|
||||
return self._data[ATTR_BUILD_FROM][arch]
|
||||
|
||||
@property
|
||||
def squash(self) -> bool:
|
||||
@@ -48,6 +64,11 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
||||
"""Return additional Docker build arguments."""
|
||||
return self._data[ATTR_ARGS]
|
||||
|
||||
@property
|
||||
def additional_labels(self) -> Dict[str, str]:
|
||||
"""Return additional Docker labels."""
|
||||
return self._data[ATTR_LABELS]
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
"""Return true if the build env is valid."""
|
||||
@@ -64,7 +85,7 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
||||
"path": str(self.addon.path_location),
|
||||
"tag": f"{self.addon.image}:{version!s}",
|
||||
"pull": True,
|
||||
"forcerm": True,
|
||||
"forcerm": not self.sys_dev,
|
||||
"squash": self.squash,
|
||||
"labels": {
|
||||
"io.hass.version": version,
|
||||
@@ -72,6 +93,7 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
||||
"io.hass.type": META_ADDON,
|
||||
"io.hass.name": self._fix_label("name"),
|
||||
"io.hass.description": self._fix_label("description"),
|
||||
**self.additional_labels,
|
||||
},
|
||||
"buildargs": {
|
||||
"BUILD_FROM": self.base_image,
|
||||
|
12
supervisor/addons/const.py
Normal file
12
supervisor/addons/const.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""Add-on static data."""
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class AddonBackupMode(str, Enum):
|
||||
"""Backup mode of an Add-on."""
|
||||
|
||||
HOT = "hot"
|
||||
COLD = "cold"
|
||||
|
||||
|
||||
ATTR_BACKUP = "backup"
|
@@ -1,6 +1,5 @@
|
||||
"""Init file for Supervisor add-on data."""
|
||||
from copy import deepcopy
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from ..const import (
|
||||
@@ -13,16 +12,14 @@ from ..const import (
|
||||
)
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..store.addon import AddonStore
|
||||
from ..utils.json import JsonConfig
|
||||
from ..utils.common import FileConfiguration
|
||||
from .addon import Addon
|
||||
from .validate import SCHEMA_ADDONS_FILE
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
Config = Dict[str, Any]
|
||||
|
||||
|
||||
class AddonsData(JsonConfig, CoreSysAttributes):
|
||||
class AddonsData(FileConfiguration, CoreSysAttributes):
|
||||
"""Hold data for installed Add-ons inside Supervisor."""
|
||||
|
||||
def __init__(self, coresys: CoreSys):
|
||||
|
@@ -4,7 +4,8 @@ from pathlib import Path
|
||||
from typing import Any, Awaitable, Dict, List, Optional
|
||||
|
||||
from awesomeversion import AwesomeVersion, AwesomeVersionException
|
||||
import voluptuous as vol
|
||||
|
||||
from supervisor.addons.const import AddonBackupMode
|
||||
|
||||
from ..const import (
|
||||
ATTR_ADVANCED,
|
||||
@@ -12,6 +13,9 @@ from ..const import (
|
||||
ATTR_ARCH,
|
||||
ATTR_AUDIO,
|
||||
ATTR_AUTH_API,
|
||||
ATTR_BACKUP_EXCLUDE,
|
||||
ATTR_BACKUP_POST,
|
||||
ATTR_BACKUP_PRE,
|
||||
ATTR_BOOT,
|
||||
ATTR_DESCRIPTON,
|
||||
ATTR_DEVICES,
|
||||
@@ -31,7 +35,9 @@ from ..const import (
|
||||
ATTR_HOST_PID,
|
||||
ATTR_IMAGE,
|
||||
ATTR_INGRESS,
|
||||
ATTR_INGRESS_STREAM,
|
||||
ATTR_INIT,
|
||||
ATTR_JOURNALD,
|
||||
ATTR_KERNEL_MODULES,
|
||||
ATTR_LEGACY,
|
||||
ATTR_LOCATON,
|
||||
@@ -45,16 +51,17 @@ from ..const import (
|
||||
ATTR_PORTS,
|
||||
ATTR_PORTS_DESCRIPTION,
|
||||
ATTR_PRIVILEGED,
|
||||
ATTR_REALTIME,
|
||||
ATTR_REPOSITORY,
|
||||
ATTR_SCHEMA,
|
||||
ATTR_SERVICES,
|
||||
ATTR_SLUG,
|
||||
ATTR_SNAPSHOT_EXCLUDE,
|
||||
ATTR_STAGE,
|
||||
ATTR_STARTUP,
|
||||
ATTR_STDIN,
|
||||
ATTR_TIMEOUT,
|
||||
ATTR_TMPFS,
|
||||
ATTR_TRANSLATIONS,
|
||||
ATTR_UART,
|
||||
ATTR_UDEV,
|
||||
ATTR_URL,
|
||||
@@ -71,6 +78,8 @@ from ..const import (
|
||||
AddonStartup,
|
||||
)
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..docker.const import Capabilities
|
||||
from .const import ATTR_BACKUP
|
||||
from .options import AddonOptions, UiOptions
|
||||
from .validate import RE_SERVICE, RE_VOLUME
|
||||
|
||||
@@ -183,6 +192,11 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
"""Return repository of add-on."""
|
||||
return self.data[ATTR_REPOSITORY]
|
||||
|
||||
@property
|
||||
def translations(self) -> dict:
|
||||
"""Return add-on translations."""
|
||||
return self.data[ATTR_TRANSLATIONS]
|
||||
|
||||
@property
|
||||
def latest_version(self) -> AwesomeVersion:
|
||||
"""Return latest version of add-on."""
|
||||
@@ -307,7 +321,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return self.data.get(ATTR_ENVIRONMENT)
|
||||
|
||||
@property
|
||||
def privileged(self) -> List[str]:
|
||||
def privileged(self) -> List[Capabilities]:
|
||||
"""Return list of privilege."""
|
||||
return self.data.get(ATTR_PRIVILEGED, [])
|
||||
|
||||
@@ -346,9 +360,24 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return self.data[ATTR_HASSIO_ROLE]
|
||||
|
||||
@property
|
||||
def snapshot_exclude(self) -> List[str]:
|
||||
"""Return Exclude list for snapshot."""
|
||||
return self.data.get(ATTR_SNAPSHOT_EXCLUDE, [])
|
||||
def backup_exclude(self) -> List[str]:
|
||||
"""Return Exclude list for backup."""
|
||||
return self.data.get(ATTR_BACKUP_EXCLUDE, [])
|
||||
|
||||
@property
|
||||
def backup_pre(self) -> Optional[str]:
|
||||
"""Return pre-backup command."""
|
||||
return self.data.get(ATTR_BACKUP_PRE)
|
||||
|
||||
@property
|
||||
def backup_post(self) -> Optional[str]:
|
||||
"""Return post-backup command."""
|
||||
return self.data.get(ATTR_BACKUP_POST)
|
||||
|
||||
@property
|
||||
def backup_mode(self) -> AddonBackupMode:
|
||||
"""Return if backup is hot/cold."""
|
||||
return self.data[ATTR_BACKUP]
|
||||
|
||||
@property
|
||||
def default_init(self) -> bool:
|
||||
@@ -370,6 +399,11 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
"""Return True if the add-on access support ingress."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def ingress_stream(self) -> bool:
|
||||
"""Return True if post requests to ingress should be streamed."""
|
||||
return self.data[ATTR_INGRESS_STREAM]
|
||||
|
||||
@property
|
||||
def with_gpio(self) -> bool:
|
||||
"""Return True if the add-on access to GPIO interface."""
|
||||
@@ -395,6 +429,11 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
"""Return True if the add-on access to kernel modules."""
|
||||
return self.data[ATTR_KERNEL_MODULES]
|
||||
|
||||
@property
|
||||
def with_realtime(self) -> bool:
|
||||
"""Return True if the add-on need realtime schedule functions."""
|
||||
return self.data[ATTR_REALTIME]
|
||||
|
||||
@property
|
||||
def with_full_access(self) -> bool:
|
||||
"""Return True if the add-on want full access to hardware."""
|
||||
@@ -518,16 +557,16 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return Path(self.path_location, "apparmor.txt")
|
||||
|
||||
@property
|
||||
def schema(self) -> vol.Schema:
|
||||
"""Create a schema for add-on options."""
|
||||
def schema(self) -> AddonOptions:
|
||||
"""Return Addon options validation object."""
|
||||
raw_schema = self.data[ATTR_SCHEMA]
|
||||
|
||||
if isinstance(raw_schema, bool):
|
||||
raw_schema = {}
|
||||
return vol.Schema(vol.All(dict, AddonOptions(self.coresys, raw_schema)))
|
||||
|
||||
return AddonOptions(self.coresys, raw_schema, self.name, self.slug)
|
||||
|
||||
@property
|
||||
def schema_ui(self) -> Optional[List[Dict[str, Any]]]:
|
||||
def schema_ui(self) -> Optional[List[Dict[any, any]]]:
|
||||
"""Create a UI schema for add-on options."""
|
||||
raw_schema = self.data[ATTR_SCHEMA]
|
||||
|
||||
@@ -535,6 +574,11 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return None
|
||||
return UiOptions(self.coresys)(raw_schema)
|
||||
|
||||
@property
|
||||
def with_journald(self) -> bool:
|
||||
"""Return True if the add-on accesses the system journal."""
|
||||
return self.data[ATTR_JOURNALD]
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Compaired add-on objects."""
|
||||
if not isinstance(other, AddonModel):
|
||||
|
@@ -1,4 +1,5 @@
|
||||
"""Add-on Options / UI rendering."""
|
||||
import hashlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
@@ -58,14 +59,20 @@ class AddonOptions(CoreSysAttributes):
|
||||
"""Validate Add-ons Options."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coresys: CoreSys,
|
||||
raw_schema: Dict[str, Any],
|
||||
self, coresys: CoreSys, raw_schema: Dict[str, Any], name: str, slug: str
|
||||
):
|
||||
"""Validate schema."""
|
||||
self.coresys: CoreSys = coresys
|
||||
self.raw_schema: Dict[str, Any] = raw_schema
|
||||
self.devices: Set[Device] = set()
|
||||
self.pwned: Set[str] = set()
|
||||
self._name = name
|
||||
self._slug = slug
|
||||
|
||||
@property
|
||||
def validate(self) -> vol.Schema:
|
||||
"""Create a schema for add-on options."""
|
||||
return vol.Schema(vol.All(dict, self))
|
||||
|
||||
def __call__(self, struct):
|
||||
"""Create schema validator for add-ons options."""
|
||||
@@ -75,7 +82,12 @@ class AddonOptions(CoreSysAttributes):
|
||||
for key, value in struct.items():
|
||||
# Ignore unknown options / remove from list
|
||||
if key not in self.raw_schema:
|
||||
_LOGGER.warning("Unknown options %s", key)
|
||||
_LOGGER.warning(
|
||||
"Option '%s' does not exist in the schema for %s (%s)",
|
||||
key,
|
||||
self._name,
|
||||
self._slug,
|
||||
)
|
||||
continue
|
||||
|
||||
typ = self.raw_schema[key]
|
||||
@@ -90,7 +102,9 @@ class AddonOptions(CoreSysAttributes):
|
||||
# normal value
|
||||
options[key] = self._single_validate(typ, value, key)
|
||||
except (IndexError, KeyError):
|
||||
raise vol.Invalid(f"Type error for {key}") from None
|
||||
raise vol.Invalid(
|
||||
f"Type error for option '{key}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
self._check_missing_options(self.raw_schema, options, "root")
|
||||
return options
|
||||
@@ -100,20 +114,26 @@ class AddonOptions(CoreSysAttributes):
|
||||
"""Validate a single element."""
|
||||
# if required argument
|
||||
if value is None:
|
||||
raise vol.Invalid(f"Missing required option '{key}'") from None
|
||||
raise vol.Invalid(
|
||||
f"Missing required option '{key}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# Lookup secret
|
||||
if str(value).startswith("!secret "):
|
||||
secret: str = value.partition(" ")[2]
|
||||
value = self.sys_homeassistant.secrets.get(secret)
|
||||
if value is None:
|
||||
raise vol.Invalid(f"Unknown secret {secret}") from None
|
||||
raise vol.Invalid(
|
||||
f"Unknown secret '{secret}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# parse extend data from type
|
||||
match = RE_SCHEMA_ELEMENT.match(typ)
|
||||
|
||||
if not match:
|
||||
raise vol.Invalid(f"Unknown type {typ}") from None
|
||||
raise vol.Invalid(
|
||||
f"Unknown type '{typ}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# prepare range
|
||||
range_args = {}
|
||||
@@ -123,6 +143,8 @@ class AddonOptions(CoreSysAttributes):
|
||||
range_args[group_name[2:]] = float(group_value)
|
||||
|
||||
if typ.startswith(_STR) or typ.startswith(_PASSWORD):
|
||||
if typ.startswith(_PASSWORD) and value:
|
||||
self.pwned.add(hashlib.sha1(str(value).encode()).hexdigest())
|
||||
return vol.All(str(value), vol.Range(**range_args))(value)
|
||||
elif typ.startswith(_INT):
|
||||
return vol.All(vol.Coerce(int), vol.Range(**range_args))(value)
|
||||
@@ -144,7 +166,9 @@ class AddonOptions(CoreSysAttributes):
|
||||
try:
|
||||
device = self.sys_hardware.get_by_path(Path(value))
|
||||
except HardwareNotFound:
|
||||
raise vol.Invalid(f"Device {value} does not exists!") from None
|
||||
raise vol.Invalid(
|
||||
f"Device '{value}' does not exists! in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# Have filter
|
||||
if match.group("filter"):
|
||||
@@ -152,14 +176,16 @@ class AddonOptions(CoreSysAttributes):
|
||||
device_filter = _create_device_filter(str_filter)
|
||||
if device not in self.sys_hardware.filter_devices(**device_filter):
|
||||
raise vol.Invalid(
|
||||
f"Device {value} don't match the filter {str_filter}!"
|
||||
f"Device '{value}' don't match the filter {str_filter}! in {self._name} ({self._slug})"
|
||||
)
|
||||
|
||||
# Device valid
|
||||
self.devices.add(device)
|
||||
return str(device.path)
|
||||
|
||||
raise vol.Invalid(f"Fatal error for {key} type {typ}") from None
|
||||
raise vol.Invalid(
|
||||
f"Fatal error for option '{key}' with type '{typ}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
def _nested_validate_list(self, typ: Any, data_list: List[Any], key: str):
|
||||
"""Validate nested items."""
|
||||
@@ -167,7 +193,9 @@ class AddonOptions(CoreSysAttributes):
|
||||
|
||||
# Make sure it is a list
|
||||
if not isinstance(data_list, list):
|
||||
raise vol.Invalid(f"Invalid list for {key}") from None
|
||||
raise vol.Invalid(
|
||||
f"Invalid list for option '{key}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# Process list
|
||||
for element in data_list:
|
||||
@@ -188,13 +216,17 @@ class AddonOptions(CoreSysAttributes):
|
||||
|
||||
# Make sure it is a dict
|
||||
if not isinstance(data_dict, dict):
|
||||
raise vol.Invalid(f"Invalid dict for {key}") from None
|
||||
raise vol.Invalid(
|
||||
f"Invalid dict for option '{key}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# Process dict
|
||||
for c_key, c_value in data_dict.items():
|
||||
# Ignore unknown options / remove from list
|
||||
if c_key not in typ:
|
||||
_LOGGER.warning("Unknown options %s", c_key)
|
||||
_LOGGER.warning(
|
||||
"Unknown option '%s' for %s (%s)", c_key, self._name, self._slug
|
||||
)
|
||||
continue
|
||||
|
||||
# Nested?
|
||||
@@ -214,9 +246,18 @@ class AddonOptions(CoreSysAttributes):
|
||||
"""Check if all options are exists."""
|
||||
missing = set(origin) - set(exists)
|
||||
for miss_opt in missing:
|
||||
if isinstance(origin[miss_opt], str) and origin[miss_opt].endswith("?"):
|
||||
miss_schema = origin[miss_opt]
|
||||
|
||||
# If its a list then value in list decides if its optional like ["str?"]
|
||||
if isinstance(miss_schema, list) and len(miss_schema) > 0:
|
||||
miss_schema = miss_schema[0]
|
||||
|
||||
if isinstance(miss_schema, str) and miss_schema.endswith("?"):
|
||||
continue
|
||||
raise vol.Invalid(f"Missing option {miss_opt} in {root}") from None
|
||||
|
||||
raise vol.Invalid(
|
||||
f"Missing option '{miss_opt}' in {root} in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
|
||||
class UiOptions(CoreSysAttributes):
|
||||
@@ -317,7 +358,7 @@ class UiOptions(CoreSysAttributes):
|
||||
else:
|
||||
ui_node["options"] = [
|
||||
(device.by_id or device.path).as_posix()
|
||||
for device in self.sys_hardware.devices()
|
||||
for device in self.sys_hardware.devices
|
||||
]
|
||||
|
||||
ui_schema.append(ui_node)
|
||||
|
@@ -6,18 +6,8 @@ import logging
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..const import (
|
||||
PRIVILEGED_DAC_READ_SEARCH,
|
||||
PRIVILEGED_NET_ADMIN,
|
||||
PRIVILEGED_SYS_ADMIN,
|
||||
PRIVILEGED_SYS_MODULE,
|
||||
PRIVILEGED_SYS_PTRACE,
|
||||
PRIVILEGED_SYS_RAWIO,
|
||||
ROLE_ADMIN,
|
||||
ROLE_MANAGER,
|
||||
SECURITY_DISABLE,
|
||||
SECURITY_PROFILE,
|
||||
)
|
||||
from ..const import ROLE_ADMIN, ROLE_MANAGER, SECURITY_DISABLE, SECURITY_PROFILE
|
||||
from ..docker.const import Capabilities
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .model import AddonModel
|
||||
@@ -46,16 +36,19 @@ def rating_security(addon: AddonModel) -> int:
|
||||
rating += 1
|
||||
|
||||
# Privileged options
|
||||
if any(
|
||||
privilege in addon.privileged
|
||||
for privilege in (
|
||||
PRIVILEGED_NET_ADMIN,
|
||||
PRIVILEGED_SYS_ADMIN,
|
||||
PRIVILEGED_SYS_RAWIO,
|
||||
PRIVILEGED_SYS_PTRACE,
|
||||
PRIVILEGED_SYS_MODULE,
|
||||
PRIVILEGED_DAC_READ_SEARCH,
|
||||
if (
|
||||
any(
|
||||
privilege in addon.privileged
|
||||
for privilege in (
|
||||
Capabilities.NET_ADMIN,
|
||||
Capabilities.SYS_ADMIN,
|
||||
Capabilities.SYS_RAWIO,
|
||||
Capabilities.SYS_PTRACE,
|
||||
Capabilities.SYS_MODULE,
|
||||
Capabilities.DAC_READ_SEARCH,
|
||||
)
|
||||
)
|
||||
or addon.with_kernel_modules
|
||||
):
|
||||
rating += -1
|
||||
|
||||
@@ -73,12 +66,8 @@ def rating_security(addon: AddonModel) -> int:
|
||||
if addon.host_pid:
|
||||
rating += -2
|
||||
|
||||
# Full Access
|
||||
if addon.with_full_access:
|
||||
rating += -2
|
||||
|
||||
# Docker Access
|
||||
if addon.access_docker_api:
|
||||
# Docker Access & full Access
|
||||
if addon.access_docker_api or addon.with_full_access:
|
||||
rating = 1
|
||||
|
||||
return max(min(6, rating), 1)
|
||||
|
@@ -7,6 +7,8 @@ import uuid
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from supervisor.addons.const import AddonBackupMode
|
||||
|
||||
from ..const import (
|
||||
ARCH_ALL,
|
||||
ATTR_ACCESS_TOKEN,
|
||||
@@ -19,8 +21,12 @@ from ..const import (
|
||||
ATTR_AUDIO_OUTPUT,
|
||||
ATTR_AUTH_API,
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_BACKUP_EXCLUDE,
|
||||
ATTR_BACKUP_POST,
|
||||
ATTR_BACKUP_PRE,
|
||||
ATTR_BOOT,
|
||||
ATTR_BUILD_FROM,
|
||||
ATTR_CONFIGURATION,
|
||||
ATTR_DESCRIPTON,
|
||||
ATTR_DEVICES,
|
||||
ATTR_DEVICETREE,
|
||||
@@ -42,9 +48,12 @@ from ..const import (
|
||||
ATTR_INGRESS_ENTRY,
|
||||
ATTR_INGRESS_PANEL,
|
||||
ATTR_INGRESS_PORT,
|
||||
ATTR_INGRESS_STREAM,
|
||||
ATTR_INGRESS_TOKEN,
|
||||
ATTR_INIT,
|
||||
ATTR_JOURNALD,
|
||||
ATTR_KERNEL_MODULES,
|
||||
ATTR_LABELS,
|
||||
ATTR_LEGACY,
|
||||
ATTR_LOCATON,
|
||||
ATTR_MACHINE,
|
||||
@@ -59,11 +68,11 @@ from ..const import (
|
||||
ATTR_PORTS_DESCRIPTION,
|
||||
ATTR_PRIVILEGED,
|
||||
ATTR_PROTECTED,
|
||||
ATTR_REALTIME,
|
||||
ATTR_REPOSITORY,
|
||||
ATTR_SCHEMA,
|
||||
ATTR_SERVICES,
|
||||
ATTR_SLUG,
|
||||
ATTR_SNAPSHOT_EXCLUDE,
|
||||
ATTR_SQUASH,
|
||||
ATTR_STAGE,
|
||||
ATTR_STARTUP,
|
||||
@@ -72,6 +81,7 @@ from ..const import (
|
||||
ATTR_SYSTEM,
|
||||
ATTR_TIMEOUT,
|
||||
ATTR_TMPFS,
|
||||
ATTR_TRANSLATIONS,
|
||||
ATTR_UART,
|
||||
ATTR_UDEV,
|
||||
ATTR_URL,
|
||||
@@ -82,7 +92,6 @@ from ..const import (
|
||||
ATTR_VIDEO,
|
||||
ATTR_WATCHDOG,
|
||||
ATTR_WEBUI,
|
||||
PRIVILEGED_ALL,
|
||||
ROLE_ALL,
|
||||
ROLE_DEFAULT,
|
||||
AddonBoot,
|
||||
@@ -91,6 +100,7 @@ from ..const import (
|
||||
AddonState,
|
||||
)
|
||||
from ..discovery.validate import valid_discovery_service
|
||||
from ..docker.const import Capabilities
|
||||
from ..validate import (
|
||||
docker_image,
|
||||
docker_ports,
|
||||
@@ -100,6 +110,7 @@ from ..validate import (
|
||||
uuid_match,
|
||||
version_tag,
|
||||
)
|
||||
from .const import ATTR_BACKUP
|
||||
from .options import RE_SCHEMA_ELEMENT
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@@ -117,6 +128,7 @@ SCHEMA_ELEMENT = vol.Match(RE_SCHEMA_ELEMENT)
|
||||
RE_MACHINE = re.compile(
|
||||
r"^!?(?:"
|
||||
r"|intel-nuc"
|
||||
r"|generic-x86-64"
|
||||
r"|odroid-c2"
|
||||
r"|odroid-c4"
|
||||
r"|odroid-n2"
|
||||
@@ -136,6 +148,34 @@ RE_MACHINE = re.compile(
|
||||
)
|
||||
|
||||
|
||||
def _warn_addon_config(config: Dict[str, Any]):
|
||||
"""Warn about miss configs."""
|
||||
name = config.get(ATTR_NAME)
|
||||
if not name:
|
||||
raise vol.Invalid("Invalid Add-on config!")
|
||||
|
||||
if config.get(ATTR_FULL_ACCESS, False) and (
|
||||
config.get(ATTR_DEVICES)
|
||||
or config.get(ATTR_UART)
|
||||
or config.get(ATTR_USB)
|
||||
or config.get(ATTR_GPIO)
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"Add-on have full device access, and selective device access in the configuration. Please report this to the maintainer of %s",
|
||||
name,
|
||||
)
|
||||
|
||||
if config.get(ATTR_BACKUP, AddonBackupMode.HOT) == AddonBackupMode.COLD and (
|
||||
config.get(ATTR_BACKUP_POST) or config.get(ATTR_BACKUP_PRE)
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"Add-on which only support COLD backups trying to use post/pre commands. Please report this to the maintainer of %s",
|
||||
name,
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _migrate_addon_config(protocol=False):
|
||||
"""Migrate addon config."""
|
||||
|
||||
@@ -149,7 +189,7 @@ def _migrate_addon_config(protocol=False):
|
||||
value = config[ATTR_STARTUP]
|
||||
if protocol:
|
||||
_LOGGER.warning(
|
||||
"Add-on config 'startup' with '%s' is depircated. Please report this to the maintainer of %s",
|
||||
"Add-on config 'startup' with '%s' is deprecated. Please report this to the maintainer of %s",
|
||||
value,
|
||||
name,
|
||||
)
|
||||
@@ -185,6 +225,23 @@ def _migrate_addon_config(protocol=False):
|
||||
)
|
||||
config[ATTR_TMPFS] = True
|
||||
|
||||
# 2021-06 "snapshot" renamed to "backup"
|
||||
for entry in (
|
||||
"snapshot_exclude",
|
||||
"snapshot_post",
|
||||
"snapshot_pre",
|
||||
"snapshot",
|
||||
):
|
||||
if entry in config:
|
||||
new_entry = entry.replace("snapshot", "backup")
|
||||
config[new_entry] = config.pop(entry)
|
||||
_LOGGER.warning(
|
||||
"Add-on config '%s' is deprecated, '%s' should be used instead. Please report this to the maintainer of %s",
|
||||
entry,
|
||||
new_entry,
|
||||
name,
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
return _migrate
|
||||
@@ -210,7 +267,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
vol.Optional(ATTR_PORTS): docker_ports,
|
||||
vol.Optional(ATTR_PORTS_DESCRIPTION): docker_ports_description,
|
||||
vol.Optional(ATTR_WATCHDOG): vol.Match(
|
||||
r"^(?:https?|\[PROTO:\w+\]|tcp):\/\/\[HOST\]:\[PORT:\d+\].*$"
|
||||
r"^(?:https?|\[PROTO:\w+\]|tcp):\/\/\[HOST\]:(\[PORT:\d+\]|\d+).*$"
|
||||
),
|
||||
vol.Optional(ATTR_WEBUI): vol.Match(
|
||||
r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"
|
||||
@@ -220,6 +277,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
network_port, vol.Equal(0)
|
||||
),
|
||||
vol.Optional(ATTR_INGRESS_ENTRY): str,
|
||||
vol.Optional(ATTR_INGRESS_STREAM, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_PANEL_ICON, default="mdi:puzzle"): str,
|
||||
vol.Optional(ATTR_PANEL_TITLE): str,
|
||||
vol.Optional(ATTR_PANEL_ADMIN, default=True): vol.Boolean(),
|
||||
@@ -233,7 +291,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
vol.Optional(ATTR_TMPFS, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_MAP, default=list): [vol.Match(RE_VOLUME)],
|
||||
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): str},
|
||||
vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)],
|
||||
vol.Optional(ATTR_PRIVILEGED): [vol.Coerce(Capabilities)],
|
||||
vol.Optional(ATTR_APPARMOR, default=True): vol.Boolean(),
|
||||
vol.Optional(ATTR_FULL_ACCESS, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
|
||||
@@ -243,6 +301,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
vol.Optional(ATTR_UART, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_KERNEL_MODULES, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_REALTIME, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_HASSIO_ROLE, default=ROLE_DEFAULT): vol.In(ROLE_ALL),
|
||||
vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(),
|
||||
@@ -252,7 +311,12 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
vol.Optional(ATTR_AUTH_API, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
|
||||
vol.Optional(ATTR_DISCOVERY): [valid_discovery_service],
|
||||
vol.Optional(ATTR_SNAPSHOT_EXCLUDE): [str],
|
||||
vol.Optional(ATTR_BACKUP_EXCLUDE): [str],
|
||||
vol.Optional(ATTR_BACKUP_PRE): str,
|
||||
vol.Optional(ATTR_BACKUP_POST): str,
|
||||
vol.Optional(ATTR_BACKUP, default=AddonBackupMode.HOT): vol.Coerce(
|
||||
AddonBackupMode
|
||||
),
|
||||
vol.Optional(ATTR_OPTIONS, default={}): dict,
|
||||
vol.Optional(ATTR_SCHEMA, default={}): vol.Any(
|
||||
vol.Schema(
|
||||
@@ -275,11 +339,14 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
vol.Optional(ATTR_TIMEOUT, default=10): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=10, max=300)
|
||||
),
|
||||
vol.Optional(ATTR_JOURNALD, default=False): vol.Boolean(),
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
SCHEMA_ADDON_CONFIG = vol.All(_migrate_addon_config(True), _SCHEMA_ADDON_CONFIG)
|
||||
SCHEMA_ADDON_CONFIG = vol.All(
|
||||
_migrate_addon_config(True), _warn_addon_config, _SCHEMA_ADDON_CONFIG
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
@@ -289,9 +356,25 @@ SCHEMA_BUILD_CONFIG = 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(
|
||||
{vol.Coerce(str): vol.Coerce(str)}
|
||||
),
|
||||
vol.Optional(ATTR_ARGS, default=dict): vol.Schema({str: str}),
|
||||
vol.Optional(ATTR_LABELS, default=dict): vol.Schema({str: str}),
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
SCHEMA_TRANSLATION_CONFIGURATION = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_NAME): str,
|
||||
vol.Optional(ATTR_DESCRIPTON): vol.Maybe(str),
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
SCHEMA_ADDON_TRANSLATIONS = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_CONFIGURATION): {str: SCHEMA_TRANSLATION_CONFIGURATION},
|
||||
vol.Optional(ATTR_NETWORK): {str: str},
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
@@ -318,13 +401,15 @@ SCHEMA_ADDON_USER = vol.Schema(
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
SCHEMA_ADDON_SYSTEM = vol.All(
|
||||
_migrate_addon_config(),
|
||||
_SCHEMA_ADDON_CONFIG.extend(
|
||||
{
|
||||
vol.Required(ATTR_LOCATON): str,
|
||||
vol.Required(ATTR_REPOSITORY): str,
|
||||
vol.Required(ATTR_TRANSLATIONS, default=dict): {
|
||||
str: SCHEMA_ADDON_TRANSLATIONS
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -334,11 +419,12 @@ SCHEMA_ADDONS_FILE = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_USER, default=dict): {str: SCHEMA_ADDON_USER},
|
||||
vol.Optional(ATTR_SYSTEM, default=dict): {str: SCHEMA_ADDON_SYSTEM},
|
||||
}
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
SCHEMA_ADDON_SNAPSHOT = vol.Schema(
|
||||
SCHEMA_ADDON_BACKUP = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_USER): SCHEMA_ADDON_USER,
|
||||
vol.Required(ATTR_SYSTEM): SCHEMA_ADDON_SYSTEM,
|
||||
|
@@ -9,6 +9,7 @@ from ..coresys import CoreSys, CoreSysAttributes
|
||||
from .addons import APIAddons
|
||||
from .audio import APIAudio
|
||||
from .auth import APIAuth
|
||||
from .backups import APIBackups
|
||||
from .cli import APICli
|
||||
from .discovery import APIDiscovery
|
||||
from .dns import APICoreDNS
|
||||
@@ -19,15 +20,16 @@ from .host import APIHost
|
||||
from .info import APIInfo
|
||||
from .ingress import APIIngress
|
||||
from .jobs import APIJobs
|
||||
from .middleware.security import SecurityMiddleware
|
||||
from .multicast import APIMulticast
|
||||
from .network import APINetwork
|
||||
from .observer import APIObserver
|
||||
from .os import APIOS
|
||||
from .proxy import APIProxy
|
||||
from .resolution import APIResoulution
|
||||
from .security import SecurityMiddleware
|
||||
from .security import APISecurity
|
||||
from .services import APIServices
|
||||
from .snapshots import APISnapshots
|
||||
from .store import APIStore
|
||||
from .supervisor import APISupervisor
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@@ -60,6 +62,7 @@ class RestAPI(CoreSysAttributes):
|
||||
self._register_addons()
|
||||
self._register_audio()
|
||||
self._register_auth()
|
||||
self._register_backups()
|
||||
self._register_cli()
|
||||
self._register_discovery()
|
||||
self._register_dns()
|
||||
@@ -78,8 +81,9 @@ class RestAPI(CoreSysAttributes):
|
||||
self._register_proxy()
|
||||
self._register_resolution()
|
||||
self._register_services()
|
||||
self._register_snapshots()
|
||||
self._register_supervisor()
|
||||
self._register_store()
|
||||
self._register_security()
|
||||
|
||||
await self.start()
|
||||
|
||||
@@ -144,6 +148,18 @@ class RestAPI(CoreSysAttributes):
|
||||
]
|
||||
)
|
||||
|
||||
def _register_security(self) -> None:
|
||||
"""Register Security functions."""
|
||||
api_security = APISecurity()
|
||||
api_security.coresys = self.coresys
|
||||
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.get("/security/info", api_security.info),
|
||||
web.post("/security/options", api_security.options),
|
||||
]
|
||||
)
|
||||
|
||||
def _register_jobs(self) -> None:
|
||||
"""Register Jobs functions."""
|
||||
api_jobs = APIJobs()
|
||||
@@ -207,7 +223,6 @@ class RestAPI(CoreSysAttributes):
|
||||
[
|
||||
web.get("/hardware/info", api_hardware.info),
|
||||
web.get("/hardware/audio", api_hardware.audio),
|
||||
web.post("/hardware/trigger", api_hardware.trigger),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -226,6 +241,10 @@ class RestAPI(CoreSysAttributes):
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.get("/resolution/info", api_resolution.info),
|
||||
web.post(
|
||||
"/resolution/check/{check}/options", api_resolution.options_check
|
||||
),
|
||||
web.post("/resolution/check/{check}/run", api_resolution.run_check),
|
||||
web.post(
|
||||
"/resolution/suggestion/{suggestion}",
|
||||
api_resolution.apply_suggestion,
|
||||
@@ -238,6 +257,7 @@ class RestAPI(CoreSysAttributes):
|
||||
"/resolution/issue/{issue}",
|
||||
api_resolution.dismiss_issue,
|
||||
),
|
||||
web.post("/resolution/healthcheck", api_resolution.healthcheck),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -338,16 +358,15 @@ class RestAPI(CoreSysAttributes):
|
||||
web.get("/addons", api_addons.list),
|
||||
web.post("/addons/reload", api_addons.reload),
|
||||
web.get("/addons/{addon}/info", api_addons.info),
|
||||
web.post("/addons/{addon}/install", api_addons.install),
|
||||
web.post("/addons/{addon}/uninstall", api_addons.uninstall),
|
||||
web.post("/addons/{addon}/start", api_addons.start),
|
||||
web.post("/addons/{addon}/stop", api_addons.stop),
|
||||
web.post("/addons/{addon}/restart", api_addons.restart),
|
||||
web.post("/addons/{addon}/update", api_addons.update),
|
||||
web.post("/addons/{addon}/options", api_addons.options),
|
||||
web.post(
|
||||
"/addons/{addon}/options/validate", api_addons.options_validate
|
||||
),
|
||||
web.get("/addons/{addon}/options/config", api_addons.options_config),
|
||||
web.post("/addons/{addon}/rebuild", api_addons.rebuild),
|
||||
web.get("/addons/{addon}/logs", api_addons.logs),
|
||||
web.get("/addons/{addon}/icon", api_addons.icon),
|
||||
@@ -374,30 +393,41 @@ class RestAPI(CoreSysAttributes):
|
||||
]
|
||||
)
|
||||
|
||||
def _register_snapshots(self) -> None:
|
||||
"""Register snapshots functions."""
|
||||
api_snapshots = APISnapshots()
|
||||
api_snapshots.coresys = self.coresys
|
||||
def _register_backups(self) -> None:
|
||||
"""Register backups functions."""
|
||||
api_backups = APIBackups()
|
||||
api_backups.coresys = self.coresys
|
||||
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.get("/snapshots", api_snapshots.list),
|
||||
web.post("/snapshots/reload", api_snapshots.reload),
|
||||
web.post("/snapshots/new/full", api_snapshots.snapshot_full),
|
||||
web.post("/snapshots/new/partial", api_snapshots.snapshot_partial),
|
||||
web.post("/snapshots/new/upload", api_snapshots.upload),
|
||||
web.get("/snapshots/{snapshot}/info", api_snapshots.info),
|
||||
web.delete("/snapshots/{snapshot}", api_snapshots.remove),
|
||||
web.get("/snapshots", api_backups.list),
|
||||
web.post("/snapshots/reload", api_backups.reload),
|
||||
web.post("/snapshots/new/full", api_backups.backup_full),
|
||||
web.post("/snapshots/new/partial", api_backups.backup_partial),
|
||||
web.post("/snapshots/new/upload", api_backups.upload),
|
||||
web.get("/snapshots/{slug}/info", api_backups.info),
|
||||
web.delete("/snapshots/{slug}", api_backups.remove),
|
||||
web.post("/snapshots/{slug}/restore/full", api_backups.restore_full),
|
||||
web.post(
|
||||
"/snapshots/{snapshot}/restore/full", api_snapshots.restore_full
|
||||
"/snapshots/{slug}/restore/partial",
|
||||
api_backups.restore_partial,
|
||||
),
|
||||
web.get("/snapshots/{slug}/download", api_backups.download),
|
||||
web.post("/snapshots/{slug}/remove", api_backups.remove),
|
||||
# June 2021: /snapshots was renamed to /backups
|
||||
web.get("/backups", api_backups.list),
|
||||
web.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.info),
|
||||
web.delete("/backups/{slug}", api_backups.remove),
|
||||
web.post("/backups/{slug}/restore/full", api_backups.restore_full),
|
||||
web.post(
|
||||
"/snapshots/{snapshot}/restore/partial",
|
||||
api_snapshots.restore_partial,
|
||||
"/backups/{slug}/restore/partial",
|
||||
api_backups.restore_partial,
|
||||
),
|
||||
web.get("/snapshots/{snapshot}/download", api_snapshots.download),
|
||||
# Old, remove at end of 2020
|
||||
web.post("/snapshots/{snapshot}/remove", api_snapshots.remove),
|
||||
web.get("/backups/{slug}/download", api_backups.download),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -468,6 +498,46 @@ class RestAPI(CoreSysAttributes):
|
||||
]
|
||||
)
|
||||
|
||||
def _register_store(self) -> None:
|
||||
"""Register store endpoints."""
|
||||
api_store = APIStore()
|
||||
api_store.coresys = self.coresys
|
||||
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.get("/store", api_store.store_info),
|
||||
web.get("/store/addons", api_store.addons_list),
|
||||
web.get("/store/addons/{addon}", api_store.addons_addon_info),
|
||||
web.get("/store/addons/{addon}/{version}", api_store.addons_addon_info),
|
||||
web.post(
|
||||
"/store/addons/{addon}/install", api_store.addons_addon_install
|
||||
),
|
||||
web.post(
|
||||
"/store/addons/{addon}/install/{version}",
|
||||
api_store.addons_addon_install,
|
||||
),
|
||||
web.post("/store/addons/{addon}/update", api_store.addons_addon_update),
|
||||
web.post(
|
||||
"/store/addons/{addon}/update/{version}",
|
||||
api_store.addons_addon_update,
|
||||
),
|
||||
web.post("/store/reload", api_store.reload),
|
||||
web.get("/store/repositories", api_store.repositories_list),
|
||||
web.get(
|
||||
"/store/repositories/{repository}",
|
||||
api_store.repositories_repository_info,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# Reroute from legacy
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.post("/addons/{addon}/install", api_store.addons_addon_install),
|
||||
web.post("/addons/{addon}/update", api_store.addons_addon_update),
|
||||
]
|
||||
)
|
||||
|
||||
def _register_panel(self) -> None:
|
||||
"""Register panel for Home Assistant."""
|
||||
panel_dir = Path(__file__).parent.joinpath("panel")
|
||||
|
@@ -71,6 +71,7 @@ from ..const import (
|
||||
ATTR_OPTIONS,
|
||||
ATTR_PRIVILEGED,
|
||||
ATTR_PROTECTED,
|
||||
ATTR_PWNED,
|
||||
ATTR_RATING,
|
||||
ATTR_REPOSITORIES,
|
||||
ATTR_REPOSITORY,
|
||||
@@ -82,6 +83,7 @@ from ..const import (
|
||||
ATTR_STARTUP,
|
||||
ATTR_STATE,
|
||||
ATTR_STDIN,
|
||||
ATTR_TRANSLATIONS,
|
||||
ATTR_UART,
|
||||
ATTR_UDEV,
|
||||
ATTR_UPDATE_AVAILABLE,
|
||||
@@ -102,9 +104,9 @@ from ..const import (
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..docker.stats import DockerStats
|
||||
from ..exceptions import APIError
|
||||
from ..exceptions import APIError, APIForbidden, PwnedError, PwnedSecret
|
||||
from ..validate import docker_ports
|
||||
from .utils import api_process, api_process_raw, api_validate
|
||||
from .utils import api_process, api_process_raw, api_validate, json_loads
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -171,6 +173,7 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_INSTALLED: addon.is_installed,
|
||||
ATTR_AVAILABLE: addon.available,
|
||||
ATTR_DETACHED: addon.is_detached,
|
||||
ATTR_HOMEASSISTANT: addon.homeassistant_version,
|
||||
ATTR_REPOSITORY: addon.repository,
|
||||
ATTR_BUILD: addon.need_build,
|
||||
ATTR_URL: addon.url,
|
||||
@@ -264,6 +267,7 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_SERVICES: _pretty_services(addon),
|
||||
ATTR_DISCOVERY: addon.discovery,
|
||||
ATTR_IP_ADDRESS: None,
|
||||
ATTR_TRANSLATIONS: addon.translations,
|
||||
ATTR_INGRESS: addon.with_ingress,
|
||||
ATTR_INGRESS_ENTRY: None,
|
||||
ATTR_INGRESS_URL: None,
|
||||
@@ -334,15 +338,54 @@ class APIAddons(CoreSysAttributes):
|
||||
async def options_validate(self, request: web.Request) -> None:
|
||||
"""Validate user options for add-on."""
|
||||
addon = self._extract_addon_installed(request)
|
||||
data = {ATTR_MESSAGE: "", ATTR_VALID: True}
|
||||
data = {ATTR_MESSAGE: "", ATTR_VALID: True, ATTR_PWNED: False}
|
||||
|
||||
options = await request.json(loads=json_loads) or addon.options
|
||||
|
||||
# Validate config
|
||||
options_schema = addon.schema
|
||||
try:
|
||||
addon.schema(addon.options)
|
||||
options_schema.validate(options)
|
||||
except vol.Invalid as ex:
|
||||
data[ATTR_MESSAGE] = humanize_error(addon.options, ex)
|
||||
data[ATTR_MESSAGE] = humanize_error(options, ex)
|
||||
data[ATTR_VALID] = False
|
||||
|
||||
if not self.sys_security.pwned:
|
||||
return data
|
||||
|
||||
# Pwned check
|
||||
for secret in options_schema.pwned:
|
||||
try:
|
||||
await self.sys_security.verify_secret(secret)
|
||||
continue
|
||||
except PwnedSecret:
|
||||
data[ATTR_PWNED] = True
|
||||
except PwnedError:
|
||||
data[ATTR_PWNED] = None
|
||||
break
|
||||
|
||||
if self.sys_security.force and data[ATTR_PWNED] in (None, True):
|
||||
data[ATTR_VALID] = False
|
||||
if data[ATTR_PWNED] is None:
|
||||
data[ATTR_MESSAGE] = "Error happening on pwned secrets check!"
|
||||
else:
|
||||
data[ATTR_MESSAGE] = "Add-on uses pwned secrets!"
|
||||
|
||||
return data
|
||||
|
||||
@api_process
|
||||
async def options_config(self, request: web.Request) -> None:
|
||||
"""Validate user options for add-on."""
|
||||
slug: str = request.match_info.get("addon")
|
||||
if slug != "self":
|
||||
raise APIForbidden("This can be only read by the Add-on itself!")
|
||||
|
||||
addon = self._extract_addon_installed(request)
|
||||
try:
|
||||
return addon.schema.validate(addon.options)
|
||||
except vol.Invalid:
|
||||
raise APIError("Invalid configuration data for the add-on") from None
|
||||
|
||||
@api_process
|
||||
async def security(self, request: web.Request) -> None:
|
||||
"""Store security options for add-on."""
|
||||
@@ -373,12 +416,6 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_BLK_WRITE: stats.blk_write,
|
||||
}
|
||||
|
||||
@api_process
|
||||
def install(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Install add-on."""
|
||||
addon = self._extract_addon(request)
|
||||
return asyncio.shield(addon.install())
|
||||
|
||||
@api_process
|
||||
def uninstall(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Uninstall add-on."""
|
||||
@@ -397,12 +434,6 @@ class APIAddons(CoreSysAttributes):
|
||||
addon = self._extract_addon_installed(request)
|
||||
return asyncio.shield(addon.stop())
|
||||
|
||||
@api_process
|
||||
def update(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Update add-on."""
|
||||
addon: Addon = self._extract_addon_installed(request)
|
||||
return asyncio.shield(addon.update())
|
||||
|
||||
@api_process
|
||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Restart add-on."""
|
||||
@@ -472,14 +503,6 @@ class APIAddons(CoreSysAttributes):
|
||||
await asyncio.shield(addon.write_stdin(data))
|
||||
|
||||
|
||||
def _pretty_devices(addon: AnyAddon) -> List[str]:
|
||||
"""Return a simplified device list."""
|
||||
dev_list = addon.devices
|
||||
if not dev_list:
|
||||
return []
|
||||
return [row.split(":")[0] for row in dev_list]
|
||||
|
||||
|
||||
def _pretty_services(addon: AnyAddon) -> List[str]:
|
||||
"""Return a simplified services role list."""
|
||||
return [f"{name}:{access}" for name, access in addon.services_role.items()]
|
||||
|
219
supervisor/api/backups.py
Normal file
219
supervisor/api/backups.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""Backups RESTful API."""
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.hdrs import CONTENT_DISPOSITION
|
||||
import voluptuous as vol
|
||||
|
||||
from ..backups.validate import ALL_FOLDERS
|
||||
from ..const import (
|
||||
ATTR_ADDONS,
|
||||
ATTR_BACKUPS,
|
||||
ATTR_CONTENT,
|
||||
ATTR_DATE,
|
||||
ATTR_FOLDERS,
|
||||
ATTR_HOMEASSISTANT,
|
||||
ATTR_NAME,
|
||||
ATTR_PASSWORD,
|
||||
ATTR_PROTECTED,
|
||||
ATTR_REPOSITORIES,
|
||||
ATTR_SIZE,
|
||||
ATTR_SLUG,
|
||||
ATTR_TYPE,
|
||||
ATTR_VERSION,
|
||||
CONTENT_TYPE_TAR,
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..exceptions import APIError
|
||||
from .utils import api_process, api_validate
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
RE_SLUGIFY_NAME = re.compile(r"[^A-Za-z0-9]+")
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
SCHEMA_RESTORE_PARTIAL = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
|
||||
vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(),
|
||||
vol.Optional(ATTR_ADDONS): vol.All([vol.Coerce(str)], vol.Unique()),
|
||||
vol.Optional(ATTR_FOLDERS): vol.All([vol.In(ALL_FOLDERS)], vol.Unique()),
|
||||
}
|
||||
)
|
||||
|
||||
SCHEMA_RESTORE_FULL = vol.Schema(
|
||||
{vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str))}
|
||||
)
|
||||
|
||||
SCHEMA_BACKUP_FULL = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_NAME): vol.Coerce(str),
|
||||
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
|
||||
}
|
||||
)
|
||||
|
||||
SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend(
|
||||
{
|
||||
vol.Optional(ATTR_ADDONS): vol.All([vol.Coerce(str)], vol.Unique()),
|
||||
vol.Optional(ATTR_FOLDERS): vol.All([vol.In(ALL_FOLDERS)], vol.Unique()),
|
||||
vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class APIBackups(CoreSysAttributes):
|
||||
"""Handle RESTful API for backups functions."""
|
||||
|
||||
def _extract_slug(self, request):
|
||||
"""Return backup, throw an exception if it doesn't exist."""
|
||||
backup = self.sys_backups.get(request.match_info.get("slug"))
|
||||
if not backup:
|
||||
raise APIError("Backup does not exist")
|
||||
return backup
|
||||
|
||||
@api_process
|
||||
async def list(self, request):
|
||||
"""Return backup list."""
|
||||
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_PROTECTED: backup.protected,
|
||||
ATTR_CONTENT: {
|
||||
ATTR_HOMEASSISTANT: backup.homeassistant_version is not None,
|
||||
ATTR_ADDONS: backup.addon_list,
|
||||
ATTR_FOLDERS: backup.folders,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if request.path == "/snapshots":
|
||||
# Kept for backwards compability
|
||||
return {"snapshots": data_backups}
|
||||
|
||||
return {ATTR_BACKUPS: data_backups}
|
||||
|
||||
@api_process
|
||||
async def reload(self, request):
|
||||
"""Reload backup list."""
|
||||
await asyncio.shield(self.sys_backups.reload())
|
||||
return True
|
||||
|
||||
@api_process
|
||||
async def info(self, request):
|
||||
"""Return backup info."""
|
||||
backup = self._extract_slug(request)
|
||||
|
||||
data_addons = []
|
||||
for addon_data in backup.addons:
|
||||
data_addons.append(
|
||||
{
|
||||
ATTR_SLUG: addon_data[ATTR_SLUG],
|
||||
ATTR_NAME: addon_data[ATTR_NAME],
|
||||
ATTR_VERSION: addon_data[ATTR_VERSION],
|
||||
ATTR_SIZE: addon_data[ATTR_SIZE],
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
ATTR_SLUG: backup.slug,
|
||||
ATTR_TYPE: backup.sys_type,
|
||||
ATTR_NAME: backup.name,
|
||||
ATTR_DATE: backup.date,
|
||||
ATTR_SIZE: backup.size,
|
||||
ATTR_PROTECTED: backup.protected,
|
||||
ATTR_HOMEASSISTANT: backup.homeassistant_version,
|
||||
ATTR_ADDONS: data_addons,
|
||||
ATTR_REPOSITORIES: backup.repositories,
|
||||
ATTR_FOLDERS: backup.folders,
|
||||
}
|
||||
|
||||
@api_process
|
||||
async def backup_full(self, request):
|
||||
"""Create full backup."""
|
||||
body = await api_validate(SCHEMA_BACKUP_FULL, request)
|
||||
backup = await asyncio.shield(self.sys_backups.do_backup_full(**body))
|
||||
|
||||
if backup:
|
||||
return {ATTR_SLUG: backup.slug}
|
||||
return False
|
||||
|
||||
@api_process
|
||||
async def backup_partial(self, request):
|
||||
"""Create a partial backup."""
|
||||
body = await api_validate(SCHEMA_BACKUP_PARTIAL, request)
|
||||
backup = await asyncio.shield(self.sys_backups.do_backup_partial(**body))
|
||||
|
||||
if backup:
|
||||
return {ATTR_SLUG: backup.slug}
|
||||
return False
|
||||
|
||||
@api_process
|
||||
async def restore_full(self, request):
|
||||
"""Full restore of a backup."""
|
||||
backup = self._extract_slug(request)
|
||||
body = await api_validate(SCHEMA_RESTORE_FULL, request)
|
||||
|
||||
return await asyncio.shield(self.sys_backups.do_restore_full(backup, **body))
|
||||
|
||||
@api_process
|
||||
async def restore_partial(self, request):
|
||||
"""Partial restore a backup."""
|
||||
backup = self._extract_slug(request)
|
||||
body = await api_validate(SCHEMA_RESTORE_PARTIAL, request)
|
||||
|
||||
return await asyncio.shield(self.sys_backups.do_restore_partial(backup, **body))
|
||||
|
||||
@api_process
|
||||
async def remove(self, request):
|
||||
"""Remove a backup."""
|
||||
backup = self._extract_slug(request)
|
||||
return self.sys_backups.remove(backup)
|
||||
|
||||
async def download(self, request):
|
||||
"""Download a backup file."""
|
||||
backup = self._extract_slug(request)
|
||||
|
||||
_LOGGER.info("Downloading backup %s", backup.slug)
|
||||
response = web.FileResponse(backup.tarfile)
|
||||
response.content_type = CONTENT_TYPE_TAR
|
||||
response.headers[
|
||||
CONTENT_DISPOSITION
|
||||
] = f"attachment; filename={RE_SLUGIFY_NAME.sub('_', backup.name)}.tar"
|
||||
return response
|
||||
|
||||
@api_process
|
||||
async def upload(self, request):
|
||||
"""Upload a backup file."""
|
||||
with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp_dir:
|
||||
tar_file = Path(temp_dir, "backup.tar")
|
||||
reader = await request.multipart()
|
||||
contents = await reader.next()
|
||||
try:
|
||||
with tar_file.open("wb") as backup:
|
||||
while True:
|
||||
chunk = await contents.read_chunk()
|
||||
if not chunk:
|
||||
break
|
||||
backup.write(chunk)
|
||||
|
||||
except OSError as err:
|
||||
_LOGGER.error("Can't write new backup file: %s", err)
|
||||
return False
|
||||
|
||||
except asyncio.CancelledError:
|
||||
return False
|
||||
|
||||
backup = await asyncio.shield(self.sys_backups.import_backup(tar_file))
|
||||
|
||||
if backup:
|
||||
return {ATTR_SLUG: backup.slug}
|
||||
return False
|
6
supervisor/api/const.py
Normal file
6
supervisor/api/const.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Const for API."""
|
||||
|
||||
ATTR_USE_RTC = "use_rtc"
|
||||
ATTR_USE_NTP = "use_ntp"
|
||||
ATTR_DT_UTC = "dt_utc"
|
||||
ATTR_DT_SYNCHRONIZED = "dt_synchronized"
|
@@ -13,7 +13,7 @@ from ..const import (
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..discovery.validate import valid_discovery_service
|
||||
from ..exceptions import APIError, APIForbidden
|
||||
from .utils import api_process, api_validate
|
||||
from .utils import api_process, api_validate, require_home_assistant
|
||||
|
||||
SCHEMA_DISCOVERY = vol.Schema(
|
||||
{
|
||||
@@ -33,15 +33,10 @@ class APIDiscovery(CoreSysAttributes):
|
||||
raise APIError("Discovery message not found")
|
||||
return message
|
||||
|
||||
def _check_permission_ha(self, request):
|
||||
"""Check permission for API call / Home Assistant."""
|
||||
if request[REQUEST_FROM] != self.sys_homeassistant:
|
||||
raise APIForbidden("Only HomeAssistant can use this API!")
|
||||
|
||||
@api_process
|
||||
@require_home_assistant
|
||||
async def list(self, request):
|
||||
"""Show register services."""
|
||||
self._check_permission_ha(request)
|
||||
|
||||
# Get available discovery
|
||||
discovery = []
|
||||
@@ -79,13 +74,11 @@ class APIDiscovery(CoreSysAttributes):
|
||||
return {ATTR_UUID: message.uuid}
|
||||
|
||||
@api_process
|
||||
@require_home_assistant
|
||||
async def get_discovery(self, request):
|
||||
"""Read data into a discovery message."""
|
||||
message = self._extract_message(request)
|
||||
|
||||
# HomeAssistant?
|
||||
self._check_permission_ha(request)
|
||||
|
||||
return {
|
||||
ATTR_ADDON: message.addon,
|
||||
ATTR_SERVICE: message.service,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
"""Init file for Supervisor hardware RESTful API."""
|
||||
import logging
|
||||
from typing import Any, Awaitable, Dict
|
||||
from typing import Any, Dict
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
@@ -58,8 +58,3 @@ class APIHardware(CoreSysAttributes):
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@api_process
|
||||
async def trigger(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Trigger a udev device reload."""
|
||||
_LOGGER.warning("Ignoring DEPRECATED hardware trigger function call.")
|
||||
|
@@ -11,6 +11,7 @@ from ..const import (
|
||||
ATTR_DEPLOYMENT,
|
||||
ATTR_DESCRIPTON,
|
||||
ATTR_DISK_FREE,
|
||||
ATTR_DISK_LIFE_TIME,
|
||||
ATTR_DISK_TOTAL,
|
||||
ATTR_DISK_USED,
|
||||
ATTR_FEATURES,
|
||||
@@ -20,9 +21,11 @@ from ..const import (
|
||||
ATTR_OPERATING_SYSTEM,
|
||||
ATTR_SERVICES,
|
||||
ATTR_STATE,
|
||||
ATTR_TIMEZONE,
|
||||
CONTENT_TYPE_BINARY,
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from .const import ATTR_DT_SYNCHRONIZED, ATTR_DT_UTC, ATTR_USE_NTP, ATTR_USE_RTC
|
||||
from .utils import api_process, api_process_raw, api_validate
|
||||
|
||||
SERVICE = "service"
|
||||
@@ -43,10 +46,16 @@ class APIHost(CoreSysAttributes):
|
||||
ATTR_DISK_FREE: self.sys_host.info.free_space,
|
||||
ATTR_DISK_TOTAL: self.sys_host.info.total_space,
|
||||
ATTR_DISK_USED: self.sys_host.info.used_space,
|
||||
ATTR_DISK_LIFE_TIME: self.sys_host.info.disk_life_time,
|
||||
ATTR_FEATURES: self.sys_host.features,
|
||||
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
||||
ATTR_KERNEL: self.sys_host.info.kernel,
|
||||
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
|
||||
ATTR_TIMEZONE: self.sys_host.info.timezone,
|
||||
ATTR_DT_UTC: self.sys_host.info.dt_utc,
|
||||
ATTR_DT_SYNCHRONIZED: self.sys_host.info.dt_synchronized,
|
||||
ATTR_USE_NTP: self.sys_host.info.use_ntp,
|
||||
ATTR_USE_RTC: self.sys_host.info.use_rtc,
|
||||
}
|
||||
|
||||
@api_process
|
||||
|
@@ -15,6 +15,7 @@ from ..const import (
|
||||
ATTR_LOGGING,
|
||||
ATTR_MACHINE,
|
||||
ATTR_OPERATING_SYSTEM,
|
||||
ATTR_STATE,
|
||||
ATTR_SUPERVISOR,
|
||||
ATTR_SUPPORTED,
|
||||
ATTR_SUPPORTED_ARCH,
|
||||
@@ -42,9 +43,10 @@ class APIInfo(CoreSysAttributes):
|
||||
ATTR_FEATURES: self.sys_host.features,
|
||||
ATTR_MACHINE: self.sys_machine,
|
||||
ATTR_ARCH: self.sys_arch.default,
|
||||
ATTR_STATE: self.sys_core.state,
|
||||
ATTR_SUPPORTED_ARCH: self.sys_arch.supported,
|
||||
ATTR_SUPPORTED: self.sys_core.supported,
|
||||
ATTR_CHANNEL: self.sys_updater.channel,
|
||||
ATTR_LOGGING: self.sys_config.logging,
|
||||
ATTR_TIMEZONE: self.sys_config.timezone,
|
||||
ATTR_TIMEZONE: self.sys_timezone,
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import logging
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import hdrs, web
|
||||
from aiohttp import ClientTimeout, hdrs, web
|
||||
from aiohttp.web_exceptions import (
|
||||
HTTPBadGateway,
|
||||
HTTPServiceUnavailable,
|
||||
@@ -25,10 +25,9 @@ from ..const import (
|
||||
COOKIE_INGRESS,
|
||||
HEADER_TOKEN,
|
||||
HEADER_TOKEN_OLD,
|
||||
REQUEST_FROM,
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from .utils import api_process, api_validate
|
||||
from .utils import api_process, api_validate, require_home_assistant
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -50,11 +49,6 @@ class APIIngress(CoreSysAttributes):
|
||||
|
||||
return addon
|
||||
|
||||
def _check_ha_access(self, request: web.Request) -> None:
|
||||
if request[REQUEST_FROM] != self.sys_homeassistant:
|
||||
_LOGGER.warning("Ingress is only available behind Home Assistant")
|
||||
raise HTTPUnauthorized()
|
||||
|
||||
def _create_url(self, addon: Addon, path: str) -> str:
|
||||
"""Create URL to container."""
|
||||
return f"http://{addon.ip_address}:{addon.ingress_port}/{path}"
|
||||
@@ -74,18 +68,16 @@ class APIIngress(CoreSysAttributes):
|
||||
return {ATTR_PANELS: addons}
|
||||
|
||||
@api_process
|
||||
@require_home_assistant
|
||||
async def create_session(self, request: web.Request) -> Dict[str, Any]:
|
||||
"""Create a new session."""
|
||||
self._check_ha_access(request)
|
||||
|
||||
session = self.sys_ingress.create_session()
|
||||
return {ATTR_SESSION: session}
|
||||
|
||||
@api_process
|
||||
@require_home_assistant
|
||||
async def validate_session(self, request: web.Request) -> Dict[str, Any]:
|
||||
"""Validate session and extending how long it's valid for."""
|
||||
self._check_ha_access(request)
|
||||
|
||||
data = await api_validate(VALIDATE_SESSION_DATA, request)
|
||||
|
||||
# Check Ingress Session
|
||||
@@ -93,11 +85,11 @@ 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
|
||||
) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]:
|
||||
"""Route data to Supervisor ingress service."""
|
||||
self._check_ha_access(request)
|
||||
|
||||
# Check Ingress Session
|
||||
session = request.cookies.get(COOKIE_INGRESS)
|
||||
@@ -170,9 +162,18 @@ class APIIngress(CoreSysAttributes):
|
||||
) -> Union[web.Response, web.StreamResponse]:
|
||||
"""Ingress route for request."""
|
||||
url = self._create_url(addon, path)
|
||||
data = await request.read()
|
||||
source_header = _init_header(request, addon)
|
||||
|
||||
# Passing the raw stream breaks requests for some webservers
|
||||
# since we just need it for POST requests really, for all other methods
|
||||
# we read the bytes and pass that to the request to the add-on
|
||||
# add-ons needs to add support with that in the configuration
|
||||
data = (
|
||||
request.content
|
||||
if request.method == "POST" and addon.ingress_stream
|
||||
else await request.read()
|
||||
)
|
||||
|
||||
async with self.sys_websession.request(
|
||||
request.method,
|
||||
url,
|
||||
@@ -180,6 +181,7 @@ class APIIngress(CoreSysAttributes):
|
||||
params=request.query,
|
||||
allow_redirects=False,
|
||||
data=data,
|
||||
timeout=ClientTimeout(total=None),
|
||||
) as result:
|
||||
headers = _response_header(result)
|
||||
|
||||
@@ -227,6 +229,7 @@ def _init_header(
|
||||
if name in (
|
||||
hdrs.CONTENT_LENGTH,
|
||||
hdrs.CONTENT_ENCODING,
|
||||
hdrs.TRANSFER_ENCODING,
|
||||
hdrs.SEC_WEBSOCKET_EXTENSIONS,
|
||||
hdrs.SEC_WEBSOCKET_PROTOCOL,
|
||||
hdrs.SEC_WEBSOCKET_VERSION,
|
||||
|
@@ -36,6 +36,8 @@ class APIJobs(CoreSysAttributes):
|
||||
|
||||
self.sys_jobs.save_data()
|
||||
|
||||
await self.sys_resolution.evaluate.evaluate_system()
|
||||
|
||||
@api_process
|
||||
async def reset(self, request: web.Request) -> None:
|
||||
"""Reset options for JobManager."""
|
||||
|
1
supervisor/api/middleware/__init__.py
Normal file
1
supervisor/api/middleware/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""API middleware for aiohttp."""
|
208
supervisor/api/middleware/security.py
Normal file
208
supervisor/api/middleware/security.py
Normal file
@@ -0,0 +1,208 @@
|
||||
"""Handle security part of this API."""
|
||||
import logging
|
||||
import re
|
||||
|
||||
from aiohttp.web import Request, RequestHandler, Response, middleware
|
||||
from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized
|
||||
|
||||
from ...const import (
|
||||
REQUEST_FROM,
|
||||
ROLE_ADMIN,
|
||||
ROLE_BACKUP,
|
||||
ROLE_DEFAULT,
|
||||
ROLE_HOMEASSISTANT,
|
||||
ROLE_MANAGER,
|
||||
CoreState,
|
||||
)
|
||||
from ...coresys import CoreSys, CoreSysAttributes
|
||||
from ..utils import api_return_error, excract_supervisor_token
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
# fmt: off
|
||||
|
||||
# Block Anytime
|
||||
BLACKLIST = re.compile(
|
||||
r"^(?:"
|
||||
r"|/homeassistant/api/hassio/.*"
|
||||
r"|/core/api/hassio/.*"
|
||||
r")$"
|
||||
)
|
||||
|
||||
# Free to call or have own security concepts
|
||||
NO_SECURITY_CHECK = re.compile(
|
||||
r"^(?:"
|
||||
r"|/homeassistant/api/.*"
|
||||
r"|/homeassistant/websocket"
|
||||
r"|/core/api/.*"
|
||||
r"|/core/websocket"
|
||||
r"|/supervisor/ping"
|
||||
r")$"
|
||||
)
|
||||
|
||||
# Observer allow API calls
|
||||
OBSERVER_CHECK = re.compile(
|
||||
r"^(?:"
|
||||
r"|/.+/info"
|
||||
r")$"
|
||||
)
|
||||
|
||||
# Can called by every add-on
|
||||
ADDONS_API_BYPASS = re.compile(
|
||||
r"^(?:"
|
||||
r"|/addons/self/(?!security|update)[^/]+"
|
||||
r"|/addons/self/options/config"
|
||||
r"|/info"
|
||||
r"|/services.*"
|
||||
r"|/discovery.*"
|
||||
r"|/auth"
|
||||
r")$"
|
||||
)
|
||||
|
||||
# Policy role add-on API access
|
||||
ADDONS_ROLE_ACCESS = {
|
||||
ROLE_DEFAULT: re.compile(
|
||||
r"^(?:"
|
||||
r"|/.+/info"
|
||||
r")$"
|
||||
),
|
||||
ROLE_HOMEASSISTANT: re.compile(
|
||||
r"^(?:"
|
||||
r"|/.+/info"
|
||||
r"|/core/.+"
|
||||
r"|/homeassistant/.+"
|
||||
r")$"
|
||||
),
|
||||
ROLE_BACKUP: re.compile(
|
||||
r"^(?:"
|
||||
r"|/.+/info"
|
||||
r"|/backups.*"
|
||||
r"|/snapshots.*"
|
||||
r")$"
|
||||
),
|
||||
ROLE_MANAGER: re.compile(
|
||||
r"^(?:"
|
||||
r"|/.+/info"
|
||||
r"|/addons(?:/[^/]+/(?!security).+|/reload)?"
|
||||
r"|/audio/.+"
|
||||
r"|/auth/cache"
|
||||
r"|/cli/.+"
|
||||
r"|/core/.+"
|
||||
r"|/dns/.+"
|
||||
r"|/docker/.+"
|
||||
r"|/jobs/.+"
|
||||
r"|/hardware/.+"
|
||||
r"|/hassos/.+"
|
||||
r"|/homeassistant/.+"
|
||||
r"|/host/.+"
|
||||
r"|/multicast/.+"
|
||||
r"|/network/.+"
|
||||
r"|/observer/.+"
|
||||
r"|/os/.+"
|
||||
r"|/resolution/.+"
|
||||
r"|/backups.*"
|
||||
r"|/snapshots.*"
|
||||
r"|/store.*"
|
||||
r"|/supervisor/.+"
|
||||
r"|/security/.+"
|
||||
r")$"
|
||||
),
|
||||
ROLE_ADMIN: re.compile(
|
||||
r".*"
|
||||
),
|
||||
}
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
class SecurityMiddleware(CoreSysAttributes):
|
||||
"""Security middleware functions."""
|
||||
|
||||
def __init__(self, coresys: CoreSys):
|
||||
"""Initialize security middleware."""
|
||||
self.coresys: CoreSys = coresys
|
||||
|
||||
@middleware
|
||||
async def system_validation(
|
||||
self, request: Request, handler: RequestHandler
|
||||
) -> Response:
|
||||
"""Check if core is ready to response."""
|
||||
if self.sys_core.state not in (
|
||||
CoreState.STARTUP,
|
||||
CoreState.RUNNING,
|
||||
CoreState.FREEZE,
|
||||
):
|
||||
return api_return_error(
|
||||
message=f"System is not ready with state: {self.sys_core.state.value}"
|
||||
)
|
||||
|
||||
return await handler(request)
|
||||
|
||||
@middleware
|
||||
async def token_validation(
|
||||
self, request: Request, handler: RequestHandler
|
||||
) -> Response:
|
||||
"""Check security access of this layer."""
|
||||
request_from = None
|
||||
supervisor_token = excract_supervisor_token(request)
|
||||
|
||||
# Blacklist
|
||||
if BLACKLIST.match(request.path):
|
||||
_LOGGER.error("%s is blacklisted!", request.path)
|
||||
raise HTTPForbidden()
|
||||
|
||||
# Ignore security check
|
||||
if NO_SECURITY_CHECK.match(request.path):
|
||||
_LOGGER.debug("Passthrough %s", request.path)
|
||||
return await handler(request)
|
||||
|
||||
# Not token
|
||||
if not supervisor_token:
|
||||
_LOGGER.warning("No API token provided for %s", request.path)
|
||||
raise HTTPUnauthorized()
|
||||
|
||||
# Home-Assistant
|
||||
if supervisor_token == self.sys_homeassistant.supervisor_token:
|
||||
_LOGGER.debug("%s access from Home Assistant", request.path)
|
||||
request_from = self.sys_homeassistant
|
||||
|
||||
# Host
|
||||
if supervisor_token == self.sys_plugins.cli.supervisor_token:
|
||||
_LOGGER.debug("%s access from Host", request.path)
|
||||
request_from = self.sys_host
|
||||
|
||||
# Observer
|
||||
if supervisor_token == self.sys_plugins.observer.supervisor_token:
|
||||
if not OBSERVER_CHECK.match(request.url):
|
||||
_LOGGER.warning("%s invalid Observer access", request.path)
|
||||
raise HTTPForbidden()
|
||||
_LOGGER.debug("%s access from Observer", request.path)
|
||||
request_from = self.sys_plugins.observer
|
||||
|
||||
# Add-on
|
||||
addon = None
|
||||
if supervisor_token and not request_from:
|
||||
addon = self.sys_addons.from_token(supervisor_token)
|
||||
|
||||
# Check Add-on API access
|
||||
if addon and ADDONS_API_BYPASS.match(request.path):
|
||||
_LOGGER.debug("Passthrough %s from %s", request.path, addon.slug)
|
||||
request_from = addon
|
||||
elif addon and addon.access_hassio_api:
|
||||
# Check Role
|
||||
if ADDONS_ROLE_ACCESS[addon.hassio_role].match(request.path):
|
||||
_LOGGER.info("%s access from %s", request.path, addon.slug)
|
||||
request_from = addon
|
||||
else:
|
||||
_LOGGER.warning("%s no role for %s", request.path, addon.slug)
|
||||
elif addon:
|
||||
_LOGGER.warning(
|
||||
"%s missing API permission for %s", addon.slug, request.path
|
||||
)
|
||||
|
||||
if request_from:
|
||||
request[REQUEST_FROM] = request_from
|
||||
return await handler(request)
|
||||
|
||||
_LOGGER.error("Invalid token for access %s", request.path)
|
||||
raise HTTPForbidden()
|
@@ -1,9 +1,16 @@
|
||||
|
||||
try {
|
||||
new Function("import('/api/hassio/app/frontend_latest/entrypoint.cf81f6d9.js')")();
|
||||
} catch (err) {
|
||||
function loadES5() {
|
||||
var el = document.createElement('script');
|
||||
el.src = '/api/hassio/app/frontend_es5/entrypoint.c258a457.js';
|
||||
el.src = '/api/hassio/app/frontend_es5/entrypoint.e3a9299f.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.48b24b78.js')")();
|
||||
} catch (err) {
|
||||
loadES5();
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
2
supervisor/api/panel/frontend_es5/01378878.js
Normal file
2
supervisor/api/panel/frontend_es5/01378878.js
Normal file
@@ -0,0 +1,2 @@
|
||||
(self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[]).push([[534],{68441:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var l=new WeakMap;t.default=function(e){var t=l.get(e);return t||(t=Object.create(null),l.set(e,t)),t}},78643:function(e,t,l){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.PluralRules=void 0;var a=l(87480),n=l(43965),r=a.__importDefault(l(68441));function o(e,t){if(!(e instanceof u))throw new TypeError("Method Intl.PluralRules.prototype."+t+" called on incompatible receiver "+String(e))}function i(e,t,l,a){var n=a.IntegerDigits,r=a.NumberOfFractionDigits,o=a.FractionDigits;return u.localeData[e].fn(r?n+"."+o:n,"ordinal"===t)}var u=function(){function e(t,l){if(!(this&&this instanceof e?this.constructor:void 0))throw new TypeError("Intl.PluralRules must be called with 'new'");return n.InitializePluralRules(this,t,l,{availableLocales:e.availableLocales,relevantExtensionKeys:e.relevantExtensionKeys,localeData:e.localeData,getDefaultLocale:e.getDefaultLocale,getInternalSlots:r.default})}return e.prototype.resolvedOptions=function(){o(this,"resolvedOptions");var t=Object.create(null),l=r.default(this);return t.locale=l.locale,t.type=l.type,["minimumIntegerDigits","minimumFractionDigits","maximumFractionDigits","minimumSignificantDigits","maximumSignificantDigits"].forEach((function(e){var a=l[e];void 0!==a&&(t[e]=a)})),t.pluralCategories=a.__spreadArray([],e.localeData[t.locale].categories[t.type]),t},e.prototype.select=function(e){o(this,"select");var t=n.ToNumber(e);return n.ResolvePlural(this,t,{getInternalSlots:r.default,PluralRuleSelect:i})},e.prototype.toString=function(){return"[object Intl.PluralRules]"},e.supportedLocalesOf=function(t,l){return n.SupportedLocales(e.availableLocales,n.CanonicalizeLocaleList(t),l)},e.__addLocaleData=function(){for(var t=[],l=0;l<arguments.length;l++)t[l]=arguments[l];for(var a=0,n=t;a<n.length;a++){var r=n[a],o=r.data,i=r.locale;e.localeData[i]=o,e.availableLocales.add(i),e.__defaultLocale||(e.__defaultLocale=i)}},e.getDefaultLocale=function(){return e.__defaultLocale},e.localeData={},e.availableLocales=new Set,e.__defaultLocale="",e.relevantExtensionKeys=[],e.polyfilled=!0,e}();t.PluralRules=u;try{"undefined"!=typeof Symbol&&Object.defineProperty(u.prototype,Symbol.toStringTag,{value:"Intl.PluralRules",writable:!1,enumerable:!1,configurable:!0});try{Object.defineProperty(u,"length",{value:0,writable:!1,enumerable:!1,configurable:!0})}catch(c){}Object.defineProperty(u.prototype.constructor,"length",{value:0,writable:!1,enumerable:!1,configurable:!0}),Object.defineProperty(u.supportedLocalesOf,"length",{value:1,writable:!1,enumerable:!1,configurable:!0})}catch(s){}},25534:function(e,t,l){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var a=l(78643);l(27815).shouldPolyfill()&&Object.defineProperty(Intl,"PluralRules",{value:a.PluralRules,writable:!0,enumerable:!1,configurable:!0})},27815:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.shouldPolyfill=void 0,t.shouldPolyfill=function(){return"undefined"==typeof Intl||!("PluralRules"in Intl)||"one"===new Intl.PluralRules("en",{minimumFractionDigits:2}).select(1)}}}]);
|
||||
//# sourceMappingURL=01378878.js.map
|
BIN
supervisor/api/panel/frontend_es5/01378878.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/01378878.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/01378878.js.map
Normal file
1
supervisor/api/panel/frontend_es5/01378878.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"01378878.js","sources":["webpack://home-assistant-frontend/01378878.js"],"mappings":"AAAA","sourceRoot":""}
|
2
supervisor/api/panel/frontend_es5/02e63dc5.js
Normal file
2
supervisor/api/panel/frontend_es5/02e63dc5.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/02e63dc5.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/02e63dc5.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/02e63dc5.js.map
Normal file
1
supervisor/api/panel/frontend_es5/02e63dc5.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"02e63dc5.js","sources":["webpack://home-assistant-frontend/02e63dc5.js"],"mappings":"AAAA","sourceRoot":""}
|
3
supervisor/api/panel/frontend_es5/0bbd5196.js
Normal file
3
supervisor/api/panel/frontend_es5/0bbd5196.js
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
/*! js-yaml 4.1.0 https://github.com/nodeca/js-yaml @license MIT */
|
BIN
supervisor/api/panel/frontend_es5/0bbd5196.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/0bbd5196.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/0bbd5196.js.map
Normal file
1
supervisor/api/panel/frontend_es5/0bbd5196.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"0bbd5196.js","sources":["webpack://home-assistant-frontend/0bbd5196.js"],"mappings":";AAAA","sourceRoot":""}
|
2
supervisor/api/panel/frontend_es5/2995ba79.js
Normal file
2
supervisor/api/panel/frontend_es5/2995ba79.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/2995ba79.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/2995ba79.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/2995ba79.js.map
Normal file
1
supervisor/api/panel/frontend_es5/2995ba79.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"2995ba79.js","sources":["webpack://home-assistant-frontend/2995ba79.js"],"mappings":"AAAA","sourceRoot":""}
|
2
supervisor/api/panel/frontend_es5/2f1a948c.js
Normal file
2
supervisor/api/panel/frontend_es5/2f1a948c.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/2f1a948c.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/2f1a948c.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/2f1a948c.js.map
Normal file
1
supervisor/api/panel/frontend_es5/2f1a948c.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"2f1a948c.js","sources":["webpack://home-assistant-frontend/2f1a948c.js"],"mappings":"AAAA","sourceRoot":""}
|
2
supervisor/api/panel/frontend_es5/4b7385c7.js
Normal file
2
supervisor/api/panel/frontend_es5/4b7385c7.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/4b7385c7.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/4b7385c7.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/4b7385c7.js.map
Normal file
1
supervisor/api/panel/frontend_es5/4b7385c7.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"4b7385c7.js","sources":["webpack://home-assistant-frontend/4b7385c7.js"],"mappings":"AAAA","sourceRoot":""}
|
3
supervisor/api/panel/frontend_es5/5891fe3e.js
Normal file
3
supervisor/api/panel/frontend_es5/5891fe3e.js
Normal file
File diff suppressed because one or more lines are too long
20
supervisor/api/panel/frontend_es5/5891fe3e.js.LICENSE.txt
Normal file
20
supervisor/api/panel/frontend_es5/5891fe3e.js.LICENSE.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
@license
|
||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||
This code may only be used under the BSD style license found at
|
||||
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||
part of the polymer project is also subject to an additional IP rights grant
|
||||
found at http://polymer.github.io/PATENTS.txt
|
||||
*/
|
||||
|
||||
/**
|
||||
@license
|
||||
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
|
||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||
Code distributed by Google as part of the polymer project is also
|
||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||
*/
|
BIN
supervisor/api/panel/frontend_es5/5891fe3e.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/5891fe3e.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/5891fe3e.js.map
Normal file
1
supervisor/api/panel/frontend_es5/5891fe3e.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"5891fe3e.js","sources":["webpack://home-assistant-frontend/5891fe3e.js"],"mappings":";AAAA","sourceRoot":""}
|
2
supervisor/api/panel/frontend_es5/6aa86e64.js
Normal file
2
supervisor/api/panel/frontend_es5/6aa86e64.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/6aa86e64.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/6aa86e64.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/6aa86e64.js.map
Normal file
1
supervisor/api/panel/frontend_es5/6aa86e64.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"6aa86e64.js","sources":["webpack://home-assistant-frontend/6aa86e64.js"],"mappings":"AAAA","sourceRoot":""}
|
2
supervisor/api/panel/frontend_es5/799b0b1b.js
Normal file
2
supervisor/api/panel/frontend_es5/799b0b1b.js
Normal file
@@ -0,0 +1,2 @@
|
||||
!function(){"use strict";var r,t,n={5425:function(r,t,n){var e=n(91107);n(58556);function o(r,t){return function(r){if(Array.isArray(r))return r}(r)||function(r,t){var n=null==r?null:"undefined"!=typeof Symbol&&r[Symbol.iterator]||r["@@iterator"];if(null==n)return;var e,o,u=[],i=!0,a=!1;try{for(n=n.call(r);!(i=(e=n.next()).done)&&(u.push(e.value),!t||u.length!==t);i=!0);}catch(f){a=!0,o=f}finally{try{i||null==n.return||n.return()}finally{if(a)throw o}}return u}(r,t)||function(r,t){if(!r)return;if("string"==typeof r)return u(r,t);var n=Object.prototype.toString.call(r).slice(8,-1);"Object"===n&&r.constructor&&(n=r.constructor.name);if("Map"===n||"Set"===n)return Array.from(r);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return u(r,t)}(r,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function u(r,t){(null==t||t>r.length)&&(t=r.length);for(var n=0,e=new Array(t);n<t;n++)e[n]=r[n];return e}var i={filterData:function(r,t,n){return n=n.toUpperCase(),r.filter((function(r){return Object.entries(t).some((function(t){var e=o(t,2),u=e[0],i=e[1];return!(!i.filterable||!String(i.filterKey?r[i.valueColumn||u][i.filterKey]:r[i.valueColumn||u]).toUpperCase().includes(n))}))}))},sortData:function(r,t,n,e){return r.sort((function(r,o){var u=1;"desc"===n&&(u=-1);var i=t.filterKey?r[t.valueColumn||e][t.filterKey]:r[t.valueColumn||e],a=t.filterKey?o[t.valueColumn||e][t.filterKey]:o[t.valueColumn||e];return"string"==typeof i&&(i=i.toUpperCase()),"string"==typeof a&&(a=a.toUpperCase()),void 0===i&&void 0!==a?1:void 0===a&&void 0!==i?-1:i<a?-1*u:i>a?1*u:0}))}};(0,e.Jj)(i)}},e={};function o(r){var t=e[r];if(void 0!==t)return t.exports;var u=e[r]={exports:{}};return n[r](u,u.exports,o),u.exports}o.m=n,o.x=function(){var r=o.O(void 0,[354],(function(){return o(5425)}));return r=o.O(r)},r=[],o.O=function(t,n,e,u){if(!n){var i=1/0;for(l=0;l<r.length;l++){n=r[l][0],e=r[l][1],u=r[l][2];for(var a=!0,f=0;f<n.length;f++)(!1&u||i>=u)&&Object.keys(o.O).every((function(r){return o.O[r](n[f])}))?n.splice(f--,1):(a=!1,u<i&&(i=u));a&&(r.splice(l--,1),t=e())}return t}u=u||0;for(var l=r.length;l>0&&r[l-1][2]>u;l--)r[l]=r[l-1];r[l]=[n,e,u]},o.n=function(r){var t=r&&r.__esModule?function(){return r.default}:function(){return r};return o.d(t,{a:t}),t},o.d=function(r,t){for(var n in t)o.o(t,n)&&!o.o(r,n)&&Object.defineProperty(r,n,{enumerable:!0,get:t[n]})},o.f={},o.e=function(r){return Promise.all(Object.keys(o.f).reduce((function(t,n){return o.f[n](r,t),t}),[]))},o.u=function(r){return"e612d98f.js"},o.o=function(r,t){return Object.prototype.hasOwnProperty.call(r,t)},o.p="/api/hassio/app/frontend_es5/",function(){var r={425:1,477:1};o.f.i=function(t,n){r[t]||importScripts(o.p+o.u(t))};var t=self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[],n=t.push.bind(t);t.push=function(t){var e=t[0],u=t[1],i=t[2];for(var a in u)o.o(u,a)&&(o.m[a]=u[a]);for(i&&i(o);e.length;)r[e.pop()]=1;n(t)}}(),t=o.x,o.x=function(){return o.e(354).then(t)};o.x()}();
|
||||
//# sourceMappingURL=799b0b1b.js.map
|
BIN
supervisor/api/panel/frontend_es5/799b0b1b.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/799b0b1b.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/799b0b1b.js.map
Normal file
1
supervisor/api/panel/frontend_es5/799b0b1b.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"799b0b1b.js","sources":["webpack://home-assistant-frontend/799b0b1b.js"],"mappings":"AAAA","sourceRoot":""}
|
2
supervisor/api/panel/frontend_es5/7f7c3271.js
Normal file
2
supervisor/api/panel/frontend_es5/7f7c3271.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/7f7c3271.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/7f7c3271.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/7f7c3271.js.map
Normal file
1
supervisor/api/panel/frontend_es5/7f7c3271.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"7f7c3271.js","sources":["webpack://home-assistant-frontend/7f7c3271.js"],"mappings":"AAAA","sourceRoot":""}
|
2
supervisor/api/panel/frontend_es5/9476142e.js
Normal file
2
supervisor/api/panel/frontend_es5/9476142e.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/9476142e.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/9476142e.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/9476142e.js.map
Normal file
1
supervisor/api/panel/frontend_es5/9476142e.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"9476142e.js","sources":["webpack://home-assistant-frontend/9476142e.js"],"mappings":"AAAA","sourceRoot":""}
|
2
supervisor/api/panel/frontend_es5/9b327635.js
Normal file
2
supervisor/api/panel/frontend_es5/9b327635.js
Normal file
@@ -0,0 +1,2 @@
|
||||
(self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[]).push([[827],{64827:function(){Intl.PluralRules&&"function"==typeof Intl.PluralRules.__addLocaleData&&Intl.PluralRules.__addLocaleData({data:{categories:{cardinal:["one","other"],ordinal:["one","two","few","other"]},fn:function(e,n){var t=String(e).split("."),a=!t[1],l=Number(t[0])==e,o=l&&t[0].slice(-1),r=l&&t[0].slice(-2);return n?1==o&&11!=r?"one":2==o&&12!=r?"two":3==o&&13!=r?"few":"other":1==e&&a?"one":"other"}},locale:"en"})}}]);
|
||||
//# sourceMappingURL=9b327635.js.map
|
BIN
supervisor/api/panel/frontend_es5/9b327635.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/9b327635.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/9b327635.js.map
Normal file
1
supervisor/api/panel/frontend_es5/9b327635.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"9b327635.js","sources":["webpack://home-assistant-frontend/9b327635.js"],"mappings":"AAAA","sourceRoot":""}
|
2
supervisor/api/panel/frontend_es5/9e6dea06.js
Normal file
2
supervisor/api/panel/frontend_es5/9e6dea06.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/9e6dea06.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/9e6dea06.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/9e6dea06.js.map
Normal file
1
supervisor/api/panel/frontend_es5/9e6dea06.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"9e6dea06.js","sources":["webpack://home-assistant-frontend/9e6dea06.js"],"mappings":"AAAA","sourceRoot":""}
|
2
supervisor/api/panel/frontend_es5/a17514d4.js
Normal file
2
supervisor/api/panel/frontend_es5/a17514d4.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/a17514d4.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/a17514d4.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/a17514d4.js.map
Normal file
1
supervisor/api/panel/frontend_es5/a17514d4.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"a17514d4.js","sources":["webpack://home-assistant-frontend/a17514d4.js"],"mappings":"AAAA","sourceRoot":""}
|
2
supervisor/api/panel/frontend_es5/af420a02.js
Normal file
2
supervisor/api/panel/frontend_es5/af420a02.js
Normal file
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/af420a02.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/af420a02.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/af420a02.js.map
Normal file
1
supervisor/api/panel/frontend_es5/af420a02.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"af420a02.js","sources":["webpack://home-assistant-frontend/af420a02.js"],"mappings":"AAAA","sourceRoot":""}
|
2
supervisor/api/panel/frontend_es5/b7bea667.js
Normal file
2
supervisor/api/panel/frontend_es5/b7bea667.js
Normal file
@@ -0,0 +1,2 @@
|
||||
!function(){"use strict";var n,t,r={14971:function(n,t,r){var e,o,i=r(91107),u=r(9902),a=r.n(u),f=(r(58556),r(62173)),s={renderMarkdown:function(n,t){var r,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return e||(e=Object.assign({},(0,f.getDefaultWhiteList)(),{"ha-icon":["icon"],"ha-svg-icon":["path"]})),i.allowSvg?(o||(o=Object.assign({},e,{svg:["xmlns","height","width"],path:["transform","stroke","d"],img:["src"]})),r=o):r=e,(0,f.filterXSS)(a()(n,t),{whiteList:r})}};(0,i.Jj)(s)}},e={};function o(n){var t=e[n];if(void 0!==t)return t.exports;var i=e[n]={exports:{}};return r[n].call(i.exports,i,i.exports,o),i.exports}o.m=r,o.x=function(){var n=o.O(void 0,[354,468],(function(){return o(14971)}));return n=o.O(n)},n=[],o.O=function(t,r,e,i){if(!r){var u=1/0;for(s=0;s<n.length;s++){r=n[s][0],e=n[s][1],i=n[s][2];for(var a=!0,f=0;f<r.length;f++)(!1&i||u>=i)&&Object.keys(o.O).every((function(n){return o.O[n](r[f])}))?r.splice(f--,1):(a=!1,i<u&&(u=i));a&&(n.splice(s--,1),t=e())}return t}i=i||0;for(var s=n.length;s>0&&n[s-1][2]>i;s--)n[s]=n[s-1];n[s]=[r,e,i]},o.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return o.d(t,{a:t}),t},o.d=function(n,t){for(var r in t)o.o(t,r)&&!o.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:t[r]})},o.f={},o.e=function(n){return Promise.all(Object.keys(o.f).reduce((function(t,r){return o.f[r](n,t),t}),[]))},o.u=function(n){return{354:"e612d98f",468:"2995ba79"}[n]+".js"},o.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)},o.p="/api/hassio/app/frontend_es5/",function(){var n={971:1};o.f.i=function(t,r){n[t]||importScripts(o.p+o.u(t))};var t=self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[],r=t.push.bind(t);t.push=function(t){var e=t[0],i=t[1],u=t[2];for(var a in i)o.o(i,a)&&(o.m[a]=i[a]);for(u&&u(o);e.length;)n[e.pop()]=1;r(t)}}(),t=o.x,o.x=function(){return Promise.all([o.e(354),o.e(468)]).then(t)};o.x()}();
|
||||
//# sourceMappingURL=b7bea667.js.map
|
BIN
supervisor/api/panel/frontend_es5/b7bea667.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/b7bea667.js.gz
Normal file
Binary file not shown.
1
supervisor/api/panel/frontend_es5/b7bea667.js.map
Normal file
1
supervisor/api/panel/frontend_es5/b7bea667.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"b7bea667.js","sources":["webpack://home-assistant-frontend/b7bea667.js"],"mappings":"AAAA","sourceRoot":""}
|
2
supervisor/api/panel/frontend_es5/c858d7b3.js
Normal file
2
supervisor/api/panel/frontend_es5/c858d7b3.js
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user