mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-10-24 02:59:38 +00:00
Compare commits
675 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c51496ad2f | ||
|
|
fbe409337b | ||
|
|
443a43cc5b | ||
|
|
0e25fad1c0 | ||
|
|
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 | ||
|
|
15a6f38ebb | ||
|
|
227f2e5a21 | ||
|
|
517d6ee981 | ||
|
|
184eeb7f49 | ||
|
|
a3555c74e8 | ||
|
|
657bafd458 | ||
|
|
2ad5df420c | ||
|
|
b24d489ec5 | ||
|
|
6bb0210f1f | ||
|
|
c1de50266a | ||
|
|
562e02bc64 | ||
|
|
f71ec7913a | ||
|
|
d98baaf660 | ||
|
|
72db591576 | ||
|
|
509a37fc04 | ||
|
|
17f62b6e86 | ||
|
|
b09aee7644 | ||
|
|
babcc0de0c | ||
|
|
5cc47c9222 | ||
|
|
833559a3b3 | ||
|
|
b8a976b344 | ||
|
|
10b14132b9 | ||
|
|
18953f0b7c | ||
|
|
19f5fba3aa | ||
|
|
636bc3e61a | ||
|
|
521037e1a6 | ||
|
|
e024c3e38d | ||
|
|
6a0206c1e7 | ||
|
|
69a8a83528 | ||
|
|
0307d700fa | ||
|
|
919c383b41 | ||
|
|
d331af4d5a | ||
|
|
b5467d3c23 | ||
|
|
3ef0040d66 | ||
|
|
ec4dfd2172 | ||
|
|
8f54d7c8e9 | ||
|
|
b59e709dc0 | ||
|
|
b236e6c886 | ||
|
|
8acbb7d6f0 | ||
|
|
49e4bc9381 | ||
|
|
36106cc08d | ||
|
|
8f1763abe2 | ||
|
|
480eebc6cb | ||
|
|
dccfffd979 | ||
|
|
1a978f4762 | ||
|
|
1fbc8f4060 | ||
|
|
db3fc1421c | ||
|
|
9ecd03db0e | ||
|
|
f111ccb1b6 | ||
|
|
a32341cc5d | ||
|
|
f73e277230 | ||
|
|
8d8587ca29 | ||
|
|
88eb9511bf | ||
|
|
e1068997ea | ||
|
|
560e04c64a | ||
|
|
621ec03971 | ||
|
|
d14a47d3f7 | ||
|
|
3e9de0c210 | ||
|
|
01a6e074a5 | ||
|
|
1434077f4e | ||
|
|
3922175af1 | ||
|
|
ec6852a8d7 | ||
|
|
b0b908b4ae | ||
|
|
5a00336ef1 | ||
|
|
d53f5e21f4 | ||
|
|
bd173fa333 | ||
|
|
6b32fa31b6 | ||
|
|
d1dba89e39 | ||
|
|
b2f2806465 | ||
|
|
3b70cd58a3 | ||
|
|
6769bfd824 | ||
|
|
bff4af2534 | ||
|
|
aabf575ac5 | ||
|
|
4aacaf6bd6 | ||
|
|
268070e89c | ||
|
|
4fa2134cc6 | ||
|
|
97c35de49a | ||
|
|
32fb550969 | ||
|
|
3457441929 | ||
|
|
32f0fc7a46 | ||
|
|
c891a8f164 | ||
|
|
991764af94 | ||
|
|
1df447272e | ||
|
|
f032ae757b | ||
|
|
e529b2859e | ||
|
|
d1cb2368fa | ||
|
|
7b812184d1 | ||
|
|
4f7ce1c6ee | ||
|
|
44a5ac63db | ||
|
|
0989ee88cc | ||
|
|
ba8b72c2c8 | ||
|
|
2dbb4583f4 | ||
|
|
81ad42e029 | ||
|
|
5d3695f8ba | ||
|
|
c771694aa0 | ||
|
|
1ac0fd4c10 | ||
|
|
631ff8caef | ||
|
|
7382182132 | ||
|
|
488a2327fb | ||
|
|
b99ed631c5 | ||
|
|
726dd3a8f9 | ||
|
|
4ff9da68ef | ||
|
|
dee2998ee3 | ||
|
|
1d43236211 | ||
|
|
792bc610a3 | ||
|
|
7a51c828c2 | ||
|
|
fab6fcd5ac | ||
|
|
c8e00ba160 | ||
|
|
6245b6d823 | ||
|
|
0c55bf20fc | ||
|
|
f8fd7b5933 | ||
|
|
6462eea2ef | ||
|
|
3d79891249 | ||
|
|
80763c4bbf | ||
|
|
59102afd45 | ||
|
|
0b085354db | ||
|
|
e2a473baa3 | ||
|
|
06e10fdd3c | ||
|
|
fb4386a7ad | ||
|
|
2d294f6841 | ||
|
|
e09a839148 | ||
|
|
d9c4dae739 | ||
|
|
49853e92a4 | ||
|
|
19620d6808 | ||
|
|
f6bf44de1c | ||
|
|
06fae59fc8 | ||
|
|
5c25fcd84c | ||
|
|
aa5297026f | ||
|
|
b94810d044 | ||
|
|
841520b75e | ||
|
|
d9e20307de | ||
|
|
fda1b523ba | ||
|
|
7cccbc682c | ||
|
|
621eb4c4c0 | ||
|
|
056926242f | ||
|
|
84294f286f | ||
|
|
b6bc6b8498 | ||
|
|
845c935b39 | ||
|
|
19d8de89df | ||
|
|
cfae20a3ec | ||
|
|
6db6ab96e6 | ||
|
|
48695c6805 | ||
|
|
d74908e3b5 | ||
|
|
7e94537e36 | ||
|
|
1427e0ae96 | ||
|
|
01e27dfa2f | ||
|
|
f48249c9d1 | ||
|
|
e607d4feeb | ||
|
|
5367ac257e | ||
|
|
46dc6dc63b | ||
|
|
a59ea72c66 | ||
|
|
2daf46c444 | ||
|
|
1bf38bdc99 | ||
|
|
131909973c | ||
|
|
ecdf4e53b8 | ||
|
|
7aa039d162 | ||
|
|
3dd3340e35 | ||
|
|
2f9fc39b72 | ||
|
|
80f4309799 | ||
|
|
550fca4bcd | ||
|
|
4b500ef873 | ||
|
|
476f021fbf | ||
|
|
8393ca5b23 | ||
|
|
4eb7a60b88 | ||
|
|
2040102e21 | ||
|
|
7ee5737f75 | ||
|
|
8d499753a0 | ||
|
|
028ec277eb | ||
|
|
5552b1da49 | ||
|
|
06ab7e904f | ||
|
|
e4bf820038 | ||
|
|
c209d2fa8d | ||
|
|
784c5d3b7c | ||
|
|
a18b706f99 | ||
|
|
cd34a40dd8 | ||
|
|
ced72e1273 | ||
|
|
5416eda1d6 | ||
|
|
c76a4ff422 | ||
|
|
d558ad2d76 | ||
|
|
5f2d183b1d | ||
|
|
46e92036ec | ||
|
|
280d423bfe | ||
|
|
c4847ad10d | ||
|
|
0a7c75830b | ||
|
|
7aceb21123 | ||
|
|
9264d437b1 | ||
|
|
edc8d8960f | ||
|
|
0f4f196dc9 | ||
|
|
a027f4b5fc | ||
|
|
f025d1df05 | ||
|
|
4c560d7c54 | ||
|
|
a976ef6e67 | ||
|
|
e1b9d754af | ||
|
|
ee49935b7d | ||
|
|
36694c9ef0 | ||
|
|
bd786811a3 | ||
|
|
ffaeb2b96d | ||
|
|
517e6cb437 | ||
|
|
9479672b88 | ||
|
|
934e59596a | ||
|
|
8f4ac10361 | ||
|
|
50e0fd159f | ||
|
|
28344ff5f3 | ||
|
|
608ae14246 | ||
|
|
c0c0d44c2d | ||
|
|
5e947348ae | ||
|
|
1f4032f56f | ||
|
|
336ab0d2b1 | ||
|
|
0f9d80dde4 | ||
|
|
0fcab4d92b | ||
|
|
abd35b62c8 | ||
|
|
7b721ad8c6 | ||
|
|
37eaaf356d | ||
|
|
8cbb4b510b | ||
|
|
f71549e3df | ||
|
|
fe15bb6a30 | ||
|
|
50d36b857a | ||
|
|
db260dfbde | ||
|
|
a0c99615aa | ||
|
|
c66c806e6e | ||
|
|
a432d28ee3 | ||
|
|
742bc43500 | ||
|
|
223e2f1df5 | ||
|
|
ed9aea6219 | ||
|
|
aa7b68d4d5 | ||
|
|
8e57cd2751 | ||
|
|
64229a188e | ||
|
|
10cbbcc2de | ||
|
|
5318e4fbcd | ||
|
|
007251a04c | ||
|
|
fd4b3ee539 | ||
|
|
976ae96633 | ||
|
|
042bdcdf37 | ||
|
|
c423e9cf8e | ||
|
|
1f13d6aa91 | ||
|
|
78c09a0fa6 | ||
|
|
e6d6f2ee8c | ||
|
|
2918ef6225 | ||
|
|
35b626a1c5 | ||
|
|
6f26536d97 | ||
|
|
01064564b4 | ||
|
|
0c6c6a6620 | ||
|
|
be166d533f | ||
|
|
d3e5535221 | ||
|
|
7206213cd8 | ||
|
|
605782d707 | ||
|
|
2c387349c9 | ||
|
|
e9c9f98168 | ||
|
|
a98c7819b0 | ||
|
|
7c42c5758d | ||
|
|
c555146094 | ||
|
|
f48c9b5774 | ||
|
|
8d1732e5eb | ||
|
|
f2843db421 | ||
|
|
b513512551 | ||
|
|
1a59839b1b | ||
|
|
45861617b9 | ||
|
|
353544085e | ||
|
|
144d3921f7 | ||
|
|
e44d22880e | ||
|
|
c7692b43e8 | ||
|
|
9c53caae80 | ||
|
|
7a1d85ca2b | ||
|
|
e684223f32 | ||
|
|
9744f3354b | ||
|
|
c6e3787681 | ||
|
|
eab76a6d1d | ||
|
|
6549a10935 | ||
|
|
530d40dbbd | ||
|
|
50f2d8e7d8 | ||
|
|
7a9aac491e | ||
|
|
7dcb609fd5 | ||
|
|
d119e99001 | ||
|
|
fe0e41adec | ||
|
|
034393bd42 | ||
|
|
02e72726a5 | ||
|
|
d599c3ad76 | ||
|
|
b00f7c44df | ||
|
|
028b170cff | ||
|
|
8da686fc34 | ||
|
|
3f6453aa89 | ||
|
|
7967254673 | ||
|
|
0f60fdd20b | ||
|
|
ccb8e5fe06 | ||
|
|
ba576d8748 | ||
|
|
edcd9ca6e6 | ||
|
|
ac4277cd7b | ||
|
|
4c525de5e2 | ||
|
|
cb751e0397 | ||
|
|
ac457c1c28 | ||
|
|
caa77b9337 | ||
|
|
96d8785349 | ||
|
|
e4f57d2269 | ||
|
|
f946de1e46 | ||
|
|
d588987b8b | ||
|
|
4925b5fa97 | ||
|
|
aa3f6390d3 | ||
|
|
6ba413f452 | ||
|
|
10b6706e4a | ||
|
|
17559bfc8e | ||
|
|
9dc2f43ffb | ||
|
|
38db375fea | ||
|
|
f35b6d0b00 | ||
|
|
8d75583a07 | ||
|
|
361fc51477 | ||
|
|
f6019b4e68 | ||
|
|
998dd5387b | ||
|
|
3b7776ca01 | ||
|
|
5788d1dd32 | ||
|
|
ff04d339f4 | ||
|
|
e9d03c5c8e | ||
|
|
ab4b98470e | ||
|
|
c4f0702595 | ||
|
|
736c9cb2bd | ||
|
|
f24e8535d3 | ||
|
|
8e4f3e0526 | ||
|
|
a9abd933b5 | ||
|
|
f1121fe66f | ||
|
|
c26a2e399c | ||
|
|
1af90721cc | ||
|
|
9274a0fa17 | ||
|
|
9443032c2a | ||
|
|
8deb1cf2e6 | ||
|
|
13c7ce6a0a | ||
|
|
0f54824cdb | ||
|
|
10cd722806 | ||
|
|
4aca056c5b | ||
|
|
15a8f40f6f | ||
|
|
2ca2701f7a | ||
|
|
78f63380f2 | ||
|
|
7633d26806 | ||
|
|
15cae6562f | ||
|
|
c3546eb566 | ||
|
|
2a77a29f48 | ||
|
|
77be115eec | ||
|
|
64a6c2e07c | ||
|
|
045a3ba416 | ||
|
|
4da2715d14 | ||
|
|
2f0e99d420 | ||
|
|
8694eaaf1a | ||
|
|
0761885ebb | ||
|
|
ca60a69b22 | ||
|
|
c9db42583b | ||
|
|
052a691a4d | ||
|
|
2bcb0e5195 | ||
|
|
745845db19 | ||
|
|
de064d1d9c | ||
|
|
3b2351af0b | ||
|
|
5cf833e3d6 | ||
|
|
409b53109b | ||
|
|
da83edf231 | ||
|
|
7be508214a | ||
|
|
8b4a137252 | ||
|
|
4565b01eeb | ||
|
|
0675f66ee6 | ||
|
|
b60d57c3a0 | ||
|
|
0e3d95cac0 | ||
|
|
548737a559 | ||
|
|
598108d294 | ||
|
|
a0261dbbcc | ||
|
|
2418122b46 | ||
|
|
f104e60afa | ||
|
|
ed45f27f3e | ||
|
|
40aa5c9caf | ||
|
|
14b1ea4eb0 | ||
|
|
5052a339e3 | ||
|
|
2321890dde | ||
|
|
4cb5770ee0 | ||
|
|
3a35561d1d | ||
|
|
6fbec53f8a | ||
|
|
c707934018 | ||
|
|
efd8efa248 | ||
|
|
979861b764 | ||
|
|
cdc53a159c | ||
|
|
a203ed9cc5 | ||
|
|
5cab5f0c08 | ||
|
|
25ea80e169 | ||
|
|
f43b4e9e24 | ||
|
|
160fbb2589 | ||
|
|
c85aa664e1 | ||
|
|
51dcbf5db7 | ||
|
|
fa114a4a03 | ||
|
|
d7fd58bdb9 | ||
|
|
38b0aea8e2 | ||
|
|
41eade9325 | ||
|
|
e64cf41aec | ||
|
|
02872b5e75 | ||
|
|
e4d49bb459 | ||
|
|
d38b7d5a82 | ||
|
|
537c5d3197 | ||
|
|
575df2fcf6 | ||
|
|
c08c3c6b37 | ||
|
|
2acf28609e | ||
|
|
bb59d0431e | ||
|
|
1c7b1f1462 | ||
|
|
f32d17d924 | ||
|
|
928a4d8dce | ||
|
|
dd3ba93308 | ||
|
|
7e1b179cdd | ||
|
|
a9a2c35f06 | ||
|
|
58b88a6919 | ||
|
|
f937876a1b | ||
|
|
8193f43634 | ||
|
|
1d3f880f82 | ||
|
|
ef2fa8d2e2 | ||
|
|
51997b3e7c | ||
|
|
98785b00e2 | ||
|
|
8d3694884d | ||
|
|
a2821a98ad | ||
|
|
8d552ae15c | ||
|
|
6db4c60f47 | ||
|
|
805c0385a0 | ||
|
|
cea6e7a9f2 | ||
|
|
127073c01b | ||
|
|
30fe36ae05 | ||
|
|
58bd677832 | ||
|
|
1a3b369dd7 | ||
|
|
6e38216abd | ||
|
|
efcfc1f841 | ||
|
|
8dea50ce83 | ||
|
|
7a5a01bdcc | ||
|
|
bd1450a682 | ||
|
|
c538c1ce7f | ||
|
|
b6d59c4f64 | ||
|
|
a758ccaf5c | ||
|
|
e8b04cc20a | ||
|
|
9bcb15dbc0 | ||
|
|
1e953167b6 | ||
|
|
979586cdb2 | ||
|
|
cd31fad56d | ||
|
|
ff57d88e2a | ||
|
|
06cb5e171e | ||
|
|
a8b70a2e13 | ||
|
|
948019ccee | ||
|
|
89ed109505 | ||
|
|
fae246c503 | ||
|
|
2411b4287d | ||
|
|
b3308ecbe0 | ||
|
|
3541cbff5e | ||
|
|
838ba7ff36 | ||
|
|
e9802f92c9 | ||
|
|
016fd24859 | ||
|
|
d315e81ab2 | ||
|
|
97c38b8534 | ||
|
|
011e2b3df5 | ||
|
|
e3ee9a299f | ||
|
|
d73c10f874 | ||
|
|
9e448b46ba | ||
|
|
9f09c46789 | ||
|
|
fe6634551a | ||
|
|
22a7931a7c | ||
|
|
94f112512f | ||
|
|
b6509dca1f | ||
|
|
620234e708 | ||
|
|
d50e866cec | ||
|
|
76ad6dca02 | ||
|
|
cdb1520a63 | ||
|
|
bbef706a33 | ||
|
|
835509901f | ||
|
|
b51f9586c4 | ||
|
|
fc83cb9559 | ||
|
|
f5f5f829ac | ||
|
|
930eed4500 | ||
|
|
01a8b58054 | ||
|
|
eba1d01fc2 | ||
|
|
84755836c9 | ||
|
|
c9585033cb | ||
|
|
2d312c276f | ||
|
|
3b0d0e9928 | ||
|
|
8307b153e3 | ||
|
|
dfaffe3ec5 | ||
|
|
8d7b15cbeb | ||
|
|
00969a67ac | ||
|
|
a374d4e817 | ||
|
|
f5dda39f63 | ||
|
|
fb5d54d5fe |
@@ -1,14 +1,24 @@
|
|||||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.8
|
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.8
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
SHELL ["/bin/bash", "-c"]
|
||||||
|
|
||||||
WORKDIR /workspaces
|
WORKDIR /workspaces
|
||||||
|
|
||||||
|
# Set Docker daemon config
|
||||||
|
RUN \
|
||||||
|
mkdir -p /etc/docker \
|
||||||
|
&& echo '{"storage-driver": "vfs"}' > /etc/docker/daemon.json
|
||||||
|
|
||||||
# Install Node/Yarn for Frontent
|
# 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 \
|
curl \
|
||||||
git \
|
git \
|
||||||
apt-utils \
|
apt-utils \
|
||||||
apt-transport-https \
|
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 \
|
&& 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 \
|
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||||
nodejs \
|
nodejs \
|
||||||
@@ -39,10 +49,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
dbus \
|
dbus \
|
||||||
network-manager \
|
network-manager \
|
||||||
libpulse0 \
|
libpulse0 \
|
||||||
|
&& bash <(curl https://getvcn.codenotary.com -L) \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install Python dependencies from requirements.txt if it exists
|
# Install Python dependencies from requirements.txt if it exists
|
||||||
COPY requirements.txt requirements_tests.txt ./
|
COPY requirements.txt requirements_tests.txt ./
|
||||||
RUN pip3 install -r requirements.txt -r requirements_tests.txt \
|
RUN pip3 install -U setuptools pip \
|
||||||
|
&& pip3 install -r requirements.txt -r requirements_tests.txt \
|
||||||
&& pip3 install tox \
|
&& pip3 install tox \
|
||||||
&& rm -f requirements.txt requirements_tests.txt
|
&& rm -f requirements.txt requirements_tests.txt
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"appPort": "9123:8123",
|
"appPort": "9123:8123",
|
||||||
"postCreateCommand": "pre-commit install",
|
"postCreateCommand": "pre-commit install",
|
||||||
"runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
|
"runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
|
||||||
|
"containerEnv": {"NVM_DIR":"/usr/local/share/nvm"},
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"ms-python.python",
|
"ms-python.python",
|
||||||
"ms-python.vscode-pylance",
|
"ms-python.vscode-pylance",
|
||||||
|
|||||||
65
.github/ISSUE_TEMPLATE.md
vendored
65
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,28 +1,69 @@
|
|||||||
|
---
|
||||||
|
name: Report a bug with the Supervisor on a supported System
|
||||||
|
about: Report an issue related to the Home Assistant Supervisor.
|
||||||
|
labels: bug
|
||||||
|
---
|
||||||
|
|
||||||
<!-- READ THIS FIRST:
|
<!-- READ THIS FIRST:
|
||||||
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/core/releases
|
|
||||||
- Do not report issues for integrations here, please refer to https://github.com/home-assistant/core/issues
|
|
||||||
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
||||||
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
|
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
|
||||||
- If you have a problem with an add-on, make an issue in its repository.
|
- If you have a problem with an add-on, make an issue in it's repository.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
**Home Assistant release with the issue:**
|
|
||||||
<!--
|
<!--
|
||||||
- Frontend -> Configuration -> Info
|
Important: You can only fill a bug repport for an supported system! If you run an unsupported installation. This report would be closed without comment.
|
||||||
- Or use this command: hass --version
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
**Operating environment (HassOS/Generic):**
|
### Describe the issue
|
||||||
<!--
|
|
||||||
Please provide details about your environment.
|
|
||||||
-->
|
|
||||||
|
|
||||||
**Supervisor logs:**
|
<!-- Provide as many details as possible. -->
|
||||||
|
|
||||||
|
### Steps to reproduce
|
||||||
|
|
||||||
|
<!-- What do you do to encounter the issue. -->
|
||||||
|
|
||||||
|
1. ...
|
||||||
|
2. ...
|
||||||
|
3. ...
|
||||||
|
|
||||||
|
### Enviroment details
|
||||||
|
|
||||||
|
<!-- You can find these details in the system tab of the supervisor panel, or by using the `ha` CLI. -->
|
||||||
|
|
||||||
|
- **Operating System:**: xxx
|
||||||
|
- **Supervisor version:**: xxx
|
||||||
|
- **Home Assistant version**: xxx
|
||||||
|
|
||||||
|
### Supervisor logs
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Supervisor logs</summary>
|
||||||
<!--
|
<!--
|
||||||
- Frontend -> Supervisor -> System
|
- Frontend -> Supervisor -> System
|
||||||
- Or use this command: ha supervisor logs
|
- Or use this command: ha supervisor logs
|
||||||
|
- Logs are more than just errors, even if you don't think it's important, it is.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
```
|
||||||
|
Paste supervisor logs here
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### System Information
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>System Information</summary>
|
||||||
|
<!--
|
||||||
|
- Use this command: ha info
|
||||||
|
-->
|
||||||
|
|
||||||
|
```
|
||||||
|
Paste system info here
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
**Description of problem:**
|
|
||||||
|
|||||||
106
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
106
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
name: Bug Report Form
|
||||||
|
about: Report an issue related to the Home Assistant Supervisor.
|
||||||
|
labels: bug
|
||||||
|
title: ""
|
||||||
|
issue_body: true
|
||||||
|
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: 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?
|
||||||
|
description: >
|
||||||
|
If you don't know, you can find it in: Configuration panel -> Info.
|
||||||
|
options:
|
||||||
|
- Home Assistant OS
|
||||||
|
- Home Assistant Supervised
|
||||||
|
- type: dropdown
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Which operating system are you running on?
|
||||||
|
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?
|
||||||
|
placeholder: "5.11"
|
||||||
|
description: Can be found in the Supervisor panel -> System tab.
|
||||||
|
- type: input
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: What version of Home Assistant Core is installed?
|
||||||
|
placeholder: core-
|
||||||
|
description: >
|
||||||
|
Can be found in the Supervisor panel -> System tab. Starts with
|
||||||
|
`core-....`.
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# Details
|
||||||
|
- type: textarea
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce the issue
|
||||||
|
description: |
|
||||||
|
Please tell us exactly how to reproduce your issue.
|
||||||
|
Provide clear and concise step by step instructions and add code snippets if needed.
|
||||||
|
value: |
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
...
|
||||||
|
- type: textarea
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Anything in the Supervisor logs that might be useful for us?
|
||||||
|
description: >
|
||||||
|
The Supervisor logs can be found in the Supervisor panel -> System tab.
|
||||||
|
value: |
|
||||||
|
```txt
|
||||||
|
# Put your logs below this line
|
||||||
|
|
||||||
|
```
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Additional information
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
If you have any additional information for us, use the field below.
|
||||||
|
Please note, you can attach screenshots or screen recordings here.
|
||||||
25
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
25
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Report a bug/issues with an unsupported Supervisor
|
||||||
|
url: https://community.home-assistant.io
|
||||||
|
about: The Community guide can help or was updated to solve your issue
|
||||||
|
|
||||||
|
- name: Report a bug for the Supervisor panel
|
||||||
|
url: https://github.com/home-assistant/frontend/issues
|
||||||
|
about: The Supervisor panel is a part of the Home Assistant frontend
|
||||||
|
|
||||||
|
- name: Report incorrect or missing information on our developer documentation
|
||||||
|
url: https://github.com/home-assistant/developers.home-assistant.io/issues
|
||||||
|
about: Our documentation has its own issue tracker. Please report issues with the website there.
|
||||||
|
|
||||||
|
- name: Request a feature for the Supervisor
|
||||||
|
url: https://community.home-assistant.io/c/feature-requests
|
||||||
|
about: Request an new feature for the Supervisor.
|
||||||
|
|
||||||
|
- name: I have a question or need support
|
||||||
|
url: https://www.home-assistant.io/help
|
||||||
|
about: We use GitHub for tracking bugs, check our website for resources on getting help.
|
||||||
|
|
||||||
|
- name: I'm unsure where to go?
|
||||||
|
url: https://www.home-assistant.io/join-chat
|
||||||
|
about: If you are unsure where to go, then joining our chat is recommended; Just ask!
|
||||||
69
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
69
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<!--
|
||||||
|
You are amazing! Thanks for contributing to our project!
|
||||||
|
Please, DO NOT DELETE ANY TEXT from this template! (unless instructed).
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Proposed change
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe the big picture of your changes here to communicate to the
|
||||||
|
maintainers why we should accept this pull request. If it fixes a bug
|
||||||
|
or resolves a feature request, be sure to link to that issue in the
|
||||||
|
additional information section.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Type of change
|
||||||
|
|
||||||
|
<!--
|
||||||
|
What type of change does your PR introduce to Home Assistant?
|
||||||
|
NOTE: Please, check only 1! box!
|
||||||
|
If your PR requires multiple boxes to be checked, you'll most likely need to
|
||||||
|
split it into multiple PRs. This makes things easier and faster to code review.
|
||||||
|
-->
|
||||||
|
|
||||||
|
- [ ] Dependency upgrade
|
||||||
|
- [ ] Bugfix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] New feature (which adds functionality to the supervisor)
|
||||||
|
- [ ] Breaking change (fix/feature causing existing functionality to break)
|
||||||
|
- [ ] Code quality improvements to existing code or addition of tests
|
||||||
|
|
||||||
|
## Additional information
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Details are important, and help maintainers processing your PR.
|
||||||
|
Please be sure to fill out additional details, if applicable.
|
||||||
|
-->
|
||||||
|
|
||||||
|
- This PR fixes or closes issue: fixes #
|
||||||
|
- This PR is related to issue:
|
||||||
|
- Link to documentation pull request:
|
||||||
|
- Link to cli pull request:
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Put an `x` in the boxes that apply. You can also fill these out after
|
||||||
|
creating the PR. If you're unsure about any of them, don't hesitate to ask.
|
||||||
|
We're here to help! This is simply a reminder of what we are going to look
|
||||||
|
for before merging your code.
|
||||||
|
-->
|
||||||
|
|
||||||
|
- [ ] The code change is tested and works locally.
|
||||||
|
- [ ] Local tests pass. **Your PR cannot be merged unless tests pass**
|
||||||
|
- [ ] There is no commented out code in this PR.
|
||||||
|
- [ ] I have followed the [development checklist][dev-checklist]
|
||||||
|
- [ ] The code has been formatted using Black (`black --fast supervisor tests`)
|
||||||
|
- [ ] Tests have been added to verify that the new code works.
|
||||||
|
|
||||||
|
If API endpoints of add-on configuration are added/changed:
|
||||||
|
|
||||||
|
- [ ] Documentation added/updated for [developers.home-assistant.io][docs-repository]
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Thank you for contributing <3
|
||||||
|
|
||||||
|
Below, some useful links you could explore:
|
||||||
|
-->
|
||||||
|
|
||||||
|
[dev-checklist]: https://developers.home-assistant.io/docs/en/development_checklist.html
|
||||||
|
[docs-repository]: https://github.com/home-assistant/developers.home-assistant
|
||||||
27
.github/lock.yml
vendored
27
.github/lock.yml
vendored
@@ -1,27 +0,0 @@
|
|||||||
# Configuration for Lock Threads - https://github.com/dessant/lock-threads
|
|
||||||
|
|
||||||
# Number of days of inactivity before a closed issue or pull request is locked
|
|
||||||
daysUntilLock: 1
|
|
||||||
|
|
||||||
# Skip issues and pull requests created before a given timestamp. Timestamp must
|
|
||||||
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
|
|
||||||
skipCreatedBefore: 2020-01-01
|
|
||||||
|
|
||||||
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
|
|
||||||
exemptLabels: []
|
|
||||||
|
|
||||||
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
|
||||||
lockLabel: false
|
|
||||||
|
|
||||||
# Comment to post before locking. Set to `false` to disable
|
|
||||||
lockComment: false
|
|
||||||
|
|
||||||
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
|
||||||
setLockReason: false
|
|
||||||
|
|
||||||
# Limit to only `issues` or `pulls`
|
|
||||||
only: pulls
|
|
||||||
|
|
||||||
# Optionally, specify configuration settings just for `issues` or `pulls`
|
|
||||||
issues:
|
|
||||||
daysUntilLock: 30
|
|
||||||
47
.github/release-drafter.yml
vendored
47
.github/release-drafter.yml
vendored
@@ -1,4 +1,49 @@
|
|||||||
|
change-template: "- #$NUMBER $TITLE @$AUTHOR"
|
||||||
|
sort-direction: ascending
|
||||||
|
|
||||||
|
categories:
|
||||||
|
- title: ":boom: Breaking Changes"
|
||||||
|
label: "breaking-change"
|
||||||
|
|
||||||
|
- title: ":wrench: Build"
|
||||||
|
label: "build"
|
||||||
|
|
||||||
|
- title: ":boar: Chore"
|
||||||
|
label: "chore"
|
||||||
|
|
||||||
|
- title: ":sparkles: New Features"
|
||||||
|
label: "new-feature"
|
||||||
|
|
||||||
|
- title: ":zap: Performance"
|
||||||
|
label: "performance"
|
||||||
|
|
||||||
|
- title: ":recycle: Refactor"
|
||||||
|
label: "refactor"
|
||||||
|
|
||||||
|
- title: ":green_heart: CI"
|
||||||
|
label: "ci"
|
||||||
|
|
||||||
|
- title: ":bug: Bug Fixes"
|
||||||
|
label: "bugfix"
|
||||||
|
|
||||||
|
- title: ":white_check_mark: Test"
|
||||||
|
label: "test"
|
||||||
|
|
||||||
|
- title: ":arrow_up: Dependency Updates"
|
||||||
|
label: "dependencies"
|
||||||
|
|
||||||
|
include-labels:
|
||||||
|
- "breaking-change"
|
||||||
|
- "build"
|
||||||
|
- "chore"
|
||||||
|
- "performance"
|
||||||
|
- "refactor"
|
||||||
|
- "new-feature"
|
||||||
|
- "bugfix"
|
||||||
|
- "dependencies"
|
||||||
|
- "test"
|
||||||
|
- "ci"
|
||||||
|
|
||||||
template: |
|
template: |
|
||||||
## What's Changed
|
|
||||||
|
|
||||||
$CHANGES
|
$CHANGES
|
||||||
|
|||||||
17
.github/stale.yml
vendored
17
.github/stale.yml
vendored
@@ -1,17 +0,0 @@
|
|||||||
# Number of days of inactivity before an issue becomes stale
|
|
||||||
daysUntilStale: 60
|
|
||||||
# Number of days of inactivity before a stale issue is closed
|
|
||||||
daysUntilClose: 7
|
|
||||||
# Issues with these labels will never be considered stale
|
|
||||||
exemptLabels:
|
|
||||||
- pinned
|
|
||||||
- security
|
|
||||||
# Label to use when marking an issue as stale
|
|
||||||
staleLabel: stale
|
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
|
||||||
markComment: >
|
|
||||||
This issue has been automatically marked as stale because it has not had
|
|
||||||
recent activity. It will be closed if no further activity occurs. Thank you
|
|
||||||
for your contributions.
|
|
||||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
|
||||||
closeComment: false
|
|
||||||
252
.github/workflows/builder.yml
vendored
Normal file
252
.github/workflows/builder.yml
vendored
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
name: Build supervisor
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
channel:
|
||||||
|
description: "Channel"
|
||||||
|
required: true
|
||||||
|
default: "dev"
|
||||||
|
version:
|
||||||
|
description: "Version"
|
||||||
|
required: true
|
||||||
|
publish:
|
||||||
|
description: "Publish"
|
||||||
|
required: true
|
||||||
|
default: "false"
|
||||||
|
stable:
|
||||||
|
description: "Stable"
|
||||||
|
required: true
|
||||||
|
default: "false"
|
||||||
|
pull_request:
|
||||||
|
branches: ["main"]
|
||||||
|
release:
|
||||||
|
types: ["published"]
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
paths:
|
||||||
|
- "rootfs/**"
|
||||||
|
- "supervisor/**"
|
||||||
|
- build.json
|
||||||
|
- Dockerfile
|
||||||
|
- requirements.txt
|
||||||
|
- setup.py
|
||||||
|
|
||||||
|
env:
|
||||||
|
BUILD_NAME: supervisor
|
||||||
|
BUILD_TYPE: supervisor
|
||||||
|
WHEELS_TAG: 3.8-alpine3.13
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
init:
|
||||||
|
name: Initialize build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
architectures: ${{ steps.info.outputs.architectures }}
|
||||||
|
version: ${{ steps.version.outputs.version }}
|
||||||
|
channel: ${{ steps.version.outputs.channel }}
|
||||||
|
publish: ${{ steps.version.outputs.publish }}
|
||||||
|
requirements: ${{ steps.requirements.outputs.changed }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Get information
|
||||||
|
id: info
|
||||||
|
uses: home-assistant/actions/helpers/info@master
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
id: version
|
||||||
|
uses: home-assistant/actions/helpers/version@master
|
||||||
|
with:
|
||||||
|
type: ${{ env.BUILD_TYPE }}
|
||||||
|
|
||||||
|
- name: Get changed files
|
||||||
|
id: changed_files
|
||||||
|
if: steps.version.outputs.publish == 'false'
|
||||||
|
uses: jitterbit/get-changed-files@v1
|
||||||
|
|
||||||
|
- name: Check if requirements files changed
|
||||||
|
id: requirements
|
||||||
|
run: |
|
||||||
|
if [[ "${{ steps.changed_files.outputs.all }}" =~ requirements.txt ]]; then
|
||||||
|
echo "::set-output name=changed::true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build ${{ matrix.arch }} supervisor
|
||||||
|
needs: init
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Build wheels
|
||||||
|
if: needs.init.outputs.requirements == 'true'
|
||||||
|
uses: home-assistant/wheels@master
|
||||||
|
with:
|
||||||
|
tag: ${{ env.WHEELS_TAG }}
|
||||||
|
arch: ${{ matrix.arch }}
|
||||||
|
wheels-host: ${{ secrets.WHEELS_HOST }}
|
||||||
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
|
wheels-user: wheels
|
||||||
|
apk: "build-base;libffi-dev;openssl-dev;cargo"
|
||||||
|
skip-binary: aiohttp
|
||||||
|
requirements: "requirements.txt"
|
||||||
|
|
||||||
|
- name: Set version
|
||||||
|
if: needs.init.outputs.publish == 'true'
|
||||||
|
uses: home-assistant/actions/helpers/version@master
|
||||||
|
with:
|
||||||
|
type: ${{ env.BUILD_TYPE }}
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
if: needs.init.outputs.publish == 'true'
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
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
|
||||||
|
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.04.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 }}" \
|
||||||
|
--validate-cache "${{ 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
|
||||||
|
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"]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
if: needs.init.outputs.publish == 'true'
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Initialize git
|
||||||
|
if: needs.init.outputs.publish == 'true'
|
||||||
|
uses: home-assistant/actions/helpers/git-init@master
|
||||||
|
with:
|
||||||
|
name: ${{ secrets.GIT_NAME }}
|
||||||
|
email: ${{ secrets.GIT_EMAIL }}
|
||||||
|
token: ${{ secrets.GIT_TOKEN }}
|
||||||
|
|
||||||
|
- name: Update version file
|
||||||
|
if: needs.init.outputs.publish == 'true'
|
||||||
|
uses: home-assistant/actions/helpers/version-push@master
|
||||||
|
with:
|
||||||
|
key: ${{ env.BUILD_NAME }}
|
||||||
|
version: ${{ needs.init.outputs.version }}
|
||||||
|
channel: ${{ needs.init.outputs.channel }}
|
||||||
|
|
||||||
|
run_supervisor:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Run the Supervisor
|
||||||
|
needs: ["build", "codenotary"]
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Build the Supervisor
|
||||||
|
uses: home-assistant/builder@2021.04.0
|
||||||
|
with:
|
||||||
|
args: |
|
||||||
|
--test \
|
||||||
|
--amd64 \
|
||||||
|
--target /data \
|
||||||
|
--generic runner
|
||||||
|
|
||||||
|
- name: Create the Supervisor
|
||||||
|
run: |
|
||||||
|
mkdir -p /tmp/supervisor/data
|
||||||
|
docker create --name hassio_supervisor \
|
||||||
|
--privileged \
|
||||||
|
--security-opt seccomp=unconfined \
|
||||||
|
--security-opt apparmor:unconfined \
|
||||||
|
-v /run/docker.sock:/run/docker.sock \
|
||||||
|
-v /run/dbus:/run/dbus \
|
||||||
|
-v /tmp/supervisor/data:/data \
|
||||||
|
-v /etc/machine-id:/etc/machine-id:ro \
|
||||||
|
-e SUPERVISOR_SHARE="/tmp/supervisor/data" \
|
||||||
|
-e SUPERVISOR_NAME=hassio_supervisor \
|
||||||
|
-e SUPERVISOR_DEV=1 \
|
||||||
|
-e SUPERVISOR_MACHINE="qemux86-64" \
|
||||||
|
homeassistant/amd64-hassio-supervisor:runner
|
||||||
|
|
||||||
|
- name: Start the Supervisor
|
||||||
|
run: docker start hassio_supervisor
|
||||||
|
|
||||||
|
- name: Wait for Supervisor to come up
|
||||||
|
run: |
|
||||||
|
SUPERVISOR=$(docker inspect --format='{{.NetworkSettings.IPAddress}}' hassio_supervisor)
|
||||||
|
ping="error"
|
||||||
|
while [ "$ping" != "ok" ]; do
|
||||||
|
ping=$(curl -sSL "http://$SUPERVISOR/supervisor/ping" | jq -r .result)
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Check the Supervisor
|
||||||
|
run: |
|
||||||
|
echo "Checking supervisor info"
|
||||||
|
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)
|
||||||
|
if [ "$test" != "ok" ];then
|
||||||
|
docker logs hassio_supervisor
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
19
.github/workflows/check_pr_labels.yml
vendored
Normal file
19
.github/workflows/check_pr_labels.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: Check PR
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: ["main"]
|
||||||
|
types: [labeled, unlabeled, synchronize]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
init:
|
||||||
|
name: Check labels
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check labels
|
||||||
|
run: |
|
||||||
|
labels=$(jq -r '.pull_request.labels[] | .name' ${{github.event_path }})
|
||||||
|
echo "$labels"
|
||||||
|
if [ "$labels" == "cla-signed" ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
61
.github/workflows/ci.yaml
vendored
61
.github/workflows/ci.yaml
vendored
@@ -4,8 +4,7 @@ name: CI
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- main
|
||||||
- master
|
|
||||||
pull_request: ~
|
pull_request: ~
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -26,12 +25,12 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v2.1.2
|
uses: actions/setup-python@v2.2.2
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.5
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -48,7 +47,7 @@ jobs:
|
|||||||
pip install -r requirements.txt -r requirements_tests.txt
|
pip install -r requirements.txt -r requirements_tests.txt
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.5
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_HOME }}
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
key: |
|
key: |
|
||||||
@@ -69,13 +68,13 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2.1.2
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.5
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -113,13 +112,13 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2.1.2
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.5
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -131,7 +130,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.5
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_HOME }}
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
key: |
|
key: |
|
||||||
@@ -157,13 +156,13 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2.1.2
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.5
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -189,13 +188,13 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2.1.2
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.5
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -207,7 +206,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.5
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_HOME }}
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
key: |
|
key: |
|
||||||
@@ -230,13 +229,13 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2.1.2
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.5
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -248,7 +247,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.5
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_HOME }}
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
key: |
|
key: |
|
||||||
@@ -274,13 +273,13 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2.1.2
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.5
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -306,13 +305,13 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2.1.2
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.5
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -324,7 +323,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.5
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_HOME }}
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
key: |
|
key: |
|
||||||
@@ -350,13 +349,17 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2.1.2
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install CodeNotary
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
bash <(curl https://getvcn.codenotary.com -L)
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.5
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -391,7 +394,7 @@ jobs:
|
|||||||
-o console_output_style=count \
|
-o console_output_style=count \
|
||||||
tests
|
tests
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
uses: actions/upload-artifact@v2.1.4
|
uses: actions/upload-artifact@v2.2.3
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}
|
name: coverage-${{ matrix.python-version }}
|
||||||
path: .coverage
|
path: .coverage
|
||||||
@@ -404,13 +407,13 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v2.1.2
|
uses: actions/setup-python@v2.2.2
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2.1.5
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -429,4 +432,4 @@ jobs:
|
|||||||
coverage report
|
coverage report
|
||||||
coverage xml
|
coverage xml
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v1.0.12
|
uses: codecov/codecov-action@v1.3.2
|
||||||
|
|||||||
20
.github/workflows/lock.yml
vendored
Normal file
20
.github/workflows/lock.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: Lock
|
||||||
|
|
||||||
|
# yamllint disable-line rule:truthy
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lock:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: dessant/lock-threads@v2.0.3
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
issue-lock-inactive-days: "30"
|
||||||
|
issue-exclude-created-before: "2020-10-01T00:00:00Z"
|
||||||
|
issue-lock-reason: ""
|
||||||
|
pr-lock-inactive-days: "1"
|
||||||
|
pr-exclude-created-before: "2020-11-01T00:00:00Z"
|
||||||
|
pr-lock-reason: ""
|
||||||
35
.github/workflows/release-drafter.yml
vendored
35
.github/workflows/release-drafter.yml
vendored
@@ -2,14 +2,43 @@ name: Release Drafter
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
# branches to consider in the event; optional, defaults to all
|
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update_release_draft:
|
update_release_draft:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
name: Release Drafter
|
||||||
steps:
|
steps:
|
||||||
- uses: release-drafter/release-drafter@v5
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Find Next Version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
declare -i newpost
|
||||||
|
latest=$(git describe --tags $(git rev-list --tags --max-count=1))
|
||||||
|
latestpre=$(echo "$latest" | awk '{split($0,a,"."); print a[1] "." a[2]}')
|
||||||
|
datepre=$(date --utc '+%Y.%m')
|
||||||
|
|
||||||
|
|
||||||
|
if [[ "$latestpre" == "$datepre" ]]; then
|
||||||
|
latestpost=$(echo "$latest" | awk '{split($0,a,"."); print a[3]}')
|
||||||
|
newpost=$latestpost+1
|
||||||
|
else
|
||||||
|
newpost=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo Current version: $latest
|
||||||
|
echo New target version: $datepre.$newpost
|
||||||
|
echo "::set-output name=version::$datepre.$newpost"
|
||||||
|
|
||||||
|
- name: Run Release Drafter
|
||||||
|
uses: release-drafter/release-drafter@v5
|
||||||
|
with:
|
||||||
|
tag: ${{ steps.version.outputs.version }}
|
||||||
|
name: ${{ steps.version.outputs.version }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
21
.github/workflows/sentry.yaml
vendored
Normal file
21
.github/workflows/sentry.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Sentry Release
|
||||||
|
|
||||||
|
# yamllint disable-line rule:truthy
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published, prereleased]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
createSentryRelease:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Sentry Release
|
||||||
|
uses: getsentry/action-release@v1.1
|
||||||
|
env:
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||||
|
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||||
|
with:
|
||||||
|
environment: production
|
||||||
39
.github/workflows/stale.yml
vendored
Normal file
39
.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Stale
|
||||||
|
|
||||||
|
# yamllint disable-line rule:truthy
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 * * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v3.0.18
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
days-before-stale: 60
|
||||||
|
days-before-close: 7
|
||||||
|
stale-issue-label: "stale"
|
||||||
|
exempt-issue-labels: "no-stale,Help%20wanted,help-wanted,pinned,rfc,security"
|
||||||
|
stale-issue-message: >
|
||||||
|
There hasn't been any activity on this issue recently. Due to the
|
||||||
|
high number of incoming GitHub notifications, we have to clean some
|
||||||
|
of the old issues, as many of them have already been resolved with
|
||||||
|
the latest updates.
|
||||||
|
|
||||||
|
Please make sure to update to the latest version and check if that
|
||||||
|
solves the issue. Let us know if that works for you by
|
||||||
|
adding a comment 👍
|
||||||
|
|
||||||
|
This issue has now been marked as stale and will be closed if no
|
||||||
|
further activity occurs. Thank you for your contributions.
|
||||||
|
|
||||||
|
stale-pr-label: "stale"
|
||||||
|
exempt-pr-labels: "no-stale,pinned,rfc,security"
|
||||||
|
stale-pr-message: >
|
||||||
|
There hasn't been any activity on this pull request recently. This
|
||||||
|
pull request has been automatically marked as stale because of that
|
||||||
|
and will be closed if no further activity occurs within 7 days.
|
||||||
|
|
||||||
|
Thank you for your contributions.
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
ignored:
|
ignored:
|
||||||
- DL3018
|
- DL3003
|
||||||
- DL3006
|
- DL3006
|
||||||
- DL3013
|
- DL3013
|
||||||
|
- DL3018
|
||||||
- SC2155
|
- SC2155
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 19.10b0
|
rev: 20.8b1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args:
|
args:
|
||||||
- --safe
|
- --safe
|
||||||
- --quiet
|
- --quiet
|
||||||
|
- --target-version
|
||||||
|
- py38
|
||||||
files: ^((supervisor|tests)/.+)?[^/]+\.py$
|
files: ^((supervisor|tests)/.+)?[^/]+\.py$
|
||||||
- repo: https://gitlab.com/pycqa/flake8
|
- repo: https://gitlab.com/pycqa/flake8
|
||||||
rev: 3.8.3
|
rev: 3.8.3
|
||||||
|
|||||||
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/
|
||||||
22
.vscode/tasks.json
vendored
22
.vscode/tasks.json
vendored
@@ -2,9 +2,9 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"label": "Run Testenv",
|
"label": "Run Supervisor",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "./scripts/test_env.sh",
|
"command": "./scripts/run-supervisor.sh",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
@@ -16,7 +16,21 @@
|
|||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Run Testenv CLI",
|
"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",
|
"type": "shell",
|
||||||
"command": "docker exec -ti hassio_cli /usr/bin/cli.sh",
|
"command": "docker exec -ti hassio_cli /usr/bin/cli.sh",
|
||||||
"group": {
|
"group": {
|
||||||
@@ -30,7 +44,7 @@
|
|||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Update UI",
|
"label": "Update Supervisor Panel",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "./scripts/update-frontend.sh",
|
"command": "./scripts/update-frontend.sh",
|
||||||
"group": {
|
"group": {
|
||||||
|
|||||||
46
Dockerfile
46
Dockerfile
@@ -1,12 +1,18 @@
|
|||||||
ARG BUILD_FROM
|
ARG BUILD_FROM
|
||||||
FROM $BUILD_FROM
|
FROM ${BUILD_FROM}
|
||||||
|
|
||||||
ENV \
|
ENV \
|
||||||
S6_SERVICES_GRACETIME=10000
|
S6_SERVICES_GRACETIME=10000 \
|
||||||
|
SUPERVISOR_API=http://localhost
|
||||||
|
|
||||||
|
ARG BUILD_ARCH
|
||||||
|
ARG VCN_VERSION
|
||||||
|
WORKDIR /usr/src
|
||||||
|
|
||||||
# Install base
|
# Install base
|
||||||
RUN \
|
RUN \
|
||||||
apk add --no-cache \
|
set -x \
|
||||||
|
&& apk add --no-cache \
|
||||||
eudev \
|
eudev \
|
||||||
eudev-libs \
|
eudev-libs \
|
||||||
git \
|
git \
|
||||||
@@ -15,10 +21,36 @@ RUN \
|
|||||||
libpulse \
|
libpulse \
|
||||||
musl \
|
musl \
|
||||||
openssl \
|
openssl \
|
||||||
socat
|
&& apk add --no-cache --virtual .build-dependencies \
|
||||||
|
build-base \
|
||||||
ARG BUILD_ARCH
|
go \
|
||||||
WORKDIR /usr/src
|
\
|
||||||
|
&& git clone -b v${VCN_VERSION} --depth 1 \
|
||||||
|
https://github.com/codenotary/vcn \
|
||||||
|
&& cd vcn \
|
||||||
|
\
|
||||||
|
# Fix: https://github.com/codenotary/vcn/issues/131
|
||||||
|
&& go get github.com/codenotary/immudb@4cf9e2ae06ac2e6ec98a60364c3de3eab5524757 \
|
||||||
|
\
|
||||||
|
&& 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
|
# Install requirements
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -10,17 +10,23 @@ network settings or installing and updating software.
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Installation instructions can be found at https://home-assistant.io/hassio.
|
Installation instructions can be found at https://home-assistant.io/getting-started.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
The development of the Supervisor is not difficult but tricky.
|
For small changes and bugfixes you can just follow this, but for significant changes open a RFC first.
|
||||||
|
Development instructions can be found [here][development].
|
||||||
|
|
||||||
- You can use the builder to create your Supervisor: https://github.com/home-assistant/hassio-builder
|
## Release
|
||||||
- Access a HassOS device or VM and pull your Supervisor.
|
|
||||||
- Set the developer modus with the CLI tool: `ha supervisor options --channel=dev`
|
|
||||||
- Tag it as `homeassistant/xy-hassio-supervisor:latest`
|
|
||||||
- Restart the service with `systemctl restart hassos-supervisor | journalctl -fu hassos-supervisor`
|
|
||||||
- Test your changes
|
|
||||||
|
|
||||||
For small bugfixes or improvements, make a PR. For significant changes open a RFC first, please. Thanks.
|
Releases are done in 3 stages (channels) with this structure:
|
||||||
|
|
||||||
|
1. Pull requests are merged to the `main` branch.
|
||||||
|
2. A new build is pushed to the `dev` stage.
|
||||||
|
3. Releases are published.
|
||||||
|
4. A new build is pushed to the `beta` stage.
|
||||||
|
5. The [`stable.json`][stable] file is updated.
|
||||||
|
6. The build that was pushed to `beta` will now be pushed to `stable`.
|
||||||
|
|
||||||
|
[development]: https://developers.home-assistant.io/docs/supervisor/development
|
||||||
|
[stable]: https://github.com/home-assistant/version/blob/master/stable.json
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
# https://dev.azure.com/home-assistant
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
batch: true
|
|
||||||
branches:
|
|
||||||
include:
|
|
||||||
- master
|
|
||||||
- dev
|
|
||||||
pr:
|
|
||||||
- dev
|
|
||||||
variables:
|
|
||||||
- name: versionHadolint
|
|
||||||
value: "v1.16.3"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- job: "Tox"
|
|
||||||
pool:
|
|
||||||
vmImage: "ubuntu-latest"
|
|
||||||
steps:
|
|
||||||
- script: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libpulse0 libudev1
|
|
||||||
displayName: "Install Host library"
|
|
||||||
- task: UsePythonVersion@0
|
|
||||||
displayName: "Use Python 3.8"
|
|
||||||
inputs:
|
|
||||||
versionSpec: "3.8"
|
|
||||||
- script: pip install tox
|
|
||||||
displayName: "Install Tox"
|
|
||||||
- script: tox
|
|
||||||
displayName: "Run Tox"
|
|
||||||
- job: "JQ"
|
|
||||||
pool:
|
|
||||||
vmImage: "ubuntu-latest"
|
|
||||||
steps:
|
|
||||||
- script: sudo apt-get install -y jq
|
|
||||||
displayName: "Install JQ"
|
|
||||||
- bash: |
|
|
||||||
shopt -s globstar
|
|
||||||
cat **/*.json | jq '.'
|
|
||||||
displayName: "Run JQ"
|
|
||||||
- job: "Hadolint"
|
|
||||||
pool:
|
|
||||||
vmImage: "ubuntu-latest"
|
|
||||||
steps:
|
|
||||||
- script: sudo docker pull hadolint/hadolint:$(versionHadolint)
|
|
||||||
displayName: "Install Hadolint"
|
|
||||||
- script: |
|
|
||||||
sudo docker run --rm -i \
|
|
||||||
-v $(pwd)/.hadolint.yaml:/.hadolint.yaml:ro \
|
|
||||||
hadolint/hadolint:$(versionHadolint) < Dockerfile
|
|
||||||
displayName: "Run Hadolint"
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# https://dev.azure.com/home-assistant
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
batch: true
|
|
||||||
branches:
|
|
||||||
include:
|
|
||||||
- dev
|
|
||||||
tags:
|
|
||||||
include:
|
|
||||||
- "*"
|
|
||||||
pr: none
|
|
||||||
variables:
|
|
||||||
- name: versionBuilder
|
|
||||||
value: "7.0"
|
|
||||||
- group: docker
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- job: "VersionValidate"
|
|
||||||
pool:
|
|
||||||
vmImage: "ubuntu-latest"
|
|
||||||
steps:
|
|
||||||
- task: UsePythonVersion@0
|
|
||||||
displayName: "Use Python 3.8"
|
|
||||||
inputs:
|
|
||||||
versionSpec: "3.8"
|
|
||||||
- script: |
|
|
||||||
setup_version="$(python setup.py -V)"
|
|
||||||
branch_version="$(Build.SourceBranchName)"
|
|
||||||
|
|
||||||
if [ "${branch_version}" == "dev" ]; then
|
|
||||||
exit 0
|
|
||||||
elif [ "${setup_version}" != "${branch_version}" ]; then
|
|
||||||
echo "Version of tag ${branch_version} don't match with ${setup_version}!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
displayName: "Check version of branch/tag"
|
|
||||||
- job: "Release"
|
|
||||||
dependsOn:
|
|
||||||
- "VersionValidate"
|
|
||||||
pool:
|
|
||||||
vmImage: "ubuntu-latest"
|
|
||||||
steps:
|
|
||||||
- script: sudo docker login -u $(dockerUser) -p $(dockerPassword)
|
|
||||||
displayName: "Docker hub login"
|
|
||||||
- script: sudo docker pull homeassistant/amd64-builder:$(versionBuilder)
|
|
||||||
displayName: "Install Builder"
|
|
||||||
- script: |
|
|
||||||
sudo docker run --rm --privileged \
|
|
||||||
-v ~/.docker:/root/.docker \
|
|
||||||
-v /run/docker.sock:/run/docker.sock:rw -v $(pwd):/data:ro \
|
|
||||||
homeassistant/amd64-builder:$(versionBuilder) \
|
|
||||||
--generic $(Build.SourceBranchName) --all -t /data
|
|
||||||
displayName: "Build Release"
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# https://dev.azure.com/home-assistant
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
batch: true
|
|
||||||
branches:
|
|
||||||
include:
|
|
||||||
- dev
|
|
||||||
pr: none
|
|
||||||
variables:
|
|
||||||
- name: versionWheels
|
|
||||||
value: '1.13.0-3.8-alpine3.12'
|
|
||||||
resources:
|
|
||||||
repositories:
|
|
||||||
- repository: azure
|
|
||||||
type: github
|
|
||||||
name: 'home-assistant/ci-azure'
|
|
||||||
endpoint: 'home-assistant'
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- template: templates/azp-job-wheels.yaml@azure
|
|
||||||
parameters:
|
|
||||||
builderVersion: '$(versionWheels)'
|
|
||||||
builderApk: 'build-base;libffi-dev;openssl-dev'
|
|
||||||
builderPip: 'Cython'
|
|
||||||
skipBinary: 'aiohttp'
|
|
||||||
wheelsRequirement: 'requirements.txt'
|
|
||||||
17
build.json
17
build.json
@@ -1,13 +1,18 @@
|
|||||||
{
|
{
|
||||||
"image": "homeassistant/{arch}-hassio-supervisor",
|
"image": "homeassistant/{arch}-hassio-supervisor",
|
||||||
|
"shadow_repository": "ghcr.io/home-assistant",
|
||||||
"build_from": {
|
"build_from": {
|
||||||
"aarch64": "homeassistant/aarch64-base-python:3.8-alpine3.12",
|
"aarch64": "ghcr.io/home-assistant/aarch64-base-python:3.8-alpine3.13",
|
||||||
"armhf": "homeassistant/armhf-base-python:3.8-alpine3.12",
|
"armhf": "ghcr.io/home-assistant/armhf-base-python:3.8-alpine3.13",
|
||||||
"armv7": "homeassistant/armv7-base-python:3.8-alpine3.12",
|
"armv7": "ghcr.io/home-assistant/armv7-base-python:3.8-alpine3.13",
|
||||||
"amd64": "homeassistant/amd64-base-python:3.8-alpine3.12",
|
"amd64": "ghcr.io/home-assistant/amd64-base-python:3.8-alpine3.13",
|
||||||
"i386": "homeassistant/i386-base-python:3.8-alpine3.12"
|
"i386": "ghcr.io/home-assistant/i386-base-python:3.8-alpine3.13"
|
||||||
|
},
|
||||||
|
"args": {
|
||||||
|
"VCN_VERSION": "0.9.4"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"io.hass.type": "supervisor"
|
"io.hass.type": "supervisor",
|
||||||
|
"org.opencontainers.image.source": "https://github.com/home-assistant/supervisor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,3 +7,5 @@ coverage:
|
|||||||
target: 40
|
target: 40
|
||||||
threshold: 0.09
|
threshold: 0.09
|
||||||
comment: false
|
comment: false
|
||||||
|
github_checks:
|
||||||
|
annotations: false
|
||||||
Submodule home-assistant-polymer updated: 77b25f5132...8dd3d78f21
@@ -1,19 +1,20 @@
|
|||||||
aiohttp==3.6.2
|
aiohttp==3.7.4.post0
|
||||||
async_timeout==3.0.1
|
async_timeout==3.0.1
|
||||||
attrs==19.3.0
|
atomicwrites==1.4.0
|
||||||
cchardet==2.1.6
|
attrs==20.3.0
|
||||||
colorlog==4.2.1
|
awesomeversion==21.4.0
|
||||||
|
brotli==1.0.9
|
||||||
|
cchardet==2.1.7
|
||||||
|
colorlog==4.8.0
|
||||||
cpe==1.2.1
|
cpe==1.2.1
|
||||||
cryptography==3.0
|
cryptography==3.4.6
|
||||||
debugpy==1.0.0rc1
|
debugpy==1.2.1
|
||||||
docker==4.3.0
|
docker==5.0.0
|
||||||
gitpython==3.1.7
|
gitpython==3.1.14
|
||||||
jinja2==2.11.2
|
jinja2==2.11.3
|
||||||
packaging==20.4
|
pulsectl==21.3.4
|
||||||
pulsectl==20.5.1
|
pytz==2021.1
|
||||||
pytz==2020.1
|
|
||||||
pyudev==0.22.0
|
pyudev==0.22.0
|
||||||
ruamel.yaml==0.15.100
|
ruamel.yaml==0.15.100
|
||||||
sentry-sdk==0.16.3
|
sentry-sdk==1.0.0
|
||||||
uvloop==0.14.0
|
voluptuous==0.12.1
|
||||||
voluptuous==0.11.7
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
black==19.10b0
|
black==20.8b1
|
||||||
codecov==2.1.8
|
codecov==2.1.11
|
||||||
coverage==5.2.1
|
coverage==5.5
|
||||||
flake8-docstrings==1.5.0
|
flake8-docstrings==1.6.0
|
||||||
flake8==3.8.3
|
flake8==3.9.0
|
||||||
pre-commit==2.6.0
|
pre-commit==2.12.0
|
||||||
pydocstyle==5.0.2
|
pydocstyle==6.0.0
|
||||||
pylint==2.5.3
|
pylint==2.7.4
|
||||||
pytest-aiohttp==0.3.0
|
pytest-aiohttp==0.3.0
|
||||||
pytest-cov==2.10.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-timeout==1.4.2
|
pytest-timeout==1.4.2
|
||||||
pytest==6.0.1
|
pytest==6.2.3
|
||||||
pyupgrade==2.7.2
|
pyupgrade==2.12.0
|
||||||
|
|||||||
@@ -2,9 +2,17 @@
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Start udev service
|
# Start udev service
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
|
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
|
udevd --daemon
|
||||||
|
|
||||||
bashio::log.info "Update udev information"
|
bashio::log.info "Update udev information"
|
||||||
|
touch /run/.old_udev
|
||||||
if udevadm trigger; then
|
if udevadm trigger; then
|
||||||
udevadm settle || true
|
udevadm settle || true
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ autospawn = no
|
|||||||
; daemon-binary = /usr/bin/pulseaudio
|
; daemon-binary = /usr/bin/pulseaudio
|
||||||
; extra-arguments = --log-target=syslog
|
; extra-arguments = --log-target=syslog
|
||||||
|
|
||||||
; cookie-file =
|
cookie-file = /run/pulse-cookie
|
||||||
|
|
||||||
; enable-shm = yes
|
; enable-shm = yes
|
||||||
; shm-size-bytes = 0 # setting this 0 will use the system-default, usually 64 MiB
|
; shm-size-bytes = 0 # setting this 0 will use the system-default, usually 64 MiB
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#!/usr/bin/execlineb -S0
|
#!/usr/bin/execlineb -S1
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Take down the S6 supervision tree when Supervisor fails
|
# Take down the S6 supervision tree when Supervisor fails
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
if { s6-test ${1} -ne 100 }
|
||||||
|
if { s6-test ${1} -ne 256 }
|
||||||
|
|
||||||
redirfd -w 2 /dev/null s6-svscanctl -t /var/run/s6/services
|
redirfd -w 2 /dev/null s6-svscanctl -t /var/run/s6/services
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/with-contenv bashio
|
#!/usr/bin/with-contenv bashio
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Start Service service
|
# Start Supervisor service
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
export LD_PRELOAD="/usr/local/lib/libjemalloc.so.2"
|
export LD_PRELOAD="/usr/local/lib/libjemalloc.so.2"
|
||||||
|
|
||||||
|
|||||||
8
rootfs/etc/services.d/watchdog/finish
Normal file
8
rootfs/etc/services.d/watchdog/finish
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/execlineb -S1
|
||||||
|
# ==============================================================================
|
||||||
|
# Take down the S6 supervision tree when Watchdog fails
|
||||||
|
# ==============================================================================
|
||||||
|
if { s6-test ${1} -ne 0 }
|
||||||
|
if { s6-test ${1} -ne 256 }
|
||||||
|
|
||||||
|
s6-svscanctl -t /var/run/s6/services
|
||||||
34
rootfs/etc/services.d/watchdog/run
Normal file
34
rootfs/etc/services.d/watchdog/run
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/with-contenv bashio
|
||||||
|
# ==============================================================================
|
||||||
|
# Start Watchdog service
|
||||||
|
# ==============================================================================
|
||||||
|
declare failed_count=0
|
||||||
|
declare supervisor_state
|
||||||
|
|
||||||
|
bashio::log.info "Starting local supervisor watchdog..."
|
||||||
|
|
||||||
|
while [[ failed_count -lt 2 ]];
|
||||||
|
do
|
||||||
|
sleep 300
|
||||||
|
supervisor_state="$(cat /run/supervisor)"
|
||||||
|
|
||||||
|
if [[ "${supervisor_state}" = "running" ]]; then
|
||||||
|
|
||||||
|
# Check API
|
||||||
|
if bashio::supervisor.ping; then
|
||||||
|
failed_count=0
|
||||||
|
else
|
||||||
|
bashio::log.warning "Maybe found an issue on API healthy"
|
||||||
|
((failed_count++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "close stopping" = *"${supervisor_state}"* ]]; then
|
||||||
|
bashio::log.warning "Maybe found an issue on shutdown"
|
||||||
|
((failed_count++))
|
||||||
|
else
|
||||||
|
failed_count=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
basio::exit.nok "Watchdog detected issue with Supervisor - taking container down!"
|
||||||
28
scripts/build-supervisor.sh
Executable file
28
scripts/build-supervisor.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/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
|
||||||
75
scripts/test_env.sh → scripts/common.sh
Executable file → Normal file
75
scripts/test_env.sh → scripts/common.sh
Executable file → Normal file
@@ -1,9 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -eE
|
|
||||||
|
|
||||||
DOCKER_TIMEOUT=30
|
|
||||||
DOCKER_PID=0
|
|
||||||
|
|
||||||
|
|
||||||
function start_docker() {
|
function start_docker() {
|
||||||
local starttime
|
local starttime
|
||||||
@@ -28,7 +23,6 @@ function start_docker() {
|
|||||||
echo "Docker was initialized"
|
echo "Docker was initialized"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function stop_docker() {
|
function stop_docker() {
|
||||||
local starttime
|
local starttime
|
||||||
local endtime
|
local endtime
|
||||||
@@ -54,17 +48,6 @@ function stop_docker() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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 dev -t /data --test --amd64 --no-cache
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function cleanup_lastboot() {
|
function cleanup_lastboot() {
|
||||||
if [[ -f /workspaces/test_supervisor/config.json ]]; then
|
if [[ -f /workspaces/test_supervisor/config.json ]]; then
|
||||||
echo "Cleaning up last boot"
|
echo "Cleaning up last boot"
|
||||||
@@ -73,61 +56,3 @@ function cleanup_lastboot() {
|
|||||||
rm /tmp/config.json
|
rm /tmp/config.json
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function cleanup_docker() {
|
|
||||||
echo "Cleaning up stopped containers..."
|
|
||||||
docker rm $(docker ps -a -q) || true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function setup_test_env() {
|
|
||||||
mkdir -p /workspaces/test_supervisor
|
|
||||||
|
|
||||||
echo "Start Supervisor"
|
|
||||||
docker run --rm --privileged \
|
|
||||||
--name hassio_supervisor \
|
|
||||||
--security-opt seccomp=unconfined \
|
|
||||||
--security-opt apparmor:unconfined \
|
|
||||||
-v /run/docker.sock:/run/docker.sock \
|
|
||||||
-v /run/dbus:/run/dbus \
|
|
||||||
-v "/workspaces/test_supervisor":/data \
|
|
||||||
-v /etc/machine-id:/etc/machine-id:ro \
|
|
||||||
-e SUPERVISOR_SHARE="/workspaces/test_supervisor" \
|
|
||||||
-e SUPERVISOR_NAME=hassio_supervisor \
|
|
||||||
-e SUPERVISOR_DEV=1 \
|
|
||||||
-e SUPERVISOR_MACHINE="qemux86-64" \
|
|
||||||
homeassistant/amd64-hassio-supervisor:latest
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function init_dbus() {
|
|
||||||
if pgrep dbus-daemon; then
|
|
||||||
echo "Dbus is running"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Startup dbus"
|
|
||||||
mkdir -p /var/lib/dbus
|
|
||||||
cp -f /etc/machine-id /var/lib/dbus/machine-id
|
|
||||||
|
|
||||||
# cleanups
|
|
||||||
mkdir -p /run/dbus
|
|
||||||
rm -f /run/dbus/pid
|
|
||||||
|
|
||||||
# run
|
|
||||||
dbus-daemon --system --print-address
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Start Test-Env"
|
|
||||||
|
|
||||||
start_docker
|
|
||||||
trap "stop_docker" ERR
|
|
||||||
|
|
||||||
build_supervisor
|
|
||||||
cleanup_lastboot
|
|
||||||
cleanup_docker
|
|
||||||
init_dbus
|
|
||||||
setup_test_env
|
|
||||||
stop_docker
|
|
||||||
102
scripts/run-supervisor.sh
Executable file
102
scripts/run-supervisor.sh
Executable file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
source "${BASH_SOURCE[0]%/*}/common.sh"
|
||||||
|
source "${BASH_SOURCE[0]%/*}/build-supervisor.sh"
|
||||||
|
|
||||||
|
set -eE
|
||||||
|
|
||||||
|
DOCKER_TIMEOUT=30
|
||||||
|
DOCKER_PID=0
|
||||||
|
|
||||||
|
|
||||||
|
function cleanup_docker() {
|
||||||
|
echo "Cleaning up stopped containers..."
|
||||||
|
docker rm $(docker ps -a -q) || true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function run_supervisor() {
|
||||||
|
mkdir -p /workspaces/test_supervisor
|
||||||
|
|
||||||
|
echo "Start Supervisor"
|
||||||
|
docker run --rm --privileged \
|
||||||
|
--name hassio_supervisor \
|
||||||
|
--privileged \
|
||||||
|
--security-opt seccomp=unconfined \
|
||||||
|
--security-opt apparmor:unconfined \
|
||||||
|
-v /run/docker.sock:/run/docker.sock:rw \
|
||||||
|
-v /run/dbus:/run/dbus:ro \
|
||||||
|
-v /run/udev:/run/udev:ro \
|
||||||
|
-v "/workspaces/test_supervisor":/data:rw \
|
||||||
|
-v /etc/machine-id:/etc/machine-id:ro \
|
||||||
|
-v /workspaces/supervisor:/usr/src/supervisor \
|
||||||
|
-e SUPERVISOR_SHARE="/workspaces/test_supervisor" \
|
||||||
|
-e SUPERVISOR_NAME=hassio_supervisor \
|
||||||
|
-e SUPERVISOR_DEV=1 \
|
||||||
|
-e SUPERVISOR_MACHINE="qemux86-64" \
|
||||||
|
homeassistant/amd64-hassio-supervisor:latest
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function init_dbus() {
|
||||||
|
if pgrep dbus-daemon; then
|
||||||
|
echo "Dbus is running"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Startup dbus"
|
||||||
|
mkdir -p /var/lib/dbus
|
||||||
|
cp -f /etc/machine-id /var/lib/dbus/machine-id
|
||||||
|
|
||||||
|
# cleanups
|
||||||
|
mkdir -p /run/dbus
|
||||||
|
rm -f /run/dbus/pid
|
||||||
|
|
||||||
|
# run
|
||||||
|
dbus-daemon --system --print-address
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function init_udev() {
|
||||||
|
if pgrep systemd-udevd; then
|
||||||
|
echo "udev is running"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Startup udev"
|
||||||
|
|
||||||
|
# cleanups
|
||||||
|
mkdir -p /run/udev
|
||||||
|
|
||||||
|
# run
|
||||||
|
/lib/systemd/systemd-udevd --daemon
|
||||||
|
sleep 3
|
||||||
|
udevadm trigger && udevadm settle
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Run Supervisor"
|
||||||
|
|
||||||
|
start_docker
|
||||||
|
trap "stop_docker" ERR
|
||||||
|
|
||||||
|
|
||||||
|
if [ "$( docker container inspect -f '{{.State.Status}}' hassio_supervisor )" == "running" ]; then
|
||||||
|
echo "Restarting Supervisor"
|
||||||
|
docker rm -f hassio_supervisor
|
||||||
|
init_dbus
|
||||||
|
init_udev
|
||||||
|
cleanup_lastboot
|
||||||
|
run_supervisor
|
||||||
|
stop_docker
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "Starting Supervisor"
|
||||||
|
docker system prune -f
|
||||||
|
build_supervisor
|
||||||
|
cleanup_lastboot
|
||||||
|
cleanup_docker
|
||||||
|
init_dbus
|
||||||
|
init_udev
|
||||||
|
run_supervisor
|
||||||
|
stop_docker
|
||||||
|
fi
|
||||||
21
setup.py
21
setup.py
@@ -31,14 +31,29 @@ setup(
|
|||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
platforms="any",
|
platforms="any",
|
||||||
packages=[
|
packages=[
|
||||||
"supervisor",
|
|
||||||
"supervisor.docker",
|
|
||||||
"supervisor.addons",
|
"supervisor.addons",
|
||||||
"supervisor.api",
|
"supervisor.api",
|
||||||
|
"supervisor.dbus.network",
|
||||||
|
"supervisor.dbus.payloads",
|
||||||
|
"supervisor.dbus",
|
||||||
|
"supervisor.discovery.services",
|
||||||
|
"supervisor.discovery",
|
||||||
|
"supervisor.docker",
|
||||||
|
"supervisor.homeassistant",
|
||||||
|
"supervisor.host",
|
||||||
|
"supervisor.jobs",
|
||||||
"supervisor.misc",
|
"supervisor.misc",
|
||||||
"supervisor.utils",
|
|
||||||
"supervisor.plugins",
|
"supervisor.plugins",
|
||||||
|
"supervisor.resolution.checks",
|
||||||
|
"supervisor.resolution.evaluations",
|
||||||
|
"supervisor.resolution.fixups",
|
||||||
|
"supervisor.resolution",
|
||||||
|
"supervisor.services.modules",
|
||||||
|
"supervisor.services",
|
||||||
"supervisor.snapshots",
|
"supervisor.snapshots",
|
||||||
|
"supervisor.store",
|
||||||
|
"supervisor.utils",
|
||||||
|
"supervisor",
|
||||||
],
|
],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,24 +2,25 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from supervisor import bootstrap
|
from supervisor import bootstrap
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONTAINER_OS_STARTUP_CHECK = Path("/run/os/startup-marker")
|
||||||
|
|
||||||
|
|
||||||
|
def run_os_startup_check_cleanup() -> None:
|
||||||
|
"""Cleanup OS startup check."""
|
||||||
|
if not CONTAINER_OS_STARTUP_CHECK.exists():
|
||||||
|
return
|
||||||
|
|
||||||
def initialize_event_loop():
|
|
||||||
"""Attempt to use uvloop."""
|
|
||||||
try:
|
try:
|
||||||
# pylint: disable=import-outside-toplevel
|
CONTAINER_OS_STARTUP_CHECK.unlink()
|
||||||
import uvloop
|
except OSError as err:
|
||||||
|
_LOGGER.warning("Not able to remove the startup health file: %s", err)
|
||||||
uvloop.install()
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return asyncio.get_event_loop()
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
@@ -27,7 +28,7 @@ if __name__ == "__main__":
|
|||||||
bootstrap.initialize_logging()
|
bootstrap.initialize_logging()
|
||||||
|
|
||||||
# Init async event loop
|
# Init async event loop
|
||||||
loop = initialize_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
# Check if all information are available to setup Supervisor
|
# Check if all information are available to setup Supervisor
|
||||||
bootstrap.check_environment()
|
bootstrap.check_environment()
|
||||||
@@ -36,27 +37,27 @@ if __name__ == "__main__":
|
|||||||
executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker")
|
executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker")
|
||||||
loop.set_default_executor(executor)
|
loop.set_default_executor(executor)
|
||||||
|
|
||||||
_LOGGER.info("Initialize Supervisor setup")
|
_LOGGER.info("Initializing Supervisor setup")
|
||||||
coresys = loop.run_until_complete(bootstrap.initialize_coresys())
|
coresys = loop.run_until_complete(bootstrap.initialize_coresys())
|
||||||
loop.run_until_complete(coresys.core.connect())
|
loop.run_until_complete(coresys.core.connect())
|
||||||
|
|
||||||
bootstrap.supervisor_debugger(coresys)
|
bootstrap.supervisor_debugger(coresys)
|
||||||
bootstrap.migrate_system_env(coresys)
|
bootstrap.migrate_system_env(coresys)
|
||||||
|
|
||||||
_LOGGER.info("Setup Supervisor")
|
# Signal health startup for container
|
||||||
|
run_os_startup_check_cleanup()
|
||||||
|
|
||||||
|
_LOGGER.info("Setting up Supervisor")
|
||||||
loop.run_until_complete(coresys.core.setup())
|
loop.run_until_complete(coresys.core.setup())
|
||||||
|
|
||||||
loop.call_soon_threadsafe(loop.create_task, coresys.core.start())
|
loop.call_soon_threadsafe(loop.create_task, coresys.core.start())
|
||||||
loop.call_soon_threadsafe(bootstrap.reg_signal, loop)
|
loop.call_soon_threadsafe(bootstrap.reg_signal, loop, coresys)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_LOGGER.info("Run Supervisor")
|
_LOGGER.info("Running Supervisor")
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
finally:
|
finally:
|
||||||
_LOGGER.info("Stopping Supervisor")
|
|
||||||
loop.run_until_complete(coresys.core.stop())
|
|
||||||
executor.shutdown(wait=False)
|
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|
||||||
_LOGGER.info("Close Supervisor")
|
_LOGGER.info("Closing Supervisor")
|
||||||
sys.exit(0)
|
sys.exit(coresys.core.exit_code)
|
||||||
|
|||||||
@@ -5,17 +5,24 @@ import logging
|
|||||||
import tarfile
|
import tarfile
|
||||||
from typing import Dict, List, Optional, Union
|
from typing import Dict, List, Optional, Union
|
||||||
|
|
||||||
from ..const import BOOT_AUTO, STATE_STARTED, AddonStartup
|
from ..const import AddonBoot, AddonStartup, AddonState
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..exceptions import (
|
from ..exceptions import (
|
||||||
|
AddonConfigurationError,
|
||||||
AddonsError,
|
AddonsError,
|
||||||
|
AddonsJobError,
|
||||||
AddonsNotSupportedError,
|
AddonsNotSupportedError,
|
||||||
CoreDNSError,
|
CoreDNSError,
|
||||||
DockerAPIError,
|
DockerAPIError,
|
||||||
|
DockerError,
|
||||||
|
DockerNotFound,
|
||||||
HomeAssistantAPIError,
|
HomeAssistantAPIError,
|
||||||
HostAppArmorError,
|
HostAppArmorError,
|
||||||
)
|
)
|
||||||
|
from ..jobs.decorator import Job, JobCondition
|
||||||
|
from ..resolution.const import ContextType, IssueType, SuggestionType
|
||||||
from ..store.addon import AddonStore
|
from ..store.addon import AddonStore
|
||||||
|
from ..utils import check_exception_chain
|
||||||
from .addon import Addon
|
from .addon import Addon
|
||||||
from .data import AddonsData
|
from .data import AddonsData
|
||||||
|
|
||||||
@@ -45,7 +52,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
"""Return a list of all installed add-ons."""
|
"""Return a list of all installed add-ons."""
|
||||||
return list(self.local.values())
|
return list(self.local.values())
|
||||||
|
|
||||||
def get(self, addon_slug: str) -> Optional[AnyAddon]:
|
def get(self, addon_slug: str, local_only: bool = False) -> Optional[AnyAddon]:
|
||||||
"""Return an add-on from slug.
|
"""Return an add-on from slug.
|
||||||
|
|
||||||
Prio:
|
Prio:
|
||||||
@@ -54,7 +61,9 @@ class AddonManager(CoreSysAttributes):
|
|||||||
"""
|
"""
|
||||||
if addon_slug in self.local:
|
if addon_slug in self.local:
|
||||||
return self.local[addon_slug]
|
return self.local[addon_slug]
|
||||||
|
if not local_only:
|
||||||
return self.store.get(addon_slug)
|
return self.store.get(addon_slug)
|
||||||
|
return None
|
||||||
|
|
||||||
def from_token(self, token: str) -> Optional[Addon]:
|
def from_token(self, token: str) -> Optional[Addon]:
|
||||||
"""Return an add-on from Supervisor token."""
|
"""Return an add-on from Supervisor token."""
|
||||||
@@ -82,12 +91,12 @@ class AddonManager(CoreSysAttributes):
|
|||||||
"""Boot add-ons with mode auto."""
|
"""Boot add-ons with mode auto."""
|
||||||
tasks: List[Addon] = []
|
tasks: List[Addon] = []
|
||||||
for addon in self.installed:
|
for addon in self.installed:
|
||||||
if addon.boot != BOOT_AUTO or addon.startup != stage:
|
if addon.boot != AddonBoot.AUTO or addon.startup != stage:
|
||||||
continue
|
continue
|
||||||
tasks.append(addon)
|
tasks.append(addon)
|
||||||
|
|
||||||
# Evaluate add-ons which need to be started
|
# Evaluate add-ons which need to be started
|
||||||
_LOGGER.info("Phase '%s' start %d add-ons", stage, len(tasks))
|
_LOGGER.info("Phase '%s' starting %d add-ons", stage, len(tasks))
|
||||||
if not tasks:
|
if not tasks:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -96,9 +105,19 @@ class AddonManager(CoreSysAttributes):
|
|||||||
for addon in tasks:
|
for addon in tasks:
|
||||||
try:
|
try:
|
||||||
await addon.start()
|
await addon.start()
|
||||||
|
except AddonsError as err:
|
||||||
|
# Check if there is an system/user issue
|
||||||
|
if check_exception_chain(
|
||||||
|
err, (DockerAPIError, DockerNotFound, AddonConfigurationError)
|
||||||
|
):
|
||||||
|
addon.boot = AddonBoot.MANUAL
|
||||||
|
addon.save_persist()
|
||||||
except Exception as err: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
_LOGGER.warning("Can't start Add-on %s: %s", addon.slug, err)
|
|
||||||
self.sys_capture_exception(err)
|
self.sys_capture_exception(err)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
_LOGGER.warning("Can't start Add-on %s", addon.slug)
|
||||||
|
|
||||||
await asyncio.sleep(self.sys_config.wait_boot)
|
await asyncio.sleep(self.sys_config.wait_boot)
|
||||||
|
|
||||||
@@ -106,12 +125,12 @@ class AddonManager(CoreSysAttributes):
|
|||||||
"""Shutdown addons."""
|
"""Shutdown addons."""
|
||||||
tasks: List[Addon] = []
|
tasks: List[Addon] = []
|
||||||
for addon in self.installed:
|
for addon in self.installed:
|
||||||
if await addon.state() != STATE_STARTED or addon.startup != stage:
|
if addon.state != AddonState.STARTED or addon.startup != stage:
|
||||||
continue
|
continue
|
||||||
tasks.append(addon)
|
tasks.append(addon)
|
||||||
|
|
||||||
# Evaluate add-ons which need to be stopped
|
# Evaluate add-ons which need to be stopped
|
||||||
_LOGGER.info("Phase '%s' stop %d add-ons", stage, len(tasks))
|
_LOGGER.info("Phase '%s' stopping %d add-ons", stage, len(tasks))
|
||||||
if not tasks:
|
if not tasks:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -124,26 +143,35 @@ class AddonManager(CoreSysAttributes):
|
|||||||
_LOGGER.warning("Can't stop Add-on %s: %s", addon.slug, err)
|
_LOGGER.warning("Can't stop Add-on %s: %s", addon.slug, err)
|
||||||
self.sys_capture_exception(err)
|
self.sys_capture_exception(err)
|
||||||
|
|
||||||
|
@Job(
|
||||||
|
conditions=[
|
||||||
|
JobCondition.FREE_SPACE,
|
||||||
|
JobCondition.INTERNET_HOST,
|
||||||
|
JobCondition.HEALTHY,
|
||||||
|
],
|
||||||
|
on_condition=AddonsJobError,
|
||||||
|
)
|
||||||
async def install(self, slug: str) -> None:
|
async def install(self, slug: str) -> None:
|
||||||
"""Install an add-on."""
|
"""Install an add-on."""
|
||||||
if slug in self.local:
|
if slug in self.local:
|
||||||
_LOGGER.warning("Add-on %s is already installed", slug)
|
raise AddonsError(f"Add-on {slug} is already installed", _LOGGER.warning)
|
||||||
return
|
|
||||||
store = self.store.get(slug)
|
store = self.store.get(slug)
|
||||||
|
|
||||||
if not store:
|
if not store:
|
||||||
_LOGGER.error("Add-on %s not exists", slug)
|
raise AddonsError(f"Add-on {slug} not exists", _LOGGER.error)
|
||||||
raise AddonsError()
|
|
||||||
|
|
||||||
if not store.available:
|
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)
|
self.data.install(store)
|
||||||
addon = Addon(self.coresys, slug)
|
addon = Addon(self.coresys, slug)
|
||||||
|
|
||||||
if not addon.path_data.is_dir():
|
if not addon.path_data.is_dir():
|
||||||
_LOGGER.info("Create Home Assistant add-on data folder %s", addon.path_data)
|
_LOGGER.info(
|
||||||
|
"Creating Home Assistant add-on data folder %s", addon.path_data
|
||||||
|
)
|
||||||
addon.path_data.mkdir()
|
addon.path_data.mkdir()
|
||||||
|
|
||||||
# Setup/Fix AppArmor profile
|
# Setup/Fix AppArmor profile
|
||||||
@@ -151,11 +179,16 @@ class AddonManager(CoreSysAttributes):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await addon.instance.install(store.version, store.image)
|
await addon.instance.install(store.version, store.image)
|
||||||
except DockerAPIError:
|
except DockerError as err:
|
||||||
self.data.uninstall(addon)
|
self.data.uninstall(addon)
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
else:
|
else:
|
||||||
self.local[slug] = addon
|
self.local[slug] = addon
|
||||||
|
|
||||||
|
# Reload ingress tokens
|
||||||
|
if addon.with_ingress:
|
||||||
|
await self.sys_ingress.reload()
|
||||||
|
|
||||||
_LOGGER.info("Add-on '%s' successfully installed", slug)
|
_LOGGER.info("Add-on '%s' successfully installed", slug)
|
||||||
|
|
||||||
async def uninstall(self, slug: str) -> None:
|
async def uninstall(self, slug: str) -> None:
|
||||||
@@ -167,8 +200,10 @@ class AddonManager(CoreSysAttributes):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await addon.instance.remove()
|
await addon.instance.remove()
|
||||||
except DockerAPIError:
|
except DockerError as err:
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
else:
|
||||||
|
addon.state = AddonState.UNKNOWN
|
||||||
|
|
||||||
await addon.remove_data()
|
await addon.remove_data()
|
||||||
|
|
||||||
@@ -188,6 +223,8 @@ class AddonManager(CoreSysAttributes):
|
|||||||
await self.sys_ingress.update_hass_panel(addon)
|
await self.sys_ingress.update_hass_panel(addon)
|
||||||
|
|
||||||
# Cleanup Ingress dynamic port assignment
|
# Cleanup Ingress dynamic port assignment
|
||||||
|
if addon.with_ingress:
|
||||||
|
self.sys_create_task(self.sys_ingress.reload())
|
||||||
self.sys_ingress.del_dynamic_port(slug)
|
self.sys_ingress.del_dynamic_port(slug)
|
||||||
|
|
||||||
# Cleanup discovery data
|
# Cleanup discovery data
|
||||||
@@ -207,48 +244,65 @@ class AddonManager(CoreSysAttributes):
|
|||||||
|
|
||||||
_LOGGER.info("Add-on '%s' successfully removed", slug)
|
_LOGGER.info("Add-on '%s' successfully removed", slug)
|
||||||
|
|
||||||
|
@Job(
|
||||||
|
conditions=[
|
||||||
|
JobCondition.FREE_SPACE,
|
||||||
|
JobCondition.INTERNET_HOST,
|
||||||
|
JobCondition.HEALTHY,
|
||||||
|
],
|
||||||
|
on_condition=AddonsJobError,
|
||||||
|
)
|
||||||
async def update(self, slug: str) -> None:
|
async def update(self, slug: str) -> None:
|
||||||
"""Update add-on."""
|
"""Update add-on."""
|
||||||
if slug not in self.local:
|
if slug not in self.local:
|
||||||
_LOGGER.error("Add-on %s is not installed", slug)
|
raise AddonsError(f"Add-on {slug} is not installed", _LOGGER.error)
|
||||||
raise AddonsError()
|
|
||||||
addon = self.local[slug]
|
addon = self.local[slug]
|
||||||
|
|
||||||
if addon.is_detached:
|
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]
|
store = self.store[slug]
|
||||||
|
|
||||||
if addon.version == store.version:
|
if addon.version == store.version:
|
||||||
_LOGGER.warning("No update available for add-on %s", slug)
|
raise AddonsError(f"No update available for add-on {slug}", _LOGGER.warning)
|
||||||
return
|
|
||||||
|
|
||||||
# Check if available, Maybe something have changed
|
# Check if available, Maybe something have changed
|
||||||
if not store.available:
|
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
|
# Update instance
|
||||||
last_state = await addon.state()
|
last_state: AddonState = addon.state
|
||||||
|
old_image = addon.image
|
||||||
try:
|
try:
|
||||||
await addon.instance.update(store.version, store.image)
|
await addon.instance.update(store.version, store.image)
|
||||||
|
except DockerError as err:
|
||||||
|
raise AddonsError() from err
|
||||||
|
|
||||||
|
_LOGGER.info("Add-on '%s' successfully updated", slug)
|
||||||
|
self.data.update(store)
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
with suppress(DockerAPIError):
|
with suppress(DockerError):
|
||||||
await addon.instance.cleanup()
|
await addon.instance.cleanup(old_image=old_image)
|
||||||
except DockerAPIError:
|
|
||||||
raise AddonsError() from None
|
|
||||||
else:
|
|
||||||
self.data.update(store)
|
|
||||||
_LOGGER.info("Add-on '%s' successfully updated", slug)
|
|
||||||
|
|
||||||
# Setup/Fix AppArmor profile
|
# Setup/Fix AppArmor profile
|
||||||
await addon.install_apparmor()
|
await addon.install_apparmor()
|
||||||
|
|
||||||
# restore state
|
# restore state
|
||||||
if last_state == STATE_STARTED:
|
if last_state == AddonState.STARTED:
|
||||||
await addon.start()
|
await addon.start()
|
||||||
|
|
||||||
|
@Job(
|
||||||
|
conditions=[
|
||||||
|
JobCondition.FREE_SPACE,
|
||||||
|
JobCondition.INTERNET_HOST,
|
||||||
|
JobCondition.HEALTHY,
|
||||||
|
],
|
||||||
|
on_condition=AddonsJobError,
|
||||||
|
)
|
||||||
async def rebuild(self, slug: str) -> None:
|
async def rebuild(self, slug: str) -> None:
|
||||||
"""Perform a rebuild of local build add-on."""
|
"""Perform a rebuild of local build add-on."""
|
||||||
if slug not in self.local:
|
if slug not in self.local:
|
||||||
@@ -270,20 +324,28 @@ class AddonManager(CoreSysAttributes):
|
|||||||
raise AddonsNotSupportedError()
|
raise AddonsNotSupportedError()
|
||||||
|
|
||||||
# remove docker container but not addon config
|
# remove docker container but not addon config
|
||||||
last_state = await addon.state()
|
last_state: AddonState = addon.state
|
||||||
try:
|
try:
|
||||||
await addon.instance.remove()
|
await addon.instance.remove()
|
||||||
await addon.instance.install(addon.version)
|
await addon.instance.install(addon.version)
|
||||||
except DockerAPIError:
|
except DockerError as err:
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
else:
|
else:
|
||||||
self.data.update(store)
|
self.data.update(store)
|
||||||
_LOGGER.info("Add-on '%s' successfully rebuilt", slug)
|
_LOGGER.info("Add-on '%s' successfully rebuilt", slug)
|
||||||
|
|
||||||
# restore state
|
# restore state
|
||||||
if last_state == STATE_STARTED:
|
if last_state == AddonState.STARTED:
|
||||||
await addon.start()
|
await addon.start()
|
||||||
|
|
||||||
|
@Job(
|
||||||
|
conditions=[
|
||||||
|
JobCondition.FREE_SPACE,
|
||||||
|
JobCondition.INTERNET_HOST,
|
||||||
|
JobCondition.HEALTHY,
|
||||||
|
],
|
||||||
|
on_condition=AddonsJobError,
|
||||||
|
)
|
||||||
async def restore(self, slug: str, tar_file: tarfile.TarFile) -> None:
|
async def restore(self, slug: str, tar_file: tarfile.TarFile) -> None:
|
||||||
"""Restore state of an add-on."""
|
"""Restore state of an add-on."""
|
||||||
if slug not in self.local:
|
if slug not in self.local:
|
||||||
@@ -302,9 +364,11 @@ class AddonManager(CoreSysAttributes):
|
|||||||
|
|
||||||
# Update ingress
|
# Update ingress
|
||||||
if addon.with_ingress:
|
if addon.with_ingress:
|
||||||
|
await self.sys_ingress.reload()
|
||||||
with suppress(HomeAssistantAPIError):
|
with suppress(HomeAssistantAPIError):
|
||||||
await self.sys_ingress.update_hass_panel(addon)
|
await self.sys_ingress.update_hass_panel(addon)
|
||||||
|
|
||||||
|
@Job(conditions=[JobCondition.FREE_SPACE, JobCondition.INTERNET_HOST])
|
||||||
async def repair(self) -> None:
|
async def repair(self) -> None:
|
||||||
"""Repair local add-ons."""
|
"""Repair local add-ons."""
|
||||||
needs_repair: List[Addon] = []
|
needs_repair: List[Addon] = []
|
||||||
@@ -320,12 +384,8 @@ class AddonManager(CoreSysAttributes):
|
|||||||
return
|
return
|
||||||
|
|
||||||
for addon in needs_repair:
|
for addon in needs_repair:
|
||||||
_LOGGER.info("Start repair for add-on: %s", addon.slug)
|
_LOGGER.info("Repairing for add-on: %s", addon.slug)
|
||||||
await self.sys_run_in_executor(
|
with suppress(DockerError, KeyError):
|
||||||
self.sys_docker.network.stale_cleanup, addon.instance.name
|
|
||||||
)
|
|
||||||
|
|
||||||
with suppress(DockerAPIError, KeyError):
|
|
||||||
# Need pull a image again
|
# Need pull a image again
|
||||||
if not addon.need_build:
|
if not addon.need_build:
|
||||||
await addon.instance.install(addon.version, addon.image)
|
await addon.instance.install(addon.version, addon.image)
|
||||||
@@ -347,8 +407,19 @@ class AddonManager(CoreSysAttributes):
|
|||||||
"""Sync add-ons DNS names."""
|
"""Sync add-ons DNS names."""
|
||||||
# Update hosts
|
# Update hosts
|
||||||
for addon in self.installed:
|
for addon in self.installed:
|
||||||
|
try:
|
||||||
if not await addon.instance.is_running():
|
if not await addon.instance.is_running():
|
||||||
continue
|
continue
|
||||||
|
except DockerError as err:
|
||||||
|
_LOGGER.warning("Add-on %s is corrupt: %s", addon.slug, err)
|
||||||
|
self.sys_resolution.create_issue(
|
||||||
|
IssueType.CORRUPT_DOCKER,
|
||||||
|
ContextType.ADDON,
|
||||||
|
reference=addon.slug,
|
||||||
|
suggestions=[SuggestionType.EXECUTE_REPAIR],
|
||||||
|
)
|
||||||
|
self.sys_capture_exception(err)
|
||||||
|
else:
|
||||||
self.sys_plugins.dns.add_host(
|
self.sys_plugins.dns.add_host(
|
||||||
ipv4=addon.ip_address, names=[addon.hostname], write=False
|
ipv4=addon.ip_address, names=[addon.hostname], write=False
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor add-ons."""
|
"""Init file for Supervisor add-ons."""
|
||||||
|
import asyncio
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
@@ -9,8 +10,9 @@ import secrets
|
|||||||
import shutil
|
import shutil
|
||||||
import tarfile
|
import tarfile
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from typing import Any, Awaitable, Dict, List, Optional
|
from typing import Any, Awaitable, Dict, List, Optional, Set
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous.humanize import humanize_error
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
@@ -20,6 +22,8 @@ from ..const import (
|
|||||||
ATTR_AUDIO_OUTPUT,
|
ATTR_AUDIO_OUTPUT,
|
||||||
ATTR_AUTO_UPDATE,
|
ATTR_AUTO_UPDATE,
|
||||||
ATTR_BOOT,
|
ATTR_BOOT,
|
||||||
|
ATTR_DATA,
|
||||||
|
ATTR_EVENT,
|
||||||
ATTR_IMAGE,
|
ATTR_IMAGE,
|
||||||
ATTR_INGRESS_ENTRY,
|
ATTR_INGRESS_ENTRY,
|
||||||
ATTR_INGRESS_PANEL,
|
ATTR_INGRESS_PANEL,
|
||||||
@@ -30,31 +34,41 @@ from ..const import (
|
|||||||
ATTR_PORTS,
|
ATTR_PORTS,
|
||||||
ATTR_PROTECTED,
|
ATTR_PROTECTED,
|
||||||
ATTR_SCHEMA,
|
ATTR_SCHEMA,
|
||||||
|
ATTR_SLUG,
|
||||||
ATTR_STATE,
|
ATTR_STATE,
|
||||||
ATTR_SYSTEM,
|
ATTR_SYSTEM,
|
||||||
|
ATTR_TYPE,
|
||||||
ATTR_USER,
|
ATTR_USER,
|
||||||
ATTR_UUID,
|
ATTR_UUID,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
|
ATTR_WATCHDOG,
|
||||||
DNS_SUFFIX,
|
DNS_SUFFIX,
|
||||||
STATE_STARTED,
|
AddonBoot,
|
||||||
STATE_STOPPED,
|
AddonStartup,
|
||||||
|
AddonState,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSys
|
from ..coresys import CoreSys
|
||||||
from ..docker.addon import DockerAddon
|
from ..docker.addon import DockerAddon
|
||||||
from ..docker.stats import DockerStats
|
from ..docker.stats import DockerStats
|
||||||
from ..exceptions import (
|
from ..exceptions import (
|
||||||
|
AddonConfigurationError,
|
||||||
AddonsError,
|
AddonsError,
|
||||||
AddonsNotSupportedError,
|
AddonsNotSupportedError,
|
||||||
DockerAPIError,
|
ConfigurationFileError,
|
||||||
|
DockerError,
|
||||||
|
DockerRequestError,
|
||||||
HostAppArmorError,
|
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.apparmor import adjust_profile
|
||||||
from ..utils.json import read_json_file, write_json_file
|
from ..utils.json import read_json_file, write_json_file
|
||||||
from ..utils.tar import atomic_contents_add, secure_path
|
from ..utils.tar import atomic_contents_add, secure_path
|
||||||
from .model import AddonModel, Data
|
from .model import AddonModel, Data
|
||||||
|
from .options import AddonOptions
|
||||||
from .utils import remove_data
|
from .utils import remove_data
|
||||||
from .validate import SCHEMA_ADDON_SNAPSHOT, validate_options
|
from .validate import SCHEMA_ADDON_SNAPSHOT
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -63,8 +77,15 @@ RE_WEBUI = re.compile(
|
|||||||
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$"
|
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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>.*)$"
|
||||||
|
)
|
||||||
|
|
||||||
RE_OLD_AUDIO = re.compile(r"\d+,\d+")
|
RE_OLD_AUDIO = re.compile(r"\d+,\d+")
|
||||||
|
|
||||||
|
WATCHDOG_TIMEOUT = aiohttp.ClientTimeout(total=10)
|
||||||
|
|
||||||
|
|
||||||
class Addon(AddonModel):
|
class Addon(AddonModel):
|
||||||
"""Hold data for add-on inside Supervisor."""
|
"""Hold data for add-on inside Supervisor."""
|
||||||
@@ -73,11 +94,49 @@ class Addon(AddonModel):
|
|||||||
"""Initialize data holder."""
|
"""Initialize data holder."""
|
||||||
super().__init__(coresys, slug)
|
super().__init__(coresys, slug)
|
||||||
self.instance: DockerAddon = DockerAddon(coresys, self)
|
self.instance: DockerAddon = DockerAddon(coresys, self)
|
||||||
|
self._state: AddonState = AddonState.UNKNOWN
|
||||||
|
|
||||||
|
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."""
|
||||||
|
return self.instance.in_progress
|
||||||
|
|
||||||
async def load(self) -> None:
|
async def load(self) -> None:
|
||||||
"""Async initialize of object."""
|
"""Async initialize of object."""
|
||||||
with suppress(DockerAPIError):
|
with suppress(DockerError):
|
||||||
await self.instance.attach(tag=self.version)
|
await self.instance.attach(version=self.version)
|
||||||
|
|
||||||
|
# Evaluate state
|
||||||
|
if await self.instance.is_running():
|
||||||
|
self.state = AddonState.STARTED
|
||||||
|
else:
|
||||||
|
self.state = AddonState.STOPPED
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ip_address(self) -> IPv4Address:
|
def ip_address(self) -> IPv4Address:
|
||||||
@@ -119,6 +178,13 @@ class Addon(AddonModel):
|
|||||||
"""Return installed version."""
|
"""Return installed version."""
|
||||||
return self.persist[ATTR_VERSION]
|
return self.persist[ATTR_VERSION]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def need_update(self) -> bool:
|
||||||
|
"""Return True if an update is available."""
|
||||||
|
if self.is_detached:
|
||||||
|
return False
|
||||||
|
return self.version != self.latest_version
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dns(self) -> List[str]:
|
def dns(self) -> List[str]:
|
||||||
"""Return list of DNS name for that add-on."""
|
"""Return list of DNS name for that add-on."""
|
||||||
@@ -135,12 +201,12 @@ class Addon(AddonModel):
|
|||||||
self.persist[ATTR_OPTIONS] = {} if value is None else deepcopy(value)
|
self.persist[ATTR_OPTIONS] = {} if value is None else deepcopy(value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def boot(self) -> bool:
|
def boot(self) -> AddonBoot:
|
||||||
"""Return boot config with prio local settings."""
|
"""Return boot config with prio local settings."""
|
||||||
return self.persist.get(ATTR_BOOT, super().boot)
|
return self.persist.get(ATTR_BOOT, super().boot)
|
||||||
|
|
||||||
@boot.setter
|
@boot.setter
|
||||||
def boot(self, value: bool) -> None:
|
def boot(self, value: AddonBoot) -> None:
|
||||||
"""Store user boot options."""
|
"""Store user boot options."""
|
||||||
self.persist[ATTR_BOOT] = value
|
self.persist[ATTR_BOOT] = value
|
||||||
|
|
||||||
@@ -154,6 +220,21 @@ class Addon(AddonModel):
|
|||||||
"""Set auto update."""
|
"""Set auto update."""
|
||||||
self.persist[ATTR_AUTO_UPDATE] = value
|
self.persist[ATTR_AUTO_UPDATE] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def watchdog(self) -> bool:
|
||||||
|
"""Return True if watchdog is enable."""
|
||||||
|
return self.persist[ATTR_WATCHDOG]
|
||||||
|
|
||||||
|
@watchdog.setter
|
||||||
|
def watchdog(self, value: bool) -> None:
|
||||||
|
"""Set watchdog enable/disable."""
|
||||||
|
if value and self.startup == AddonStartup.ONCE:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Ignoring watchdog for %s because startup type is 'once'", self.slug
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.persist[ATTR_WATCHDOG] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uuid(self) -> str:
|
def uuid(self) -> str:
|
||||||
"""Return an API token for this add-on."""
|
"""Return an API token for this add-on."""
|
||||||
@@ -229,8 +310,6 @@ class Addon(AddonModel):
|
|||||||
if not url:
|
if not url:
|
||||||
return None
|
return None
|
||||||
webui = RE_WEBUI.match(url)
|
webui = RE_WEBUI.match(url)
|
||||||
if not webui:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# extract arguments
|
# extract arguments
|
||||||
t_port = webui.group("t_port")
|
t_port = webui.group("t_port")
|
||||||
@@ -244,10 +323,6 @@ class Addon(AddonModel):
|
|||||||
else:
|
else:
|
||||||
port = self.ports.get(f"{t_port}/tcp", t_port)
|
port = self.ports.get(f"{t_port}/tcp", t_port)
|
||||||
|
|
||||||
# for interface config or port lists
|
|
||||||
if isinstance(port, (tuple, list)):
|
|
||||||
port = port[-1]
|
|
||||||
|
|
||||||
# lookup the correct protocol from config
|
# lookup the correct protocol from config
|
||||||
if t_proto:
|
if t_proto:
|
||||||
proto = "https" if self.options.get(t_proto) else "http"
|
proto = "https" if self.options.get(t_proto) else "http"
|
||||||
@@ -348,41 +423,108 @@ class Addon(AddonModel):
|
|||||||
"""Return path to asound config for Docker."""
|
"""Return path to asound config for Docker."""
|
||||||
return Path(self.sys_config.path_extern_tmp, f"{self.slug}_pulse")
|
return Path(self.sys_config.path_extern_tmp, f"{self.slug}_pulse")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def devices(self) -> Set[Device]:
|
||||||
|
"""Extract devices from 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, self.name, self.slug)
|
||||||
|
with suppress(vol.Invalid):
|
||||||
|
options_validator(self.options)
|
||||||
|
|
||||||
|
return options_validator.devices
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pwned(self) -> Set[str]:
|
||||||
|
"""Extract pwned data 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, self.name, self.slug)
|
||||||
|
with suppress(vol.Invalid):
|
||||||
|
options_validator(self.options)
|
||||||
|
|
||||||
|
return options_validator.pwned
|
||||||
|
|
||||||
def save_persist(self) -> None:
|
def save_persist(self) -> None:
|
||||||
"""Save data of add-on."""
|
"""Save data of add-on."""
|
||||||
self.sys_addons.data.save_data()
|
self.sys_addons.data.save_data()
|
||||||
|
|
||||||
|
async def watchdog_application(self) -> bool:
|
||||||
|
"""Return True if application is running."""
|
||||||
|
url = super().watchdog
|
||||||
|
if not url:
|
||||||
|
return True
|
||||||
|
application = RE_WATCHDOG.match(url)
|
||||||
|
|
||||||
|
# extract arguments
|
||||||
|
t_port = application.group("t_port")
|
||||||
|
t_proto = application.group("t_proto")
|
||||||
|
s_prefix = application.group("s_prefix") or ""
|
||||||
|
s_suffix = application.group("s_suffix") or ""
|
||||||
|
|
||||||
|
# search host port for this docker port
|
||||||
|
if self.host_network:
|
||||||
|
port = self.ports.get(f"{t_port}/tcp", t_port)
|
||||||
|
else:
|
||||||
|
port = t_port
|
||||||
|
|
||||||
|
# TCP monitoring
|
||||||
|
if s_prefix == "tcp":
|
||||||
|
return await self.sys_run_in_executor(check_port, self.ip_address, port)
|
||||||
|
|
||||||
|
# lookup the correct protocol from config
|
||||||
|
if t_proto:
|
||||||
|
proto = "https" if self.options.get(t_proto) else "http"
|
||||||
|
else:
|
||||||
|
proto = s_prefix
|
||||||
|
|
||||||
|
# Make HTTP request
|
||||||
|
try:
|
||||||
|
url = f"{proto}://{self.ip_address}:{port}{s_suffix}"
|
||||||
|
async with self.sys_websession_ssl.get(
|
||||||
|
url, timeout=WATCHDOG_TIMEOUT
|
||||||
|
) as req:
|
||||||
|
if req.status < 300:
|
||||||
|
return True
|
||||||
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
async def write_options(self) -> None:
|
async def write_options(self) -> None:
|
||||||
"""Return True if add-on options is written to data."""
|
"""Return True if add-on options is written to data."""
|
||||||
schema = self.schema
|
|
||||||
options = self.options
|
|
||||||
|
|
||||||
# Update secrets for validation
|
# Update secrets for validation
|
||||||
await self.sys_secrets.reload()
|
await self.sys_homeassistant.secrets.reload()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
options = schema(options)
|
options = self.schema(self.options)
|
||||||
write_json_file(self.path_options, options)
|
write_json_file(self.path_options, options)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Add-on %s has invalid options: %s",
|
"Add-on %s has invalid options: %s",
|
||||||
self.slug,
|
self.slug,
|
||||||
humanize_error(options, ex),
|
humanize_error(self.options, ex),
|
||||||
)
|
)
|
||||||
except JsonFileError:
|
except ConfigurationFileError:
|
||||||
_LOGGER.error("Add-on %s can't write options", self.slug)
|
_LOGGER.error("Add-on %s can't write options", self.slug)
|
||||||
else:
|
else:
|
||||||
_LOGGER.debug("Add-on %s write options: %s", self.slug, options)
|
_LOGGER.debug("Add-on %s write options: %s", self.slug, options)
|
||||||
return
|
return
|
||||||
|
|
||||||
raise AddonsError()
|
raise AddonConfigurationError()
|
||||||
|
|
||||||
async def remove_data(self) -> None:
|
async def remove_data(self) -> None:
|
||||||
"""Remove add-on data."""
|
"""Remove add-on data."""
|
||||||
if not self.path_data.is_dir():
|
if not self.path_data.is_dir():
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.info("Remove add-on data folder %s", self.path_data)
|
_LOGGER.info("Removing add-on data folder %s", self.path_data)
|
||||||
await remove_data(self.path_data)
|
await remove_data(self.path_data)
|
||||||
|
|
||||||
def write_pulse(self) -> None:
|
def write_pulse(self) -> None:
|
||||||
@@ -450,7 +592,9 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
# create voluptuous
|
# create voluptuous
|
||||||
new_schema = vol.Schema(
|
new_schema = vol.Schema(
|
||||||
vol.All(dict, validate_options(self.coresys, new_raw_schema))
|
vol.All(
|
||||||
|
dict, AddonOptions(self.coresys, new_raw_schema, self.name, self.slug)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# validate
|
# validate
|
||||||
@@ -461,16 +605,10 @@ class Addon(AddonModel):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def state(self) -> str:
|
|
||||||
"""Return running state of add-on."""
|
|
||||||
if await self.instance.is_running():
|
|
||||||
return STATE_STARTED
|
|
||||||
return STATE_STOPPED
|
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
"""Set options and start add-on."""
|
"""Set options and start add-on."""
|
||||||
if await self.instance.is_running():
|
if await self.instance.is_running():
|
||||||
_LOGGER.warning("%s already running!", self.slug)
|
_LOGGER.warning("%s is already running!", self.slug)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Access Token
|
# Access Token
|
||||||
@@ -487,15 +625,27 @@ class Addon(AddonModel):
|
|||||||
# Start Add-on
|
# Start Add-on
|
||||||
try:
|
try:
|
||||||
await self.instance.run()
|
await self.instance.run()
|
||||||
except DockerAPIError:
|
except DockerRequestError as err:
|
||||||
raise AddonsError() from None
|
self.state = AddonState.ERROR
|
||||||
|
raise AddonsError() from err
|
||||||
|
except DockerError as err:
|
||||||
|
self.state = AddonState.ERROR
|
||||||
|
raise AddonsError() from err
|
||||||
|
else:
|
||||||
|
self.state = AddonState.STARTED
|
||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
"""Stop add-on."""
|
"""Stop add-on."""
|
||||||
try:
|
try:
|
||||||
return await self.instance.stop()
|
await self.instance.stop()
|
||||||
except DockerAPIError:
|
except DockerRequestError as err:
|
||||||
raise AddonsError() from None
|
self.state = AddonState.ERROR
|
||||||
|
raise AddonsError() from err
|
||||||
|
except DockerError as err:
|
||||||
|
self.state = AddonState.ERROR
|
||||||
|
raise AddonsError() from err
|
||||||
|
else:
|
||||||
|
self.state = AddonState.STOPPED
|
||||||
|
|
||||||
async def restart(self) -> None:
|
async def restart(self) -> None:
|
||||||
"""Restart add-on."""
|
"""Restart add-on."""
|
||||||
@@ -510,12 +660,19 @@ class Addon(AddonModel):
|
|||||||
"""
|
"""
|
||||||
return self.instance.logs()
|
return self.instance.logs()
|
||||||
|
|
||||||
|
def is_running(self) -> Awaitable[bool]:
|
||||||
|
"""Return True if Docker container is running.
|
||||||
|
|
||||||
|
Return a coroutine.
|
||||||
|
"""
|
||||||
|
return self.instance.is_running()
|
||||||
|
|
||||||
async def stats(self) -> DockerStats:
|
async def stats(self) -> DockerStats:
|
||||||
"""Return stats of container."""
|
"""Return stats of container."""
|
||||||
try:
|
try:
|
||||||
return await self.instance.stats()
|
return await self.instance.stats()
|
||||||
except DockerAPIError:
|
except DockerError as err:
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
async def write_stdin(self, data) -> None:
|
async def write_stdin(self, data) -> None:
|
||||||
"""Write data to add-on stdin.
|
"""Write data to add-on stdin.
|
||||||
@@ -528,8 +685,8 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return await self.instance.write_stdin(data)
|
return await self.instance.write_stdin(data)
|
||||||
except DockerAPIError:
|
except DockerError as err:
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
async def snapshot(self, tar_file: tarfile.TarFile) -> None:
|
async def snapshot(self, tar_file: tarfile.TarFile) -> None:
|
||||||
"""Snapshot state of an add-on."""
|
"""Snapshot state of an add-on."""
|
||||||
@@ -540,31 +697,31 @@ class Addon(AddonModel):
|
|||||||
if self.need_build:
|
if self.need_build:
|
||||||
try:
|
try:
|
||||||
await self.instance.export_image(temp_path.joinpath("image.tar"))
|
await self.instance.export_image(temp_path.joinpath("image.tar"))
|
||||||
except DockerAPIError:
|
except DockerError as err:
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
ATTR_USER: self.persist,
|
ATTR_USER: self.persist,
|
||||||
ATTR_SYSTEM: self.data,
|
ATTR_SYSTEM: self.data,
|
||||||
ATTR_VERSION: self.version,
|
ATTR_VERSION: self.version,
|
||||||
ATTR_STATE: await self.state(),
|
ATTR_STATE: self.state,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Store local configs/state
|
# Store local configs/state
|
||||||
try:
|
try:
|
||||||
write_json_file(temp_path.joinpath("addon.json"), data)
|
write_json_file(temp_path.joinpath("addon.json"), data)
|
||||||
except JsonFileError:
|
except ConfigurationFileError as err:
|
||||||
_LOGGER.error("Can't save meta for %s", self.slug)
|
_LOGGER.error("Can't save meta for %s", self.slug)
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
# Store AppArmor Profile
|
# Store AppArmor Profile
|
||||||
if self.sys_host.apparmor.exists(self.slug):
|
if self.sys_host.apparmor.exists(self.slug):
|
||||||
profile = temp_path.joinpath("apparmor.txt")
|
profile = temp_path.joinpath("apparmor.txt")
|
||||||
try:
|
try:
|
||||||
self.sys_host.apparmor.backup_profile(self.slug, profile)
|
self.sys_host.apparmor.backup_profile(self.slug, profile)
|
||||||
except HostAppArmorError:
|
except HostAppArmorError as err:
|
||||||
_LOGGER.error("Can't backup AppArmor profile")
|
_LOGGER.error("Can't backup AppArmor profile")
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
# write into tarfile
|
# write into tarfile
|
||||||
def _write_tarfile():
|
def _write_tarfile():
|
||||||
@@ -583,11 +740,11 @@ class Addon(AddonModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_LOGGER.info("Build snapshot for add-on %s", self.slug)
|
_LOGGER.info("Building snapshot for add-on %s", self.slug)
|
||||||
await self.sys_run_in_executor(_write_tarfile)
|
await self.sys_run_in_executor(_write_tarfile)
|
||||||
except (tarfile.TarError, OSError) as err:
|
except (tarfile.TarError, OSError) as err:
|
||||||
_LOGGER.error("Can't write tarfile %s: %s", tar_file, err)
|
_LOGGER.error("Can't write tarfile %s: %s", tar_file, err)
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
_LOGGER.info("Finish snapshot for addon %s", self.slug)
|
_LOGGER.info("Finish snapshot for addon %s", self.slug)
|
||||||
|
|
||||||
@@ -604,13 +761,13 @@ class Addon(AddonModel):
|
|||||||
await self.sys_run_in_executor(_extract_tarfile)
|
await self.sys_run_in_executor(_extract_tarfile)
|
||||||
except tarfile.TarError as err:
|
except tarfile.TarError as err:
|
||||||
_LOGGER.error("Can't read tarfile %s: %s", tar_file, err)
|
_LOGGER.error("Can't read tarfile %s: %s", tar_file, err)
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
# Read snapshot data
|
# Read snapshot data
|
||||||
try:
|
try:
|
||||||
data = read_json_file(Path(temp, "addon.json"))
|
data = read_json_file(Path(temp, "addon.json"))
|
||||||
except JsonFileError:
|
except ConfigurationFileError as err:
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
# Validate
|
# Validate
|
||||||
try:
|
try:
|
||||||
@@ -621,7 +778,7 @@ class Addon(AddonModel):
|
|||||||
self.slug,
|
self.slug,
|
||||||
humanize_error(data, err),
|
humanize_error(data, err),
|
||||||
)
|
)
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
# If available
|
# If available
|
||||||
if not self._available(data[ATTR_SYSTEM]):
|
if not self._available(data[ATTR_SYSTEM]):
|
||||||
@@ -638,22 +795,22 @@ class Addon(AddonModel):
|
|||||||
# Check version / restore image
|
# Check version / restore image
|
||||||
version = data[ATTR_VERSION]
|
version = data[ATTR_VERSION]
|
||||||
if not await self.instance.exists():
|
if not await self.instance.exists():
|
||||||
_LOGGER.info("Restore/Install image for addon %s", self.slug)
|
_LOGGER.info("Restore/Install of image for addon %s", self.slug)
|
||||||
|
|
||||||
image_file = Path(temp, "image.tar")
|
image_file = Path(temp, "image.tar")
|
||||||
if image_file.is_file():
|
if image_file.is_file():
|
||||||
with suppress(DockerAPIError):
|
with suppress(DockerError):
|
||||||
await self.instance.import_image(image_file)
|
await self.instance.import_image(image_file)
|
||||||
else:
|
else:
|
||||||
with suppress(DockerAPIError):
|
with suppress(DockerError):
|
||||||
await self.instance.install(version, restore_image)
|
await self.instance.install(version, restore_image)
|
||||||
await self.instance.cleanup()
|
await self.instance.cleanup()
|
||||||
elif self.instance.version != version or self.legacy:
|
elif self.instance.version != version or self.legacy:
|
||||||
_LOGGER.info("Restore/Update image for addon %s", self.slug)
|
_LOGGER.info("Restore/Update of image for addon %s", self.slug)
|
||||||
with suppress(DockerAPIError):
|
with suppress(DockerError):
|
||||||
await self.instance.update(version, restore_image)
|
await self.instance.update(version, restore_image)
|
||||||
else:
|
else:
|
||||||
with suppress(DockerAPIError):
|
with suppress(DockerError):
|
||||||
await self.instance.stop()
|
await self.instance.stop()
|
||||||
|
|
||||||
# Restore data
|
# Restore data
|
||||||
@@ -661,28 +818,28 @@ class Addon(AddonModel):
|
|||||||
"""Restore data."""
|
"""Restore data."""
|
||||||
shutil.copytree(Path(temp, "data"), self.path_data, symlinks=True)
|
shutil.copytree(Path(temp, "data"), self.path_data, symlinks=True)
|
||||||
|
|
||||||
_LOGGER.info("Restore data for addon %s", self.slug)
|
_LOGGER.info("Restoring data for addon %s", self.slug)
|
||||||
if self.path_data.is_dir():
|
if self.path_data.is_dir():
|
||||||
await remove_data(self.path_data)
|
await remove_data(self.path_data)
|
||||||
try:
|
try:
|
||||||
await self.sys_run_in_executor(_restore_data)
|
await self.sys_run_in_executor(_restore_data)
|
||||||
except shutil.Error as err:
|
except shutil.Error as err:
|
||||||
_LOGGER.error("Can't restore origin data: %s", err)
|
_LOGGER.error("Can't restore origin data: %s", err)
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
# Restore AppArmor
|
# Restore AppArmor
|
||||||
profile_file = Path(temp, "apparmor.txt")
|
profile_file = Path(temp, "apparmor.txt")
|
||||||
if profile_file.exists():
|
if profile_file.exists():
|
||||||
try:
|
try:
|
||||||
await self.sys_host.apparmor.load_profile(self.slug, profile_file)
|
await self.sys_host.apparmor.load_profile(self.slug, profile_file)
|
||||||
except HostAppArmorError:
|
except HostAppArmorError as err:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Can't restore AppArmor profile for add-on %s", self.slug
|
"Can't restore AppArmor profile for add-on %s", self.slug
|
||||||
)
|
)
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
# Run add-on
|
# Run add-on
|
||||||
if data[ATTR_STATE] == STATE_STARTED:
|
if data[ATTR_STATE] == AddonState.STARTED:
|
||||||
return await self.start()
|
return await self.start()
|
||||||
|
|
||||||
_LOGGER.info("Finish restore for add-on %s", self.slug)
|
_LOGGER.info("Finished restore for add-on %s", self.slug)
|
||||||
|
|||||||
@@ -4,16 +4,25 @@ from __future__ import annotations
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Dict
|
from typing import TYPE_CHECKING, Dict
|
||||||
|
|
||||||
from ..const import ATTR_ARGS, ATTR_BUILD_FROM, ATTR_SQUASH, META_ADDON
|
from awesomeversion import AwesomeVersion
|
||||||
|
|
||||||
|
from ..const import (
|
||||||
|
ATTR_ARGS,
|
||||||
|
ATTR_BUILD_FROM,
|
||||||
|
ATTR_SQUASH,
|
||||||
|
FILE_SUFFIX_CONFIGURATION,
|
||||||
|
META_ADDON,
|
||||||
|
)
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
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
|
from .validate import SCHEMA_BUILD_CONFIG
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import AnyAddon
|
from . import AnyAddon
|
||||||
|
|
||||||
|
|
||||||
class AddonBuild(JsonConfig, CoreSysAttributes):
|
class AddonBuild(FileConfiguration, CoreSysAttributes):
|
||||||
"""Handle build options for add-ons."""
|
"""Handle build options for add-ons."""
|
||||||
|
|
||||||
def __init__(self, coresys: CoreSys, addon: AnyAddon) -> None:
|
def __init__(self, coresys: CoreSys, addon: AnyAddon) -> None:
|
||||||
@@ -21,9 +30,14 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
|||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self.addon = addon
|
self.addon = addon
|
||||||
|
|
||||||
super().__init__(
|
try:
|
||||||
Path(self.addon.path_location, "build.json"), SCHEMA_BUILD_CONFIG
|
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):
|
def save_data(self):
|
||||||
"""Ignore save function."""
|
"""Ignore save function."""
|
||||||
@@ -46,11 +60,21 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
|||||||
"""Return additional Docker build arguments."""
|
"""Return additional Docker build arguments."""
|
||||||
return self._data[ATTR_ARGS]
|
return self._data[ATTR_ARGS]
|
||||||
|
|
||||||
def get_docker_args(self, version):
|
@property
|
||||||
|
def is_valid(self) -> bool:
|
||||||
|
"""Return true if the build env is valid."""
|
||||||
|
return all(
|
||||||
|
[
|
||||||
|
self.addon.path_location.is_dir(),
|
||||||
|
Path(self.addon.path_location, "Dockerfile").is_file(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_docker_args(self, version: AwesomeVersion):
|
||||||
"""Create a dict with Docker build arguments."""
|
"""Create a dict with Docker build arguments."""
|
||||||
args = {
|
args = {
|
||||||
"path": str(self.addon.path_location),
|
"path": str(self.addon.path_location),
|
||||||
"tag": f"{self.addon.image}:{version}",
|
"tag": f"{self.addon.image}:{version!s}",
|
||||||
"pull": True,
|
"pull": True,
|
||||||
"forcerm": True,
|
"forcerm": True,
|
||||||
"squash": self.squash,
|
"squash": self.squash,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Init file for Supervisor add-on data."""
|
"""Init file for Supervisor add-on data."""
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
import logging
|
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
@@ -13,16 +12,14 @@ from ..const import (
|
|||||||
)
|
)
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..store.addon import AddonStore
|
from ..store.addon import AddonStore
|
||||||
from ..utils.json import JsonConfig
|
from ..utils.common import FileConfiguration
|
||||||
from .addon import Addon
|
from .addon import Addon
|
||||||
from .validate import SCHEMA_ADDONS_FILE
|
from .validate import SCHEMA_ADDONS_FILE
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
Config = Dict[str, Any]
|
Config = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
class AddonsData(JsonConfig, CoreSysAttributes):
|
class AddonsData(FileConfiguration, CoreSysAttributes):
|
||||||
"""Hold data for installed Add-ons inside Supervisor."""
|
"""Hold data for installed Add-ons inside Supervisor."""
|
||||||
|
|
||||||
def __init__(self, coresys: CoreSys):
|
def __init__(self, coresys: CoreSys):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from abc import ABC, abstractmethod
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Awaitable, Dict, List, Optional
|
from typing import Any, Awaitable, Dict, List, Optional
|
||||||
|
|
||||||
from packaging import version as pkg_version
|
from awesomeversion import AwesomeVersion, AwesomeVersionException
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
@@ -12,7 +12,6 @@ from ..const import (
|
|||||||
ATTR_ARCH,
|
ATTR_ARCH,
|
||||||
ATTR_AUDIO,
|
ATTR_AUDIO,
|
||||||
ATTR_AUTH_API,
|
ATTR_AUTH_API,
|
||||||
ATTR_AUTO_UART,
|
|
||||||
ATTR_BOOT,
|
ATTR_BOOT,
|
||||||
ATTR_DESCRIPTON,
|
ATTR_DESCRIPTON,
|
||||||
ATTR_DEVICES,
|
ATTR_DEVICES,
|
||||||
@@ -33,6 +32,7 @@ from ..const import (
|
|||||||
ATTR_IMAGE,
|
ATTR_IMAGE,
|
||||||
ATTR_INGRESS,
|
ATTR_INGRESS,
|
||||||
ATTR_INIT,
|
ATTR_INIT,
|
||||||
|
ATTR_JOURNALD,
|
||||||
ATTR_KERNEL_MODULES,
|
ATTR_KERNEL_MODULES,
|
||||||
ATTR_LEGACY,
|
ATTR_LEGACY,
|
||||||
ATTR_LOCATON,
|
ATTR_LOCATON,
|
||||||
@@ -46,6 +46,7 @@ from ..const import (
|
|||||||
ATTR_PORTS,
|
ATTR_PORTS,
|
||||||
ATTR_PORTS_DESCRIPTION,
|
ATTR_PORTS_DESCRIPTION,
|
||||||
ATTR_PRIVILEGED,
|
ATTR_PRIVILEGED,
|
||||||
|
ATTR_REALTIME,
|
||||||
ATTR_REPOSITORY,
|
ATTR_REPOSITORY,
|
||||||
ATTR_SCHEMA,
|
ATTR_SCHEMA,
|
||||||
ATTR_SERVICES,
|
ATTR_SERVICES,
|
||||||
@@ -56,19 +57,26 @@ from ..const import (
|
|||||||
ATTR_STDIN,
|
ATTR_STDIN,
|
||||||
ATTR_TIMEOUT,
|
ATTR_TIMEOUT,
|
||||||
ATTR_TMPFS,
|
ATTR_TMPFS,
|
||||||
|
ATTR_TRANSLATIONS,
|
||||||
|
ATTR_UART,
|
||||||
ATTR_UDEV,
|
ATTR_UDEV,
|
||||||
ATTR_URL,
|
ATTR_URL,
|
||||||
|
ATTR_USB,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
ATTR_VIDEO,
|
ATTR_VIDEO,
|
||||||
|
ATTR_WATCHDOG,
|
||||||
ATTR_WEBUI,
|
ATTR_WEBUI,
|
||||||
SECURITY_DEFAULT,
|
SECURITY_DEFAULT,
|
||||||
SECURITY_DISABLE,
|
SECURITY_DISABLE,
|
||||||
SECURITY_PROFILE,
|
SECURITY_PROFILE,
|
||||||
AddonStages,
|
AddonBoot,
|
||||||
|
AddonStage,
|
||||||
AddonStartup,
|
AddonStartup,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from .validate import RE_SERVICE, RE_VOLUME, schema_ui_options, validate_options
|
from ..docker.const import Capabilities
|
||||||
|
from .options import AddonOptions, UiOptions
|
||||||
|
from .validate import RE_SERVICE, RE_VOLUME
|
||||||
|
|
||||||
Data = Dict[str, Any]
|
Data = Dict[str, Any]
|
||||||
|
|
||||||
@@ -107,7 +115,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data[ATTR_OPTIONS]
|
return self.data[ATTR_OPTIONS]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def boot(self) -> bool:
|
def boot(self) -> AddonBoot:
|
||||||
"""Return boot config with prio local settings."""
|
"""Return boot config with prio local settings."""
|
||||||
return self.data[ATTR_BOOT]
|
return self.data[ATTR_BOOT]
|
||||||
|
|
||||||
@@ -180,12 +188,17 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data[ATTR_REPOSITORY]
|
return self.data[ATTR_REPOSITORY]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latest_version(self) -> str:
|
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."""
|
"""Return latest version of add-on."""
|
||||||
return self.data[ATTR_VERSION]
|
return self.data[ATTR_VERSION]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self) -> Optional[str]:
|
def version(self) -> AwesomeVersion:
|
||||||
"""Return version of add-on."""
|
"""Return version of add-on."""
|
||||||
return self.data[ATTR_VERSION]
|
return self.data[ATTR_VERSION]
|
||||||
|
|
||||||
@@ -205,7 +218,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data[ATTR_ADVANCED]
|
return self.data[ATTR_ADVANCED]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stage(self) -> AddonStages:
|
def stage(self) -> AddonStage:
|
||||||
"""Return stage mode of add-on."""
|
"""Return stage mode of add-on."""
|
||||||
return self.data[ATTR_STAGE]
|
return self.data[ATTR_STAGE]
|
||||||
|
|
||||||
@@ -247,6 +260,11 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
"""Return URL to webui or None."""
|
"""Return URL to webui or None."""
|
||||||
return self.data.get(ATTR_WEBUI)
|
return self.data.get(ATTR_WEBUI)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def watchdog(self) -> Optional[str]:
|
||||||
|
"""Return URL to for watchdog or None."""
|
||||||
|
return self.data.get(ATTR_WATCHDOG)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ingress_port(self) -> Optional[int]:
|
def ingress_port(self) -> Optional[int]:
|
||||||
"""Return Ingress port."""
|
"""Return Ingress port."""
|
||||||
@@ -288,19 +306,9 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data[ATTR_HOST_DBUS]
|
return self.data[ATTR_HOST_DBUS]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def devices(self) -> List[str]:
|
def static_devices(self) -> List[Path]:
|
||||||
"""Return devices of add-on."""
|
"""Return static devices of add-on."""
|
||||||
return self.data.get(ATTR_DEVICES, [])
|
return [Path(node) for node in self.data.get(ATTR_DEVICES, [])]
|
||||||
|
|
||||||
@property
|
|
||||||
def auto_uart(self) -> bool:
|
|
||||||
"""Return True if we should map all UART device."""
|
|
||||||
return self.data[ATTR_AUTO_UART]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def tmpfs(self) -> Optional[str]:
|
|
||||||
"""Return tmpfs of add-on."""
|
|
||||||
return self.data.get(ATTR_TMPFS)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def environment(self) -> Optional[Dict[str, str]]:
|
def environment(self) -> Optional[Dict[str, str]]:
|
||||||
@@ -308,7 +316,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return self.data.get(ATTR_ENVIRONMENT)
|
return self.data.get(ATTR_ENVIRONMENT)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def privileged(self) -> List[str]:
|
def privileged(self) -> List[Capabilities]:
|
||||||
"""Return list of privilege."""
|
"""Return list of privilege."""
|
||||||
return self.data.get(ATTR_PRIVILEGED, [])
|
return self.data.get(ATTR_PRIVILEGED, [])
|
||||||
|
|
||||||
@@ -376,6 +384,16 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
"""Return True if the add-on access to GPIO interface."""
|
"""Return True if the add-on access to GPIO interface."""
|
||||||
return self.data[ATTR_GPIO]
|
return self.data[ATTR_GPIO]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_usb(self) -> bool:
|
||||||
|
"""Return True if the add-on need USB access."""
|
||||||
|
return self.data[ATTR_USB]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_uart(self) -> bool:
|
||||||
|
"""Return True if we should map all UART device."""
|
||||||
|
return self.data[ATTR_UART]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def with_udev(self) -> bool:
|
def with_udev(self) -> bool:
|
||||||
"""Return True if the add-on have his own udev."""
|
"""Return True if the add-on have his own udev."""
|
||||||
@@ -386,6 +404,11 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
"""Return True if the add-on access to kernel modules."""
|
"""Return True if the add-on access to kernel modules."""
|
||||||
return self.data[ATTR_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
|
@property
|
||||||
def with_full_access(self) -> bool:
|
def with_full_access(self) -> bool:
|
||||||
"""Return True if the add-on want full access to hardware."""
|
"""Return True if the add-on want full access to hardware."""
|
||||||
@@ -396,6 +419,11 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
"""Return True if the add-on read access to devicetree."""
|
"""Return True if the add-on read access to devicetree."""
|
||||||
return self.data[ATTR_DEVICETREE]
|
return self.data[ATTR_DEVICETREE]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_tmpfs(self) -> Optional[str]:
|
||||||
|
"""Return if tmp is in memory of add-on."""
|
||||||
|
return self.data[ATTR_TMPFS]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def access_auth_api(self) -> bool:
|
def access_auth_api(self) -> bool:
|
||||||
"""Return True if the add-on access to login/auth backend."""
|
"""Return True if the add-on access to login/auth backend."""
|
||||||
@@ -509,8 +537,10 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
raw_schema = self.data[ATTR_SCHEMA]
|
raw_schema = self.data[ATTR_SCHEMA]
|
||||||
|
|
||||||
if isinstance(raw_schema, bool):
|
if isinstance(raw_schema, bool):
|
||||||
return vol.Schema(dict)
|
raw_schema = {}
|
||||||
return vol.Schema(vol.All(dict, validate_options(self.coresys, raw_schema)))
|
return vol.Schema(
|
||||||
|
vol.All(dict, AddonOptions(self.coresys, raw_schema, self.name, self.slug))
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def schema_ui(self) -> Optional[List[Dict[str, Any]]]:
|
def schema_ui(self) -> Optional[List[Dict[str, Any]]]:
|
||||||
@@ -519,7 +549,12 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
|
|
||||||
if isinstance(raw_schema, bool):
|
if isinstance(raw_schema, bool):
|
||||||
return None
|
return None
|
||||||
return schema_ui_options(raw_schema)
|
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):
|
def __eq__(self, other):
|
||||||
"""Compaired add-on objects."""
|
"""Compaired add-on objects."""
|
||||||
@@ -541,15 +576,10 @@ class AddonModel(CoreSysAttributes, ABC):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Home Assistant
|
# Home Assistant
|
||||||
version = config.get(ATTR_HOMEASSISTANT)
|
version: Optional[AwesomeVersion] = config.get(ATTR_HOMEASSISTANT)
|
||||||
if version is None or self.sys_homeassistant.version is None:
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return pkg_version.parse(
|
return self.sys_homeassistant.version >= version
|
||||||
self.sys_homeassistant.version
|
except (AwesomeVersionException, TypeError):
|
||||||
) >= pkg_version.parse(version)
|
|
||||||
except pkg_version.InvalidVersion:
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _image(self, config) -> str:
|
def _image(self, config) -> str:
|
||||||
|
|||||||
417
supervisor/addons/options.py
Normal file
417
supervisor/addons/options.py
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
"""Add-on Options / UI rendering."""
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
from typing import Any, Dict, List, Set, Union
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
|
from ..exceptions import HardwareNotFound
|
||||||
|
from ..hardware.const import UdevSubsystem
|
||||||
|
from ..hardware.data import Device
|
||||||
|
from ..validate import network_port
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_STR = "str"
|
||||||
|
_INT = "int"
|
||||||
|
_FLOAT = "float"
|
||||||
|
_BOOL = "bool"
|
||||||
|
_PASSWORD = "password"
|
||||||
|
_EMAIL = "email"
|
||||||
|
_URL = "url"
|
||||||
|
_PORT = "port"
|
||||||
|
_MATCH = "match"
|
||||||
|
_LIST = "list"
|
||||||
|
_DEVICE = "device"
|
||||||
|
|
||||||
|
RE_SCHEMA_ELEMENT = re.compile(
|
||||||
|
r"^(?:"
|
||||||
|
r"|bool"
|
||||||
|
r"|email"
|
||||||
|
r"|url"
|
||||||
|
r"|port"
|
||||||
|
r"|device(?:\((?P<filter>subsystem=[a-z]+)\))?"
|
||||||
|
r"|str(?:\((?P<s_min>\d+)?,(?P<s_max>\d+)?\))?"
|
||||||
|
r"|password(?:\((?P<p_min>\d+)?,(?P<p_max>\d+)?\))?"
|
||||||
|
r"|int(?:\((?P<i_min>\d+)?,(?P<i_max>\d+)?\))?"
|
||||||
|
r"|float(?:\((?P<f_min>[\d\.]+)?,(?P<f_max>[\d\.]+)?\))?"
|
||||||
|
r"|match\((?P<match>.*)\)"
|
||||||
|
r"|list\((?P<list>.+)\)"
|
||||||
|
r")\??$"
|
||||||
|
)
|
||||||
|
|
||||||
|
_SCHEMA_LENGTH_PARTS = (
|
||||||
|
"i_min",
|
||||||
|
"i_max",
|
||||||
|
"f_min",
|
||||||
|
"f_max",
|
||||||
|
"s_min",
|
||||||
|
"s_max",
|
||||||
|
"p_min",
|
||||||
|
"p_max",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AddonOptions(CoreSysAttributes):
|
||||||
|
"""Validate Add-ons Options."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
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
|
||||||
|
|
||||||
|
def __call__(self, struct):
|
||||||
|
"""Create schema validator for add-ons options."""
|
||||||
|
options = {}
|
||||||
|
|
||||||
|
# read options
|
||||||
|
for key, value in struct.items():
|
||||||
|
# Ignore unknown options / remove from list
|
||||||
|
if key not in self.raw_schema:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Option '%s' does not exist in the schema for %s (%s)",
|
||||||
|
key,
|
||||||
|
self._name,
|
||||||
|
self._slug,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
typ = self.raw_schema[key]
|
||||||
|
try:
|
||||||
|
if isinstance(typ, list):
|
||||||
|
# nested value list
|
||||||
|
options[key] = self._nested_validate_list(typ[0], value, key)
|
||||||
|
elif isinstance(typ, dict):
|
||||||
|
# nested value dict
|
||||||
|
options[key] = self._nested_validate_dict(typ, value, key)
|
||||||
|
else:
|
||||||
|
# normal value
|
||||||
|
options[key] = self._single_validate(typ, value, key)
|
||||||
|
except (IndexError, KeyError):
|
||||||
|
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
|
||||||
|
|
||||||
|
# pylint: disable=no-value-for-parameter
|
||||||
|
def _single_validate(self, typ: str, value: Any, key: str):
|
||||||
|
"""Validate a single element."""
|
||||||
|
# if required argument
|
||||||
|
if value is 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}' 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}' in {self._name} ({self._slug})"
|
||||||
|
) from None
|
||||||
|
|
||||||
|
# prepare range
|
||||||
|
range_args = {}
|
||||||
|
for group_name in _SCHEMA_LENGTH_PARTS:
|
||||||
|
group_value = match.group(group_name)
|
||||||
|
if group_value:
|
||||||
|
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)
|
||||||
|
elif typ.startswith(_FLOAT):
|
||||||
|
return vol.All(vol.Coerce(float), vol.Range(**range_args))(value)
|
||||||
|
elif typ.startswith(_BOOL):
|
||||||
|
return vol.Boolean()(value)
|
||||||
|
elif typ.startswith(_EMAIL):
|
||||||
|
return vol.Email()(value)
|
||||||
|
elif typ.startswith(_URL):
|
||||||
|
return vol.Url()(value)
|
||||||
|
elif typ.startswith(_PORT):
|
||||||
|
return network_port(value)
|
||||||
|
elif typ.startswith(_MATCH):
|
||||||
|
return vol.Match(match.group("match"))(str(value))
|
||||||
|
elif typ.startswith(_LIST):
|
||||||
|
return vol.In(match.group("list").split("|"))(str(value))
|
||||||
|
elif typ.startswith(_DEVICE):
|
||||||
|
try:
|
||||||
|
device = self.sys_hardware.get_by_path(Path(value))
|
||||||
|
except HardwareNotFound:
|
||||||
|
raise vol.Invalid(
|
||||||
|
f"Device '{value}' does not exists! in {self._name} ({self._slug})"
|
||||||
|
) from None
|
||||||
|
|
||||||
|
# Have filter
|
||||||
|
if match.group("filter"):
|
||||||
|
str_filter = match.group("filter")
|
||||||
|
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}! in {self._name} ({self._slug})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Device valid
|
||||||
|
self.devices.add(device)
|
||||||
|
return str(device.path)
|
||||||
|
|
||||||
|
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."""
|
||||||
|
options = []
|
||||||
|
|
||||||
|
# Make sure it is a list
|
||||||
|
if not isinstance(data_list, list):
|
||||||
|
raise vol.Invalid(
|
||||||
|
f"Invalid list for option '{key}' in {self._name} ({self._slug})"
|
||||||
|
) from None
|
||||||
|
|
||||||
|
# Process list
|
||||||
|
for element in data_list:
|
||||||
|
# Nested?
|
||||||
|
if isinstance(typ, dict):
|
||||||
|
c_options = self._nested_validate_dict(typ, element, key)
|
||||||
|
options.append(c_options)
|
||||||
|
else:
|
||||||
|
options.append(self._single_validate(typ, element, key))
|
||||||
|
|
||||||
|
return options
|
||||||
|
|
||||||
|
def _nested_validate_dict(
|
||||||
|
self, typ: Dict[Any, Any], data_dict: Dict[Any, Any], key: str
|
||||||
|
):
|
||||||
|
"""Validate nested items."""
|
||||||
|
options = {}
|
||||||
|
|
||||||
|
# Make sure it is a dict
|
||||||
|
if not isinstance(data_dict, dict):
|
||||||
|
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 option '%s' for %s (%s)", c_key, self._name, self._slug
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Nested?
|
||||||
|
if isinstance(typ[c_key], list):
|
||||||
|
options[c_key] = self._nested_validate_list(
|
||||||
|
typ[c_key][0], c_value, c_key
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
options[c_key] = self._single_validate(typ[c_key], c_value, c_key)
|
||||||
|
|
||||||
|
self._check_missing_options(typ, options, key)
|
||||||
|
return options
|
||||||
|
|
||||||
|
def _check_missing_options(
|
||||||
|
self, origin: Dict[Any, Any], exists: Dict[Any, Any], root: str
|
||||||
|
) -> None:
|
||||||
|
"""Check if all options are exists."""
|
||||||
|
missing = set(origin) - set(exists)
|
||||||
|
for miss_opt in missing:
|
||||||
|
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} in {self._name} ({self._slug})"
|
||||||
|
) from None
|
||||||
|
|
||||||
|
|
||||||
|
class UiOptions(CoreSysAttributes):
|
||||||
|
"""Render UI Add-ons Options."""
|
||||||
|
|
||||||
|
def __init__(self, coresys: CoreSys) -> None:
|
||||||
|
"""Initialize UI option render."""
|
||||||
|
self.coresys = coresys
|
||||||
|
|
||||||
|
def __call__(self, raw_schema: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""Generate UI schema."""
|
||||||
|
ui_schema: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
# read options
|
||||||
|
for key, value in raw_schema.items():
|
||||||
|
if isinstance(value, list):
|
||||||
|
# nested value list
|
||||||
|
self._nested_ui_list(ui_schema, value, key)
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
# nested value dict
|
||||||
|
self._nested_ui_dict(ui_schema, value, key)
|
||||||
|
else:
|
||||||
|
# normal value
|
||||||
|
self._single_ui_option(ui_schema, value, key)
|
||||||
|
|
||||||
|
return ui_schema
|
||||||
|
|
||||||
|
def _single_ui_option(
|
||||||
|
self,
|
||||||
|
ui_schema: List[Dict[str, Any]],
|
||||||
|
value: str,
|
||||||
|
key: str,
|
||||||
|
multiple: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""Validate a single element."""
|
||||||
|
ui_node: Dict[str, Union[str, bool, float, List[str]]] = {"name": key}
|
||||||
|
|
||||||
|
# If multiple
|
||||||
|
if multiple:
|
||||||
|
ui_node["multiple"] = True
|
||||||
|
|
||||||
|
# Parse extend data from type
|
||||||
|
match = RE_SCHEMA_ELEMENT.match(value)
|
||||||
|
if not match:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Prepare range
|
||||||
|
for group_name in _SCHEMA_LENGTH_PARTS:
|
||||||
|
group_value = match.group(group_name)
|
||||||
|
if not group_value:
|
||||||
|
continue
|
||||||
|
if group_name[2:] == "min":
|
||||||
|
ui_node["lengthMin"] = float(group_value)
|
||||||
|
elif group_name[2:] == "max":
|
||||||
|
ui_node["lengthMax"] = float(group_value)
|
||||||
|
|
||||||
|
# If required
|
||||||
|
if value.endswith("?"):
|
||||||
|
ui_node["optional"] = True
|
||||||
|
else:
|
||||||
|
ui_node["required"] = True
|
||||||
|
|
||||||
|
# Data types
|
||||||
|
if value.startswith(_STR):
|
||||||
|
ui_node["type"] = "string"
|
||||||
|
elif value.startswith(_PASSWORD):
|
||||||
|
ui_node["type"] = "string"
|
||||||
|
ui_node["format"] = "password"
|
||||||
|
elif value.startswith(_INT):
|
||||||
|
ui_node["type"] = "integer"
|
||||||
|
elif value.startswith(_FLOAT):
|
||||||
|
ui_node["type"] = "float"
|
||||||
|
elif value.startswith(_BOOL):
|
||||||
|
ui_node["type"] = "boolean"
|
||||||
|
elif value.startswith(_EMAIL):
|
||||||
|
ui_node["type"] = "string"
|
||||||
|
ui_node["format"] = "email"
|
||||||
|
elif value.startswith(_URL):
|
||||||
|
ui_node["type"] = "string"
|
||||||
|
ui_node["format"] = "url"
|
||||||
|
elif value.startswith(_PORT):
|
||||||
|
ui_node["type"] = "integer"
|
||||||
|
elif value.startswith(_MATCH):
|
||||||
|
ui_node["type"] = "string"
|
||||||
|
elif value.startswith(_LIST):
|
||||||
|
ui_node["type"] = "select"
|
||||||
|
ui_node["options"] = match.group("list").split("|")
|
||||||
|
elif value.startswith(_DEVICE):
|
||||||
|
ui_node["type"] = "select"
|
||||||
|
|
||||||
|
# Have filter
|
||||||
|
if match.group("filter"):
|
||||||
|
device_filter = _create_device_filter(match.group("filter"))
|
||||||
|
ui_node["options"] = [
|
||||||
|
(device.by_id or device.path).as_posix()
|
||||||
|
for device in self.sys_hardware.filter_devices(**device_filter)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
ui_node["options"] = [
|
||||||
|
(device.by_id or device.path).as_posix()
|
||||||
|
for device in self.sys_hardware.devices
|
||||||
|
]
|
||||||
|
|
||||||
|
ui_schema.append(ui_node)
|
||||||
|
|
||||||
|
def _nested_ui_list(
|
||||||
|
self,
|
||||||
|
ui_schema: List[Dict[str, Any]],
|
||||||
|
option_list: List[Any],
|
||||||
|
key: str,
|
||||||
|
) -> None:
|
||||||
|
"""UI nested list items."""
|
||||||
|
try:
|
||||||
|
element = option_list[0]
|
||||||
|
except IndexError:
|
||||||
|
_LOGGER.error("Invalid schema %s", key)
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(element, dict):
|
||||||
|
self._nested_ui_dict(ui_schema, element, key, multiple=True)
|
||||||
|
else:
|
||||||
|
self._single_ui_option(ui_schema, element, key, multiple=True)
|
||||||
|
|
||||||
|
def _nested_ui_dict(
|
||||||
|
self,
|
||||||
|
ui_schema: List[Dict[str, Any]],
|
||||||
|
option_dict: Dict[str, Any],
|
||||||
|
key: str,
|
||||||
|
multiple: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""UI nested dict items."""
|
||||||
|
ui_node = {
|
||||||
|
"name": key,
|
||||||
|
"type": "schema",
|
||||||
|
"optional": True,
|
||||||
|
"multiple": multiple,
|
||||||
|
}
|
||||||
|
|
||||||
|
nested_schema = []
|
||||||
|
for c_key, c_value in option_dict.items():
|
||||||
|
# Nested?
|
||||||
|
if isinstance(c_value, list):
|
||||||
|
self._nested_ui_list(nested_schema, c_value, c_key)
|
||||||
|
else:
|
||||||
|
self._single_ui_option(nested_schema, c_value, c_key)
|
||||||
|
|
||||||
|
ui_node["schema"] = nested_schema
|
||||||
|
ui_schema.append(ui_node)
|
||||||
|
|
||||||
|
|
||||||
|
def _create_device_filter(str_filter: str) -> Dict[str, Any]:
|
||||||
|
"""Generate device Filter."""
|
||||||
|
raw_filter = dict(value.split("=") for value in str_filter.split(";"))
|
||||||
|
|
||||||
|
clean_filter = {}
|
||||||
|
for key, value in raw_filter.items():
|
||||||
|
if key == "subsystem":
|
||||||
|
clean_filter[key] = UdevSubsystem(value)
|
||||||
|
else:
|
||||||
|
clean_filter[key] = value
|
||||||
|
|
||||||
|
return clean_filter
|
||||||
@@ -6,18 +6,8 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from ..const import (
|
from ..const import ROLE_ADMIN, ROLE_MANAGER, SECURITY_DISABLE, SECURITY_PROFILE
|
||||||
PRIVILEGED_DAC_READ_SEARCH,
|
from ..docker.const import Capabilities
|
||||||
PRIVILEGED_NET_ADMIN,
|
|
||||||
PRIVILEGED_SYS_ADMIN,
|
|
||||||
PRIVILEGED_SYS_MODULE,
|
|
||||||
PRIVILEGED_SYS_PTRACE,
|
|
||||||
PRIVILEGED_SYS_RAWIO,
|
|
||||||
ROLE_ADMIN,
|
|
||||||
ROLE_MANAGER,
|
|
||||||
SECURITY_DISABLE,
|
|
||||||
SECURITY_PROFILE,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .model import AddonModel
|
from .model import AddonModel
|
||||||
@@ -46,16 +36,19 @@ def rating_security(addon: AddonModel) -> int:
|
|||||||
rating += 1
|
rating += 1
|
||||||
|
|
||||||
# Privileged options
|
# Privileged options
|
||||||
if any(
|
if (
|
||||||
|
any(
|
||||||
privilege in addon.privileged
|
privilege in addon.privileged
|
||||||
for privilege in (
|
for privilege in (
|
||||||
PRIVILEGED_NET_ADMIN,
|
Capabilities.NET_ADMIN,
|
||||||
PRIVILEGED_SYS_ADMIN,
|
Capabilities.SYS_ADMIN,
|
||||||
PRIVILEGED_SYS_RAWIO,
|
Capabilities.SYS_RAWIO,
|
||||||
PRIVILEGED_SYS_PTRACE,
|
Capabilities.SYS_PTRACE,
|
||||||
PRIVILEGED_SYS_MODULE,
|
Capabilities.SYS_MODULE,
|
||||||
PRIVILEGED_DAC_READ_SEARCH,
|
Capabilities.DAC_READ_SEARCH,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
or addon.with_kernel_modules
|
||||||
):
|
):
|
||||||
rating += -1
|
rating += -1
|
||||||
|
|
||||||
@@ -73,12 +66,8 @@ def rating_security(addon: AddonModel) -> int:
|
|||||||
if addon.host_pid:
|
if addon.host_pid:
|
||||||
rating += -2
|
rating += -2
|
||||||
|
|
||||||
# Full Access
|
# Docker Access & full Access
|
||||||
if addon.with_full_access:
|
if addon.access_docker_api or addon.with_full_access:
|
||||||
rating += -2
|
|
||||||
|
|
||||||
# Docker Access
|
|
||||||
if addon.access_docker_api:
|
|
||||||
rating = 1
|
rating = 1
|
||||||
|
|
||||||
return max(min(6, rating), 1)
|
return max(min(6, rating), 1)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
from typing import Any, Dict, List, Union
|
from typing import Any, Dict
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -18,10 +18,10 @@ from ..const import (
|
|||||||
ATTR_AUDIO_INPUT,
|
ATTR_AUDIO_INPUT,
|
||||||
ATTR_AUDIO_OUTPUT,
|
ATTR_AUDIO_OUTPUT,
|
||||||
ATTR_AUTH_API,
|
ATTR_AUTH_API,
|
||||||
ATTR_AUTO_UART,
|
|
||||||
ATTR_AUTO_UPDATE,
|
ATTR_AUTO_UPDATE,
|
||||||
ATTR_BOOT,
|
ATTR_BOOT,
|
||||||
ATTR_BUILD_FROM,
|
ATTR_BUILD_FROM,
|
||||||
|
ATTR_CONFIGURATION,
|
||||||
ATTR_DESCRIPTON,
|
ATTR_DESCRIPTON,
|
||||||
ATTR_DEVICES,
|
ATTR_DEVICES,
|
||||||
ATTR_DEVICETREE,
|
ATTR_DEVICETREE,
|
||||||
@@ -45,6 +45,7 @@ from ..const import (
|
|||||||
ATTR_INGRESS_PORT,
|
ATTR_INGRESS_PORT,
|
||||||
ATTR_INGRESS_TOKEN,
|
ATTR_INGRESS_TOKEN,
|
||||||
ATTR_INIT,
|
ATTR_INIT,
|
||||||
|
ATTR_JOURNALD,
|
||||||
ATTR_KERNEL_MODULES,
|
ATTR_KERNEL_MODULES,
|
||||||
ATTR_LEGACY,
|
ATTR_LEGACY,
|
||||||
ATTR_LOCATON,
|
ATTR_LOCATON,
|
||||||
@@ -60,6 +61,7 @@ from ..const import (
|
|||||||
ATTR_PORTS_DESCRIPTION,
|
ATTR_PORTS_DESCRIPTION,
|
||||||
ATTR_PRIVILEGED,
|
ATTR_PRIVILEGED,
|
||||||
ATTR_PROTECTED,
|
ATTR_PROTECTED,
|
||||||
|
ATTR_REALTIME,
|
||||||
ATTR_REPOSITORY,
|
ATTR_REPOSITORY,
|
||||||
ATTR_SCHEMA,
|
ATTR_SCHEMA,
|
||||||
ATTR_SERVICES,
|
ATTR_SERVICES,
|
||||||
@@ -73,78 +75,43 @@ from ..const import (
|
|||||||
ATTR_SYSTEM,
|
ATTR_SYSTEM,
|
||||||
ATTR_TIMEOUT,
|
ATTR_TIMEOUT,
|
||||||
ATTR_TMPFS,
|
ATTR_TMPFS,
|
||||||
|
ATTR_TRANSLATIONS,
|
||||||
|
ATTR_UART,
|
||||||
ATTR_UDEV,
|
ATTR_UDEV,
|
||||||
ATTR_URL,
|
ATTR_URL,
|
||||||
|
ATTR_USB,
|
||||||
ATTR_USER,
|
ATTR_USER,
|
||||||
ATTR_UUID,
|
ATTR_UUID,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
ATTR_VIDEO,
|
ATTR_VIDEO,
|
||||||
|
ATTR_WATCHDOG,
|
||||||
ATTR_WEBUI,
|
ATTR_WEBUI,
|
||||||
BOOT_AUTO,
|
|
||||||
BOOT_MANUAL,
|
|
||||||
PRIVILEGED_ALL,
|
|
||||||
ROLE_ALL,
|
ROLE_ALL,
|
||||||
ROLE_DEFAULT,
|
ROLE_DEFAULT,
|
||||||
STATE_STARTED,
|
AddonBoot,
|
||||||
STATE_STOPPED,
|
AddonStage,
|
||||||
AddonStages,
|
|
||||||
AddonStartup,
|
AddonStartup,
|
||||||
|
AddonState,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSys
|
|
||||||
from ..discovery.validate import valid_discovery_service
|
from ..discovery.validate import valid_discovery_service
|
||||||
|
from ..docker.const import Capabilities
|
||||||
from ..validate import (
|
from ..validate import (
|
||||||
DOCKER_PORTS,
|
docker_image,
|
||||||
DOCKER_PORTS_DESCRIPTION,
|
docker_ports,
|
||||||
|
docker_ports_description,
|
||||||
network_port,
|
network_port,
|
||||||
token,
|
token,
|
||||||
uuid_match,
|
uuid_match,
|
||||||
version_tag,
|
version_tag,
|
||||||
)
|
)
|
||||||
|
from .options import RE_SCHEMA_ELEMENT
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share|media)(?::(rw|ro))?$")
|
||||||
RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share)(?::(rw|ro))?$")
|
|
||||||
RE_SERVICE = re.compile(r"^(?P<service>mqtt|mysql):(?P<rights>provide|want|need)$")
|
RE_SERVICE = re.compile(r"^(?P<service>mqtt|mysql):(?P<rights>provide|want|need)$")
|
||||||
|
|
||||||
V_STR = "str"
|
|
||||||
V_INT = "int"
|
|
||||||
V_FLOAT = "float"
|
|
||||||
V_BOOL = "bool"
|
|
||||||
V_PASSWORD = "password"
|
|
||||||
V_EMAIL = "email"
|
|
||||||
V_URL = "url"
|
|
||||||
V_PORT = "port"
|
|
||||||
V_MATCH = "match"
|
|
||||||
V_LIST = "list"
|
|
||||||
|
|
||||||
RE_SCHEMA_ELEMENT = re.compile(
|
|
||||||
r"^(?:"
|
|
||||||
r"|bool"
|
|
||||||
r"|email"
|
|
||||||
r"|url"
|
|
||||||
r"|port"
|
|
||||||
r"|str(?:\((?P<s_min>\d+)?,(?P<s_max>\d+)?\))?"
|
|
||||||
r"|password(?:\((?P<p_min>\d+)?,(?P<p_max>\d+)?\))?"
|
|
||||||
r"|int(?:\((?P<i_min>\d+)?,(?P<i_max>\d+)?\))?"
|
|
||||||
r"|float(?:\((?P<f_min>[\d\.]+)?,(?P<f_max>[\d\.]+)?\))?"
|
|
||||||
r"|match\((?P<match>.*)\)"
|
|
||||||
r"|list\((?P<list>.+)\)"
|
|
||||||
r")\??$"
|
|
||||||
)
|
|
||||||
|
|
||||||
_SCHEMA_LENGTH_PARTS = (
|
|
||||||
"i_min",
|
|
||||||
"i_max",
|
|
||||||
"f_min",
|
|
||||||
"f_max",
|
|
||||||
"s_min",
|
|
||||||
"s_max",
|
|
||||||
"p_min",
|
|
||||||
"p_max",
|
|
||||||
)
|
|
||||||
|
|
||||||
RE_DOCKER_IMAGE = re.compile(r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)$")
|
|
||||||
RE_DOCKER_IMAGE_BUILD = re.compile(
|
RE_DOCKER_IMAGE_BUILD = re.compile(
|
||||||
r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)(:[\.\-\w{}]+)?$"
|
r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)(:[\.\-\w{}]+)?$"
|
||||||
)
|
)
|
||||||
@@ -155,6 +122,7 @@ RE_MACHINE = re.compile(
|
|||||||
r"^!?(?:"
|
r"^!?(?:"
|
||||||
r"|intel-nuc"
|
r"|intel-nuc"
|
||||||
r"|odroid-c2"
|
r"|odroid-c2"
|
||||||
|
r"|odroid-c4"
|
||||||
r"|odroid-n2"
|
r"|odroid-n2"
|
||||||
r"|odroid-xu"
|
r"|odroid-xu"
|
||||||
r"|qemuarm-64"
|
r"|qemuarm-64"
|
||||||
@@ -172,32 +140,102 @@ RE_MACHINE = re.compile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _simple_startup(value) -> str:
|
def _warn_addon_config(config: Dict[str, Any]):
|
||||||
"""Define startup schema."""
|
"""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,
|
||||||
|
)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_addon_config(protocol=False):
|
||||||
|
"""Migrate addon config."""
|
||||||
|
|
||||||
|
def _migrate(config: Dict[str, Any]):
|
||||||
|
name = config.get(ATTR_NAME)
|
||||||
|
if not name:
|
||||||
|
raise vol.Invalid("Invalid Add-on config!")
|
||||||
|
|
||||||
|
# Startup 2018-03-30
|
||||||
|
if config.get(ATTR_STARTUP) in ("before", "after"):
|
||||||
|
value = config[ATTR_STARTUP]
|
||||||
|
if protocol:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Add-on config 'startup' with '%s' is deprecated. Please report this to the maintainer of %s",
|
||||||
|
value,
|
||||||
|
name,
|
||||||
|
)
|
||||||
if value == "before":
|
if value == "before":
|
||||||
return AddonStartup.SERVICES.value
|
config[ATTR_STARTUP] = AddonStartup.SERVICES.value
|
||||||
if value == "after":
|
elif value == "after":
|
||||||
return AddonStartup.APPLICATION.value
|
config[ATTR_STARTUP] = AddonStartup.APPLICATION.value
|
||||||
return value
|
|
||||||
|
# UART 2021-01-20
|
||||||
|
if "auto_uart" in config:
|
||||||
|
if protocol:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Add-on config 'auto_uart' is deprecated, use 'uart'. Please report this to the maintainer of %s",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
config[ATTR_UART] = config.pop("auto_uart")
|
||||||
|
|
||||||
|
# Device 2021-01-20
|
||||||
|
if ATTR_DEVICES in config and any(":" in line for line in config[ATTR_DEVICES]):
|
||||||
|
if protocol:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Add-on config 'devices' use a deprecated format, the new format uses a list of paths only. Please report this to the maintainer of %s",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
config[ATTR_DEVICES] = [line.split(":")[0] for line in config[ATTR_DEVICES]]
|
||||||
|
|
||||||
|
# TMPFS 2021-02-01
|
||||||
|
if ATTR_TMPFS in config and not isinstance(config[ATTR_TMPFS], bool):
|
||||||
|
if protocol:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Add-on config 'tmpfs' use a deprecated format, new it's only a boolean. Please report this to the maintainer of %s",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
config[ATTR_TMPFS] = True
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
return _migrate
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
SCHEMA_ADDON_CONFIG = vol.Schema(
|
_SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_NAME): vol.Coerce(str),
|
vol.Required(ATTR_NAME): str,
|
||||||
vol.Required(ATTR_VERSION): vol.All(version_tag, str),
|
vol.Required(ATTR_VERSION): version_tag,
|
||||||
vol.Required(ATTR_SLUG): vol.Coerce(str),
|
vol.Required(ATTR_SLUG): str,
|
||||||
vol.Required(ATTR_DESCRIPTON): vol.Coerce(str),
|
vol.Required(ATTR_DESCRIPTON): str,
|
||||||
vol.Required(ATTR_ARCH): [vol.In(ARCH_ALL)],
|
vol.Required(ATTR_ARCH): [vol.In(ARCH_ALL)],
|
||||||
vol.Optional(ATTR_MACHINE): vol.All([vol.Match(RE_MACHINE)], vol.Unique()),
|
vol.Optional(ATTR_MACHINE): vol.All([vol.Match(RE_MACHINE)], vol.Unique()),
|
||||||
vol.Optional(ATTR_URL): vol.Url(),
|
vol.Optional(ATTR_URL): vol.Url(),
|
||||||
vol.Required(ATTR_STARTUP): vol.All(_simple_startup, vol.Coerce(AddonStartup)),
|
vol.Optional(ATTR_STARTUP, default=AddonStartup.APPLICATION): vol.Coerce(
|
||||||
vol.Required(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
AddonStartup
|
||||||
|
),
|
||||||
|
vol.Optional(ATTR_BOOT, default=AddonBoot.AUTO): vol.Coerce(AddonBoot),
|
||||||
vol.Optional(ATTR_INIT, default=True): vol.Boolean(),
|
vol.Optional(ATTR_INIT, default=True): vol.Boolean(),
|
||||||
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),
|
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_STAGE, default=AddonStages.STABLE): vol.Coerce(AddonStages),
|
vol.Optional(ATTR_STAGE, default=AddonStage.STABLE): vol.Coerce(AddonStage),
|
||||||
vol.Optional(ATTR_PORTS): DOCKER_PORTS,
|
vol.Optional(ATTR_PORTS): docker_ports,
|
||||||
vol.Optional(ATTR_PORTS_DESCRIPTION): DOCKER_PORTS_DESCRIPTION,
|
vol.Optional(ATTR_PORTS_DESCRIPTION): docker_ports_description,
|
||||||
|
vol.Optional(ATTR_WATCHDOG): vol.Match(
|
||||||
|
r"^(?:https?|\[PROTO:\w+\]|tcp):\/\/\[HOST\]:(\[PORT:\d+\]|\d+).*$"
|
||||||
|
),
|
||||||
vol.Optional(ATTR_WEBUI): vol.Match(
|
vol.Optional(ATTR_WEBUI): vol.Match(
|
||||||
r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"
|
r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"
|
||||||
),
|
),
|
||||||
@@ -205,29 +243,31 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
vol.Optional(ATTR_INGRESS_PORT, default=8099): vol.Any(
|
vol.Optional(ATTR_INGRESS_PORT, default=8099): vol.Any(
|
||||||
network_port, vol.Equal(0)
|
network_port, vol.Equal(0)
|
||||||
),
|
),
|
||||||
vol.Optional(ATTR_INGRESS_ENTRY): vol.Coerce(str),
|
vol.Optional(ATTR_INGRESS_ENTRY): str,
|
||||||
vol.Optional(ATTR_PANEL_ICON, default="mdi:puzzle"): vol.Coerce(str),
|
vol.Optional(ATTR_PANEL_ICON, default="mdi:puzzle"): str,
|
||||||
vol.Optional(ATTR_PANEL_TITLE): vol.Coerce(str),
|
vol.Optional(ATTR_PANEL_TITLE): str,
|
||||||
vol.Optional(ATTR_PANEL_ADMIN, default=True): vol.Boolean(),
|
vol.Optional(ATTR_PANEL_ADMIN, default=True): vol.Boolean(),
|
||||||
vol.Optional(ATTR_HOMEASSISTANT): vol.Maybe(vol.Coerce(str)),
|
vol.Optional(ATTR_HOMEASSISTANT): vol.Maybe(version_tag),
|
||||||
vol.Optional(ATTR_HOST_NETWORK, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HOST_NETWORK, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_HOST_PID, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HOST_PID, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_HOST_IPC, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HOST_IPC, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_HOST_DBUS, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HOST_DBUS, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_DEVICES): [vol.Match(r"^(.*):(.*):([rwm]{1,3})$")],
|
vol.Optional(ATTR_DEVICES): [str],
|
||||||
vol.Optional(ATTR_AUTO_UART, default=False): vol.Boolean(),
|
|
||||||
vol.Optional(ATTR_UDEV, default=False): vol.Boolean(),
|
vol.Optional(ATTR_UDEV, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_TMPFS): vol.Match(r"^size=(\d)*[kmg](,uid=\d{1,4})?(,rw)?$"),
|
vol.Optional(ATTR_TMPFS, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_MAP, default=list): [vol.Match(RE_VOLUME)],
|
vol.Optional(ATTR_MAP, default=list): [vol.Match(RE_VOLUME)],
|
||||||
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)},
|
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_APPARMOR, default=True): vol.Boolean(),
|
||||||
vol.Optional(ATTR_FULL_ACCESS, default=False): vol.Boolean(),
|
vol.Optional(ATTR_FULL_ACCESS, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
|
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_VIDEO, default=False): vol.Boolean(),
|
vol.Optional(ATTR_VIDEO, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_GPIO, default=False): vol.Boolean(),
|
vol.Optional(ATTR_GPIO, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_USB, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_UART, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(),
|
vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_KERNEL_MODULES, 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_API, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_HASSIO_ROLE, default=ROLE_DEFAULT): vol.In(ROLE_ALL),
|
vol.Optional(ATTR_HASSIO_ROLE, default=ROLE_DEFAULT): vol.In(ROLE_ALL),
|
||||||
vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(),
|
||||||
@@ -237,39 +277,38 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
vol.Optional(ATTR_AUTH_API, default=False): vol.Boolean(),
|
vol.Optional(ATTR_AUTH_API, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
|
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
|
||||||
vol.Optional(ATTR_DISCOVERY): [valid_discovery_service],
|
vol.Optional(ATTR_DISCOVERY): [valid_discovery_service],
|
||||||
vol.Optional(ATTR_SNAPSHOT_EXCLUDE): [vol.Coerce(str)],
|
vol.Optional(ATTR_SNAPSHOT_EXCLUDE): [str],
|
||||||
vol.Required(ATTR_OPTIONS): dict,
|
vol.Optional(ATTR_OPTIONS, default={}): dict,
|
||||||
vol.Required(ATTR_SCHEMA): vol.Any(
|
vol.Optional(ATTR_SCHEMA, default={}): vol.Any(
|
||||||
vol.Schema(
|
vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Coerce(str): vol.Any(
|
str: vol.Any(
|
||||||
SCHEMA_ELEMENT,
|
SCHEMA_ELEMENT,
|
||||||
[
|
[
|
||||||
vol.Any(
|
vol.Any(
|
||||||
SCHEMA_ELEMENT,
|
SCHEMA_ELEMENT,
|
||||||
{
|
{str: vol.Any(SCHEMA_ELEMENT, [SCHEMA_ELEMENT])},
|
||||||
vol.Coerce(str): vol.Any(
|
|
||||||
SCHEMA_ELEMENT, [SCHEMA_ELEMENT]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
vol.Schema(
|
vol.Schema({str: vol.Any(SCHEMA_ELEMENT, [SCHEMA_ELEMENT])}),
|
||||||
{vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [SCHEMA_ELEMENT])}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
False,
|
False,
|
||||||
),
|
),
|
||||||
vol.Optional(ATTR_IMAGE): vol.Match(RE_DOCKER_IMAGE),
|
vol.Optional(ATTR_IMAGE): docker_image,
|
||||||
vol.Optional(ATTR_TIMEOUT, default=10): vol.All(
|
vol.Optional(ATTR_TIMEOUT, default=10): vol.All(
|
||||||
vol.Coerce(int), vol.Range(min=10, max=300)
|
vol.Coerce(int), vol.Range(min=10, max=300)
|
||||||
),
|
),
|
||||||
|
vol.Optional(ATTR_JOURNALD, default=False): vol.Boolean(),
|
||||||
},
|
},
|
||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SCHEMA_ADDON_CONFIG = vol.All(
|
||||||
|
_migrate_addon_config(True), _warn_addon_config, _SCHEMA_ADDON_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
SCHEMA_BUILD_CONFIG = vol.Schema(
|
SCHEMA_BUILD_CONFIG = vol.Schema(
|
||||||
@@ -285,43 +324,65 @@ SCHEMA_BUILD_CONFIG = vol.Schema(
|
|||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SCHEMA_TRANSLATION_CONFIGURATION = vol.Schema(
|
||||||
# pylint: disable=no-value-for-parameter
|
|
||||||
SCHEMA_ADDON_USER = vol.Schema(
|
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_VERSION): vol.Coerce(str),
|
vol.Required(ATTR_NAME): str,
|
||||||
vol.Optional(ATTR_IMAGE): vol.Coerce(str),
|
vol.Optional(ATTR_DESCRIPTON): vol.Maybe(str),
|
||||||
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): uuid_match,
|
|
||||||
vol.Optional(ATTR_ACCESS_TOKEN): token,
|
|
||||||
vol.Optional(ATTR_INGRESS_TOKEN, default=secrets.token_urlsafe): vol.Coerce(
|
|
||||||
str
|
|
||||||
),
|
|
||||||
vol.Optional(ATTR_OPTIONS, default=dict): dict,
|
|
||||||
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
|
|
||||||
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
|
||||||
vol.Optional(ATTR_NETWORK): DOCKER_PORTS,
|
|
||||||
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
|
|
||||||
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
|
|
||||||
vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(),
|
|
||||||
vol.Optional(ATTR_INGRESS_PANEL, default=False): vol.Boolean(),
|
|
||||||
},
|
},
|
||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
SCHEMA_ADDON_SYSTEM = SCHEMA_ADDON_CONFIG.extend(
|
SCHEMA_ADDON_TRANSLATIONS = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_LOCATON): vol.Coerce(str),
|
vol.Optional(ATTR_CONFIGURATION): {str: SCHEMA_TRANSLATION_CONFIGURATION},
|
||||||
vol.Required(ATTR_REPOSITORY): vol.Coerce(str),
|
vol.Optional(ATTR_NETWORK): {str: str},
|
||||||
|
},
|
||||||
|
extra=vol.REMOVE_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=no-value-for-parameter
|
||||||
|
SCHEMA_ADDON_USER = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_VERSION): version_tag,
|
||||||
|
vol.Optional(ATTR_IMAGE): docker_image,
|
||||||
|
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): uuid_match,
|
||||||
|
vol.Optional(ATTR_ACCESS_TOKEN): token,
|
||||||
|
vol.Optional(ATTR_INGRESS_TOKEN, default=secrets.token_urlsafe): str,
|
||||||
|
vol.Optional(ATTR_OPTIONS, default=dict): dict,
|
||||||
|
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_BOOT): vol.Coerce(AddonBoot),
|
||||||
|
vol.Optional(ATTR_NETWORK): docker_ports,
|
||||||
|
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(str),
|
||||||
|
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(str),
|
||||||
|
vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_INGRESS_PANEL, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_WATCHDOG, default=False): vol.Boolean(),
|
||||||
|
},
|
||||||
|
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
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
SCHEMA_ADDONS_FILE = vol.Schema(
|
SCHEMA_ADDONS_FILE = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(ATTR_USER, default=dict): {vol.Coerce(str): SCHEMA_ADDON_USER},
|
vol.Optional(ATTR_USER, default=dict): {str: SCHEMA_ADDON_USER},
|
||||||
vol.Optional(ATTR_SYSTEM, default=dict): {vol.Coerce(str): SCHEMA_ADDON_SYSTEM},
|
vol.Optional(ATTR_SYSTEM, default=dict): {str: SCHEMA_ADDON_SYSTEM},
|
||||||
}
|
},
|
||||||
|
extra=vol.REMOVE_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -329,264 +390,8 @@ SCHEMA_ADDON_SNAPSHOT = vol.Schema(
|
|||||||
{
|
{
|
||||||
vol.Required(ATTR_USER): SCHEMA_ADDON_USER,
|
vol.Required(ATTR_USER): SCHEMA_ADDON_USER,
|
||||||
vol.Required(ATTR_SYSTEM): SCHEMA_ADDON_SYSTEM,
|
vol.Required(ATTR_SYSTEM): SCHEMA_ADDON_SYSTEM,
|
||||||
vol.Required(ATTR_STATE): vol.In([STATE_STARTED, STATE_STOPPED]),
|
vol.Required(ATTR_STATE): vol.Coerce(AddonState),
|
||||||
vol.Required(ATTR_VERSION): vol.Coerce(str),
|
vol.Required(ATTR_VERSION): version_tag,
|
||||||
},
|
},
|
||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_options(coresys: CoreSys, raw_schema: Dict[str, Any]):
|
|
||||||
"""Validate schema."""
|
|
||||||
|
|
||||||
def validate(struct):
|
|
||||||
"""Create schema validator for add-ons options."""
|
|
||||||
options = {}
|
|
||||||
|
|
||||||
# read options
|
|
||||||
for key, value in struct.items():
|
|
||||||
# Ignore unknown options / remove from list
|
|
||||||
if key not in raw_schema:
|
|
||||||
_LOGGER.warning("Unknown options %s", key)
|
|
||||||
continue
|
|
||||||
|
|
||||||
typ = raw_schema[key]
|
|
||||||
try:
|
|
||||||
if isinstance(typ, list):
|
|
||||||
# nested value list
|
|
||||||
options[key] = _nested_validate_list(coresys, typ[0], value, key)
|
|
||||||
elif isinstance(typ, dict):
|
|
||||||
# nested value dict
|
|
||||||
options[key] = _nested_validate_dict(coresys, typ, value, key)
|
|
||||||
else:
|
|
||||||
# normal value
|
|
||||||
options[key] = _single_validate(coresys, typ, value, key)
|
|
||||||
except (IndexError, KeyError):
|
|
||||||
raise vol.Invalid(f"Type error for {key}") from None
|
|
||||||
|
|
||||||
_check_missing_options(raw_schema, options, "root")
|
|
||||||
return options
|
|
||||||
|
|
||||||
return validate
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-value-for-parameter
|
|
||||||
# pylint: disable=inconsistent-return-statements
|
|
||||||
def _single_validate(coresys: CoreSys, typ: str, value: Any, key: str):
|
|
||||||
"""Validate a single element."""
|
|
||||||
# if required argument
|
|
||||||
if value is None:
|
|
||||||
raise vol.Invalid(f"Missing required option '{key}'")
|
|
||||||
|
|
||||||
# Lookup secret
|
|
||||||
if str(value).startswith("!secret "):
|
|
||||||
secret: str = value.partition(" ")[2]
|
|
||||||
value = coresys.secrets.get(secret)
|
|
||||||
if value is None:
|
|
||||||
raise vol.Invalid(f"Unknown secret {secret}")
|
|
||||||
|
|
||||||
# parse extend data from type
|
|
||||||
match = RE_SCHEMA_ELEMENT.match(typ)
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
raise vol.Invalid(f"Unknown type {typ}")
|
|
||||||
|
|
||||||
# prepare range
|
|
||||||
range_args = {}
|
|
||||||
for group_name in _SCHEMA_LENGTH_PARTS:
|
|
||||||
group_value = match.group(group_name)
|
|
||||||
if group_value:
|
|
||||||
range_args[group_name[2:]] = float(group_value)
|
|
||||||
|
|
||||||
if typ.startswith(V_STR) or typ.startswith(V_PASSWORD):
|
|
||||||
return vol.All(str(value), vol.Range(**range_args))(value)
|
|
||||||
elif typ.startswith(V_INT):
|
|
||||||
return vol.All(vol.Coerce(int), vol.Range(**range_args))(value)
|
|
||||||
elif typ.startswith(V_FLOAT):
|
|
||||||
return vol.All(vol.Coerce(float), vol.Range(**range_args))(value)
|
|
||||||
elif typ.startswith(V_BOOL):
|
|
||||||
return vol.Boolean()(value)
|
|
||||||
elif typ.startswith(V_EMAIL):
|
|
||||||
return vol.Email()(value)
|
|
||||||
elif typ.startswith(V_URL):
|
|
||||||
return vol.Url()(value)
|
|
||||||
elif typ.startswith(V_PORT):
|
|
||||||
return network_port(value)
|
|
||||||
elif typ.startswith(V_MATCH):
|
|
||||||
return vol.Match(match.group("match"))(str(value))
|
|
||||||
elif typ.startswith(V_LIST):
|
|
||||||
return vol.In(match.group("list").split("|"))(str(value))
|
|
||||||
|
|
||||||
raise vol.Invalid(f"Fatal error for {key} type {typ}")
|
|
||||||
|
|
||||||
|
|
||||||
def _nested_validate_list(coresys, typ, data_list, key):
|
|
||||||
"""Validate nested items."""
|
|
||||||
options = []
|
|
||||||
|
|
||||||
# Make sure it is a list
|
|
||||||
if not isinstance(data_list, list):
|
|
||||||
raise vol.Invalid(f"Invalid list for {key}")
|
|
||||||
|
|
||||||
# Process list
|
|
||||||
for element in data_list:
|
|
||||||
# Nested?
|
|
||||||
if isinstance(typ, dict):
|
|
||||||
c_options = _nested_validate_dict(coresys, typ, element, key)
|
|
||||||
options.append(c_options)
|
|
||||||
else:
|
|
||||||
options.append(_single_validate(coresys, typ, element, key))
|
|
||||||
|
|
||||||
return options
|
|
||||||
|
|
||||||
|
|
||||||
def _nested_validate_dict(coresys, typ, data_dict, key):
|
|
||||||
"""Validate nested items."""
|
|
||||||
options = {}
|
|
||||||
|
|
||||||
# Make sure it is a dict
|
|
||||||
if not isinstance(data_dict, dict):
|
|
||||||
raise vol.Invalid(f"Invalid dict for {key}")
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Nested?
|
|
||||||
if isinstance(typ[c_key], list):
|
|
||||||
options[c_key] = _nested_validate_list(
|
|
||||||
coresys, typ[c_key][0], c_value, c_key
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
options[c_key] = _single_validate(coresys, typ[c_key], c_value, c_key)
|
|
||||||
|
|
||||||
_check_missing_options(typ, options, key)
|
|
||||||
return options
|
|
||||||
|
|
||||||
|
|
||||||
def _check_missing_options(origin, exists, root):
|
|
||||||
"""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("?"):
|
|
||||||
continue
|
|
||||||
raise vol.Invalid(f"Missing option {miss_opt} in {root}")
|
|
||||||
|
|
||||||
|
|
||||||
def schema_ui_options(raw_schema: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
||||||
"""Generate UI schema."""
|
|
||||||
ui_schema: List[Dict[str, Any]] = []
|
|
||||||
|
|
||||||
# read options
|
|
||||||
for key, value in raw_schema.items():
|
|
||||||
if isinstance(value, list):
|
|
||||||
# nested value list
|
|
||||||
_nested_ui_list(ui_schema, value, key)
|
|
||||||
elif isinstance(value, dict):
|
|
||||||
# nested value dict
|
|
||||||
_nested_ui_dict(ui_schema, value, key)
|
|
||||||
else:
|
|
||||||
# normal value
|
|
||||||
_single_ui_option(ui_schema, value, key)
|
|
||||||
|
|
||||||
return ui_schema
|
|
||||||
|
|
||||||
|
|
||||||
def _single_ui_option(
|
|
||||||
ui_schema: List[Dict[str, Any]], value: str, key: str, multiple: bool = False
|
|
||||||
) -> None:
|
|
||||||
"""Validate a single element."""
|
|
||||||
ui_node: Dict[str, Union[str, bool, float, List[str]]] = {"name": key}
|
|
||||||
|
|
||||||
# If multiple
|
|
||||||
if multiple:
|
|
||||||
ui_node["multiple"] = True
|
|
||||||
|
|
||||||
# Parse extend data from type
|
|
||||||
match = RE_SCHEMA_ELEMENT.match(value)
|
|
||||||
if not match:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Prepare range
|
|
||||||
for group_name in _SCHEMA_LENGTH_PARTS:
|
|
||||||
group_value = match.group(group_name)
|
|
||||||
if not group_value:
|
|
||||||
continue
|
|
||||||
if group_name[2:] == "min":
|
|
||||||
ui_node["lengthMin"] = float(group_value)
|
|
||||||
elif group_name[2:] == "max":
|
|
||||||
ui_node["lengthMax"] = float(group_value)
|
|
||||||
|
|
||||||
# If required
|
|
||||||
if value.endswith("?"):
|
|
||||||
ui_node["optional"] = True
|
|
||||||
else:
|
|
||||||
ui_node["required"] = True
|
|
||||||
|
|
||||||
# Data types
|
|
||||||
if value.startswith(V_STR):
|
|
||||||
ui_node["type"] = "string"
|
|
||||||
elif value.startswith(V_PASSWORD):
|
|
||||||
ui_node["type"] = "string"
|
|
||||||
ui_node["format"] = "password"
|
|
||||||
elif value.startswith(V_INT):
|
|
||||||
ui_node["type"] = "integer"
|
|
||||||
elif value.startswith(V_FLOAT):
|
|
||||||
ui_node["type"] = "float"
|
|
||||||
elif value.startswith(V_BOOL):
|
|
||||||
ui_node["type"] = "boolean"
|
|
||||||
elif value.startswith(V_EMAIL):
|
|
||||||
ui_node["type"] = "string"
|
|
||||||
ui_node["format"] = "email"
|
|
||||||
elif value.startswith(V_URL):
|
|
||||||
ui_node["type"] = "string"
|
|
||||||
ui_node["format"] = "url"
|
|
||||||
elif value.startswith(V_PORT):
|
|
||||||
ui_node["type"] = "integer"
|
|
||||||
elif value.startswith(V_MATCH):
|
|
||||||
ui_node["type"] = "string"
|
|
||||||
elif value.startswith(V_LIST):
|
|
||||||
ui_node["type"] = "select"
|
|
||||||
ui_node["options"] = match.group("list").split("|")
|
|
||||||
|
|
||||||
ui_schema.append(ui_node)
|
|
||||||
|
|
||||||
|
|
||||||
def _nested_ui_list(
|
|
||||||
ui_schema: List[Dict[str, Any]], option_list: List[Any], key: str
|
|
||||||
) -> None:
|
|
||||||
"""UI nested list items."""
|
|
||||||
try:
|
|
||||||
element = option_list[0]
|
|
||||||
except IndexError:
|
|
||||||
_LOGGER.error("Invalid schema %s", key)
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(element, dict):
|
|
||||||
_nested_ui_dict(ui_schema, element, key, multiple=True)
|
|
||||||
else:
|
|
||||||
_single_ui_option(ui_schema, element, key, multiple=True)
|
|
||||||
|
|
||||||
|
|
||||||
def _nested_ui_dict(
|
|
||||||
ui_schema: List[Dict[str, Any]],
|
|
||||||
option_dict: Dict[str, Any],
|
|
||||||
key: str,
|
|
||||||
multiple: bool = False,
|
|
||||||
) -> None:
|
|
||||||
"""UI nested dict items."""
|
|
||||||
ui_node = {"name": key, "type": "schema", "optional": True, "multiple": multiple}
|
|
||||||
|
|
||||||
nested_schema = []
|
|
||||||
for c_key, c_value in option_dict.items():
|
|
||||||
# Nested?
|
|
||||||
if isinstance(c_value, list):
|
|
||||||
_nested_ui_list(nested_schema, c_value, c_key)
|
|
||||||
else:
|
|
||||||
_single_ui_option(nested_schema, c_value, c_key)
|
|
||||||
|
|
||||||
ui_node["schema"] = nested_schema
|
|
||||||
ui_schema.append(ui_node)
|
|
||||||
|
|||||||
@@ -12,17 +12,23 @@ from .auth import APIAuth
|
|||||||
from .cli import APICli
|
from .cli import APICli
|
||||||
from .discovery import APIDiscovery
|
from .discovery import APIDiscovery
|
||||||
from .dns import APICoreDNS
|
from .dns import APICoreDNS
|
||||||
|
from .docker import APIDocker
|
||||||
from .hardware import APIHardware
|
from .hardware import APIHardware
|
||||||
from .homeassistant import APIHomeAssistant
|
from .homeassistant import APIHomeAssistant
|
||||||
from .host import APIHost
|
from .host import APIHost
|
||||||
from .info import APIInfo
|
from .info import APIInfo
|
||||||
from .ingress import APIIngress
|
from .ingress import APIIngress
|
||||||
|
from .jobs import APIJobs
|
||||||
from .multicast import APIMulticast
|
from .multicast import APIMulticast
|
||||||
|
from .network import APINetwork
|
||||||
|
from .observer import APIObserver
|
||||||
from .os import APIOS
|
from .os import APIOS
|
||||||
from .proxy import APIProxy
|
from .proxy import APIProxy
|
||||||
|
from .resolution import APIResoulution
|
||||||
from .security import SecurityMiddleware
|
from .security import SecurityMiddleware
|
||||||
from .services import APIServices
|
from .services import APIServices
|
||||||
from .snapshots import APISnapshots
|
from .snapshots import APISnapshots
|
||||||
|
from .store import APIStore
|
||||||
from .supervisor import APISupervisor
|
from .supervisor import APISupervisor
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@@ -40,7 +46,10 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self.security: SecurityMiddleware = SecurityMiddleware(coresys)
|
self.security: SecurityMiddleware = SecurityMiddleware(coresys)
|
||||||
self.webapp: web.Application = web.Application(
|
self.webapp: web.Application = web.Application(
|
||||||
client_max_size=MAX_CLIENT_SIZE,
|
client_max_size=MAX_CLIENT_SIZE,
|
||||||
middlewares=[self.security.token_validation],
|
middlewares=[
|
||||||
|
self.security.system_validation,
|
||||||
|
self.security.token_validation,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
# service stuff
|
# service stuff
|
||||||
@@ -49,24 +58,32 @@ class RestAPI(CoreSysAttributes):
|
|||||||
|
|
||||||
async def load(self) -> None:
|
async def load(self) -> None:
|
||||||
"""Register REST API Calls."""
|
"""Register REST API Calls."""
|
||||||
self._register_supervisor()
|
self._register_addons()
|
||||||
self._register_host()
|
self._register_audio()
|
||||||
self._register_os()
|
self._register_auth()
|
||||||
self._register_cli()
|
self._register_cli()
|
||||||
self._register_multicast()
|
self._register_discovery()
|
||||||
|
self._register_dns()
|
||||||
|
self._register_docker()
|
||||||
self._register_hardware()
|
self._register_hardware()
|
||||||
self._register_homeassistant()
|
self._register_homeassistant()
|
||||||
self._register_proxy()
|
self._register_host()
|
||||||
self._register_panel()
|
|
||||||
self._register_addons()
|
|
||||||
self._register_ingress()
|
|
||||||
self._register_snapshots()
|
|
||||||
self._register_discovery()
|
|
||||||
self._register_services()
|
|
||||||
self._register_info()
|
self._register_info()
|
||||||
self._register_auth()
|
self._register_ingress()
|
||||||
self._register_dns()
|
self._register_multicast()
|
||||||
self._register_audio()
|
self._register_network()
|
||||||
|
self._register_observer()
|
||||||
|
self._register_os()
|
||||||
|
self._register_jobs()
|
||||||
|
self._register_panel()
|
||||||
|
self._register_proxy()
|
||||||
|
self._register_resolution()
|
||||||
|
self._register_services()
|
||||||
|
self._register_snapshots()
|
||||||
|
self._register_supervisor()
|
||||||
|
self._register_store()
|
||||||
|
|
||||||
|
await self.start()
|
||||||
|
|
||||||
def _register_host(self) -> None:
|
def _register_host(self) -> None:
|
||||||
"""Register hostcontrol functions."""
|
"""Register hostcontrol functions."""
|
||||||
@@ -89,6 +106,33 @@ class RestAPI(CoreSysAttributes):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _register_network(self) -> None:
|
||||||
|
"""Register network functions."""
|
||||||
|
api_network = APINetwork()
|
||||||
|
api_network.coresys = self.coresys
|
||||||
|
|
||||||
|
self.webapp.add_routes(
|
||||||
|
[
|
||||||
|
web.get("/network/info", api_network.info),
|
||||||
|
web.post("/network/reload", api_network.reload),
|
||||||
|
web.get(
|
||||||
|
"/network/interface/{interface}/info", api_network.interface_info
|
||||||
|
),
|
||||||
|
web.post(
|
||||||
|
"/network/interface/{interface}/update",
|
||||||
|
api_network.interface_update,
|
||||||
|
),
|
||||||
|
web.get(
|
||||||
|
"/network/interface/{interface}/accesspoints",
|
||||||
|
api_network.scan_accesspoints,
|
||||||
|
),
|
||||||
|
web.post(
|
||||||
|
"/network/interface/{interface}/vlan/{vlan}",
|
||||||
|
api_network.create_vlan,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def _register_os(self) -> None:
|
def _register_os(self) -> None:
|
||||||
"""Register OS functions."""
|
"""Register OS functions."""
|
||||||
api_os = APIOS()
|
api_os = APIOS()
|
||||||
@@ -102,6 +146,19 @@ class RestAPI(CoreSysAttributes):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _register_jobs(self) -> None:
|
||||||
|
"""Register Jobs functions."""
|
||||||
|
api_jobs = APIJobs()
|
||||||
|
api_jobs.coresys = self.coresys
|
||||||
|
|
||||||
|
self.webapp.add_routes(
|
||||||
|
[
|
||||||
|
web.get("/jobs/info", api_jobs.info),
|
||||||
|
web.post("/jobs/options", api_jobs.options),
|
||||||
|
web.post("/jobs/reset", api_jobs.reset),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def _register_cli(self) -> None:
|
def _register_cli(self) -> None:
|
||||||
"""Register HA cli functions."""
|
"""Register HA cli functions."""
|
||||||
api_cli = APICli()
|
api_cli = APICli()
|
||||||
@@ -115,6 +172,19 @@ class RestAPI(CoreSysAttributes):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _register_observer(self) -> None:
|
||||||
|
"""Register Observer functions."""
|
||||||
|
api_observer = APIObserver()
|
||||||
|
api_observer.coresys = self.coresys
|
||||||
|
|
||||||
|
self.webapp.add_routes(
|
||||||
|
[
|
||||||
|
web.get("/observer/info", api_observer.info),
|
||||||
|
web.get("/observer/stats", api_observer.stats),
|
||||||
|
web.post("/observer/update", api_observer.update),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def _register_multicast(self) -> None:
|
def _register_multicast(self) -> None:
|
||||||
"""Register Multicast functions."""
|
"""Register Multicast functions."""
|
||||||
api_multicast = APIMulticast()
|
api_multicast = APIMulticast()
|
||||||
@@ -150,13 +220,46 @@ class RestAPI(CoreSysAttributes):
|
|||||||
|
|
||||||
self.webapp.add_routes([web.get("/info", api_info.info)])
|
self.webapp.add_routes([web.get("/info", api_info.info)])
|
||||||
|
|
||||||
|
def _register_resolution(self) -> None:
|
||||||
|
"""Register info functions."""
|
||||||
|
api_resolution = APIResoulution()
|
||||||
|
api_resolution.coresys = self.coresys
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
web.delete(
|
||||||
|
"/resolution/suggestion/{suggestion}",
|
||||||
|
api_resolution.dismiss_suggestion,
|
||||||
|
),
|
||||||
|
web.delete(
|
||||||
|
"/resolution/issue/{issue}",
|
||||||
|
api_resolution.dismiss_issue,
|
||||||
|
),
|
||||||
|
web.post("/resolution/healthcheck", api_resolution.healthcheck),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def _register_auth(self) -> None:
|
def _register_auth(self) -> None:
|
||||||
"""Register auth functions."""
|
"""Register auth functions."""
|
||||||
api_auth = APIAuth()
|
api_auth = APIAuth()
|
||||||
api_auth.coresys = self.coresys
|
api_auth.coresys = self.coresys
|
||||||
|
|
||||||
self.webapp.add_routes(
|
self.webapp.add_routes(
|
||||||
[web.post("/auth", api_auth.auth), web.post("/auth/reset", api_auth.reset)]
|
[
|
||||||
|
web.get("/auth", api_auth.auth),
|
||||||
|
web.post("/auth", api_auth.auth),
|
||||||
|
web.post("/auth/reset", api_auth.reset),
|
||||||
|
web.delete("/auth/cache", api_auth.cache),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def _register_supervisor(self) -> None:
|
def _register_supervisor(self) -> None:
|
||||||
@@ -172,6 +275,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.get("/supervisor/logs", api_supervisor.logs),
|
web.get("/supervisor/logs", api_supervisor.logs),
|
||||||
web.post("/supervisor/update", api_supervisor.update),
|
web.post("/supervisor/update", api_supervisor.update),
|
||||||
web.post("/supervisor/reload", api_supervisor.reload),
|
web.post("/supervisor/reload", api_supervisor.reload),
|
||||||
|
web.post("/supervisor/restart", api_supervisor.restart),
|
||||||
web.post("/supervisor/options", api_supervisor.options),
|
web.post("/supervisor/options", api_supervisor.options),
|
||||||
web.post("/supervisor/repair", api_supervisor.repair),
|
web.post("/supervisor/repair", api_supervisor.repair),
|
||||||
]
|
]
|
||||||
@@ -241,13 +345,15 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.get("/addons", api_addons.list),
|
web.get("/addons", api_addons.list),
|
||||||
web.post("/addons/reload", api_addons.reload),
|
web.post("/addons/reload", api_addons.reload),
|
||||||
web.get("/addons/{addon}/info", api_addons.info),
|
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}/uninstall", api_addons.uninstall),
|
||||||
web.post("/addons/{addon}/start", api_addons.start),
|
web.post("/addons/{addon}/start", api_addons.start),
|
||||||
web.post("/addons/{addon}/stop", api_addons.stop),
|
web.post("/addons/{addon}/stop", api_addons.stop),
|
||||||
web.post("/addons/{addon}/restart", api_addons.restart),
|
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", 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.post("/addons/{addon}/rebuild", api_addons.rebuild),
|
||||||
web.get("/addons/{addon}/logs", api_addons.logs),
|
web.get("/addons/{addon}/logs", api_addons.logs),
|
||||||
web.get("/addons/{addon}/icon", api_addons.icon),
|
web.get("/addons/{addon}/icon", api_addons.icon),
|
||||||
@@ -268,6 +374,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self.webapp.add_routes(
|
self.webapp.add_routes(
|
||||||
[
|
[
|
||||||
web.post("/ingress/session", api_ingress.create_session),
|
web.post("/ingress/session", api_ingress.create_session),
|
||||||
|
web.post("/ingress/validate_session", api_ingress.validate_session),
|
||||||
web.get("/ingress/panels", api_ingress.panels),
|
web.get("/ingress/panels", api_ingress.panels),
|
||||||
web.view("/ingress/{token}/{path:.*}", api_ingress.handler),
|
web.view("/ingress/{token}/{path:.*}", api_ingress.handler),
|
||||||
]
|
]
|
||||||
@@ -286,7 +393,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.post("/snapshots/new/partial", api_snapshots.snapshot_partial),
|
web.post("/snapshots/new/partial", api_snapshots.snapshot_partial),
|
||||||
web.post("/snapshots/new/upload", api_snapshots.upload),
|
web.post("/snapshots/new/upload", api_snapshots.upload),
|
||||||
web.get("/snapshots/{snapshot}/info", api_snapshots.info),
|
web.get("/snapshots/{snapshot}/info", api_snapshots.info),
|
||||||
web.post("/snapshots/{snapshot}/remove", api_snapshots.remove),
|
web.delete("/snapshots/{snapshot}", api_snapshots.remove),
|
||||||
web.post(
|
web.post(
|
||||||
"/snapshots/{snapshot}/restore/full", api_snapshots.restore_full
|
"/snapshots/{snapshot}/restore/full", api_snapshots.restore_full
|
||||||
),
|
),
|
||||||
@@ -295,6 +402,8 @@ class RestAPI(CoreSysAttributes):
|
|||||||
api_snapshots.restore_partial,
|
api_snapshots.restore_partial,
|
||||||
),
|
),
|
||||||
web.get("/snapshots/{snapshot}/download", api_snapshots.download),
|
web.get("/snapshots/{snapshot}/download", api_snapshots.download),
|
||||||
|
# Old, remove at end of 2020
|
||||||
|
web.post("/snapshots/{snapshot}/remove", api_snapshots.remove),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -365,11 +474,65 @@ 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:
|
def _register_panel(self) -> None:
|
||||||
"""Register panel for Home Assistant."""
|
"""Register panel for Home Assistant."""
|
||||||
panel_dir = Path(__file__).parent.joinpath("panel")
|
panel_dir = Path(__file__).parent.joinpath("panel")
|
||||||
self.webapp.add_routes([web.static("/app", panel_dir)])
|
self.webapp.add_routes([web.static("/app", panel_dir)])
|
||||||
|
|
||||||
|
def _register_docker(self) -> None:
|
||||||
|
"""Register docker configuration functions."""
|
||||||
|
api_docker = APIDocker()
|
||||||
|
api_docker.coresys = self.coresys
|
||||||
|
|
||||||
|
self.webapp.add_routes(
|
||||||
|
[
|
||||||
|
web.get("/docker/info", api_docker.info),
|
||||||
|
web.get("/docker/registries", api_docker.registries),
|
||||||
|
web.post("/docker/registries", api_docker.create_registry),
|
||||||
|
web.delete("/docker/registries/{hostname}", api_docker.remove_registry),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
"""Run RESTful API webserver."""
|
"""Run RESTful API webserver."""
|
||||||
await self._runner.setup()
|
await self._runner.setup()
|
||||||
@@ -382,7 +545,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
except OSError as err:
|
except OSError as err:
|
||||||
_LOGGER.critical("Failed to create HTTP server at 0.0.0.0:80 -> %s", err)
|
_LOGGER.critical("Failed to create HTTP server at 0.0.0.0:80 -> %s", err)
|
||||||
else:
|
else:
|
||||||
_LOGGER.info("Start API on %s", self.sys_docker.network.supervisor)
|
_LOGGER.info("Starting API on %s", self.sys_docker.network.supervisor)
|
||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
"""Stop RESTful API webserver."""
|
"""Stop RESTful API webserver."""
|
||||||
@@ -393,4 +556,4 @@ class RestAPI(CoreSysAttributes):
|
|||||||
await self._site.stop()
|
await self._site.stop()
|
||||||
await self._runner.cleanup()
|
await self._runner.cleanup()
|
||||||
|
|
||||||
_LOGGER.info("Stop API on %s", self.sys_docker.network.supervisor)
|
_LOGGER.info("Stopping API on %s", self.sys_docker.network.supervisor)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from typing import Any, Awaitable, Dict, List
|
|||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
from ..addons import AnyAddon
|
from ..addons import AnyAddon
|
||||||
from ..addons.addon import Addon
|
from ..addons.addon import Addon
|
||||||
@@ -61,6 +62,7 @@ from ..const import (
|
|||||||
ATTR_MEMORY_LIMIT,
|
ATTR_MEMORY_LIMIT,
|
||||||
ATTR_MEMORY_PERCENT,
|
ATTR_MEMORY_PERCENT,
|
||||||
ATTR_MEMORY_USAGE,
|
ATTR_MEMORY_USAGE,
|
||||||
|
ATTR_MESSAGE,
|
||||||
ATTR_NAME,
|
ATTR_NAME,
|
||||||
ATTR_NETWORK,
|
ATTR_NETWORK,
|
||||||
ATTR_NETWORK_DESCRIPTION,
|
ATTR_NETWORK_DESCRIPTION,
|
||||||
@@ -77,26 +79,33 @@ from ..const import (
|
|||||||
ATTR_SLUG,
|
ATTR_SLUG,
|
||||||
ATTR_SOURCE,
|
ATTR_SOURCE,
|
||||||
ATTR_STAGE,
|
ATTR_STAGE,
|
||||||
|
ATTR_STARTUP,
|
||||||
ATTR_STATE,
|
ATTR_STATE,
|
||||||
ATTR_STDIN,
|
ATTR_STDIN,
|
||||||
|
ATTR_TRANSLATIONS,
|
||||||
|
ATTR_UART,
|
||||||
ATTR_UDEV,
|
ATTR_UDEV,
|
||||||
|
ATTR_UPDATE_AVAILABLE,
|
||||||
ATTR_URL,
|
ATTR_URL,
|
||||||
|
ATTR_USB,
|
||||||
|
ATTR_VALID,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
ATTR_VERSION_LATEST,
|
ATTR_VERSION_LATEST,
|
||||||
ATTR_VIDEO,
|
ATTR_VIDEO,
|
||||||
|
ATTR_WATCHDOG,
|
||||||
ATTR_WEBUI,
|
ATTR_WEBUI,
|
||||||
BOOT_AUTO,
|
|
||||||
BOOT_MANUAL,
|
|
||||||
CONTENT_TYPE_BINARY,
|
CONTENT_TYPE_BINARY,
|
||||||
CONTENT_TYPE_PNG,
|
CONTENT_TYPE_PNG,
|
||||||
CONTENT_TYPE_TEXT,
|
CONTENT_TYPE_TEXT,
|
||||||
REQUEST_FROM,
|
REQUEST_FROM,
|
||||||
STATE_NONE,
|
AddonBoot,
|
||||||
|
AddonState,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..docker.stats import DockerStats
|
from ..docker.stats import DockerStats
|
||||||
from ..exceptions import APIError
|
from ..exceptions import APIError, APIForbidden, PwnedError, PwnedSecret
|
||||||
from ..validate import DOCKER_PORTS
|
from ..utils.pwned import check_pwned_password
|
||||||
|
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
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@@ -106,12 +115,13 @@ SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
|
|||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
SCHEMA_OPTIONS = vol.Schema(
|
SCHEMA_OPTIONS = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
vol.Optional(ATTR_BOOT): vol.Coerce(AddonBoot),
|
||||||
vol.Optional(ATTR_NETWORK): vol.Maybe(DOCKER_PORTS),
|
vol.Optional(ATTR_NETWORK): vol.Maybe(docker_ports),
|
||||||
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
|
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
|
||||||
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
|
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
|
||||||
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
|
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
|
||||||
vol.Optional(ATTR_INGRESS_PANEL): vol.Boolean(),
|
vol.Optional(ATTR_INGRESS_PANEL): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -155,10 +165,15 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_DESCRIPTON: addon.description,
|
ATTR_DESCRIPTON: addon.description,
|
||||||
ATTR_ADVANCED: addon.advanced,
|
ATTR_ADVANCED: addon.advanced,
|
||||||
ATTR_STAGE: addon.stage,
|
ATTR_STAGE: addon.stage,
|
||||||
ATTR_VERSION: addon.latest_version,
|
ATTR_VERSION: addon.version if addon.is_installed else None,
|
||||||
ATTR_INSTALLED: addon.version if addon.is_installed else None,
|
ATTR_VERSION_LATEST: addon.latest_version,
|
||||||
|
ATTR_UPDATE_AVAILABLE: addon.need_update
|
||||||
|
if addon.is_installed
|
||||||
|
else False,
|
||||||
|
ATTR_INSTALLED: addon.is_installed,
|
||||||
ATTR_AVAILABLE: addon.available,
|
ATTR_AVAILABLE: addon.available,
|
||||||
ATTR_DETACHED: addon.is_detached,
|
ATTR_DETACHED: addon.is_detached,
|
||||||
|
ATTR_HOMEASSISTANT: addon.homeassistant_version,
|
||||||
ATTR_REPOSITORY: addon.repository,
|
ATTR_REPOSITORY: addon.repository,
|
||||||
ATTR_BUILD: addon.need_build,
|
ATTR_BUILD: addon.need_build,
|
||||||
ATTR_URL: addon.url,
|
ATTR_URL: addon.url,
|
||||||
@@ -203,6 +218,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_REPOSITORY: addon.repository,
|
ATTR_REPOSITORY: addon.repository,
|
||||||
ATTR_VERSION: None,
|
ATTR_VERSION: None,
|
||||||
ATTR_VERSION_LATEST: addon.latest_version,
|
ATTR_VERSION_LATEST: addon.latest_version,
|
||||||
|
ATTR_UPDATE_AVAILABLE: False,
|
||||||
ATTR_PROTECTED: addon.protected,
|
ATTR_PROTECTED: addon.protected,
|
||||||
ATTR_RATING: rating_security(addon),
|
ATTR_RATING: rating_security(addon),
|
||||||
ATTR_BOOT: addon.boot,
|
ATTR_BOOT: addon.boot,
|
||||||
@@ -212,7 +228,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_MACHINE: addon.supported_machine,
|
ATTR_MACHINE: addon.supported_machine,
|
||||||
ATTR_HOMEASSISTANT: addon.homeassistant_version,
|
ATTR_HOMEASSISTANT: addon.homeassistant_version,
|
||||||
ATTR_URL: addon.url,
|
ATTR_URL: addon.url,
|
||||||
ATTR_STATE: STATE_NONE,
|
ATTR_STATE: AddonState.UNKNOWN,
|
||||||
ATTR_DETACHED: addon.is_detached,
|
ATTR_DETACHED: addon.is_detached,
|
||||||
ATTR_AVAILABLE: addon.available,
|
ATTR_AVAILABLE: addon.available,
|
||||||
ATTR_BUILD: addon.need_build,
|
ATTR_BUILD: addon.need_build,
|
||||||
@@ -225,7 +241,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_PRIVILEGED: addon.privileged,
|
ATTR_PRIVILEGED: addon.privileged,
|
||||||
ATTR_FULL_ACCESS: addon.with_full_access,
|
ATTR_FULL_ACCESS: addon.with_full_access,
|
||||||
ATTR_APPARMOR: addon.apparmor,
|
ATTR_APPARMOR: addon.apparmor,
|
||||||
ATTR_DEVICES: _pretty_devices(addon),
|
ATTR_DEVICES: addon.static_devices,
|
||||||
ATTR_ICON: addon.with_icon,
|
ATTR_ICON: addon.with_icon,
|
||||||
ATTR_LOGO: addon.with_logo,
|
ATTR_LOGO: addon.with_logo,
|
||||||
ATTR_CHANGELOG: addon.with_changelog,
|
ATTR_CHANGELOG: addon.with_changelog,
|
||||||
@@ -237,6 +253,8 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_AUTH_API: addon.access_auth_api,
|
ATTR_AUTH_API: addon.access_auth_api,
|
||||||
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
|
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
|
||||||
ATTR_GPIO: addon.with_gpio,
|
ATTR_GPIO: addon.with_gpio,
|
||||||
|
ATTR_USB: addon.with_usb,
|
||||||
|
ATTR_UART: addon.with_uart,
|
||||||
ATTR_KERNEL_MODULES: addon.with_kernel_modules,
|
ATTR_KERNEL_MODULES: addon.with_kernel_modules,
|
||||||
ATTR_DEVICETREE: addon.with_devicetree,
|
ATTR_DEVICETREE: addon.with_devicetree,
|
||||||
ATTR_UDEV: addon.with_udev,
|
ATTR_UDEV: addon.with_udev,
|
||||||
@@ -245,20 +263,23 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_AUDIO: addon.with_audio,
|
ATTR_AUDIO: addon.with_audio,
|
||||||
ATTR_AUDIO_INPUT: None,
|
ATTR_AUDIO_INPUT: None,
|
||||||
ATTR_AUDIO_OUTPUT: None,
|
ATTR_AUDIO_OUTPUT: None,
|
||||||
|
ATTR_STARTUP: addon.startup,
|
||||||
ATTR_SERVICES: _pretty_services(addon),
|
ATTR_SERVICES: _pretty_services(addon),
|
||||||
ATTR_DISCOVERY: addon.discovery,
|
ATTR_DISCOVERY: addon.discovery,
|
||||||
ATTR_IP_ADDRESS: None,
|
ATTR_IP_ADDRESS: None,
|
||||||
|
ATTR_TRANSLATIONS: addon.translations,
|
||||||
ATTR_INGRESS: addon.with_ingress,
|
ATTR_INGRESS: addon.with_ingress,
|
||||||
ATTR_INGRESS_ENTRY: None,
|
ATTR_INGRESS_ENTRY: None,
|
||||||
ATTR_INGRESS_URL: None,
|
ATTR_INGRESS_URL: None,
|
||||||
ATTR_INGRESS_PORT: None,
|
ATTR_INGRESS_PORT: None,
|
||||||
ATTR_INGRESS_PANEL: None,
|
ATTR_INGRESS_PANEL: None,
|
||||||
|
ATTR_WATCHDOG: None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if isinstance(addon, Addon) and addon.is_installed:
|
if isinstance(addon, Addon) and addon.is_installed:
|
||||||
data.update(
|
data.update(
|
||||||
{
|
{
|
||||||
ATTR_STATE: await addon.state(),
|
ATTR_STATE: addon.state,
|
||||||
ATTR_WEBUI: addon.webui,
|
ATTR_WEBUI: addon.webui,
|
||||||
ATTR_INGRESS_ENTRY: addon.ingress_entry,
|
ATTR_INGRESS_ENTRY: addon.ingress_entry,
|
||||||
ATTR_INGRESS_URL: addon.ingress_url,
|
ATTR_INGRESS_URL: addon.ingress_url,
|
||||||
@@ -269,6 +290,10 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_AUTO_UPDATE: addon.auto_update,
|
ATTR_AUTO_UPDATE: addon.auto_update,
|
||||||
ATTR_IP_ADDRESS: str(addon.ip_address),
|
ATTR_IP_ADDRESS: str(addon.ip_address),
|
||||||
ATTR_VERSION: addon.version,
|
ATTR_VERSION: addon.version,
|
||||||
|
ATTR_UPDATE_AVAILABLE: addon.need_update,
|
||||||
|
ATTR_WATCHDOG: addon.watchdog,
|
||||||
|
ATTR_DEVICES: addon.static_devices
|
||||||
|
+ [device.path for device in addon.devices],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -280,7 +305,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
addon = self._extract_addon_installed(request)
|
addon = self._extract_addon_installed(request)
|
||||||
|
|
||||||
# Update secrets for validation
|
# Update secrets for validation
|
||||||
await self.sys_secrets.reload()
|
await self.sys_homeassistant.secrets.reload()
|
||||||
|
|
||||||
# Extend schema with add-on specific validation
|
# Extend schema with add-on specific validation
|
||||||
addon_schema = SCHEMA_OPTIONS.extend(
|
addon_schema = SCHEMA_OPTIONS.extend(
|
||||||
@@ -304,9 +329,55 @@ class APIAddons(CoreSysAttributes):
|
|||||||
if ATTR_INGRESS_PANEL in body:
|
if ATTR_INGRESS_PANEL in body:
|
||||||
addon.ingress_panel = body[ATTR_INGRESS_PANEL]
|
addon.ingress_panel = body[ATTR_INGRESS_PANEL]
|
||||||
await self.sys_ingress.update_hass_panel(addon)
|
await self.sys_ingress.update_hass_panel(addon)
|
||||||
|
if ATTR_WATCHDOG in body:
|
||||||
|
addon.watchdog = body[ATTR_WATCHDOG]
|
||||||
|
|
||||||
addon.save_persist()
|
addon.save_persist()
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
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}
|
||||||
|
|
||||||
|
# Validate config
|
||||||
|
try:
|
||||||
|
addon.schema(addon.options)
|
||||||
|
except vol.Invalid as ex:
|
||||||
|
data[ATTR_MESSAGE] = humanize_error(addon.options, ex)
|
||||||
|
data[ATTR_VALID] = False
|
||||||
|
|
||||||
|
# Validate security
|
||||||
|
if self.sys_config.force_security:
|
||||||
|
for secret in addon.pwned:
|
||||||
|
try:
|
||||||
|
await check_pwned_password(self.sys_websession, secret)
|
||||||
|
continue
|
||||||
|
except PwnedSecret:
|
||||||
|
data[ATTR_MESSAGE] = "Add-on use pwned secrets!"
|
||||||
|
except PwnedError as err:
|
||||||
|
data[
|
||||||
|
ATTR_MESSAGE
|
||||||
|
] = f"Error happening on pwned secrets check: {err!s}!"
|
||||||
|
|
||||||
|
data[ATTR_VALID] = False
|
||||||
|
break
|
||||||
|
|
||||||
|
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(addon.options)
|
||||||
|
except vol.Invalid:
|
||||||
|
raise APIError("Invalid configuration data for the add-on") from None
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def security(self, request: web.Request) -> None:
|
async def security(self, request: web.Request) -> None:
|
||||||
"""Store security options for add-on."""
|
"""Store security options for add-on."""
|
||||||
@@ -314,7 +385,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
body: Dict[str, Any] = await api_validate(SCHEMA_SECURITY, request)
|
body: Dict[str, Any] = await api_validate(SCHEMA_SECURITY, request)
|
||||||
|
|
||||||
if ATTR_PROTECTED in body:
|
if ATTR_PROTECTED in body:
|
||||||
_LOGGER.warning("Protected flag changing for %s!", addon.slug)
|
_LOGGER.warning("Changing protected flag for %s!", addon.slug)
|
||||||
addon.protected = body[ATTR_PROTECTED]
|
addon.protected = body[ATTR_PROTECTED]
|
||||||
|
|
||||||
addon.save_persist()
|
addon.save_persist()
|
||||||
@@ -337,12 +408,6 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_BLK_WRITE: stats.blk_write,
|
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
|
@api_process
|
||||||
def uninstall(self, request: web.Request) -> Awaitable[None]:
|
def uninstall(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Uninstall add-on."""
|
"""Uninstall add-on."""
|
||||||
@@ -361,12 +426,6 @@ class APIAddons(CoreSysAttributes):
|
|||||||
addon = self._extract_addon_installed(request)
|
addon = self._extract_addon_installed(request)
|
||||||
return asyncio.shield(addon.stop())
|
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
|
@api_process
|
||||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Restart add-on."""
|
"""Restart add-on."""
|
||||||
@@ -436,14 +495,6 @@ class APIAddons(CoreSysAttributes):
|
|||||||
await asyncio.shield(addon.write_stdin(data))
|
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]:
|
def _pretty_services(addon: AnyAddon) -> List[str]:
|
||||||
"""Return a simplified services role list."""
|
"""Return a simplified services role list."""
|
||||||
return [f"{name}:{access}" for name, access in addon.services_role.items()]
|
return [f"{name}:{access}" for name, access in addon.services_role.items()]
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from ..const import (
|
|||||||
ATTR_NETWORK_RX,
|
ATTR_NETWORK_RX,
|
||||||
ATTR_NETWORK_TX,
|
ATTR_NETWORK_TX,
|
||||||
ATTR_OUTPUT,
|
ATTR_OUTPUT,
|
||||||
|
ATTR_UPDATE_AVAILABLE,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
ATTR_VERSION_LATEST,
|
ATTR_VERSION_LATEST,
|
||||||
ATTR_VOLUME,
|
ATTR_VOLUME,
|
||||||
@@ -71,6 +72,7 @@ class APIAudio(CoreSysAttributes):
|
|||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_plugins.audio.version,
|
ATTR_VERSION: self.sys_plugins.audio.version,
|
||||||
ATTR_VERSION_LATEST: self.sys_plugins.audio.latest_version,
|
ATTR_VERSION_LATEST: self.sys_plugins.audio.latest_version,
|
||||||
|
ATTR_UPDATE_AVAILABLE: self.sys_plugins.audio.need_update,
|
||||||
ATTR_HOST: str(self.sys_docker.network.audio),
|
ATTR_HOST: str(self.sys_docker.network.audio),
|
||||||
ATTR_AUDIO: {
|
ATTR_AUDIO: {
|
||||||
ATTR_CARD: [attr.asdict(card) for card in self.sys_host.sound.cards],
|
ATTR_CARD: [attr.asdict(card) for card in self.sys_host.sound.cards],
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ SCHEMA_PASSWORD_RESET = vol.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
REALM_HEADER: Dict[str, str] = {
|
||||||
|
WWW_AUTHENTICATE: 'Basic realm="Home Assistant Authentication"'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class APIAuth(CoreSysAttributes):
|
class APIAuth(CoreSysAttributes):
|
||||||
"""Handle RESTful API for auth functions."""
|
"""Handle RESTful API for auth functions."""
|
||||||
@@ -63,7 +67,9 @@ class APIAuth(CoreSysAttributes):
|
|||||||
|
|
||||||
# BasicAuth
|
# BasicAuth
|
||||||
if AUTHORIZATION in request.headers:
|
if AUTHORIZATION in request.headers:
|
||||||
return await self._process_basic(request, addon)
|
if not await self._process_basic(request, addon):
|
||||||
|
raise HTTPUnauthorized(headers=REALM_HEADER)
|
||||||
|
return True
|
||||||
|
|
||||||
# Json
|
# Json
|
||||||
if request.headers.get(CONTENT_TYPE) == CONTENT_TYPE_JSON:
|
if request.headers.get(CONTENT_TYPE) == CONTENT_TYPE_JSON:
|
||||||
@@ -75,9 +81,7 @@ class APIAuth(CoreSysAttributes):
|
|||||||
data = await request.post()
|
data = await request.post()
|
||||||
return await self._process_dict(request, addon, data)
|
return await self._process_dict(request, addon, data)
|
||||||
|
|
||||||
raise HTTPUnauthorized(
|
raise HTTPUnauthorized(headers=REALM_HEADER)
|
||||||
headers={WWW_AUTHENTICATE: 'Basic realm="Home Assistant Authentication"'}
|
|
||||||
)
|
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def reset(self, request: web.Request) -> None:
|
async def reset(self, request: web.Request) -> None:
|
||||||
@@ -86,3 +90,8 @@ class APIAuth(CoreSysAttributes):
|
|||||||
await asyncio.shield(
|
await asyncio.shield(
|
||||||
self.sys_auth.change_password(body[ATTR_USERNAME], body[ATTR_PASSWORD])
|
self.sys_auth.change_password(body[ATTR_USERNAME], body[ATTR_PASSWORD])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def cache(self, request: web.Request) -> None:
|
||||||
|
"""Process cache reset request."""
|
||||||
|
self.sys_auth.reset_data()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from ..const import (
|
|||||||
ATTR_MEMORY_USAGE,
|
ATTR_MEMORY_USAGE,
|
||||||
ATTR_NETWORK_RX,
|
ATTR_NETWORK_RX,
|
||||||
ATTR_NETWORK_TX,
|
ATTR_NETWORK_TX,
|
||||||
|
ATTR_UPDATE_AVAILABLE,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
ATTR_VERSION_LATEST,
|
ATTR_VERSION_LATEST,
|
||||||
)
|
)
|
||||||
@@ -36,6 +37,7 @@ class APICli(CoreSysAttributes):
|
|||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_plugins.cli.version,
|
ATTR_VERSION: self.sys_plugins.cli.version,
|
||||||
ATTR_VERSION_LATEST: self.sys_plugins.cli.latest_version,
|
ATTR_VERSION_LATEST: self.sys_plugins.cli.latest_version,
|
||||||
|
ATTR_UPDATE_AVAILABLE: self.sys_plugins.cli.need_update,
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from ..const import (
|
|||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..discovery.validate import valid_discovery_service
|
from ..discovery.validate import valid_discovery_service
|
||||||
from ..exceptions import APIError, APIForbidden
|
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(
|
SCHEMA_DISCOVERY = vol.Schema(
|
||||||
{
|
{
|
||||||
@@ -33,15 +33,10 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
raise APIError("Discovery message not found")
|
raise APIError("Discovery message not found")
|
||||||
return message
|
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
|
@api_process
|
||||||
|
@require_home_assistant
|
||||||
async def list(self, request):
|
async def list(self, request):
|
||||||
"""Show register services."""
|
"""Show register services."""
|
||||||
self._check_permission_ha(request)
|
|
||||||
|
|
||||||
# Get available discovery
|
# Get available discovery
|
||||||
discovery = []
|
discovery = []
|
||||||
@@ -79,13 +74,11 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
return {ATTR_UUID: message.uuid}
|
return {ATTR_UUID: message.uuid}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
|
@require_home_assistant
|
||||||
async def get_discovery(self, request):
|
async def get_discovery(self, request):
|
||||||
"""Read data into a discovery message."""
|
"""Read data into a discovery message."""
|
||||||
message = self._extract_message(request)
|
message = self._extract_message(request)
|
||||||
|
|
||||||
# HomeAssistant?
|
|
||||||
self._check_permission_ha(request)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ATTR_ADDON: message.addon,
|
ATTR_ADDON: message.addon,
|
||||||
ATTR_SERVICE: message.service,
|
ATTR_SERVICE: message.service,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from ..const import (
|
|||||||
ATTR_NETWORK_RX,
|
ATTR_NETWORK_RX,
|
||||||
ATTR_NETWORK_TX,
|
ATTR_NETWORK_TX,
|
||||||
ATTR_SERVERS,
|
ATTR_SERVERS,
|
||||||
|
ATTR_UPDATE_AVAILABLE,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
ATTR_VERSION_LATEST,
|
ATTR_VERSION_LATEST,
|
||||||
CONTENT_TYPE_BINARY,
|
CONTENT_TYPE_BINARY,
|
||||||
@@ -44,9 +45,10 @@ class APICoreDNS(CoreSysAttributes):
|
|||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_plugins.dns.version,
|
ATTR_VERSION: self.sys_plugins.dns.version,
|
||||||
ATTR_VERSION_LATEST: self.sys_plugins.dns.latest_version,
|
ATTR_VERSION_LATEST: self.sys_plugins.dns.latest_version,
|
||||||
|
ATTR_UPDATE_AVAILABLE: self.sys_plugins.dns.need_update,
|
||||||
ATTR_HOST: str(self.sys_docker.network.dns),
|
ATTR_HOST: str(self.sys_docker.network.dns),
|
||||||
ATTR_SERVERS: self.sys_plugins.dns.servers,
|
ATTR_SERVERS: self.sys_plugins.dns.servers,
|
||||||
ATTR_LOCALS: self.sys_host.network.dns_servers,
|
ATTR_LOCALS: self.sys_plugins.dns.locals,
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
|
|||||||
76
supervisor/api/docker.py
Normal file
76
supervisor/api/docker.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
"""Init file for Supervisor Home Assistant RESTful API."""
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from ..const import (
|
||||||
|
ATTR_HOSTNAME,
|
||||||
|
ATTR_LOGGING,
|
||||||
|
ATTR_PASSWORD,
|
||||||
|
ATTR_REGISTRIES,
|
||||||
|
ATTR_STORAGE,
|
||||||
|
ATTR_USERNAME,
|
||||||
|
ATTR_VERSION,
|
||||||
|
)
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SCHEMA_DOCKER_REGISTRY = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Coerce(str): {
|
||||||
|
vol.Required(ATTR_USERNAME): str,
|
||||||
|
vol.Required(ATTR_PASSWORD): str,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class APIDocker(CoreSysAttributes):
|
||||||
|
"""Handle RESTful API for Docker configuration."""
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def registries(self, request) -> Dict[str, Any]:
|
||||||
|
"""Return the list of registries."""
|
||||||
|
data_registries = {}
|
||||||
|
for hostname, registry in self.sys_docker.config.registries.items():
|
||||||
|
data_registries[hostname] = {
|
||||||
|
ATTR_USERNAME: registry[ATTR_USERNAME],
|
||||||
|
}
|
||||||
|
|
||||||
|
return {ATTR_REGISTRIES: data_registries}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def create_registry(self, request: web.Request):
|
||||||
|
"""Create a new docker registry."""
|
||||||
|
body = await api_validate(SCHEMA_DOCKER_REGISTRY, request)
|
||||||
|
|
||||||
|
for hostname, registry in body.items():
|
||||||
|
self.sys_docker.config.registries[hostname] = registry
|
||||||
|
|
||||||
|
self.sys_docker.config.save_data()
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def remove_registry(self, request: web.Request):
|
||||||
|
"""Delete a docker registry."""
|
||||||
|
hostname = request.match_info.get(ATTR_HOSTNAME)
|
||||||
|
del self.sys_docker.config.registries[hostname]
|
||||||
|
self.sys_docker.config.save_data()
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def info(self, request: web.Request):
|
||||||
|
"""Get docker info."""
|
||||||
|
data_registries = {}
|
||||||
|
for hostname, registry in self.sys_docker.config.registries.items():
|
||||||
|
data_registries[hostname] = {
|
||||||
|
ATTR_USERNAME: registry[ATTR_USERNAME],
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ATTR_VERSION: self.sys_docker.info.version,
|
||||||
|
ATTR_STORAGE: self.sys_docker.info.storage,
|
||||||
|
ATTR_LOGGING: self.sys_docker.info.logging,
|
||||||
|
ATTR_REGISTRIES: data_registries,
|
||||||
|
}
|
||||||
@@ -1,24 +1,36 @@
|
|||||||
"""Init file for Supervisor hardware RESTful API."""
|
"""Init file for Supervisor hardware RESTful API."""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Awaitable, Dict
|
from typing import Any, Awaitable, Dict
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from ..const import (
|
from ..const import ATTR_AUDIO, ATTR_DEVICES, ATTR_INPUT, ATTR_NAME, ATTR_OUTPUT
|
||||||
ATTR_AUDIO,
|
|
||||||
ATTR_DISK,
|
|
||||||
ATTR_GPIO,
|
|
||||||
ATTR_INPUT,
|
|
||||||
ATTR_OUTPUT,
|
|
||||||
ATTR_SERIAL,
|
|
||||||
)
|
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..hardware.const import (
|
||||||
|
ATTR_ATTRIBUTES,
|
||||||
|
ATTR_BY_ID,
|
||||||
|
ATTR_DEV_PATH,
|
||||||
|
ATTR_SUBSYSTEM,
|
||||||
|
ATTR_SYSFS,
|
||||||
|
)
|
||||||
|
from ..hardware.data import Device
|
||||||
from .utils import api_process
|
from .utils import api_process
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def device_struct(device: Device) -> Dict[str, Any]:
|
||||||
|
"""Return a dict with information of a interface to be used in th API."""
|
||||||
|
return {
|
||||||
|
ATTR_NAME: device.name,
|
||||||
|
ATTR_SYSFS: device.sysfs,
|
||||||
|
ATTR_DEV_PATH: device.path,
|
||||||
|
ATTR_SUBSYSTEM: device.subsystem,
|
||||||
|
ATTR_BY_ID: device.by_id,
|
||||||
|
ATTR_ATTRIBUTES: device.attributes,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class APIHardware(CoreSysAttributes):
|
class APIHardware(CoreSysAttributes):
|
||||||
"""Handle RESTful API for hardware functions."""
|
"""Handle RESTful API for hardware functions."""
|
||||||
|
|
||||||
@@ -26,13 +38,9 @@ class APIHardware(CoreSysAttributes):
|
|||||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Show hardware info."""
|
"""Show hardware info."""
|
||||||
return {
|
return {
|
||||||
ATTR_SERIAL: list(
|
ATTR_DEVICES: [
|
||||||
self.sys_hardware.serial_devices | self.sys_hardware.serial_by_id
|
device_struct(device) for device in self.sys_hardware.devices
|
||||||
),
|
]
|
||||||
ATTR_INPUT: list(self.sys_hardware.input_devices),
|
|
||||||
ATTR_DISK: list(self.sys_hardware.disk_devices),
|
|
||||||
ATTR_GPIO: list(self.sys_hardware.gpio_devices),
|
|
||||||
ATTR_AUDIO: self.sys_hardware.audio_devices,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
@@ -52,6 +60,6 @@ class APIHardware(CoreSysAttributes):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def trigger(self, request: web.Request) -> Awaitable[None]:
|
async def trigger(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Trigger a udev device reload."""
|
"""Trigger a udev device reload."""
|
||||||
return asyncio.shield(self.sys_hardware.udev_trigger())
|
_LOGGER.debug("Ignoring DEPRECATED hardware trigger function call.")
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from ..const import (
|
|||||||
ATTR_PORT,
|
ATTR_PORT,
|
||||||
ATTR_REFRESH_TOKEN,
|
ATTR_REFRESH_TOKEN,
|
||||||
ATTR_SSL,
|
ATTR_SSL,
|
||||||
|
ATTR_UPDATE_AVAILABLE,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
ATTR_VERSION_LATEST,
|
ATTR_VERSION_LATEST,
|
||||||
ATTR_WAIT_BOOT,
|
ATTR_WAIT_BOOT,
|
||||||
@@ -65,6 +66,7 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_homeassistant.version,
|
ATTR_VERSION: self.sys_homeassistant.version,
|
||||||
ATTR_VERSION_LATEST: self.sys_homeassistant.latest_version,
|
ATTR_VERSION_LATEST: self.sys_homeassistant.latest_version,
|
||||||
|
ATTR_UPDATE_AVAILABLE: self.sys_homeassistant.need_update,
|
||||||
ATTR_MACHINE: self.sys_homeassistant.machine,
|
ATTR_MACHINE: self.sys_homeassistant.machine,
|
||||||
ATTR_IP_ADDRESS: str(self.sys_homeassistant.ip_address),
|
ATTR_IP_ADDRESS: str(self.sys_homeassistant.ip_address),
|
||||||
ATTR_ARCH: self.sys_homeassistant.arch,
|
ATTR_ARCH: self.sys_homeassistant.arch,
|
||||||
@@ -117,7 +119,7 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> Dict[Any, str]:
|
async def stats(self, request: web.Request) -> Dict[Any, str]:
|
||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
stats = await self.sys_homeassistant.stats()
|
stats = await self.sys_homeassistant.core.stats()
|
||||||
if not stats:
|
if not stats:
|
||||||
raise APIError("No stats available")
|
raise APIError("No stats available")
|
||||||
|
|
||||||
@@ -138,36 +140,36 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
body = await api_validate(SCHEMA_VERSION, request)
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
version = body.get(ATTR_VERSION, self.sys_homeassistant.latest_version)
|
version = body.get(ATTR_VERSION, self.sys_homeassistant.latest_version)
|
||||||
|
|
||||||
await asyncio.shield(self.sys_homeassistant.update(version))
|
await asyncio.shield(self.sys_homeassistant.core.update(version))
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def stop(self, request: web.Request) -> Awaitable[None]:
|
def stop(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Stop Home Assistant."""
|
"""Stop Home Assistant."""
|
||||||
return asyncio.shield(self.sys_homeassistant.stop())
|
return asyncio.shield(self.sys_homeassistant.core.stop())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def start(self, request: web.Request) -> Awaitable[None]:
|
def start(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Start Home Assistant."""
|
"""Start Home Assistant."""
|
||||||
return asyncio.shield(self.sys_homeassistant.start())
|
return asyncio.shield(self.sys_homeassistant.core.start())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Restart Home Assistant."""
|
"""Restart Home Assistant."""
|
||||||
return asyncio.shield(self.sys_homeassistant.restart())
|
return asyncio.shield(self.sys_homeassistant.core.restart())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def rebuild(self, request: web.Request) -> Awaitable[None]:
|
def rebuild(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Rebuild Home Assistant."""
|
"""Rebuild Home Assistant."""
|
||||||
return asyncio.shield(self.sys_homeassistant.rebuild())
|
return asyncio.shield(self.sys_homeassistant.core.rebuild())
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
@api_process_raw(CONTENT_TYPE_BINARY)
|
||||||
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
||||||
"""Return Home Assistant Docker logs."""
|
"""Return Home Assistant Docker logs."""
|
||||||
return self.sys_homeassistant.logs()
|
return self.sys_homeassistant.core.logs()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def check(self, request: web.Request) -> None:
|
async def check(self, request: web.Request) -> None:
|
||||||
"""Check configuration of Home Assistant."""
|
"""Check configuration of Home Assistant."""
|
||||||
result = await self.sys_homeassistant.check_config()
|
result = await self.sys_homeassistant.core.check_config()
|
||||||
if not result.valid:
|
if not result.valid:
|
||||||
raise APIError(result.log)
|
raise APIError(result.log)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Init file for Supervisor host RESTful API."""
|
"""Init file for Supervisor host RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
|
||||||
from typing import Awaitable
|
from typing import Awaitable
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
@@ -11,6 +10,10 @@ from ..const import (
|
|||||||
ATTR_CPE,
|
ATTR_CPE,
|
||||||
ATTR_DEPLOYMENT,
|
ATTR_DEPLOYMENT,
|
||||||
ATTR_DESCRIPTON,
|
ATTR_DESCRIPTON,
|
||||||
|
ATTR_DISK_FREE,
|
||||||
|
ATTR_DISK_LIFE_TIME,
|
||||||
|
ATTR_DISK_TOTAL,
|
||||||
|
ATTR_DISK_USED,
|
||||||
ATTR_FEATURES,
|
ATTR_FEATURES,
|
||||||
ATTR_HOSTNAME,
|
ATTR_HOSTNAME,
|
||||||
ATTR_KERNEL,
|
ATTR_KERNEL,
|
||||||
@@ -23,8 +26,6 @@ from ..const import (
|
|||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from .utils import api_process, api_process_raw, api_validate
|
from .utils import api_process, api_process_raw, api_validate
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
SERVICE = "service"
|
SERVICE = "service"
|
||||||
|
|
||||||
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): vol.Coerce(str)})
|
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): vol.Coerce(str)})
|
||||||
@@ -39,11 +40,15 @@ class APIHost(CoreSysAttributes):
|
|||||||
return {
|
return {
|
||||||
ATTR_CHASSIS: self.sys_host.info.chassis,
|
ATTR_CHASSIS: self.sys_host.info.chassis,
|
||||||
ATTR_CPE: self.sys_host.info.cpe,
|
ATTR_CPE: self.sys_host.info.cpe,
|
||||||
ATTR_FEATURES: self.sys_host.supperted_features,
|
|
||||||
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
|
||||||
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
|
|
||||||
ATTR_DEPLOYMENT: self.sys_host.info.deployment,
|
ATTR_DEPLOYMENT: self.sys_host.info.deployment,
|
||||||
|
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_KERNEL: self.sys_host.info.kernel,
|
||||||
|
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
@@ -70,7 +75,11 @@ class APIHost(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
def reload(self, request):
|
def reload(self, request):
|
||||||
"""Reload host data."""
|
"""Reload host data."""
|
||||||
return asyncio.shield(self.sys_host.reload())
|
return asyncio.shield(
|
||||||
|
asyncio.wait(
|
||||||
|
[self.sys_host.reload(), self.sys_resolution.evaluate.evaluate_system()]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def services(self, request):
|
async def services(self, request):
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ from ..const import (
|
|||||||
ATTR_ARCH,
|
ATTR_ARCH,
|
||||||
ATTR_CHANNEL,
|
ATTR_CHANNEL,
|
||||||
ATTR_DOCKER,
|
ATTR_DOCKER,
|
||||||
|
ATTR_FEATURES,
|
||||||
ATTR_HASSOS,
|
ATTR_HASSOS,
|
||||||
ATTR_HOMEASSISTANT,
|
ATTR_HOMEASSISTANT,
|
||||||
ATTR_HOSTNAME,
|
ATTR_HOSTNAME,
|
||||||
ATTR_LOGGING,
|
ATTR_LOGGING,
|
||||||
ATTR_MACHINE,
|
ATTR_MACHINE,
|
||||||
|
ATTR_OPERATING_SYSTEM,
|
||||||
|
ATTR_STATE,
|
||||||
ATTR_SUPERVISOR,
|
ATTR_SUPERVISOR,
|
||||||
ATTR_SUPPORTED,
|
ATTR_SUPPORTED,
|
||||||
ATTR_SUPPORTED_ARCH,
|
ATTR_SUPPORTED_ARCH,
|
||||||
@@ -36,11 +39,14 @@ class APIInfo(CoreSysAttributes):
|
|||||||
ATTR_HASSOS: self.sys_hassos.version,
|
ATTR_HASSOS: self.sys_hassos.version,
|
||||||
ATTR_DOCKER: self.sys_docker.info.version,
|
ATTR_DOCKER: self.sys_docker.info.version,
|
||||||
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
||||||
|
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
|
||||||
|
ATTR_FEATURES: self.sys_host.features,
|
||||||
ATTR_MACHINE: self.sys_machine,
|
ATTR_MACHINE: self.sys_machine,
|
||||||
ATTR_ARCH: self.sys_arch.default,
|
ATTR_ARCH: self.sys_arch.default,
|
||||||
|
ATTR_STATE: self.sys_core.state,
|
||||||
ATTR_SUPPORTED_ARCH: self.sys_arch.supported,
|
ATTR_SUPPORTED_ARCH: self.sys_arch.supported,
|
||||||
ATTR_SUPPORTED: self.sys_supported,
|
ATTR_SUPPORTED: self.sys_core.supported,
|
||||||
ATTR_CHANNEL: self.sys_updater.channel,
|
ATTR_CHANNEL: self.sys_updater.channel,
|
||||||
ATTR_LOGGING: self.sys_config.logging,
|
ATTR_LOGGING: self.sys_config.logging,
|
||||||
ATTR_TIMEZONE: self.sys_timezone,
|
ATTR_TIMEZONE: self.sys_config.timezone,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from aiohttp.web_exceptions import (
|
|||||||
HTTPUnauthorized,
|
HTTPUnauthorized,
|
||||||
)
|
)
|
||||||
from multidict import CIMultiDict, istr
|
from multidict import CIMultiDict, istr
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from ..addons.addon import Addon
|
from ..addons.addon import Addon
|
||||||
from ..const import (
|
from ..const import (
|
||||||
@@ -24,13 +25,14 @@ from ..const import (
|
|||||||
COOKIE_INGRESS,
|
COOKIE_INGRESS,
|
||||||
HEADER_TOKEN,
|
HEADER_TOKEN,
|
||||||
HEADER_TOKEN_OLD,
|
HEADER_TOKEN_OLD,
|
||||||
REQUEST_FROM,
|
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from .utils import api_process
|
from .utils import api_process, api_validate, require_home_assistant
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
VALIDATE_SESSION_DATA = vol.Schema({ATTR_SESSION: str})
|
||||||
|
|
||||||
|
|
||||||
class APIIngress(CoreSysAttributes):
|
class APIIngress(CoreSysAttributes):
|
||||||
"""Ingress view to handle add-on webui routing."""
|
"""Ingress view to handle add-on webui routing."""
|
||||||
@@ -47,11 +49,6 @@ class APIIngress(CoreSysAttributes):
|
|||||||
|
|
||||||
return addon
|
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:
|
def _create_url(self, addon: Addon, path: str) -> str:
|
||||||
"""Create URL to container."""
|
"""Create URL to container."""
|
||||||
return f"http://{addon.ip_address}:{addon.ingress_port}/{path}"
|
return f"http://{addon.ip_address}:{addon.ingress_port}/{path}"
|
||||||
@@ -71,18 +68,28 @@ class APIIngress(CoreSysAttributes):
|
|||||||
return {ATTR_PANELS: addons}
|
return {ATTR_PANELS: addons}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
|
@require_home_assistant
|
||||||
async def create_session(self, request: web.Request) -> Dict[str, Any]:
|
async def create_session(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Create a new session."""
|
"""Create a new session."""
|
||||||
self._check_ha_access(request)
|
|
||||||
|
|
||||||
session = self.sys_ingress.create_session()
|
session = self.sys_ingress.create_session()
|
||||||
return {ATTR_SESSION: 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."""
|
||||||
|
data = await api_validate(VALIDATE_SESSION_DATA, request)
|
||||||
|
|
||||||
|
# Check Ingress Session
|
||||||
|
if not self.sys_ingress.validate_session(data[ATTR_SESSION]):
|
||||||
|
_LOGGER.warning("No valid ingress session %s", data[ATTR_SESSION])
|
||||||
|
raise HTTPUnauthorized()
|
||||||
|
|
||||||
|
@require_home_assistant
|
||||||
async def handler(
|
async def handler(
|
||||||
self, request: web.Request
|
self, request: web.Request
|
||||||
) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]:
|
) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]:
|
||||||
"""Route data to Supervisor ingress service."""
|
"""Route data to Supervisor ingress service."""
|
||||||
self._check_ha_access(request)
|
|
||||||
|
|
||||||
# Check Ingress Session
|
# Check Ingress Session
|
||||||
session = request.cookies.get(COOKIE_INGRESS)
|
session = request.cookies.get(COOKIE_INGRESS)
|
||||||
@@ -104,7 +111,7 @@ class APIIngress(CoreSysAttributes):
|
|||||||
except aiohttp.ClientError as err:
|
except aiohttp.ClientError as err:
|
||||||
_LOGGER.error("Ingress error: %s", err)
|
_LOGGER.error("Ingress error: %s", err)
|
||||||
|
|
||||||
raise HTTPBadGateway() from None
|
raise HTTPBadGateway()
|
||||||
|
|
||||||
async def _handle_websocket(
|
async def _handle_websocket(
|
||||||
self, request: web.Request, addon: Addon, path: str
|
self, request: web.Request, addon: Addon, path: str
|
||||||
|
|||||||
44
supervisor/api/jobs.py
Normal file
44
supervisor/api/jobs.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"""Init file for Supervisor Jobs RESTful API."""
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..jobs.const import ATTR_IGNORE_CONDITIONS, JobCondition
|
||||||
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SCHEMA_OPTIONS = vol.Schema(
|
||||||
|
{vol.Optional(ATTR_IGNORE_CONDITIONS): [vol.Coerce(JobCondition)]}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class APIJobs(CoreSysAttributes):
|
||||||
|
"""Handle RESTful API for OS functions."""
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
|
"""Return JobManager information."""
|
||||||
|
return {
|
||||||
|
ATTR_IGNORE_CONDITIONS: self.sys_jobs.ignore_conditions,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def options(self, request: web.Request) -> None:
|
||||||
|
"""Set options for JobManager."""
|
||||||
|
body = await api_validate(SCHEMA_OPTIONS, request)
|
||||||
|
|
||||||
|
if ATTR_IGNORE_CONDITIONS in body:
|
||||||
|
self.sys_jobs.ignore_conditions = body[ATTR_IGNORE_CONDITIONS]
|
||||||
|
|
||||||
|
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."""
|
||||||
|
self.sys_jobs.reset_data()
|
||||||
@@ -15,6 +15,7 @@ from ..const import (
|
|||||||
ATTR_MEMORY_USAGE,
|
ATTR_MEMORY_USAGE,
|
||||||
ATTR_NETWORK_RX,
|
ATTR_NETWORK_RX,
|
||||||
ATTR_NETWORK_TX,
|
ATTR_NETWORK_TX,
|
||||||
|
ATTR_UPDATE_AVAILABLE,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
ATTR_VERSION_LATEST,
|
ATTR_VERSION_LATEST,
|
||||||
CONTENT_TYPE_BINARY,
|
CONTENT_TYPE_BINARY,
|
||||||
@@ -38,6 +39,7 @@ class APIMulticast(CoreSysAttributes):
|
|||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_plugins.multicast.version,
|
ATTR_VERSION: self.sys_plugins.multicast.version,
|
||||||
ATTR_VERSION_LATEST: self.sys_plugins.multicast.latest_version,
|
ATTR_VERSION_LATEST: self.sys_plugins.multicast.latest_version,
|
||||||
|
ATTR_UPDATE_AVAILABLE: self.sys_plugins.multicast.need_update,
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
|
|||||||
282
supervisor/api/network.py
Normal file
282
supervisor/api/network.py
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
"""REST API for network."""
|
||||||
|
import asyncio
|
||||||
|
from ipaddress import ip_address, ip_interface
|
||||||
|
from typing import Any, Awaitable, Dict
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
import attr
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from ..const import (
|
||||||
|
ATTR_ACCESSPOINTS,
|
||||||
|
ATTR_ADDRESS,
|
||||||
|
ATTR_AUTH,
|
||||||
|
ATTR_CONNECTED,
|
||||||
|
ATTR_DNS,
|
||||||
|
ATTR_DOCKER,
|
||||||
|
ATTR_ENABLED,
|
||||||
|
ATTR_FREQUENCY,
|
||||||
|
ATTR_GATEWAY,
|
||||||
|
ATTR_HOST_INTERNET,
|
||||||
|
ATTR_ID,
|
||||||
|
ATTR_INTERFACE,
|
||||||
|
ATTR_INTERFACES,
|
||||||
|
ATTR_IPV4,
|
||||||
|
ATTR_IPV6,
|
||||||
|
ATTR_MAC,
|
||||||
|
ATTR_METHOD,
|
||||||
|
ATTR_MODE,
|
||||||
|
ATTR_NAMESERVERS,
|
||||||
|
ATTR_PARENT,
|
||||||
|
ATTR_PRIMARY,
|
||||||
|
ATTR_PSK,
|
||||||
|
ATTR_SIGNAL,
|
||||||
|
ATTR_SSID,
|
||||||
|
ATTR_SUPERVISOR_INTERNET,
|
||||||
|
ATTR_TYPE,
|
||||||
|
ATTR_VLAN,
|
||||||
|
ATTR_WIFI,
|
||||||
|
DOCKER_NETWORK,
|
||||||
|
DOCKER_NETWORK_MASK,
|
||||||
|
)
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import APIError, HostNetworkNotFound
|
||||||
|
from ..host.const import AuthMethod, InterfaceType, WifiMode
|
||||||
|
from ..host.network import (
|
||||||
|
AccessPoint,
|
||||||
|
Interface,
|
||||||
|
InterfaceMethod,
|
||||||
|
IpConfig,
|
||||||
|
VlanConfig,
|
||||||
|
WifiConfig,
|
||||||
|
)
|
||||||
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
|
_SCHEMA_IP_CONFIG = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_ADDRESS): [vol.Coerce(ip_interface)],
|
||||||
|
vol.Optional(ATTR_METHOD): vol.Coerce(InterfaceMethod),
|
||||||
|
vol.Optional(ATTR_GATEWAY): vol.Coerce(ip_address),
|
||||||
|
vol.Optional(ATTR_NAMESERVERS): [vol.Coerce(ip_address)],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
_SCHEMA_WIFI_CONFIG = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_MODE): vol.Coerce(WifiMode),
|
||||||
|
vol.Optional(ATTR_AUTH): vol.Coerce(AuthMethod),
|
||||||
|
vol.Optional(ATTR_SSID): str,
|
||||||
|
vol.Optional(ATTR_PSK): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=no-value-for-parameter
|
||||||
|
SCHEMA_UPDATE = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_IPV4): _SCHEMA_IP_CONFIG,
|
||||||
|
vol.Optional(ATTR_IPV6): _SCHEMA_IP_CONFIG,
|
||||||
|
vol.Optional(ATTR_WIFI): _SCHEMA_WIFI_CONFIG,
|
||||||
|
vol.Optional(ATTR_ENABLED): vol.Boolean(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ipconfig_struct(config: IpConfig) -> Dict[str, Any]:
|
||||||
|
"""Return a dict with information about ip configuration."""
|
||||||
|
return {
|
||||||
|
ATTR_METHOD: config.method,
|
||||||
|
ATTR_ADDRESS: [address.with_prefixlen for address in config.address],
|
||||||
|
ATTR_NAMESERVERS: [str(address) for address in config.nameservers],
|
||||||
|
ATTR_GATEWAY: str(config.gateway) if config.gateway else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def wifi_struct(config: WifiConfig) -> Dict[str, Any]:
|
||||||
|
"""Return a dict with information about wifi configuration."""
|
||||||
|
return {
|
||||||
|
ATTR_MODE: config.mode,
|
||||||
|
ATTR_AUTH: config.auth,
|
||||||
|
ATTR_SSID: config.ssid,
|
||||||
|
ATTR_SIGNAL: config.signal,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def vlan_struct(config: VlanConfig) -> Dict[str, Any]:
|
||||||
|
"""Return a dict with information about VLAN configuration."""
|
||||||
|
return {
|
||||||
|
ATTR_ID: config.id,
|
||||||
|
ATTR_PARENT: config.interface,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def interface_struct(interface: Interface) -> Dict[str, Any]:
|
||||||
|
"""Return a dict with information of a interface to be used in th API."""
|
||||||
|
return {
|
||||||
|
ATTR_INTERFACE: interface.name,
|
||||||
|
ATTR_TYPE: interface.type,
|
||||||
|
ATTR_ENABLED: interface.enabled,
|
||||||
|
ATTR_CONNECTED: interface.connected,
|
||||||
|
ATTR_PRIMARY: interface.primary,
|
||||||
|
ATTR_IPV4: ipconfig_struct(interface.ipv4) if interface.ipv4 else None,
|
||||||
|
ATTR_IPV6: ipconfig_struct(interface.ipv6) if interface.ipv6 else None,
|
||||||
|
ATTR_WIFI: wifi_struct(interface.wifi) if interface.wifi else None,
|
||||||
|
ATTR_VLAN: vlan_struct(interface.vlan) if interface.vlan else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def accesspoint_struct(accesspoint: AccessPoint) -> Dict[str, Any]:
|
||||||
|
"""Return a dict for AccessPoint."""
|
||||||
|
return {
|
||||||
|
ATTR_MODE: accesspoint.mode,
|
||||||
|
ATTR_SSID: accesspoint.ssid,
|
||||||
|
ATTR_FREQUENCY: accesspoint.frequency,
|
||||||
|
ATTR_SIGNAL: accesspoint.signal,
|
||||||
|
ATTR_MAC: accesspoint.mac,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class APINetwork(CoreSysAttributes):
|
||||||
|
"""Handle REST API for network."""
|
||||||
|
|
||||||
|
def _get_interface(self, name: str) -> Interface:
|
||||||
|
"""Get Interface by name or default."""
|
||||||
|
name = name.lower()
|
||||||
|
|
||||||
|
if name == "default":
|
||||||
|
for interface in self.sys_host.network.interfaces:
|
||||||
|
if not interface.primary:
|
||||||
|
continue
|
||||||
|
return interface
|
||||||
|
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return self.sys_host.network.get(name)
|
||||||
|
except HostNetworkNotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise APIError(f"Interface {name} does not exist") from None
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
|
"""Return network information."""
|
||||||
|
return {
|
||||||
|
ATTR_INTERFACES: [
|
||||||
|
interface_struct(interface)
|
||||||
|
for interface in self.sys_host.network.interfaces
|
||||||
|
],
|
||||||
|
ATTR_DOCKER: {
|
||||||
|
ATTR_INTERFACE: DOCKER_NETWORK,
|
||||||
|
ATTR_ADDRESS: str(DOCKER_NETWORK_MASK),
|
||||||
|
ATTR_GATEWAY: str(self.sys_docker.network.gateway),
|
||||||
|
ATTR_DNS: str(self.sys_docker.network.dns),
|
||||||
|
},
|
||||||
|
ATTR_HOST_INTERNET: self.sys_host.network.connectivity,
|
||||||
|
ATTR_SUPERVISOR_INTERNET: self.sys_supervisor.connectivity,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def interface_info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
|
"""Return network information for a interface."""
|
||||||
|
interface = self._get_interface(request.match_info.get(ATTR_INTERFACE))
|
||||||
|
|
||||||
|
return interface_struct(interface)
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def interface_update(self, request: web.Request) -> None:
|
||||||
|
"""Update the configuration of an interface."""
|
||||||
|
interface = self._get_interface(request.match_info.get(ATTR_INTERFACE))
|
||||||
|
|
||||||
|
# Validate data
|
||||||
|
body = await api_validate(SCHEMA_UPDATE, request)
|
||||||
|
if not body:
|
||||||
|
raise APIError("You need to supply at least one option to update")
|
||||||
|
|
||||||
|
# Apply config
|
||||||
|
for key, config in body.items():
|
||||||
|
if key == ATTR_IPV4:
|
||||||
|
interface.ipv4 = attr.evolve(
|
||||||
|
interface.ipv4 or IpConfig(InterfaceMethod.STATIC, [], None, []),
|
||||||
|
**config,
|
||||||
|
)
|
||||||
|
elif key == ATTR_IPV6:
|
||||||
|
interface.ipv6 = attr.evolve(
|
||||||
|
interface.ipv6 or IpConfig(InterfaceMethod.STATIC, [], None, []),
|
||||||
|
**config,
|
||||||
|
)
|
||||||
|
elif key == ATTR_WIFI:
|
||||||
|
interface.wifi = attr.evolve(
|
||||||
|
interface.wifi
|
||||||
|
or WifiConfig(
|
||||||
|
WifiMode.INFRASTRUCTURE, "", AuthMethod.OPEN, None, None
|
||||||
|
),
|
||||||
|
**config,
|
||||||
|
)
|
||||||
|
elif key == ATTR_ENABLED:
|
||||||
|
interface.enabled = config
|
||||||
|
|
||||||
|
await asyncio.shield(self.sys_host.network.apply_changes(interface))
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
def reload(self, request: web.Request) -> Awaitable[None]:
|
||||||
|
"""Reload network data."""
|
||||||
|
return asyncio.shield(self.sys_host.network.update())
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def scan_accesspoints(self, request: web.Request) -> Dict[str, Any]:
|
||||||
|
"""Scan and return a list of available networks."""
|
||||||
|
interface = self._get_interface(request.match_info.get(ATTR_INTERFACE))
|
||||||
|
|
||||||
|
# Only wlan is supported
|
||||||
|
if interface.type != InterfaceType.WIRELESS:
|
||||||
|
raise APIError(f"Interface {interface.name} is not a valid wireless card!")
|
||||||
|
|
||||||
|
ap_list = await self.sys_host.network.scan_wifi(interface)
|
||||||
|
|
||||||
|
return {ATTR_ACCESSPOINTS: [accesspoint_struct(ap) for ap in ap_list]}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def create_vlan(self, request: web.Request) -> None:
|
||||||
|
"""Create a new vlan."""
|
||||||
|
interface = self._get_interface(request.match_info.get(ATTR_INTERFACE))
|
||||||
|
vlan = int(request.match_info.get(ATTR_VLAN))
|
||||||
|
|
||||||
|
# Only ethernet is supported
|
||||||
|
if interface.type != InterfaceType.ETHERNET:
|
||||||
|
raise APIError(
|
||||||
|
f"Interface {interface.name} is not a valid ethernet card for vlan!"
|
||||||
|
)
|
||||||
|
body = await api_validate(SCHEMA_UPDATE, request)
|
||||||
|
|
||||||
|
vlan_config = VlanConfig(vlan, interface.name)
|
||||||
|
|
||||||
|
ipv4_config = None
|
||||||
|
if ATTR_IPV4 in body:
|
||||||
|
ipv4_config = IpConfig(
|
||||||
|
body[ATTR_IPV4].get(ATTR_METHOD, InterfaceMethod.AUTO),
|
||||||
|
body[ATTR_IPV4].get(ATTR_ADDRESS, []),
|
||||||
|
body[ATTR_IPV4].get(ATTR_GATEWAY, None),
|
||||||
|
body[ATTR_IPV4].get(ATTR_NAMESERVERS, []),
|
||||||
|
)
|
||||||
|
|
||||||
|
ipv6_config = None
|
||||||
|
if ATTR_IPV6 in body:
|
||||||
|
ipv6_config = IpConfig(
|
||||||
|
body[ATTR_IPV6].get(ATTR_METHOD, InterfaceMethod.AUTO),
|
||||||
|
body[ATTR_IPV6].get(ATTR_ADDRESS, []),
|
||||||
|
body[ATTR_IPV6].get(ATTR_GATEWAY, None),
|
||||||
|
body[ATTR_IPV6].get(ATTR_NAMESERVERS, []),
|
||||||
|
)
|
||||||
|
|
||||||
|
vlan_interface = Interface(
|
||||||
|
"",
|
||||||
|
True,
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
InterfaceType.VLAN,
|
||||||
|
ipv4_config,
|
||||||
|
ipv6_config,
|
||||||
|
None,
|
||||||
|
vlan_config,
|
||||||
|
)
|
||||||
|
await asyncio.shield(self.sys_host.network.apply_changes(vlan_interface))
|
||||||
67
supervisor/api/observer.py
Normal file
67
supervisor/api/observer.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
"""Init file for Supervisor Observer RESTful API."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from ..const import (
|
||||||
|
ATTR_BLK_READ,
|
||||||
|
ATTR_BLK_WRITE,
|
||||||
|
ATTR_CPU_PERCENT,
|
||||||
|
ATTR_HOST,
|
||||||
|
ATTR_MEMORY_LIMIT,
|
||||||
|
ATTR_MEMORY_PERCENT,
|
||||||
|
ATTR_MEMORY_USAGE,
|
||||||
|
ATTR_NETWORK_RX,
|
||||||
|
ATTR_NETWORK_TX,
|
||||||
|
ATTR_UPDATE_AVAILABLE,
|
||||||
|
ATTR_VERSION,
|
||||||
|
ATTR_VERSION_LATEST,
|
||||||
|
)
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..validate import version_tag
|
||||||
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag})
|
||||||
|
|
||||||
|
|
||||||
|
class APIObserver(CoreSysAttributes):
|
||||||
|
"""Handle RESTful API for Observer functions."""
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
|
"""Return HA Observer information."""
|
||||||
|
return {
|
||||||
|
ATTR_HOST: str(self.sys_docker.network.observer),
|
||||||
|
ATTR_VERSION: self.sys_plugins.observer.version,
|
||||||
|
ATTR_VERSION_LATEST: self.sys_plugins.observer.latest_version,
|
||||||
|
ATTR_UPDATE_AVAILABLE: self.sys_plugins.observer.need_update,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
||||||
|
"""Return resource information."""
|
||||||
|
stats = await self.sys_plugins.observer.stats()
|
||||||
|
|
||||||
|
return {
|
||||||
|
ATTR_CPU_PERCENT: stats.cpu_percent,
|
||||||
|
ATTR_MEMORY_USAGE: stats.memory_usage,
|
||||||
|
ATTR_MEMORY_LIMIT: stats.memory_limit,
|
||||||
|
ATTR_MEMORY_PERCENT: stats.memory_percent,
|
||||||
|
ATTR_NETWORK_RX: stats.network_rx,
|
||||||
|
ATTR_NETWORK_TX: stats.network_tx,
|
||||||
|
ATTR_BLK_READ: stats.blk_read,
|
||||||
|
ATTR_BLK_WRITE: stats.blk_write,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def update(self, request: web.Request) -> None:
|
||||||
|
"""Update HA observer."""
|
||||||
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
|
version = body.get(ATTR_VERSION, self.sys_plugins.observer.latest_version)
|
||||||
|
|
||||||
|
await asyncio.shield(self.sys_plugins.observer.update(version))
|
||||||
@@ -6,7 +6,13 @@ from typing import Any, Awaitable, Dict
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from ..const import ATTR_BOARD, ATTR_BOOT, ATTR_VERSION, ATTR_VERSION_LATEST
|
from ..const import (
|
||||||
|
ATTR_BOARD,
|
||||||
|
ATTR_BOOT,
|
||||||
|
ATTR_UPDATE_AVAILABLE,
|
||||||
|
ATTR_VERSION,
|
||||||
|
ATTR_VERSION_LATEST,
|
||||||
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..validate import version_tag
|
from ..validate import version_tag
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, api_validate
|
||||||
@@ -25,6 +31,7 @@ class APIOS(CoreSysAttributes):
|
|||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_hassos.version,
|
ATTR_VERSION: self.sys_hassos.version,
|
||||||
ATTR_VERSION_LATEST: self.sys_hassos.latest_version,
|
ATTR_VERSION_LATEST: self.sys_hassos.latest_version,
|
||||||
|
ATTR_UPDATE_AVAILABLE: self.sys_hassos.need_update,
|
||||||
ATTR_BOARD: self.sys_hassos.board,
|
ATTR_BOARD: self.sys_hassos.board,
|
||||||
ATTR_BOOT: self.sys_dbus.rauc.boot_slot,
|
ATTR_BOOT: self.sys_dbus.rauc.boot_slot,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
new Function("import('/api/hassio/app/frontend_latest/entrypoint.855567b9.js')")();
|
new Function("import('/api/hassio/app/frontend_latest/entrypoint.4050b348.js')")();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
var el = document.createElement('script');
|
var el = document.createElement('script');
|
||||||
el.src = '/api/hassio/app/frontend_es5/entrypoint.19035830.js';
|
el.src = '/api/hassio/app/frontend_es5/entrypoint.bcf8e8ff.js';
|
||||||
document.body.appendChild(el);
|
document.body.appendChild(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chunk.1274df0d4f9b61110840.js","sources":["webpack://home-assistant-frontend/chunk.1274df0d4f9b61110840.js"],"mappings":"AAAA","sourceRoot":""}
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chunk.144785285e77c1827c0e.js","sources":["webpack://home-assistant-frontend/chunk.144785285e77c1827c0e.js"],"mappings":"AAAA","sourceRoot":""}
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"chunk.1edd93e4d4c4749e55ab.js","sources":["webpack:///chunk.1edd93e4d4c4749e55ab.js"],"mappings":"AAAA","sourceRoot":""}
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chunk.23709353009814f6ec3d.js","sources":["webpack://home-assistant-frontend/chunk.23709353009814f6ec3d.js"],"mappings":"AAAA","sourceRoot":""}
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chunk.28ab065538efa89db557.js","sources":["webpack://home-assistant-frontend/chunk.28ab065538efa89db557.js"],"mappings":"AAAA","sourceRoot":""}
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chunk.2e5192dd0552c13e5623.js","sources":["webpack://home-assistant-frontend/chunk.2e5192dd0552c13e5623.js"],"mappings":"AAAA","sourceRoot":""}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,10 +1,3 @@
|
|||||||
/*!
|
|
||||||
* The buffer module from node.js, for the browser.
|
|
||||||
*
|
|
||||||
* @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@license
|
@license
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chunk.3a2a2f6b4ddd0c4883f8.js","sources":["webpack://home-assistant-frontend/chunk.3a2a2f6b4ddd0c4883f8.js"],"mappings":";AAAA","sourceRoot":""}
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chunk.3a76f0ea2b0888f970db.js","sources":["webpack://home-assistant-frontend/chunk.3a76f0ea2b0888f970db.js"],"mappings":"AAAA","sourceRoot":""}
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"chunk.3c156057c501c13568ee.js","sources":["webpack://home-assistant-frontend/chunk.3c156057c501c13568ee.js"],"mappings":"AAAA","sourceRoot":""}
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user