mirror of
				https://github.com/home-assistant/supervisor.git
				synced 2025-10-27 04:29:39 +00:00 
			
		
		
		
	Compare commits
	
		
			526 Commits
		
	
	
		
			2021.02.6
			...
			refresh-up
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | ec897081cd | ||
|   | 839361133a | ||
|   | b651d63758 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 400d3981a2 | ||
|   | 69c2517d52 | ||
|   | c8b49aba42 | ||
|   | 8071b107e7 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 603d19b075 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a5ce2ef7cb | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f392dc5492 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0c63883269 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 612d4f950b | ||
|   | 1799c765b4 | ||
|   | 809ac1ffca | ||
|   | fefc99e825 | ||
|   | d994170a9d | ||
|   | d8c934365a | ||
|   | e0fd31c390 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 22238c9c0e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 5ff96cfa5e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e22a19df1a | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f57bc0db25 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6ba6b5ea56 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 5dc9f9235e | ||
|   | 323fa2e637 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0986419b2f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f0bc952269 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9266997482 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 75d252e21a | ||
|   | 368e94f95f | ||
|   | 3fbecf89db | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 54e6ae5fd9 | ||
|   | 5b96074055 | ||
|   | 5503f93a75 | ||
|   | eadc629cd9 | ||
|   | cde45e2e7a | ||
|   | 050851a9ac | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 86bd16b2ba | ||
|   | ce9181b05f | ||
|   | f7ba364076 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 3511c19726 | ||
|   | d9ed58696b | ||
|   | 373f452774 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e54efa681f | ||
|   | 79cd8ac390 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | dc24f332f8 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 99cdf7b028 | ||
|   | 54edfa53bc | ||
|   | 571c9a05c6 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 864b7bf023 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e303431d74 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 19dd40275c | ||
|   | 4cf970e37a | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7947c27089 | ||
|   | d0e2c8b694 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 19e3a859b0 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e6557ded34 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f4aae4522d | ||
|   | 2066aefd6d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 2f56cab953 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 883399f583 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 47f53501e5 | ||
|   | b23a89e6fb | ||
|   | 7764decc37 | ||
|   | 88490140af | ||
|   | 61d56dce9c | ||
|   | 838af87ad7 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 8f263ab345 | ||
|   | 6b76086652 | ||
|   | efa5205800 | ||
|   | a0c8b77737 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9ee0efe6c0 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a2af63d050 | ||
|   | da246dc40a | ||
|   | 3c52f87cdc | ||
|   | d80d76a24d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 8653f7a0e1 | ||
|   | 8458d9e0f6 | ||
|   | 5d4ce94155 | ||
|   | 828cf773cc | ||
|   | a902b55df7 | ||
|   | f38cde4c68 | ||
|   | 4c9cbb112e | ||
|   | 3d814f3c44 | ||
|   | f269f72082 | ||
|   | f07193dc3c | ||
|   | d2b706df05 | ||
|   | e5817e9445 | ||
|   | 85313f26ea | ||
|   | f864613ffb | ||
|   | 36ea8b2bb4 | ||
|   | df9d62f874 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4a6aaa8559 | ||
|   | 435f479984 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e2f39059c6 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 531073d5ec | ||
|   | ef5b6a5f4c | ||
|   | 03f0a136ab | ||
|   | 7a6663ba80 | ||
|   | 9dd5eee1ae | ||
|   | bb474a5c14 | ||
|   | 6ab4dda5e8 | ||
|   | 8a553dbb59 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 1ee6c0491c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | cc50a91a42 | ||
|   | 637377f81d | ||
|   | a90f70e017 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 949ecb255d | ||
|   | 15f62837c8 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e5246a5b1d | ||
|   | 394d66290d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 79d541185f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | b433d129ef | ||
|   | 4b0278fee8 | ||
|   | 8c59e6d05a | ||
|   | 5c66278a1c | ||
|   | 7abe9487a0 | ||
|   | 73832dd6d6 | ||
|   | 6cc3df54e9 | ||
|   | c07c7c5146 | ||
|   | a6d1078fe3 | ||
|   | eba6da485d | ||
|   | de880e24ed | ||
|   | f344df9e5c | ||
|   | 5af62a8834 | ||
|   | 800fb683f8 | ||
|   | ad2566d58a | ||
|   | 6c679b07e1 | ||
|   | aa4f4c8d47 | ||
|   | b83da5d89f | ||
|   | 0afff9a9e2 | ||
|   | 0433d72ae6 | ||
|   | d33beb06cd | ||
|   | 279d6ccd79 | ||
|   | af628293f3 | ||
|   | df6b815175 | ||
|   | d6127832a7 | ||
|   | 8240623806 | ||
|   | 2b4527fa64 | ||
|   | 23143aede4 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 8b93f0aee7 | ||
|   | 5cc4a9a929 | ||
|   | 288d2e5bdb | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 73d84113ea | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4b15945ca1 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 10720b2988 | ||
|   | bb991b69bb | ||
|   | 7c9f6067c0 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e960a70217 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9b0a2e6da9 | ||
|   | cd0c151bd9 | ||
|   | b03c8c24dd | ||
|   | 4416b6524e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c9d3f65cc8 | ||
|   | 0407122fbe | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 5e871d9399 | ||
|   | 6df7a88666 | ||
|   | 5933b66b1c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a85e816cd7 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 96f6c07912 | ||
|   | 40bcee38f3 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6d2a38c96e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | dafc2cfec2 | ||
|   | 04f36e92e1 | ||
|   | 4f97013df4 | ||
|   | 53eae96a98 | ||
|   | 74530baeb7 | ||
|   | 271e4f0cc4 | ||
|   | f4c7f2cae1 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 24cdb4787a | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 57b1c21af4 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f0eddb6926 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7c74c1bd8c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 2d4a85ae43 | ||
|   | d48c439737 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 874c50d3e8 | ||
|   | 4beaf571c2 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 58a948447e | ||
|   | 32af7ef28b | ||
|   | 208fb549b7 | ||
|   | ab704c11cf | ||
|   | 966b962ccf | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4ea3695982 | ||
|   | b2abe37d72 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9bf8d15b01 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 70acbffc23 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c9b1eb751e | ||
|   | ad8d850ed7 | ||
|   | 1b0eb9397d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 8572f8c4e5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 66565dde87 | ||
|   | d54c23952f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 62b364ea29 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e6f00144f2 | ||
|   | 8894984c12 | ||
|   | 49fbdedf6b | ||
|   | 0899c16895 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0747a7e4b2 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0123d7935d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7a1009446b | ||
|   | 034606cd0f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ddc30cfd7d | ||
|   | f10fccaff8 | ||
|   | 31001280c8 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9638775944 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | fbec0befde | ||
|   | 81e7fac848 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 97599b3e70 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c94b23a3fd | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9758980ae0 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6ab3fbaab3 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4933ff83df | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 71e12ecb2b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 36687530e0 | ||
|   | e7b5864c03 | ||
|   | 9497f85db9 | ||
|   | 419f603571 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f4f1fc524d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6d9f44a900 | ||
|   | aeb9b26d44 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 631f78f468 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 13cedb308e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 82c183e1a8 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 25cf1e7394 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 91509a4205 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | d93ebd15a2 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 85e7f817e6 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 772cadb435 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 853aeef583 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | cd07bde307 | ||
|   | 3057df3181 | ||
|   | fe785622ec | ||
|   | 2b6829a786 | ||
|   | 7c6c982414 | ||
|   | 07eeb2eaf2 | ||
|   | 223f5b7bb1 | ||
|   | 8a9657c452 | ||
|   | 564e9811d0 | ||
|   | b944b52b21 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 24aecdddf3 | ||
|   | 7bbfb60039 | ||
|   | ece40008c7 | ||
|   | 0177b38ded | ||
|   | 16f2f63081 | ||
|   | 5f376c2a27 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 90a6f109ee | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e6fd0ef5dc | ||
|   | d46ab56901 | ||
|   | 3b1ad5c0cd | ||
|   | de8a241e72 | ||
|   | a4a0b43d91 | ||
|   | adf355e54f | ||
|   | 4f9e646b4c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ce57d384ca | ||
|   | d53d526673 | ||
|   | cd8fc16bcb | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6b58970354 | ||
|   | b70ed9a60d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 22c8ff1314 | ||
|   | ba2cf8078e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ef138b619b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9252af5ddb | ||
|   | bcef34012d | ||
|   | 2f18c177ae | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 12487fb69d | ||
|   | e629bab8ee | ||
|   | e85d7c3d2e | ||
|   | 64c59d0fe9 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 522cbe3295 | ||
|   | fd185fc326 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6ddc135266 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f8d5279d9c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 2acae9af57 | ||
|   | b425d21d05 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4c7ba20a58 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a4f325dd2e | ||
|   | a99bfa2926 | ||
|   | bb127a614b | ||
|   | 6f2f005897 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e22a20c165 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 20c2121e5f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 8a5831d6b2 | ||
|   | fb81946240 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4bec86c58c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7034b79991 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7b9a09dc4b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0746c4dec5 | ||
|   | 6dadb933bd | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 07197e6a50 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6c79fb8325 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7488750ee4 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c9574254aa | ||
|   | f466721ffa | ||
|   | 3834cead07 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 75975de201 | ||
|   | cb9f998ef1 | ||
|   | eb9ce8ea1f | ||
|   | a5ed68b641 | ||
|   | 1ef46424ea | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 53c99547d0 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a34e7622d2 | ||
|   | b234c18664 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | d8d594c728 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 1cd35841e8 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | d05b7edd87 | ||
|   | 95ef7d4508 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9812e5be6a | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 183182943d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a0189d65de | ||
|   | b59f741162 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | efc2e826a1 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a3ad23e262 | ||
|   | 5e3bcbfaac | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7f3e4558b9 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 567a01c2ed | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 2236cf146e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 8e2f33ba1e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 8190883a71 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c01218a97a | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 2437817a41 | ||
|   | 682ee4529e | ||
|   | cee520f0b5 | ||
|   | 0d915a3efc | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f3a562006a | ||
|   | d78091cc60 | ||
|   | f785c4e909 | ||
|   | cda66ba737 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ea68ffc5a4 | ||
|   | 31b0b721c8 | ||
|   | b97e33f5d5 | ||
|   | 29e55d3664 | ||
|   | 9112f27dc0 | ||
|   | 9e67df26b3 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 37d1a577ef | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 1eebb31004 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 885764ea1c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | b3d184b5c7 | ||
|   | 96d04ec17e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e0bb3ad609 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 1a8842cb81 | ||
|   | 092d526749 | ||
|   | 9db95c188a | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0e45fc7d66 | ||
|   | 4d1ddbfa2b | ||
|   | caa1c6f1bd | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 10d686b415 | ||
|   | 29fae90da5 | ||
|   | e27337da85 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 8f22316869 | ||
|   | dd10d3e037 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4a53c62af8 | ||
|   | 1ebbf2b693 | ||
|   | 62d198111c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 1fc0ab71aa | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f4402a1633 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 13a17bcb34 | ||
|   | e1b49d90c2 | ||
|   | 85ab25ea16 | ||
|   | 80131ddfa8 | ||
|   | e9c123459f | ||
|   | d3e4bb7219 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | fd98d38125 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 3237611034 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ce2bffda15 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 977e7b7adc | ||
|   | 5082078527 | ||
|   | 3615091c93 | ||
|   | fb1eb44d82 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 13910d44bf | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | cda1d15070 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | d0a1de23a6 | ||
|   | 44fd75220f | ||
|   | ed594d653f | ||
|   | 40bb3a7581 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | df7f0345e8 | ||
|   | f7ab76bb9a | ||
|   | 45e24bfa65 | ||
|   | 8cd149783c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 8e8e6e48a9 | ||
|   | 816e0d503a | ||
|   | c43acd50f4 | ||
|   | 16ce4296a2 | ||
|   | 65386b753f | ||
|   | 2be1529cb8 | ||
|   | 98f8e032e3 | ||
|   | 900b785789 | ||
|   | 9194088947 | ||
|   | 58c40cbef6 | ||
|   | e6c57dfc80 | ||
|   | 82f76f60bd | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | b9af4aec6b | ||
|   | f71ce27248 | ||
|   | 5b2b1765bc | ||
|   | 2a892544c2 | ||
|   | bedb37ca6b | ||
|   | a456cd645f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9c68094cf6 | ||
|   | 379cef9e35 | ||
|   | cb3e2dab71 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 3e89f83e0b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | af0bdd890a | ||
|   | f93f5d0e71 | ||
|   | 667672a20b | ||
|   | 9e1f899274 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 75e0741665 | ||
|   | 392d0e929b | ||
|   | b342073ba9 | ||
|   | ff4e550ba3 | ||
|   | 17aa544be5 | ||
|   | 390676dbc4 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | d423252bc7 | ||
|   | 790e887b70 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 47e377683e | ||
|   | b1232c0d8d | ||
|   | 059233c111 | ||
|   | 55382d000b | ||
|   | 75ab6eec43 | ||
|   | e30171746b | ||
|   | 73849b7468 | ||
|   | a52713611c | ||
|   | 85a66c663c | ||
|   | e478e68b70 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 16095c319a | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f4a6100fba | ||
|   | 82060dd242 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a58cfb797c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c8256a50f4 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 3ae974e9e2 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ac5e74a375 | ||
|   | 05e3d3b779 | ||
|   | 681a1ecff5 | ||
|   | 2b411b0bf9 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | fee16847d3 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 501a52a3c6 | ||
|   | 2bb014fda5 | ||
|   | 09203f67b2 | ||
|   | 169c7ec004 | ||
|   | 202e94615e | ||
|   | 5fe2a815ad | ||
|   | a13a0b4770 | ||
|   | 455bbc457b | ||
|   | d50fd3b580 | ||
|   | 455e80b07c | ||
|   | 291becbdf9 | ||
|   | 33385b46a7 | ||
|   | df17668369 | ||
|   | 43449c85bb | ||
|   | 9e86eda05a | ||
|   | b288554d9c | ||
|   | bee55d08fb | ||
|   | 7a542aeb38 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 8d42513ba8 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 89b7247aa2 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 29132e7f4c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 3fd9baf78e | ||
|   | f3aa3757ce | ||
|   | 3760967f59 | ||
|   | f7ab8e0f7f | ||
|   | 0e46ea12b2 | ||
|   | be226b2b01 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9e1239e192 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 2eba3d85b0 | ||
|   | 9b569268ab | ||
|   | 31f5033dca | ||
|   | 78d9c60be5 | ||
|   | baa86f09e5 | ||
|   | a4c4b39ba8 | ||
|   | 752068bb56 | ||
|   | 739cfbb273 | ||
|   | 115af4cadf | ||
|   | ae3274e559 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c61f096dbd | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ee7b5c42fd | ||
|   | 85d527bfbc | ||
|   | dd561da819 | ||
|   | cb5932cb8b | ||
|   | 8630adc54a | ||
|   | 90d8832cd2 | ||
|   | 3802b97bb6 | ||
|   | 2de175e181 | ||
|   | 6b7d437b00 | ||
|   | e2faf906de | ||
|   | bb44ce5cd2 | ||
|   | 15544ae589 | ||
|   | e421284471 | ||
|   | 785dc64787 | ||
|   | 7e7e3a7876 | ||
|   | 2b45c059e0 | ||
|   | 14ec61f9bd | ||
|   | 5cc72756f8 | ||
|   | 44785ef3e2 | ||
|   | e60d858feb | ||
|   | b31ecfefcd | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c342231052 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 673666837e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c8f74d6c0d | ||
|   | 7ed9de8014 | ||
|   | 8650947f04 | ||
|   | a0ac8ced31 | ||
|   | 2145bbea81 | ||
|   | 480000ee7f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9ec2ad022e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 43e40816dc | ||
|   | 941ea3ee68 | ||
|   | a6e4b5159e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6f542d58d5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | b2b5fcee7d | ||
|   | 59a82345a9 | ||
|   | b61a747876 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 72e5d800d5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c7aa6d4804 | ||
|   | b31063449d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 477672459d | ||
|   | 9c33897296 | ||
|   | 100cfb57c5 | ||
|   | 40b34071e7 | ||
|   | 341833fd8f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f647fd6fea | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 53642f2389 | ||
|   | b9bdd655ab | ||
|   | e9e1b5b54f | ||
|   | be2163d635 | ||
|   | 7f6dde3a5f | ||
|   | 334aafee23 | ||
|   | 1a20c18b19 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6e655b165c | 
| @@ -1,54 +0,0 @@ | |||||||
| FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.8 |  | ||||||
|  |  | ||||||
| WORKDIR /workspaces |  | ||||||
|  |  | ||||||
| # Set Docker daemon config |  | ||||||
| RUN \ |  | ||||||
|     mkdir -p /etc/docker \ |  | ||||||
|     && echo '{"storage-driver": "vfs"}' > /etc/docker/daemon.json |  | ||||||
|  |  | ||||||
| # Install Node/Yarn for Frontent |  | ||||||
| RUN apt-get update && apt-get install -y --no-install-recommends \ |  | ||||||
|         curl \ |  | ||||||
|         git \ |  | ||||||
|         apt-utils \ |  | ||||||
|         apt-transport-https \ |  | ||||||
|     && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ |  | ||||||
|     && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ |  | ||||||
|     && apt-get update && apt-get install -y --no-install-recommends \ |  | ||||||
|         nodejs \ |  | ||||||
|         yarn \ |  | ||||||
|     && curl -o - https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash \ |  | ||||||
|     && rm -rf /var/lib/apt/lists/* |  | ||||||
| ENV NVM_DIR /root/.nvm |  | ||||||
|  |  | ||||||
| # Install docker |  | ||||||
| # https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/ |  | ||||||
| RUN apt-get update && apt-get install -y --no-install-recommends \ |  | ||||||
|         apt-transport-https \ |  | ||||||
|         ca-certificates \ |  | ||||||
|         curl \ |  | ||||||
|         software-properties-common \ |  | ||||||
|         gpg-agent \ |  | ||||||
|     && curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add - \ |  | ||||||
|     && add-apt-repository "deb https://download.docker.com/linux/debian $(lsb_release -cs) stable" \ |  | ||||||
|     && apt-get update && apt-get install -y --no-install-recommends \ |  | ||||||
|         docker-ce \ |  | ||||||
|         docker-ce-cli \ |  | ||||||
|         containerd.io \ |  | ||||||
|     && rm -rf /var/lib/apt/lists/* |  | ||||||
|  |  | ||||||
| # Install tools |  | ||||||
| RUN apt-get update && apt-get install -y --no-install-recommends \ |  | ||||||
|         jq \ |  | ||||||
|         dbus \ |  | ||||||
|         network-manager \ |  | ||||||
|         libpulse0 \ |  | ||||||
|     && rm -rf /var/lib/apt/lists/* |  | ||||||
|  |  | ||||||
| # Install Python dependencies from requirements.txt if it exists |  | ||||||
| COPY requirements.txt requirements_tests.txt ./ |  | ||||||
| RUN pip3 install -U setuptools pip \ |  | ||||||
|     && pip3 install -r requirements.txt -r requirements_tests.txt \ |  | ||||||
|     && pip3 install tox \ |  | ||||||
|     && rm -f requirements.txt requirements_tests.txt |  | ||||||
| @@ -1,11 +1,9 @@ | |||||||
| { | { | ||||||
|   "name": "Supervisor dev", |   "name": "Supervisor dev", | ||||||
|   "context": "..", |   "image": "ghcr.io/home-assistant/devcontainer:supervisor", | ||||||
|   "dockerFile": "Dockerfile", |   "appPort": ["9123:8123", "7357:4357"], | ||||||
|   "appPort": "9123:8123", |   "postCreateCommand": "bash devcontainer_bootstrap", | ||||||
|   "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", | ||||||
| @@ -13,7 +11,12 @@ | |||||||
|     "esbenp.prettier-vscode" |     "esbenp.prettier-vscode" | ||||||
|   ], |   ], | ||||||
|   "settings": { |   "settings": { | ||||||
|     "terminal.integrated.shell.linux": "/bin/bash", |     "terminal.integrated.profiles.linux": { | ||||||
|  |       "zsh": { | ||||||
|  |         "path": "/usr/bin/zsh" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "terminal.integrated.defaultProfile.linux": "zsh", | ||||||
|     "editor.formatOnPaste": false, |     "editor.formatOnPaste": false, | ||||||
|     "editor.formatOnSave": true, |     "editor.formatOnSave": true, | ||||||
|     "editor.formatOnType": true, |     "editor.formatOnType": true, | ||||||
| @@ -22,7 +25,7 @@ | |||||||
|     "python.linting.pylintEnabled": true, |     "python.linting.pylintEnabled": true, | ||||||
|     "python.linting.enabled": true, |     "python.linting.enabled": true, | ||||||
|     "python.formatting.provider": "black", |     "python.formatting.provider": "black", | ||||||
|     "python.formatting.blackArgs": ["--target-version", "py38"], |     "python.formatting.blackArgs": ["--target-version", "py39"], | ||||||
|     "python.formatting.blackPath": "/usr/local/bin/black", |     "python.formatting.blackPath": "/usr/local/bin/black", | ||||||
|     "python.linting.banditPath": "/usr/local/bin/bandit", |     "python.linting.banditPath": "/usr/local/bin/bandit", | ||||||
|     "python.linting.flake8Path": "/usr/local/bin/flake8", |     "python.linting.flake8Path": "/usr/local/bin/flake8", | ||||||
|   | |||||||
| @@ -1,64 +1,78 @@ | |||||||
| name: Bug Report Form | name: Bug Report Form | ||||||
| about: Report an issue related to the Home Assistant Supervisor. | description: Report an issue related to the Home Assistant Supervisor. | ||||||
| labels: bug | labels: bug | ||||||
| title: "" | body: | ||||||
| issue_body: true |   - type: markdown | ||||||
| inputs: |  | ||||||
|   - type: description |  | ||||||
|     attributes: |     attributes: | ||||||
|       value: | |       value: | | ||||||
|         This issue form is for reporting bugs with **supported** setups only! |         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]. |         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 |         [fr]: https://community.home-assistant.io/c/feature-requests | ||||||
|   - type: input |   - type: textarea | ||||||
|     attributes: |     validations: | ||||||
|       label: What is the version of the Supervisor used? |  | ||||||
|       required: true |       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- |       placeholder: supervisor- | ||||||
|       description: > |       description: > | ||||||
|         Can be found in the Supervisor panel -> System tab. Starts with |         Can be found in the Supervisor panel -> System tab. Starts with | ||||||
|         `supervisor-....`. |         `supervisor-....`. | ||||||
|   - type: dropdown |   - type: dropdown | ||||||
|  |     validations: | ||||||
|  |       required: true | ||||||
|     attributes: |     attributes: | ||||||
|       label: What type of installation are you running? |       label: What type of installation are you running? | ||||||
|       required: true |  | ||||||
|       description: > |       description: > | ||||||
|         If you don't know, you can find it in: Configuration panel -> Info. |         If you don't know, you can find it in: Configuration panel -> Info. | ||||||
|       choices: |       options: | ||||||
|         - Home Assistant OS |         - Home Assistant OS | ||||||
|         - Home Assistant Supervised |         - Home Assistant Supervised | ||||||
|   - type: dropdown |   - type: dropdown | ||||||
|  |     validations: | ||||||
|  |       required: true | ||||||
|     attributes: |     attributes: | ||||||
|       label: Which operating system are you running on? |       label: Which operating system are you running on? | ||||||
|       required: true |       options: | ||||||
|       choices: |  | ||||||
|         - Home Assistant Operating System |         - Home Assistant Operating System | ||||||
|         - Debian |         - Debian | ||||||
|         - Other (e.g., Raspbian/Raspberry Pi OS/Fedora) |         - Other (e.g., Raspbian/Raspberry Pi OS/Fedora) | ||||||
|   - type: input |   - type: input | ||||||
|  |     validations: | ||||||
|  |       required: true | ||||||
|     attributes: |     attributes: | ||||||
|       label: What is the version of your installed operating system? |       label: What is the version of your installed operating system? | ||||||
|       required: true |       placeholder: "5.11" | ||||||
|       placeholder: 5.10 |  | ||||||
|       description: Can be found in the Supervisor panel -> System tab. |       description: Can be found in the Supervisor panel -> System tab. | ||||||
|   - type: input |   - type: input | ||||||
|  |     validations: | ||||||
|  |       required: true | ||||||
|     attributes: |     attributes: | ||||||
|       label: What version of Home Assistant Core is installed? |       label: What version of Home Assistant Core is installed? | ||||||
|       required: true |  | ||||||
|       placeholder: core- |       placeholder: core- | ||||||
|       description: > |       description: > | ||||||
|         Can be found in the Supervisor panel -> System tab. Starts with |         Can be found in the Supervisor panel -> System tab. Starts with | ||||||
|         `core-....`. |         `core-....`. | ||||||
|   - type: textarea |   - type: markdown | ||||||
|     attributes: |     attributes: | ||||||
|       label: Describe the issue you are experiencing |       value: | | ||||||
|       required: true |         # Details | ||||||
|       description: Provide a clear and concise description of what the bug is. |  | ||||||
|   - type: textarea |   - type: textarea | ||||||
|  |     validations: | ||||||
|  |       required: true | ||||||
|     attributes: |     attributes: | ||||||
|       label: Steps to reproduce the issue |       label: Steps to reproduce the issue | ||||||
|       required: true |  | ||||||
|       description: | |       description: | | ||||||
|         Please tell us exactly how to reproduce your issue. |         Please tell us exactly how to reproduce your issue. | ||||||
|         Provide clear and concise step by step instructions and add code snippets if needed. |         Provide clear and concise step by step instructions and add code snippets if needed. | ||||||
| @@ -68,13 +82,17 @@ inputs: | |||||||
|         3. |         3. | ||||||
|         ... |         ... | ||||||
|   - type: textarea |   - type: textarea | ||||||
|  |     validations: | ||||||
|  |       required: true | ||||||
|     attributes: |     attributes: | ||||||
|       label: Anything in the Supervisor logs that might be useful for us? |       label: Anything in the Supervisor logs that might be useful for us? | ||||||
|       required: false |  | ||||||
|       description: > |       description: > | ||||||
|         The Supervisor logs can be found in the Supervisor panel -> System tab. |         The Supervisor logs can be found in the Supervisor panel -> System tab. | ||||||
|   - type: description |       render: txt | ||||||
|  |   - type: textarea | ||||||
|     attributes: |     attributes: | ||||||
|       value: | |       label: Additional information | ||||||
|  |       description: > | ||||||
|         If you have any additional information for us, use the field below. |         If you have any additional information for us, use the field below. | ||||||
|         Please note, you can attach screenshots or screen recordings here. |         Please note, you can attach screenshots or screen recordings here, by | ||||||
|  |         dragging and dropping files in the field below. | ||||||
							
								
								
									
										1
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -37,6 +37,7 @@ | |||||||
| - This PR fixes or closes issue: fixes # | - This PR fixes or closes issue: fixes # | ||||||
| - This PR is related to issue: | - This PR is related to issue: | ||||||
| - Link to documentation pull request: | - Link to documentation pull request: | ||||||
|  | - Link to cli pull request: | ||||||
|  |  | ||||||
| ## Checklist | ## Checklist | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										119
									
								
								.github/workflows/builder.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										119
									
								
								.github/workflows/builder.yml
									
									
									
									
										vendored
									
									
								
							| @@ -27,7 +27,7 @@ on: | |||||||
|     paths: |     paths: | ||||||
|       - "rootfs/**" |       - "rootfs/**" | ||||||
|       - "supervisor/**" |       - "supervisor/**" | ||||||
|       - build.json |       - build.yaml | ||||||
|       - Dockerfile |       - Dockerfile | ||||||
|       - requirements.txt |       - requirements.txt | ||||||
|       - setup.py |       - setup.py | ||||||
| @@ -35,7 +35,7 @@ on: | |||||||
| env: | env: | ||||||
|   BUILD_NAME: supervisor |   BUILD_NAME: supervisor | ||||||
|   BUILD_TYPE: supervisor |   BUILD_TYPE: supervisor | ||||||
|   WHEELS_TAG: 3.8-alpine3.12 |   WHEELS_TAG: 3.9-alpine3.14 | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   init: |   init: | ||||||
| @@ -49,7 +49,7 @@ jobs: | |||||||
|       requirements: ${{ steps.requirements.outputs.changed }} |       requirements: ${{ steps.requirements.outputs.changed }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|         with: |         with: | ||||||
|           fetch-depth: 0 |           fetch-depth: 0 | ||||||
|  |  | ||||||
| @@ -71,7 +71,7 @@ jobs: | |||||||
|       - name: Check if requirements files changed |       - name: Check if requirements files changed | ||||||
|         id: requirements |         id: requirements | ||||||
|         run: | |         run: | | ||||||
|           if [[ "${{ steps.changed_files.outputs.all }}" =~ requirements.txt ]]; then |           if [[ "${{ steps.changed_files.outputs.all }}" =~ (requirements.txt|build.json) ]]; then | ||||||
|             echo "::set-output name=changed::true" |             echo "::set-output name=changed::true" | ||||||
|           fi |           fi | ||||||
|  |  | ||||||
| @@ -84,7 +84,7 @@ jobs: | |||||||
|         arch: ${{ fromJson(needs.init.outputs.architectures) }} |         arch: ${{ fromJson(needs.init.outputs.architectures) }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|         with: |         with: | ||||||
|           fetch-depth: 0 |           fetch-depth: 0 | ||||||
|  |  | ||||||
| @@ -94,10 +94,10 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           tag: ${{ env.WHEELS_TAG }} |           tag: ${{ env.WHEELS_TAG }} | ||||||
|           arch: ${{ matrix.arch }} |           arch: ${{ matrix.arch }} | ||||||
|           wheels-host: ${{ secrets.WHEELS_HOST }} |           wheels-host: wheels.hass.io | ||||||
|           wheels-key: ${{ secrets.WHEELS_KEY }} |           wheels-key: ${{ secrets.WHEELS_KEY }} | ||||||
|           wheels-user: wheels |           wheels-user: wheels | ||||||
|           apk: "build-base;libffi-dev;openssl-dev" |           apk: "build-base;libffi-dev;openssl-dev;cargo" | ||||||
|           skip-binary: aiohttp |           skip-binary: aiohttp | ||||||
|           requirements: "requirements.txt" |           requirements: "requirements.txt" | ||||||
|  |  | ||||||
| @@ -109,23 +109,59 @@ jobs: | |||||||
|  |  | ||||||
|       - name: Login to DockerHub |       - name: Login to DockerHub | ||||||
|         if: needs.init.outputs.publish == 'true' |         if: needs.init.outputs.publish == 'true' | ||||||
|         uses: docker/login-action@v1 |         uses: docker/login-action@v1.12.0 | ||||||
|         with: |         with: | ||||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} |           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} |           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||||
|  |  | ||||||
|  |       - name: Login to GitHub Container Registry | ||||||
|  |         if: needs.init.outputs.publish == 'true' | ||||||
|  |         uses: docker/login-action@v1.12.0 | ||||||
|  |         with: | ||||||
|  |           registry: ghcr.io | ||||||
|  |           username: ${{ github.repository_owner }} | ||||||
|  |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |  | ||||||
|       - name: Set build arguments |       - name: Set build arguments | ||||||
|         if: needs.init.outputs.publish == 'false' |         if: needs.init.outputs.publish == 'false' | ||||||
|         run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV |         run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV | ||||||
|  |  | ||||||
|       - name: Build supervisor |       - name: Build supervisor | ||||||
|         uses: home-assistant/builder@2021.01.1 |         uses: home-assistant/builder@2021.12.0 | ||||||
|         with: |         with: | ||||||
|           args: | |           args: | | ||||||
|             $BUILD_ARGS \ |             $BUILD_ARGS \ | ||||||
|             --${{ matrix.arch }} \ |             --${{ matrix.arch }} \ | ||||||
|             --target /data \ |             --target /data \ | ||||||
|             --generic ${{ needs.init.outputs.version }} |             --generic ${{ needs.init.outputs.version }} | ||||||
|  |         env: | ||||||
|  |           CAS_API_KEY: ${{ secrets.CAS_TOKEN }} | ||||||
|  |  | ||||||
|  |   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.4.0 | ||||||
|  |         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: |   version: | ||||||
|     name: Update version |     name: Update version | ||||||
| @@ -134,7 +170,7 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         if: needs.init.outputs.publish == 'true' |         if: needs.init.outputs.publish == 'true' | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|  |  | ||||||
|       - name: Initialize git |       - name: Initialize git | ||||||
|         if: needs.init.outputs.publish == 'true' |         if: needs.init.outputs.publish == 'true' | ||||||
| @@ -155,13 +191,15 @@ jobs: | |||||||
|   run_supervisor: |   run_supervisor: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     name: Run the Supervisor |     name: Run the Supervisor | ||||||
|     needs: ["build"] |     needs: ["build", "codenotary", "init"] | ||||||
|  |     timeout-minutes: 60 | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|  |  | ||||||
|       - name: Build the Supervisor |       - name: Build the Supervisor | ||||||
|         uses: home-assistant/builder@2021.01.1 |         if: needs.init.outputs.publish != 'true' | ||||||
|  |         uses: home-assistant/builder@2021.12.0 | ||||||
|         with: |         with: | ||||||
|           args: | |           args: | | ||||||
|             --test \ |             --test \ | ||||||
| @@ -169,13 +207,19 @@ jobs: | |||||||
|             --target /data \ |             --target /data \ | ||||||
|             --generic runner |             --generic runner | ||||||
|  |  | ||||||
|  |       - name: Pull Supervisor | ||||||
|  |         if: needs.init.outputs.publish == 'true' | ||||||
|  |         run: | | ||||||
|  |           docker pull ghcr.io/home-assistant/amd64-hassio-supervisor:${{ needs.init.outputs.version }} | ||||||
|  |           docker tag ghcr.io/home-assistant/amd64-hassio-supervisor:${{ needs.init.outputs.version }} homeassistant/amd64-hassio-supervisor:runner | ||||||
|  |  | ||||||
|       - name: Create the Supervisor |       - name: Create the Supervisor | ||||||
|         run: | |         run: | | ||||||
|           mkdir -p /tmp/supervisor/data |           mkdir -p /tmp/supervisor/data | ||||||
|           docker create --name hassio_supervisor \ |           docker create --name hassio_supervisor \ | ||||||
|             --privileged \ |             --privileged \ | ||||||
|             --security-opt seccomp=unconfined \ |             --security-opt seccomp=unconfined \ | ||||||
|             --security-opt apparmor:unconfined \ |             --security-opt apparmor=unconfined \ | ||||||
|             -v /run/docker.sock:/run/docker.sock \ |             -v /run/docker.sock:/run/docker.sock \ | ||||||
|             -v /run/dbus:/run/dbus \ |             -v /run/dbus:/run/dbus \ | ||||||
|             -v /tmp/supervisor/data:/data \ |             -v /tmp/supervisor/data:/data \ | ||||||
| @@ -194,22 +238,59 @@ jobs: | |||||||
|           SUPERVISOR=$(docker inspect --format='{{.NetworkSettings.IPAddress}}' hassio_supervisor) |           SUPERVISOR=$(docker inspect --format='{{.NetworkSettings.IPAddress}}' hassio_supervisor) | ||||||
|           ping="error" |           ping="error" | ||||||
|           while [ "$ping" != "ok" ]; do |           while [ "$ping" != "ok" ]; do | ||||||
|             ping=$(curl -sSL "http://$SUPERVISOR/supervisor/ping" | jq -r .result) |             ping=$(curl -sSL "http://$SUPERVISOR/supervisor/ping" | jq -r '.result') | ||||||
|             sleep 5 |             sleep 5 | ||||||
|           done |           done | ||||||
|  |  | ||||||
|       - name: Check the Supervisor |       - name: Check the Supervisor | ||||||
|         run: | |         run: | | ||||||
|           echo "Checking supervisor info" |           echo "Checking supervisor info" | ||||||
|           test=$(docker exec hassio_cli ha supervisor info --no-progress --raw-json | jq -r .result) |           test=$(docker exec hassio_cli ha supervisor info --no-progress --raw-json | jq -r '.result') | ||||||
|           if [ "$test" != "ok" ];then |           if [ "$test" != "ok" ];then | ||||||
|             docker logs hassio_supervisor |  | ||||||
|             exit 1 |             exit 1 | ||||||
|           fi |           fi | ||||||
|  |  | ||||||
|           echo "Checking supervisor network info" |           echo "Checking supervisor network info" | ||||||
|           test=$(docker exec hassio_cli ha network info --no-progress --raw-json | jq -r .result) |           test=$(docker exec hassio_cli ha network info --no-progress --raw-json | jq -r '.result') | ||||||
|           if [ "$test" != "ok" ];then |           if [ "$test" != "ok" ];then | ||||||
|             docker logs hassio_supervisor |  | ||||||
|             exit 1 |             exit 1 | ||||||
|           fi |           fi | ||||||
|  |  | ||||||
|  |       - name: Check the Store / Addon | ||||||
|  |         run: | | ||||||
|  |           echo "Install Core SSH Add-on" | ||||||
|  |           test=$(docker exec hassio_cli ha addons install core_ssh --no-progress --raw-json | jq -r '.result') | ||||||
|  |           if [ "$test" != "ok" ];then | ||||||
|  |             exit 1 | ||||||
|  |           fi | ||||||
|  |  | ||||||
|  |           echo "Start Core SSH Add-on" | ||||||
|  |           test=$(docker exec hassio_cli ha addons start core_ssh --no-progress --raw-json | jq -r '.result') | ||||||
|  |           if [ "$test" != "ok" ];then | ||||||
|  |             exit 1 | ||||||
|  |           fi | ||||||
|  |  | ||||||
|  |       - name: Check the Supervisor code sign | ||||||
|  |         if: needs.init.outputs.publish == 'true' | ||||||
|  |         run: | | ||||||
|  |           echo "Enable Content-Trust" | ||||||
|  |           test=$(docker exec hassio_cli ha security options --content-trust=true --no-progress --raw-json | jq -r '.result') | ||||||
|  |           if [ "$test" != "ok" ];then | ||||||
|  |             exit 1 | ||||||
|  |           fi | ||||||
|  |  | ||||||
|  |           echo "Run supervisor health check" | ||||||
|  |           test=$(docker exec hassio_cli ha resolution healthcheck --no-progress --raw-json | jq -r '.result') | ||||||
|  |           if [ "$test" != "ok" ];then | ||||||
|  |             exit 1 | ||||||
|  |           fi | ||||||
|  |  | ||||||
|  |           echo "Check supervisor unhealthy" | ||||||
|  |           test=$(docker exec hassio_cli ha resolution info --no-progress --raw-json | jq -r '.data.unhealthy[]') | ||||||
|  |           if [ "$test" != "" ];then | ||||||
|  |             exit 1 | ||||||
|  |           fi | ||||||
|  |  | ||||||
|  |       - name: Get supervisor logs on failiure | ||||||
|  |         if: ${{ cancelled() || failure() }} | ||||||
|  |         run: docker logs hassio_supervisor | ||||||
|   | |||||||
							
								
								
									
										90
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										90
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -8,8 +8,9 @@ on: | |||||||
|   pull_request: ~ |   pull_request: ~ | ||||||
|  |  | ||||||
| env: | env: | ||||||
|   DEFAULT_PYTHON: 3.8 |   DEFAULT_PYTHON: 3.9 | ||||||
|   PRE_COMMIT_HOME: ~/.cache/pre-commit |   PRE_COMMIT_HOME: ~/.cache/pre-commit | ||||||
|  |   DEFAULT_VCN: v0.9.8 | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   # Separate job to pre-populate the base dependency cache |   # Separate job to pre-populate the base dependency cache | ||||||
| @@ -18,26 +19,23 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     strategy: |     strategy: | ||||||
|       matrix: |       matrix: | ||||||
|         python-version: [3.8] |         python-version: [3.9] | ||||||
|     name: Prepare Python ${{ matrix.python-version }} dependencies |     name: Prepare Python ${{ matrix.python-version }} dependencies | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out code from GitHub |       - name: Check out code from GitHub | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|       - name: Set up Python ${{ matrix.python-version }} |       - name: Set up Python ${{ matrix.python-version }} | ||||||
|         id: python |         id: python | ||||||
|         uses: actions/setup-python@v2.2.1 |         uses: actions/setup-python@v2.3.1 | ||||||
|         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.1.4 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: venv |           path: venv | ||||||
|           key: | |           key: | | ||||||
|             ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} |             ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }} | ||||||
|           restore-keys: | |  | ||||||
|             ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }} |  | ||||||
|             ${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}- |  | ||||||
|       - name: Create Python virtual environment |       - name: Create Python virtual environment | ||||||
|         if: steps.cache-venv.outputs.cache-hit != 'true' |         if: steps.cache-venv.outputs.cache-hit != 'true' | ||||||
|         run: | |         run: | | ||||||
| @@ -47,7 +45,7 @@ jobs: | |||||||
|           pip install -r requirements.txt -r requirements_tests.txt |           pip install -r requirements.txt -r requirements_tests.txt | ||||||
|       - name: Restore pre-commit environment from cache |       - name: Restore pre-commit environment from cache | ||||||
|         id: cache-precommit |         id: cache-precommit | ||||||
|         uses: actions/cache@v2.1.4 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: ${{ env.PRE_COMMIT_HOME }} |           path: ${{ env.PRE_COMMIT_HOME }} | ||||||
|           key: | |           key: | | ||||||
| @@ -66,15 +64,15 @@ jobs: | |||||||
|     needs: prepare |     needs: prepare | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out code from GitHub |       - name: Check out code from GitHub | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|       - name: Set up Python ${{ env.DEFAULT_PYTHON }} |       - name: Set up Python ${{ env.DEFAULT_PYTHON }} | ||||||
|         uses: actions/setup-python@v2.2.1 |         uses: actions/setup-python@v2.3.1 | ||||||
|         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.1.4 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: venv |           path: venv | ||||||
|           key: | |           key: | | ||||||
| @@ -95,7 +93,7 @@ jobs: | |||||||
|     needs: prepare |     needs: prepare | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out code from GitHub |       - name: Check out code from GitHub | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|       - name: Register hadolint problem matcher |       - name: Register hadolint problem matcher | ||||||
|         run: | |         run: | | ||||||
|           echo "::add-matcher::.github/workflows/matchers/hadolint.json" |           echo "::add-matcher::.github/workflows/matchers/hadolint.json" | ||||||
| @@ -110,15 +108,15 @@ jobs: | |||||||
|     needs: prepare |     needs: prepare | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out code from GitHub |       - name: Check out code from GitHub | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|       - name: Set up Python ${{ env.DEFAULT_PYTHON }} |       - name: Set up Python ${{ env.DEFAULT_PYTHON }} | ||||||
|         uses: actions/setup-python@v2.2.1 |         uses: actions/setup-python@v2.3.1 | ||||||
|         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.1.4 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: venv |           path: venv | ||||||
|           key: | |           key: | | ||||||
| @@ -130,7 +128,7 @@ jobs: | |||||||
|           exit 1 |           exit 1 | ||||||
|       - name: Restore pre-commit environment from cache |       - name: Restore pre-commit environment from cache | ||||||
|         id: cache-precommit |         id: cache-precommit | ||||||
|         uses: actions/cache@v2.1.4 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: ${{ env.PRE_COMMIT_HOME }} |           path: ${{ env.PRE_COMMIT_HOME }} | ||||||
|           key: | |           key: | | ||||||
| @@ -154,15 +152,15 @@ jobs: | |||||||
|     needs: prepare |     needs: prepare | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out code from GitHub |       - name: Check out code from GitHub | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|       - name: Set up Python ${{ env.DEFAULT_PYTHON }} |       - name: Set up Python ${{ env.DEFAULT_PYTHON }} | ||||||
|         uses: actions/setup-python@v2.2.1 |         uses: actions/setup-python@v2.3.1 | ||||||
|         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.1.4 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: venv |           path: venv | ||||||
|           key: | |           key: | | ||||||
| @@ -186,15 +184,15 @@ jobs: | |||||||
|     needs: prepare |     needs: prepare | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out code from GitHub |       - name: Check out code from GitHub | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|       - name: Set up Python ${{ env.DEFAULT_PYTHON }} |       - name: Set up Python ${{ env.DEFAULT_PYTHON }} | ||||||
|         uses: actions/setup-python@v2.2.1 |         uses: actions/setup-python@v2.3.1 | ||||||
|         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.1.4 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: venv |           path: venv | ||||||
|           key: | |           key: | | ||||||
| @@ -206,7 +204,7 @@ jobs: | |||||||
|           exit 1 |           exit 1 | ||||||
|       - name: Restore pre-commit environment from cache |       - name: Restore pre-commit environment from cache | ||||||
|         id: cache-precommit |         id: cache-precommit | ||||||
|         uses: actions/cache@v2.1.4 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: ${{ env.PRE_COMMIT_HOME }} |           path: ${{ env.PRE_COMMIT_HOME }} | ||||||
|           key: | |           key: | | ||||||
| @@ -227,15 +225,15 @@ jobs: | |||||||
|     needs: prepare |     needs: prepare | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out code from GitHub |       - name: Check out code from GitHub | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|       - name: Set up Python ${{ env.DEFAULT_PYTHON }} |       - name: Set up Python ${{ env.DEFAULT_PYTHON }} | ||||||
|         uses: actions/setup-python@v2.2.1 |         uses: actions/setup-python@v2.3.1 | ||||||
|         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.1.4 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: venv |           path: venv | ||||||
|           key: | |           key: | | ||||||
| @@ -247,7 +245,7 @@ jobs: | |||||||
|           exit 1 |           exit 1 | ||||||
|       - name: Restore pre-commit environment from cache |       - name: Restore pre-commit environment from cache | ||||||
|         id: cache-precommit |         id: cache-precommit | ||||||
|         uses: actions/cache@v2.1.4 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: ${{ env.PRE_COMMIT_HOME }} |           path: ${{ env.PRE_COMMIT_HOME }} | ||||||
|           key: | |           key: | | ||||||
| @@ -271,15 +269,15 @@ jobs: | |||||||
|     needs: prepare |     needs: prepare | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out code from GitHub |       - name: Check out code from GitHub | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|       - name: Set up Python ${{ env.DEFAULT_PYTHON }} |       - name: Set up Python ${{ env.DEFAULT_PYTHON }} | ||||||
|         uses: actions/setup-python@v2.2.1 |         uses: actions/setup-python@v2.3.1 | ||||||
|         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.1.4 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: venv |           path: venv | ||||||
|           key: | |           key: | | ||||||
| @@ -303,15 +301,15 @@ jobs: | |||||||
|     needs: prepare |     needs: prepare | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out code from GitHub |       - name: Check out code from GitHub | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|       - name: Set up Python ${{ env.DEFAULT_PYTHON }} |       - name: Set up Python ${{ env.DEFAULT_PYTHON }} | ||||||
|         uses: actions/setup-python@v2.2.1 |         uses: actions/setup-python@v2.3.1 | ||||||
|         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.1.4 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: venv |           path: venv | ||||||
|           key: | |           key: | | ||||||
| @@ -323,7 +321,7 @@ jobs: | |||||||
|           exit 1 |           exit 1 | ||||||
|       - name: Restore pre-commit environment from cache |       - name: Restore pre-commit environment from cache | ||||||
|         id: cache-precommit |         id: cache-precommit | ||||||
|         uses: actions/cache@v2.1.4 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: ${{ env.PRE_COMMIT_HOME }} |           path: ${{ env.PRE_COMMIT_HOME }} | ||||||
|           key: | |           key: | | ||||||
| @@ -343,19 +341,23 @@ jobs: | |||||||
|     needs: prepare |     needs: prepare | ||||||
|     strategy: |     strategy: | ||||||
|       matrix: |       matrix: | ||||||
|         python-version: [3.8] |         python-version: [3.9] | ||||||
|     name: Run tests Python ${{ matrix.python-version }} |     name: Run tests Python ${{ matrix.python-version }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out code from GitHub |       - name: Check out code from GitHub | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|       - name: Set up Python ${{ matrix.python-version }} |       - name: Set up Python ${{ matrix.python-version }} | ||||||
|         uses: actions/setup-python@v2.2.1 |         uses: actions/setup-python@v2.3.1 | ||||||
|         id: python |         id: python | ||||||
|         with: |         with: | ||||||
|           python-version: ${{ matrix.python-version }} |           python-version: ${{ matrix.python-version }} | ||||||
|  |       - name: Install VCN tools | ||||||
|  |         uses: home-assistant/actions/helpers/vcn@master | ||||||
|  |         with: | ||||||
|  |           vcn_version: ${{ env.DEFAULT_VCN }} | ||||||
|       - name: Restore Python virtual environment |       - name: Restore Python virtual environment | ||||||
|         id: cache-venv |         id: cache-venv | ||||||
|         uses: actions/cache@v2.1.4 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: venv |           path: venv | ||||||
|           key: | |           key: | | ||||||
| @@ -390,7 +392,7 @@ jobs: | |||||||
|             -o console_output_style=count \ |             -o console_output_style=count \ | ||||||
|             tests |             tests | ||||||
|       - name: Upload coverage artifact |       - name: Upload coverage artifact | ||||||
|         uses: actions/upload-artifact@v2.2.2 |         uses: actions/upload-artifact@v2.3.1 | ||||||
|         with: |         with: | ||||||
|           name: coverage-${{ matrix.python-version }} |           name: coverage-${{ matrix.python-version }} | ||||||
|           path: .coverage |           path: .coverage | ||||||
| @@ -401,15 +403,15 @@ jobs: | |||||||
|     needs: pytest |     needs: pytest | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out code from GitHub |       - name: Check out code from GitHub | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|       - name: Set up Python ${{ env.DEFAULT_PYTHON }} |       - name: Set up Python ${{ env.DEFAULT_PYTHON }} | ||||||
|         uses: actions/setup-python@v2.2.1 |         uses: actions/setup-python@v2.3.1 | ||||||
|         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.1.4 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: venv |           path: venv | ||||||
|           key: | |           key: | | ||||||
| @@ -428,4 +430,4 @@ jobs: | |||||||
|           coverage report |           coverage report | ||||||
|           coverage xml |           coverage xml | ||||||
|       - name: Upload coverage to Codecov |       - name: Upload coverage to Codecov | ||||||
|         uses: codecov/codecov-action@v1.2.1 |         uses: codecov/codecov-action@v2.1.0 | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							| @@ -9,12 +9,12 @@ jobs: | |||||||
|   lock: |   lock: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: dessant/lock-threads@v2.0.3 |       - uses: dessant/lock-threads@v3 | ||||||
|         with: |         with: | ||||||
|           github-token: ${{ github.token }} |           github-token: ${{ github.token }} | ||||||
|           issue-lock-inactive-days: "30" |           issue-inactive-days: "30" | ||||||
|           issue-exclude-created-before: "2020-10-01T00:00:00Z" |           exclude-issue-created-before: "2020-10-01T00:00:00Z" | ||||||
|           issue-lock-reason: "" |           issue-lock-reason: "" | ||||||
|           pr-lock-inactive-days: "1" |           pr-inactive-days: "1" | ||||||
|           pr-exclude-created-before: "2020-11-01T00:00:00Z" |           exclude-pr-created-before: "2020-11-01T00:00:00Z" | ||||||
|           pr-lock-reason: "" |           pr-lock-reason: "" | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/release-drafter.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release-drafter.yml
									
									
									
									
										vendored
									
									
								
							| @@ -11,7 +11,7 @@ jobs: | |||||||
|     name: Release Drafter |     name: Release Drafter | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|         with: |         with: | ||||||
|           fetch-depth: 0 |           fetch-depth: 0 | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/workflows/sentry.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/sentry.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -10,9 +10,9 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out code from GitHub |       - name: Check out code from GitHub | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v2.4.0 | ||||||
|       - name: Sentry Release |       - name: Sentry Release | ||||||
|         uses: getsentry/action-release@v1.1 |         uses: getsentry/action-release@v1.1.6 | ||||||
|         env: |         env: | ||||||
|           SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} |           SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | ||||||
|           SENTRY_ORG: ${{ secrets.SENTRY_ORG }} |           SENTRY_ORG: ${{ secrets.SENTRY_ORG }} | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -9,7 +9,7 @@ jobs: | |||||||
|   stale: |   stale: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/stale@v3.0.16 |       - uses: actions/stale@v4 | ||||||
|         with: |         with: | ||||||
|           repo-token: ${{ secrets.GITHUB_TOKEN }} |           repo-token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|           days-before-stale: 60 |           days-before-stale: 60 | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| ignored: | ignored: | ||||||
|   - DL3018 |   - DL3003 | ||||||
|   - DL3006 |   - DL3006 | ||||||
|   - DL3013 |   - DL3013 | ||||||
|  |   - DL3018 | ||||||
|   - SC2155 |   - SC2155 | ||||||
|   | |||||||
| @@ -1,13 +1,13 @@ | |||||||
| repos: | repos: | ||||||
|   - repo: https://github.com/psf/black |   - repo: https://github.com/psf/black | ||||||
|     rev: 20.8b1 |     rev: 21.12b0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: black |       - id: black | ||||||
|         args: |         args: | ||||||
|           - --safe |           - --safe | ||||||
|           - --quiet |           - --quiet | ||||||
|           - --target-version |           - --target-version | ||||||
|           - py38 |           - py39 | ||||||
|         files: ^((supervisor|tests)/.+)?[^/]+\.py$ |         files: ^((supervisor|tests)/.+)?[^/]+\.py$ | ||||||
|   - repo: https://gitlab.com/pycqa/flake8 |   - repo: https://gitlab.com/pycqa/flake8 | ||||||
|     rev: 3.8.3 |     rev: 3.8.3 | ||||||
| @@ -23,12 +23,12 @@ repos: | |||||||
|       - id: check-executables-have-shebangs |       - id: check-executables-have-shebangs | ||||||
|         stages: [manual] |         stages: [manual] | ||||||
|       - id: check-json |       - id: check-json | ||||||
|   - repo: https://github.com/pre-commit/mirrors-isort |   - repo: https://github.com/PyCQA/isort | ||||||
|     rev: v4.3.21 |     rev: 5.9.3 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: isort |       - id: isort | ||||||
|   - repo: https://github.com/asottile/pyupgrade |   - repo: https://github.com/asottile/pyupgrade | ||||||
|     rev: v2.6.2 |     rev: v2.31.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: pyupgrade |       - id: pyupgrade | ||||||
|         args: [--py37-plus] |         args: [--py39-plus] | ||||||
|   | |||||||
							
								
								
									
										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/ | ||||||
							
								
								
									
										25
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							| @@ -4,7 +4,7 @@ | |||||||
|     { |     { | ||||||
|       "label": "Run Supervisor", |       "label": "Run Supervisor", | ||||||
|       "type": "shell", |       "type": "shell", | ||||||
|       "command": "./scripts/run-supervisor.sh", |       "command": "supervisor_run", | ||||||
|       "group": { |       "group": { | ||||||
|         "kind": "test", |         "kind": "test", | ||||||
|         "isDefault": true |         "isDefault": true | ||||||
| @@ -15,20 +15,6 @@ | |||||||
|       }, |       }, | ||||||
|       "problemMatcher": [] |       "problemMatcher": [] | ||||||
|     }, |     }, | ||||||
|     { |  | ||||||
|       "label": "Build Supervisor", |  | ||||||
|       "type": "shell", |  | ||||||
|       "command": "./scripts/build-supervisor.sh", |  | ||||||
|       "group": { |  | ||||||
|         "kind": "build", |  | ||||||
|         "isDefault": true |  | ||||||
|       }, |  | ||||||
|       "presentation": { |  | ||||||
|         "reveal": "always", |  | ||||||
|         "panel": "new" |  | ||||||
|       }, |  | ||||||
|       "problemMatcher": [] |  | ||||||
|     }, |  | ||||||
|     { |     { | ||||||
|       "label": "Run Supervisor CLI", |       "label": "Run Supervisor CLI", | ||||||
|       "type": "shell", |       "type": "shell", | ||||||
| @@ -46,7 +32,7 @@ | |||||||
|     { |     { | ||||||
|       "label": "Update Supervisor Panel", |       "label": "Update Supervisor Panel", | ||||||
|       "type": "shell", |       "type": "shell", | ||||||
|       "command": "./scripts/update-frontend.sh", |       "command": "LOKALISE_TOKEN='${input:localiseToken}' ./scripts/update-frontend.sh", | ||||||
|       "group": { |       "group": { | ||||||
|         "kind": "build", |         "kind": "build", | ||||||
|         "isDefault": true |         "isDefault": true | ||||||
| @@ -100,5 +86,12 @@ | |||||||
|       }, |       }, | ||||||
|       "problemMatcher": [] |       "problemMatcher": [] | ||||||
|     } |     } | ||||||
|  |   ], | ||||||
|  |   "inputs": [ | ||||||
|  |     { | ||||||
|  |       "id": "localiseToken", | ||||||
|  |       "type": "promptString", | ||||||
|  |       "description": "Paste your lokalise token to download frontend translations" | ||||||
|  |     } | ||||||
|   ] |   ] | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,25 +1,25 @@ | |||||||
| 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 |     SUPERVISOR_API=http://localhost | ||||||
|  |  | ||||||
|  | ARG BUILD_ARCH | ||||||
|  | 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 \ | ||||||
|         glib \ |  | ||||||
|         libffi \ |         libffi \ | ||||||
|         libpulse \ |         libpulse \ | ||||||
|         musl \ |         musl \ | ||||||
|         openssl |         openssl | ||||||
|  |  | ||||||
| ARG BUILD_ARCH |  | ||||||
| WORKDIR /usr/src |  | ||||||
|  |  | ||||||
| # Install requirements | # Install requirements | ||||||
| COPY requirements.txt . | COPY requirements.txt . | ||||||
| RUN \ | RUN \ | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								build.json
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								build.json
									
									
									
									
									
								
							| @@ -1,13 +0,0 @@ | |||||||
| { |  | ||||||
|   "image": "homeassistant/{arch}-hassio-supervisor", |  | ||||||
|   "build_from": { |  | ||||||
|     "aarch64": "homeassistant/aarch64-base-python:3.8-alpine3.12", |  | ||||||
|     "armhf": "homeassistant/armhf-base-python:3.8-alpine3.12", |  | ||||||
|     "armv7": "homeassistant/armv7-base-python:3.8-alpine3.12", |  | ||||||
|     "amd64": "homeassistant/amd64-base-python:3.8-alpine3.12", |  | ||||||
|     "i386": "homeassistant/i386-base-python:3.8-alpine3.12" |  | ||||||
|   }, |  | ||||||
|   "labels": { |  | ||||||
|     "io.hass.type": "supervisor" |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										20
									
								
								build.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								build.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | image: homeassistant/{arch}-hassio-supervisor | ||||||
|  | shadow_repository: ghcr.io/home-assistant | ||||||
|  | build_from: | ||||||
|  |   aarch64: ghcr.io/home-assistant/aarch64-base-python:3.9-alpine3.14 | ||||||
|  |   armhf: ghcr.io/home-assistant/armhf-base-python:3.9-alpine3.14 | ||||||
|  |   armv7: ghcr.io/home-assistant/armv7-base-python:3.9-alpine3.14 | ||||||
|  |   amd64: ghcr.io/home-assistant/amd64-base-python:3.9-alpine3.14 | ||||||
|  |   i386: ghcr.io/home-assistant/i386-base-python:3.9-alpine3.14 | ||||||
|  | codenotary: | ||||||
|  |   signer: notary@home-assistant.io | ||||||
|  |   base_image: notary@home-assistant.io | ||||||
|  | labels: | ||||||
|  |   io.hass.type: supervisor | ||||||
|  |   org.opencontainers.image.title: Home Assistant Supervisor | ||||||
|  |   org.opencontainers.image.description: Container-based system for managing Home Assistant Core installation | ||||||
|  |   org.opencontainers.image.source: https://github.com/home-assistant/supervisor | ||||||
|  |   org.opencontainers.image.authors: The Home Assistant Authors | ||||||
|  |   org.opencontainers.image.url: https://www.home-assistant.io/ | ||||||
|  |   org.opencontainers.image.documentation: https://www.home-assistant.io/docs/ | ||||||
|  |   org.opencontainers.image.licenses: Apache License 2.0 | ||||||
 Submodule home-assistant-polymer updated: 4273b72d71...2f9c088091
									
								
							
							
								
								
									
										6
									
								
								pylintrc
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								pylintrc
									
									
									
									
									
								
							| @@ -2,7 +2,10 @@ | |||||||
| reports=no | reports=no | ||||||
| jobs=2 | jobs=2 | ||||||
|  |  | ||||||
| good-names=id,i,j,k,ex,Run,_,fp,T | good-names=id,i,j,k,ex,Run,_,fp,T,os | ||||||
|  |  | ||||||
|  | extension-pkg-whitelist= | ||||||
|  |   ciso8601 | ||||||
|  |  | ||||||
| # Reasons disabled: | # Reasons disabled: | ||||||
| # format - handled by black | # format - handled by black | ||||||
| @@ -37,6 +40,7 @@ disable= | |||||||
|   too-many-return-statements, |   too-many-return-statements, | ||||||
|   too-many-statements, |   too-many-statements, | ||||||
|   unused-argument, |   unused-argument, | ||||||
|  |   consider-using-with | ||||||
|  |  | ||||||
| [EXCEPTIONS] | [EXCEPTIONS] | ||||||
| overgeneral-exceptions=Exception | overgeneral-exceptions=Exception | ||||||
|   | |||||||
| @@ -1,20 +1,22 @@ | |||||||
| aiohttp==3.7.3 | aiohttp==3.8.1 | ||||||
| async_timeout==3.0.1 | async_timeout==4.0.2 | ||||||
| atomicwrites==1.4.0 | atomicwrites==1.4.0 | ||||||
| attrs==20.3.0 | attrs==21.2.0 | ||||||
| awesomeversion==21.2.2 | awesomeversion==22.1.0 | ||||||
| brotli==1.0.9 | brotli==1.0.9 | ||||||
| cchardet==2.1.7 | cchardet==2.1.7 | ||||||
| colorlog==4.7.2 | ciso8601==2.2.0 | ||||||
|  | colorlog==6.6.0 | ||||||
| cpe==1.2.1 | cpe==1.2.1 | ||||||
| cryptography==3.3.1 | cryptography==36.0.1 | ||||||
| debugpy==1.2.1 | debugpy==1.5.1 | ||||||
| docker==4.4.1 | deepmerge==1.0.1 | ||||||
| gitpython==3.1.12 | docker==5.0.3 | ||||||
| jinja2==2.11.3 | gitpython==3.1.26 | ||||||
| pulsectl==20.5.1 | jinja2==3.0.3 | ||||||
| pytz==2021.1 | pulsectl==21.10.5 | ||||||
| pyudev==0.22.0 | pyudev==0.22.0 | ||||||
| ruamel.yaml==0.15.100 | ruamel.yaml==0.17.17 | ||||||
| sentry-sdk==0.19.5 | sentry-sdk==1.5.2 | ||||||
| voluptuous==0.12.1 | voluptuous==0.12.2 | ||||||
|  | dbus-next==0.2.3 | ||||||
|   | |||||||
| @@ -1,14 +1,14 @@ | |||||||
| black==20.8b1 | black==21.12b0 | ||||||
| codecov==2.1.11 | codecov==2.1.12 | ||||||
| coverage==5.4 | coverage==6.2 | ||||||
| flake8-docstrings==1.5.0 | flake8-docstrings==1.6.0 | ||||||
| flake8==3.8.4 | flake8==4.0.1 | ||||||
| pre-commit==2.10.1 | pre-commit==2.17.0 | ||||||
| pydocstyle==5.1.1 | pydocstyle==6.1.1 | ||||||
| pylint==2.6.0 | pylint==2.12.2 | ||||||
| pytest-aiohttp==0.3.0 | pytest-aiohttp==0.3.0 | ||||||
| pytest-asyncio==0.12.0 # NB!: Versions over 0.12.0 breaks pytest-aiohttp (https://github.com/aio-libs/pytest-aiohttp/issues/16) | pytest-asyncio==0.12.0 # NB!: Versions over 0.12.0 breaks pytest-aiohttp (https://github.com/aio-libs/pytest-aiohttp/issues/16) | ||||||
| pytest-cov==2.11.1 | pytest-cov==3.0.0 | ||||||
| pytest-timeout==1.4.2 | pytest-timeout==2.0.2 | ||||||
| pytest==6.2.2 | pytest==6.2.5 | ||||||
| pyupgrade==2.10.0 | pyupgrade==2.31.0 | ||||||
|   | |||||||
| @@ -1,28 +0,0 @@ | |||||||
| #!/bin/bash |  | ||||||
| source "${BASH_SOURCE[0]%/*}/common.sh" |  | ||||||
|  |  | ||||||
| set -eE |  | ||||||
|  |  | ||||||
| DOCKER_TIMEOUT=30 |  | ||||||
| DOCKER_PID=0 |  | ||||||
|  |  | ||||||
| function build_supervisor() { |  | ||||||
|     docker pull homeassistant/amd64-builder:dev |  | ||||||
|  |  | ||||||
|     docker run --rm \ |  | ||||||
|         --privileged \ |  | ||||||
|         -v /run/docker.sock:/run/docker.sock \ |  | ||||||
|         -v "$(pwd):/data" \ |  | ||||||
|         homeassistant/amd64-builder:dev \ |  | ||||||
|             --generic latest \ |  | ||||||
|             --target /data \ |  | ||||||
|             --test \ |  | ||||||
|             --amd64 \ |  | ||||||
|             --no-cache |  | ||||||
| } |  | ||||||
|  |  | ||||||
| echo "Build Supervisor" |  | ||||||
| start_docker |  | ||||||
| trap "stop_docker" ERR |  | ||||||
|  |  | ||||||
| build_supervisor |  | ||||||
| @@ -1,58 +0,0 @@ | |||||||
| #!/bin/bash |  | ||||||
|  |  | ||||||
| function start_docker() { |  | ||||||
|     local starttime |  | ||||||
|     local endtime |  | ||||||
|  |  | ||||||
|     echo "Starting docker." |  | ||||||
|     dockerd 2> /dev/null & |  | ||||||
|     DOCKER_PID=$! |  | ||||||
|  |  | ||||||
|     echo "Waiting for docker to initialize..." |  | ||||||
|     starttime="$(date +%s)" |  | ||||||
|     endtime="$(date +%s)" |  | ||||||
|     until docker info >/dev/null 2>&1; do |  | ||||||
|         if [ $((endtime - starttime)) -le $DOCKER_TIMEOUT ]; then |  | ||||||
|             sleep 1 |  | ||||||
|             endtime=$(date +%s) |  | ||||||
|         else |  | ||||||
|             echo "Timeout while waiting for docker to come up" |  | ||||||
|             exit 1 |  | ||||||
|         fi |  | ||||||
|     done |  | ||||||
|     echo "Docker was initialized" |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function stop_docker() { |  | ||||||
|     local starttime |  | ||||||
|     local endtime |  | ||||||
|  |  | ||||||
|     echo "Stopping in container docker..." |  | ||||||
|     if [ "$DOCKER_PID" -gt 0 ] && kill -0 "$DOCKER_PID" 2> /dev/null; then |  | ||||||
|         starttime="$(date +%s)" |  | ||||||
|         endtime="$(date +%s)" |  | ||||||
|  |  | ||||||
|         # Now wait for it to die |  | ||||||
|         kill "$DOCKER_PID" |  | ||||||
|         while kill -0 "$DOCKER_PID" 2> /dev/null; do |  | ||||||
|             if [ $((endtime - starttime)) -le $DOCKER_TIMEOUT ]; then |  | ||||||
|                 sleep 1 |  | ||||||
|                 endtime=$(date +%s) |  | ||||||
|             else |  | ||||||
|                 echo "Timeout while waiting for container docker to die" |  | ||||||
|                 exit 1 |  | ||||||
|             fi |  | ||||||
|         done |  | ||||||
|     else |  | ||||||
|         echo "Your host might have been left with unreleased resources" |  | ||||||
|     fi |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function cleanup_lastboot() { |  | ||||||
|     if [[ -f /workspaces/test_supervisor/config.json ]]; then |  | ||||||
|         echo "Cleaning up last boot" |  | ||||||
|         cp /workspaces/test_supervisor/config.json /tmp/config.json |  | ||||||
|         jq -rM 'del(.last_boot)' /tmp/config.json > /workspaces/test_supervisor/config.json |  | ||||||
|         rm /tmp/config.json |  | ||||||
|     fi |  | ||||||
| } |  | ||||||
| @@ -1,102 +0,0 @@ | |||||||
| #!/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 |  | ||||||
| @@ -1,4 +1,6 @@ | |||||||
| #!/bin/bash | #!/bin/bash | ||||||
|  | source "/etc/supervisor_scripts/common" | ||||||
|  |  | ||||||
| set -e | set -e | ||||||
|  |  | ||||||
| # Update frontend | # Update frontend | ||||||
| @@ -9,6 +11,10 @@ cd home-assistant-polymer | |||||||
| nvm install | nvm install | ||||||
| script/bootstrap | script/bootstrap | ||||||
|  |  | ||||||
|  | # Download translations | ||||||
|  | start_docker | ||||||
|  | ./script/translations_download | ||||||
|  |  | ||||||
| # build frontend | # build frontend | ||||||
| cd hassio | cd hassio | ||||||
| ./script/build_hassio | ./script/build_hassio | ||||||
| @@ -16,3 +22,9 @@ cd hassio | |||||||
| # Copy frontend | # Copy frontend | ||||||
| rm -rf ../../supervisor/api/panel/* | rm -rf ../../supervisor/api/panel/* | ||||||
| cp -rf build/* ../../supervisor/api/panel/ | cp -rf build/* ../../supervisor/api/panel/ | ||||||
|  |  | ||||||
|  | # Reset frontend git | ||||||
|  | cd .. | ||||||
|  | git reset --hard HEAD | ||||||
|  |  | ||||||
|  | stop_docker | ||||||
| @@ -4,9 +4,8 @@ include_trailing_comma=True | |||||||
| force_grid_wrap=0 | force_grid_wrap=0 | ||||||
| line_length=88 | line_length=88 | ||||||
| indent = "    " | indent = "    " | ||||||
| not_skip = __init__.py |  | ||||||
| force_sort_within_sections = true | force_sort_within_sections = true | ||||||
| sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER | sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER | ||||||
| default_section = THIRDPARTY | default_section = THIRDPARTY | ||||||
| forced_separate = tests | forced_separate = tests | ||||||
| combine_as_imports = true | combine_as_imports = true | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								setup.py
									
									
									
									
									
								
							| @@ -33,8 +33,9 @@ setup( | |||||||
|     packages=[ |     packages=[ | ||||||
|         "supervisor.addons", |         "supervisor.addons", | ||||||
|         "supervisor.api", |         "supervisor.api", | ||||||
|  |         "supervisor.backups", | ||||||
|         "supervisor.dbus.network", |         "supervisor.dbus.network", | ||||||
|         "supervisor.dbus.payloads", |         "supervisor.dbus.network.setting", | ||||||
|         "supervisor.dbus", |         "supervisor.dbus", | ||||||
|         "supervisor.discovery.services", |         "supervisor.discovery.services", | ||||||
|         "supervisor.discovery", |         "supervisor.discovery", | ||||||
| @@ -44,11 +45,12 @@ setup( | |||||||
|         "supervisor.jobs", |         "supervisor.jobs", | ||||||
|         "supervisor.misc", |         "supervisor.misc", | ||||||
|         "supervisor.plugins", |         "supervisor.plugins", | ||||||
|  |         "supervisor.resolution.checks", | ||||||
|         "supervisor.resolution.evaluations", |         "supervisor.resolution.evaluations", | ||||||
|  |         "supervisor.resolution.fixups", | ||||||
|         "supervisor.resolution", |         "supervisor.resolution", | ||||||
|         "supervisor.services.modules", |         "supervisor.services.modules", | ||||||
|         "supervisor.services", |         "supervisor.services", | ||||||
|         "supervisor.snapshots", |  | ||||||
|         "supervisor.store", |         "supervisor.store", | ||||||
|         "supervisor.utils", |         "supervisor.utils", | ||||||
|         "supervisor", |         "supervisor", | ||||||
|   | |||||||
| @@ -39,6 +39,7 @@ if __name__ == "__main__": | |||||||
|  |  | ||||||
|     _LOGGER.info("Initializing Supervisor setup") |     _LOGGER.info("Initializing Supervisor setup") | ||||||
|     coresys = loop.run_until_complete(bootstrap.initialize_coresys()) |     coresys = loop.run_until_complete(bootstrap.initialize_coresys()) | ||||||
|  |     loop.set_debug(coresys.config.debug) | ||||||
|     loop.run_until_complete(coresys.core.connect()) |     loop.run_until_complete(coresys.core.connect()) | ||||||
|  |  | ||||||
|     bootstrap.supervisor_debugger(coresys) |     bootstrap.supervisor_debugger(coresys) | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import asyncio | |||||||
| from contextlib import suppress | from contextlib import suppress | ||||||
| import logging | import logging | ||||||
| import tarfile | import tarfile | ||||||
| from typing import Dict, List, Optional, Union | from typing import Optional, Union | ||||||
|  |  | ||||||
| from ..const import AddonBoot, AddonStartup, AddonState | from ..const import AddonBoot, AddonStartup, AddonState | ||||||
| from ..coresys import CoreSys, CoreSysAttributes | from ..coresys import CoreSys, CoreSysAttributes | ||||||
| @@ -38,17 +38,17 @@ class AddonManager(CoreSysAttributes): | |||||||
|         """Initialize Docker base wrapper.""" |         """Initialize Docker base wrapper.""" | ||||||
|         self.coresys: CoreSys = coresys |         self.coresys: CoreSys = coresys | ||||||
|         self.data: AddonsData = AddonsData(coresys) |         self.data: AddonsData = AddonsData(coresys) | ||||||
|         self.local: Dict[str, Addon] = {} |         self.local: dict[str, Addon] = {} | ||||||
|         self.store: Dict[str, AddonStore] = {} |         self.store: dict[str, AddonStore] = {} | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def all(self) -> List[AnyAddon]: |     def all(self) -> list[AnyAddon]: | ||||||
|         """Return a list of all add-ons.""" |         """Return a list of all add-ons.""" | ||||||
|         addons: Dict[str, AnyAddon] = {**self.store, **self.local} |         addons: dict[str, AnyAddon] = {**self.store, **self.local} | ||||||
|         return list(addons.values()) |         return list(addons.values()) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def installed(self) -> List[Addon]: |     def installed(self) -> list[Addon]: | ||||||
|         """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()) | ||||||
|  |  | ||||||
| @@ -89,7 +89,7 @@ class AddonManager(CoreSysAttributes): | |||||||
|  |  | ||||||
|     async def boot(self, stage: AddonStartup) -> None: |     async def boot(self, stage: AddonStartup) -> None: | ||||||
|         """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 != AddonBoot.AUTO or addon.startup != stage: |             if addon.boot != AddonBoot.AUTO or addon.startup != stage: | ||||||
|                 continue |                 continue | ||||||
| @@ -123,7 +123,7 @@ class AddonManager(CoreSysAttributes): | |||||||
|  |  | ||||||
|     async def shutdown(self, stage: AddonStartup) -> None: |     async def shutdown(self, stage: AddonStartup) -> None: | ||||||
|         """Shutdown addons.""" |         """Shutdown addons.""" | ||||||
|         tasks: List[Addon] = [] |         tasks: list[Addon] = [] | ||||||
|         for addon in self.installed: |         for addon in self.installed: | ||||||
|             if addon.state != AddonState.STARTED or addon.startup != stage: |             if addon.state != AddonState.STARTED or addon.startup != stage: | ||||||
|                 continue |                 continue | ||||||
| @@ -154,17 +154,16 @@ class AddonManager(CoreSysAttributes): | |||||||
|     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} does not exist", _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 this platform", _LOGGER.error | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         self.data.install(store) |         self.data.install(store) | ||||||
|         addon = Addon(self.coresys, slug) |         addon = Addon(self.coresys, slug) | ||||||
| @@ -253,26 +252,33 @@ class AddonManager(CoreSysAttributes): | |||||||
|         ], |         ], | ||||||
|         on_condition=AddonsJobError, |         on_condition=AddonsJobError, | ||||||
|     ) |     ) | ||||||
|     async def update(self, slug: str) -> None: |     async def update(self, slug: str, backup: Optional[bool] = False) -> None: | ||||||
|         """Update add-on.""" |         """Update add-on.""" | ||||||
|         if slug not in self.local: |         if slug not in self.local: | ||||||
|             _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 | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         if backup: | ||||||
|  |             await self.sys_backups.do_backup_partial( | ||||||
|  |                 name=f"addon_{addon.slug}_{addon.version}", | ||||||
|  |                 homeassistant=False, | ||||||
|  |                 addons=[addon.slug], | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         # Update instance |         # Update instance | ||||||
|         last_state: AddonState = addon.state |         last_state: AddonState = addon.state | ||||||
| @@ -307,22 +313,24 @@ class AddonManager(CoreSysAttributes): | |||||||
|     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: | ||||||
|             _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] | ||||||
|  |  | ||||||
|         # Check if a rebuild is possible now |         # Check if a rebuild is possible now | ||||||
|         if addon.version != store.version: |         if addon.version != store.version: | ||||||
|             _LOGGER.error("Version changed, use Update instead Rebuild") |             raise AddonsError( | ||||||
|             raise AddonsError() |                 "Version changed, use Update instead Rebuild", _LOGGER.error | ||||||
|  |             ) | ||||||
|         if not addon.need_build: |         if not addon.need_build: | ||||||
|             _LOGGER.error("Can't rebuild a image based add-on") |             raise AddonsNotSupportedError( | ||||||
|             raise AddonsNotSupportedError() |                 "Can't rebuild a image based add-on", _LOGGER.error | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         # remove docker container but not addon config |         # remove docker container but not addon config | ||||||
|         last_state: AddonState = addon.state |         last_state: AddonState = addon.state | ||||||
| @@ -372,7 +380,7 @@ class AddonManager(CoreSysAttributes): | |||||||
|     @Job(conditions=[JobCondition.FREE_SPACE, JobCondition.INTERNET_HOST]) |     @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] = [] | ||||||
|  |  | ||||||
|         # Evaluate Add-ons to repair |         # Evaluate Add-ons to repair | ||||||
|         for addon in self.installed: |         for addon in self.installed: | ||||||
|   | |||||||
| @@ -10,9 +10,10 @@ 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, Set | from typing import Any, Awaitable, Final, Optional | ||||||
|  |  | ||||||
| import aiohttp | import aiohttp | ||||||
|  | from deepmerge import Merger | ||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
| from voluptuous.humanize import humanize_error | from voluptuous.humanize import humanize_error | ||||||
|  |  | ||||||
| @@ -22,6 +23,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, | ||||||
| @@ -32,8 +35,10 @@ 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, | ||||||
| @@ -50,20 +55,22 @@ from ..exceptions import ( | |||||||
|     AddonConfigurationError, |     AddonConfigurationError, | ||||||
|     AddonsError, |     AddonsError, | ||||||
|     AddonsNotSupportedError, |     AddonsNotSupportedError, | ||||||
|  |     ConfigurationFileError, | ||||||
|     DockerError, |     DockerError, | ||||||
|     DockerRequestError, |     DockerRequestError, | ||||||
|     HostAppArmorError, |     HostAppArmorError, | ||||||
|     JsonFileError, |  | ||||||
| ) | ) | ||||||
| from ..hardware.data import Device | from ..hardware.data import Device | ||||||
|  | from ..homeassistant.const import WSEvent, WSType | ||||||
| from ..utils import check_port | from ..utils import check_port | ||||||
| from ..utils.apparmor import adjust_profile | from ..utils.apparmor import adjust_profile | ||||||
| from ..utils.json import read_json_file, write_json_file | from ..utils.json import read_json_file, write_json_file | ||||||
| from ..utils.tar import atomic_contents_add, secure_path | from ..utils.tar import atomic_contents_add, secure_path | ||||||
|  | from .const import AddonBackupMode | ||||||
| from .model import AddonModel, Data | from .model import AddonModel, Data | ||||||
| from .options import AddonOptions | from .options import AddonOptions | ||||||
| from .utils import remove_data | from .utils import remove_data | ||||||
| from .validate import SCHEMA_ADDON_SNAPSHOT | from .validate import SCHEMA_ADDON_BACKUP | ||||||
|  |  | ||||||
| _LOGGER: logging.Logger = logging.getLogger(__name__) | _LOGGER: logging.Logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -74,13 +81,19 @@ RE_WEBUI = re.compile( | |||||||
|  |  | ||||||
| RE_WATCHDOG = re.compile( | RE_WATCHDOG = re.compile( | ||||||
|     r"^(?:(?P<s_prefix>https?|tcp)|\[PROTO:(?P<t_proto>\w+)\])" |     r"^(?:(?P<s_prefix>https?|tcp)|\[PROTO:(?P<t_proto>\w+)\])" | ||||||
|     r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$" |     r":\/\/\[HOST\]:(?:\[PORT:)?(?P<t_port>\d+)\]?(?P<s_suffix>.*)$" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| RE_OLD_AUDIO = re.compile(r"\d+,\d+") | RE_OLD_AUDIO = re.compile(r"\d+,\d+") | ||||||
|  |  | ||||||
| WATCHDOG_TIMEOUT = aiohttp.ClientTimeout(total=10) | WATCHDOG_TIMEOUT = aiohttp.ClientTimeout(total=10) | ||||||
|  |  | ||||||
|  | _OPTIONS_MERGER: Final = Merger( | ||||||
|  |     type_strategies=[(dict, ["merge"])], | ||||||
|  |     fallback_strategies=["override"], | ||||||
|  |     type_conflict_strategies=["override"], | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Addon(AddonModel): | class Addon(AddonModel): | ||||||
|     """Hold data for add-on inside Supervisor.""" |     """Hold data for add-on inside Supervisor.""" | ||||||
| @@ -89,12 +102,34 @@ 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 |         self._state: AddonState = AddonState.UNKNOWN | ||||||
|  |  | ||||||
|     def __repr__(self) -> str: |     def __repr__(self) -> str: | ||||||
|         """Return internal representation.""" |         """Return internal representation.""" | ||||||
|         return f"<Addon: {self.slug}>" |         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_message( | ||||||
|  |             { | ||||||
|  |                 ATTR_TYPE: WSType.SUPERVISOR_EVENT, | ||||||
|  |                 ATTR_DATA: { | ||||||
|  |                     ATTR_EVENT: WSEvent.ADDON, | ||||||
|  |                     ATTR_SLUG: self.slug, | ||||||
|  |                     ATTR_STATE: new_state, | ||||||
|  |                 }, | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def in_progress(self) -> bool: |     def in_progress(self) -> bool: | ||||||
|         """Return True if a task is in progress.""" |         """Return True if a task is in progress.""" | ||||||
| @@ -159,17 +194,19 @@ class Addon(AddonModel): | |||||||
|         return self.version != self.latest_version |         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.""" | ||||||
|         return [f"{self.hostname}.{DNS_SUFFIX}"] |         return [f"{self.hostname}.{DNS_SUFFIX}"] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def options(self) -> Dict[str, Any]: |     def options(self) -> dict[str, Any]: | ||||||
|         """Return options with local changes.""" |         """Return options with local changes.""" | ||||||
|         return {**self.data[ATTR_OPTIONS], **self.persist[ATTR_OPTIONS]} |         return _OPTIONS_MERGER.merge( | ||||||
|  |             deepcopy(self.data[ATTR_OPTIONS]), deepcopy(self.persist[ATTR_OPTIONS]) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     @options.setter |     @options.setter | ||||||
|     def options(self, value: Optional[Dict[str, Any]]) -> None: |     def options(self, value: Optional[dict[str, Any]]) -> None: | ||||||
|         """Store user add-on options.""" |         """Store user add-on options.""" | ||||||
|         self.persist[ATTR_OPTIONS] = {} if value is None else deepcopy(value) |         self.persist[ATTR_OPTIONS] = {} if value is None else deepcopy(value) | ||||||
|  |  | ||||||
| @@ -246,12 +283,12 @@ class Addon(AddonModel): | |||||||
|         self.persist[ATTR_PROTECTED] = value |         self.persist[ATTR_PROTECTED] = value | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def ports(self) -> Optional[Dict[str, Optional[int]]]: |     def ports(self) -> Optional[dict[str, Optional[int]]]: | ||||||
|         """Return ports of add-on.""" |         """Return ports of add-on.""" | ||||||
|         return self.persist.get(ATTR_NETWORK, super().ports) |         return self.persist.get(ATTR_NETWORK, super().ports) | ||||||
|  |  | ||||||
|     @ports.setter |     @ports.setter | ||||||
|     def ports(self, value: Optional[Dict[str, Optional[int]]]) -> None: |     def ports(self, value: Optional[dict[str, Optional[int]]]) -> None: | ||||||
|         """Set custom ports of add-on.""" |         """Set custom ports of add-on.""" | ||||||
|         if value is None: |         if value is None: | ||||||
|             self.persist.pop(ATTR_NETWORK, None) |             self.persist.pop(ATTR_NETWORK, None) | ||||||
| @@ -397,18 +434,22 @@ class Addon(AddonModel): | |||||||
|         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 |     @property | ||||||
|     def devices(self) -> Set[Device]: |     def devices(self) -> set[Device]: | ||||||
|         """Create a schema for add-on options.""" |         """Extract devices from add-on options.""" | ||||||
|         raw_schema = self.data[ATTR_SCHEMA] |         options_schema = self.schema | ||||||
|         if isinstance(raw_schema, bool) or not raw_schema: |  | ||||||
|             return set() |  | ||||||
|  |  | ||||||
|         # Validate devices |  | ||||||
|         options_validator = AddonOptions(self.coresys, raw_schema) |  | ||||||
|         with suppress(vol.Invalid): |         with suppress(vol.Invalid): | ||||||
|             options_validator(self.options) |             options_schema.validate(self.options) | ||||||
|  |  | ||||||
|         return options_validator.devices |         return options_schema.devices | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def pwned(self) -> set[str]: | ||||||
|  |         """Extract pwned data for add-on options.""" | ||||||
|  |         options_schema = self.schema | ||||||
|  |         with suppress(vol.Invalid): | ||||||
|  |             options_schema.validate(self.options) | ||||||
|  |  | ||||||
|  |         return options_schema.pwned | ||||||
|  |  | ||||||
|     def save_persist(self) -> None: |     def save_persist(self) -> None: | ||||||
|         """Save data of add-on.""" |         """Save data of add-on.""" | ||||||
| @@ -422,7 +463,7 @@ class Addon(AddonModel): | |||||||
|         application = RE_WATCHDOG.match(url) |         application = RE_WATCHDOG.match(url) | ||||||
|  |  | ||||||
|         # extract arguments |         # extract arguments | ||||||
|         t_port = application.group("t_port") |         t_port = int(application.group("t_port")) | ||||||
|         t_proto = application.group("t_proto") |         t_proto = application.group("t_proto") | ||||||
|         s_prefix = application.group("s_prefix") or "" |         s_prefix = application.group("s_prefix") or "" | ||||||
|         s_suffix = application.group("s_suffix") or "" |         s_suffix = application.group("s_suffix") or "" | ||||||
| @@ -446,8 +487,8 @@ class Addon(AddonModel): | |||||||
|         # Make HTTP request |         # Make HTTP request | ||||||
|         try: |         try: | ||||||
|             url = f"{proto}://{self.ip_address}:{port}{s_suffix}" |             url = f"{proto}://{self.ip_address}:{port}{s_suffix}" | ||||||
|             async with self.sys_websession_ssl.get( |             async with self.sys_websession.get( | ||||||
|                 url, timeout=WATCHDOG_TIMEOUT |                 url, timeout=WATCHDOG_TIMEOUT, ssl=False | ||||||
|             ) as req: |             ) as req: | ||||||
|                 if req.status < 300: |                 if req.status < 300: | ||||||
|                     return True |                     return True | ||||||
| @@ -462,7 +503,7 @@ class Addon(AddonModel): | |||||||
|         await self.sys_homeassistant.secrets.reload() |         await self.sys_homeassistant.secrets.reload() | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             options = self.schema(self.options) |             options = self.schema.validate(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( | ||||||
| @@ -470,7 +511,7 @@ class Addon(AddonModel): | |||||||
|                 self.slug, |                 self.slug, | ||||||
|                 humanize_error(self.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) | ||||||
| @@ -498,8 +539,7 @@ class Addon(AddonModel): | |||||||
|  |  | ||||||
|         # Write pulse config |         # Write pulse config | ||||||
|         try: |         try: | ||||||
|             with self.path_pulse.open("w") as config_file: |             self.path_pulse.write_text(pulse_config, encoding="utf-8") | ||||||
|                 config_file.write(pulse_config) |  | ||||||
|         except OSError as err: |         except OSError as err: | ||||||
|             _LOGGER.error( |             _LOGGER.error( | ||||||
|                 "Add-on %s can't write pulse/client.config: %s", self.slug, err |                 "Add-on %s can't write pulse/client.config: %s", self.slug, err | ||||||
| @@ -547,11 +587,15 @@ class Addon(AddonModel): | |||||||
|             return True |             return True | ||||||
|  |  | ||||||
|         # merge options |         # merge options | ||||||
|         options = {**self.persist[ATTR_OPTIONS], **default_options} |         options = _OPTIONS_MERGER.merge( | ||||||
|  |             deepcopy(default_options), deepcopy(self.persist[ATTR_OPTIONS]) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         # create voluptuous |         # create voluptuous | ||||||
|         new_schema = vol.Schema( |         new_schema = vol.Schema( | ||||||
|             vol.All(dict, AddonOptions(self.coresys, new_raw_schema)) |             vol.All( | ||||||
|  |                 dict, AddonOptions(self.coresys, new_raw_schema, self.name, self.slug) | ||||||
|  |             ) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         # validate |         # validate | ||||||
| @@ -583,7 +627,7 @@ class Addon(AddonModel): | |||||||
|         try: |         try: | ||||||
|             await self.instance.run() |             await self.instance.run() | ||||||
|         except DockerRequestError as err: |         except DockerRequestError as err: | ||||||
|             self.state = AddonState.STOPPED |             self.state = AddonState.ERROR | ||||||
|             raise AddonsError() from err |             raise AddonsError() from err | ||||||
|         except DockerError as err: |         except DockerError as err: | ||||||
|             self.state = AddonState.ERROR |             self.state = AddonState.ERROR | ||||||
| @@ -596,6 +640,7 @@ class Addon(AddonModel): | |||||||
|         try: |         try: | ||||||
|             await self.instance.stop() |             await self.instance.stop() | ||||||
|         except DockerRequestError as err: |         except DockerRequestError as err: | ||||||
|  |             self.state = AddonState.ERROR | ||||||
|             raise AddonsError() from err |             raise AddonsError() from err | ||||||
|         except DockerError as err: |         except DockerError as err: | ||||||
|             self.state = AddonState.ERROR |             self.state = AddonState.ERROR | ||||||
| @@ -636,16 +681,34 @@ class Addon(AddonModel): | |||||||
|         Return a coroutine. |         Return a coroutine. | ||||||
|         """ |         """ | ||||||
|         if not self.with_stdin: |         if not self.with_stdin: | ||||||
|             _LOGGER.error("Add-on %s does not support writing to stdin!", self.slug) |             raise AddonsNotSupportedError( | ||||||
|             raise AddonsNotSupportedError() |                 f"Add-on {self.slug} does not support writing to stdin!", _LOGGER.error | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             return await self.instance.write_stdin(data) |             return await self.instance.write_stdin(data) | ||||||
|         except DockerError as err: |         except DockerError as err: | ||||||
|             raise AddonsError() from err |             raise AddonsError() from err | ||||||
|  |  | ||||||
|     async def snapshot(self, tar_file: tarfile.TarFile) -> None: |     async def _backup_command(self, command: str) -> None: | ||||||
|         """Snapshot state of an add-on.""" |         try: | ||||||
|  |             command_return = await self.instance.run_inside(command) | ||||||
|  |             if command_return.exit_code != 0: | ||||||
|  |                 _LOGGER.error( | ||||||
|  |                     "Pre-/Post backup command returned error code: %s", | ||||||
|  |                     command_return.exit_code, | ||||||
|  |                 ) | ||||||
|  |                 raise AddonsError() | ||||||
|  |         except DockerError as err: | ||||||
|  |             _LOGGER.error( | ||||||
|  |                 "Failed running pre-/post backup command %s: %s", command, err | ||||||
|  |             ) | ||||||
|  |             raise AddonsError() from err | ||||||
|  |  | ||||||
|  |     async def backup(self, tar_file: tarfile.TarFile) -> None: | ||||||
|  |         """Backup state of an add-on.""" | ||||||
|  |         is_running = await self.is_running() | ||||||
|  |  | ||||||
|         with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp: |         with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp: | ||||||
|             temp_path = Path(temp) |             temp_path = Path(temp) | ||||||
|  |  | ||||||
| @@ -666,71 +729,95 @@ class Addon(AddonModel): | |||||||
|             # 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 as err: |             except ConfigurationFileError as err: | ||||||
|                 _LOGGER.error("Can't save meta for %s", self.slug) |                 raise AddonsError( | ||||||
|                 raise AddonsError() from err |                     f"Can't save meta for {self.slug}", _LOGGER.error | ||||||
|  |                 ) 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) |                     await self.sys_host.apparmor.backup_profile(self.slug, profile) | ||||||
|                 except HostAppArmorError as err: |                 except HostAppArmorError as err: | ||||||
|                     _LOGGER.error("Can't backup AppArmor profile") |                     raise AddonsError( | ||||||
|                     raise AddonsError() from err |                         "Can't backup AppArmor profile", _LOGGER.error | ||||||
|  |                     ) from err | ||||||
|  |  | ||||||
|             # write into tarfile |             # write into tarfile | ||||||
|             def _write_tarfile(): |             def _write_tarfile(): | ||||||
|                 """Write tar inside loop.""" |                 """Write tar inside loop.""" | ||||||
|                 with tar_file as snapshot: |                 with tar_file as backup: | ||||||
|                     # Snapshot system |                     # Backup system | ||||||
|  |  | ||||||
|                     snapshot.add(temp, arcname=".") |                     backup.add(temp, arcname=".") | ||||||
|  |  | ||||||
|                     # Snapshot data |                     # Backup data | ||||||
|                     atomic_contents_add( |                     atomic_contents_add( | ||||||
|                         snapshot, |                         backup, | ||||||
|                         self.path_data, |                         self.path_data, | ||||||
|                         excludes=self.snapshot_exclude, |                         excludes=self.backup_exclude, | ||||||
|                         arcname="data", |                         arcname="data", | ||||||
|                     ) |                     ) | ||||||
|  |  | ||||||
|  |             if ( | ||||||
|  |                 is_running | ||||||
|  |                 and self.backup_mode == AddonBackupMode.HOT | ||||||
|  |                 and self.backup_pre is not None | ||||||
|  |             ): | ||||||
|  |                 await self._backup_command(self.backup_pre) | ||||||
|  |             elif is_running and self.backup_mode == AddonBackupMode.COLD: | ||||||
|  |                 _LOGGER.info("Shutdown add-on %s for cold backup", self.slug) | ||||||
|  |                 await self.instance.stop() | ||||||
|  |  | ||||||
|             try: |             try: | ||||||
|                 _LOGGER.info("Building snapshot for add-on %s", self.slug) |                 _LOGGER.info("Building backup for add-on %s", self.slug) | ||||||
|                 await self.sys_run_in_executor(_write_tarfile) |                 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) |                 raise AddonsError( | ||||||
|                 raise AddonsError() from err |                     f"Can't write tarfile {tar_file}: {err}", _LOGGER.error | ||||||
|  |                 ) from err | ||||||
|  |             finally: | ||||||
|  |                 if ( | ||||||
|  |                     is_running | ||||||
|  |                     and self.backup_mode == AddonBackupMode.HOT | ||||||
|  |                     and self.backup_post is not None | ||||||
|  |                 ): | ||||||
|  |                     await self._backup_command(self.backup_post) | ||||||
|  |                 elif is_running and self.backup_mode is AddonBackupMode.COLD: | ||||||
|  |                     _LOGGER.info("Starting add-on %s again", self.slug) | ||||||
|  |                     await self.start() | ||||||
|  |  | ||||||
|         _LOGGER.info("Finish snapshot for addon %s", self.slug) |         _LOGGER.info("Finish backup for addon %s", self.slug) | ||||||
|  |  | ||||||
|     async def restore(self, tar_file: tarfile.TarFile) -> None: |     async def restore(self, tar_file: tarfile.TarFile) -> None: | ||||||
|         """Restore state of an add-on.""" |         """Restore state of an add-on.""" | ||||||
|         with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp: |         with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp: | ||||||
|             # extract snapshot |             # extract backup | ||||||
|             def _extract_tarfile(): |             def _extract_tarfile(): | ||||||
|                 """Extract tar snapshot.""" |                 """Extract tar backup.""" | ||||||
|                 with tar_file as snapshot: |                 with tar_file as backup: | ||||||
|                     snapshot.extractall(path=Path(temp), members=secure_path(snapshot)) |                     backup.extractall(path=Path(temp), members=secure_path(backup)) | ||||||
|  |  | ||||||
|             try: |             try: | ||||||
|                 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) |                 raise AddonsError( | ||||||
|                 raise AddonsError() from err |                     f"Can't read tarfile {tar_file}: {err}", _LOGGER.error | ||||||
|  |                 ) from err | ||||||
|  |  | ||||||
|             # Read snapshot data |             # Read backup data | ||||||
|             try: |             try: | ||||||
|                 data = read_json_file(Path(temp, "addon.json")) |                 data = read_json_file(Path(temp, "addon.json")) | ||||||
|             except JsonFileError as err: |             except ConfigurationFileError as err: | ||||||
|                 raise AddonsError() from err |                 raise AddonsError() from err | ||||||
|  |  | ||||||
|             # Validate |             # Validate | ||||||
|             try: |             try: | ||||||
|                 data = SCHEMA_ADDON_SNAPSHOT(data) |                 data = SCHEMA_ADDON_BACKUP(data) | ||||||
|             except vol.Invalid as err: |             except vol.Invalid as err: | ||||||
|                 _LOGGER.error( |                 _LOGGER.error( | ||||||
|                     "Can't validate %s, snapshot data: %s", |                     "Can't validate %s, backup data: %s", | ||||||
|                     self.slug, |                     self.slug, | ||||||
|                     humanize_error(data, err), |                     humanize_error(data, err), | ||||||
|                 ) |                 ) | ||||||
| @@ -738,8 +825,10 @@ class Addon(AddonModel): | |||||||
|  |  | ||||||
|             # If available |             # If available | ||||||
|             if not self._available(data[ATTR_SYSTEM]): |             if not self._available(data[ATTR_SYSTEM]): | ||||||
|                 _LOGGER.error("Add-on %s is not available for this platform", self.slug) |                 raise AddonsNotSupportedError( | ||||||
|                 raise AddonsNotSupportedError() |                     f"Add-on {self.slug} is not available for this platform", | ||||||
|  |                     _LOGGER.error, | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|             # Restore local add-on information |             # Restore local add-on information | ||||||
|             _LOGGER.info("Restore config for addon %s", self.slug) |             _LOGGER.info("Restore config for addon %s", self.slug) | ||||||
| @@ -772,7 +861,11 @@ class Addon(AddonModel): | |||||||
|             # Restore data |             # Restore data | ||||||
|             def _restore_data(): |             def _restore_data(): | ||||||
|                 """Restore data.""" |                 """Restore data.""" | ||||||
|                 shutil.copytree(Path(temp, "data"), self.path_data, symlinks=True) |                 temp_data = Path(temp, "data") | ||||||
|  |                 if temp_data.is_dir(): | ||||||
|  |                     shutil.copytree(temp_data, self.path_data, symlinks=True) | ||||||
|  |                 else: | ||||||
|  |                     self.path_data.mkdir() | ||||||
|  |  | ||||||
|             _LOGGER.info("Restoring 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(): | ||||||
| @@ -780,8 +873,9 @@ class Addon(AddonModel): | |||||||
|             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) |                 raise AddonsError( | ||||||
|                 raise AddonsError() from err |                     f"Can't restore origin data: {err}", _LOGGER.error | ||||||
|  |                 ) from err | ||||||
|  |  | ||||||
|             # Restore AppArmor |             # Restore AppArmor | ||||||
|             profile_file = Path(temp, "apparmor.txt") |             profile_file = Path(temp, "apparmor.txt") | ||||||
|   | |||||||
| @@ -2,20 +2,28 @@ | |||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  |  | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import TYPE_CHECKING, Dict | from typing import TYPE_CHECKING | ||||||
|  |  | ||||||
| from awesomeversion import AwesomeVersion | from awesomeversion import AwesomeVersion | ||||||
|  |  | ||||||
| from ..const import ATTR_ARGS, ATTR_BUILD_FROM, ATTR_SQUASH, META_ADDON | from ..const import ( | ||||||
|  |     ATTR_ARGS, | ||||||
|  |     ATTR_BUILD_FROM, | ||||||
|  |     ATTR_LABELS, | ||||||
|  |     ATTR_SQUASH, | ||||||
|  |     FILE_SUFFIX_CONFIGURATION, | ||||||
|  |     META_ADDON, | ||||||
|  | ) | ||||||
| from ..coresys import CoreSys, CoreSysAttributes | from ..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: | ||||||
| @@ -23,9 +31,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.""" | ||||||
| @@ -34,9 +47,12 @@ class AddonBuild(JsonConfig, CoreSysAttributes): | |||||||
|     @property |     @property | ||||||
|     def base_image(self) -> str: |     def base_image(self) -> str: | ||||||
|         """Return base image for this add-on.""" |         """Return base image for this add-on.""" | ||||||
|         return self._data[ATTR_BUILD_FROM].get( |         if not self._data[ATTR_BUILD_FROM]: | ||||||
|             self.sys_arch.default, f"homeassistant/{self.sys_arch.default}-base:latest" |             return f"ghcr.io/home-assistant/{self.sys_arch.default}-base:latest" | ||||||
|         ) |  | ||||||
|  |         # Evaluate correct base image | ||||||
|  |         arch = self.sys_arch.match(list(self._data[ATTR_BUILD_FROM].keys())) | ||||||
|  |         return self._data[ATTR_BUILD_FROM][arch] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def squash(self) -> bool: |     def squash(self) -> bool: | ||||||
| @@ -44,10 +60,15 @@ class AddonBuild(JsonConfig, CoreSysAttributes): | |||||||
|         return self._data[ATTR_SQUASH] |         return self._data[ATTR_SQUASH] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def additional_args(self) -> Dict[str, str]: |     def additional_args(self) -> dict[str, str]: | ||||||
|         """Return additional Docker build arguments.""" |         """Return additional Docker build arguments.""" | ||||||
|         return self._data[ATTR_ARGS] |         return self._data[ATTR_ARGS] | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def additional_labels(self) -> dict[str, str]: | ||||||
|  |         """Return additional Docker labels.""" | ||||||
|  |         return self._data[ATTR_LABELS] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def is_valid(self) -> bool: |     def is_valid(self) -> bool: | ||||||
|         """Return true if the build env is valid.""" |         """Return true if the build env is valid.""" | ||||||
| @@ -64,7 +85,7 @@ class AddonBuild(JsonConfig, CoreSysAttributes): | |||||||
|             "path": str(self.addon.path_location), |             "path": str(self.addon.path_location), | ||||||
|             "tag": f"{self.addon.image}:{version!s}", |             "tag": f"{self.addon.image}:{version!s}", | ||||||
|             "pull": True, |             "pull": True, | ||||||
|             "forcerm": True, |             "forcerm": not self.sys_dev, | ||||||
|             "squash": self.squash, |             "squash": self.squash, | ||||||
|             "labels": { |             "labels": { | ||||||
|                 "io.hass.version": version, |                 "io.hass.version": version, | ||||||
| @@ -72,6 +93,7 @@ class AddonBuild(JsonConfig, CoreSysAttributes): | |||||||
|                 "io.hass.type": META_ADDON, |                 "io.hass.type": META_ADDON, | ||||||
|                 "io.hass.name": self._fix_label("name"), |                 "io.hass.name": self._fix_label("name"), | ||||||
|                 "io.hass.description": self._fix_label("description"), |                 "io.hass.description": self._fix_label("description"), | ||||||
|  |                 **self.additional_labels, | ||||||
|             }, |             }, | ||||||
|             "buildargs": { |             "buildargs": { | ||||||
|                 "BUILD_FROM": self.base_image, |                 "BUILD_FROM": self.base_image, | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								supervisor/addons/const.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								supervisor/addons/const.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | """Add-on static data.""" | ||||||
|  | from enum import Enum | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AddonBackupMode(str, Enum): | ||||||
|  |     """Backup mode of an Add-on.""" | ||||||
|  |  | ||||||
|  |     HOT = "hot" | ||||||
|  |     COLD = "cold" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ATTR_BACKUP = "backup" | ||||||
| @@ -1,7 +1,6 @@ | |||||||
| """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 | ||||||
| from typing import Any, Dict |  | ||||||
|  |  | ||||||
| from ..const import ( | from ..const import ( | ||||||
|     ATTR_IMAGE, |     ATTR_IMAGE, | ||||||
| @@ -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): | ||||||
|   | |||||||
| @@ -1,10 +1,11 @@ | |||||||
| """Init file for Supervisor add-ons.""" | """Init file for Supervisor add-ons.""" | ||||||
| from abc import ABC, abstractmethod | from abc import ABC, abstractmethod | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import Any, Awaitable, Dict, List, Optional | from typing import Any, Awaitable, Optional | ||||||
|  |  | ||||||
| from awesomeversion import AwesomeVersion, AwesomeVersionException | from awesomeversion import AwesomeVersion, AwesomeVersionException | ||||||
| import voluptuous as vol |  | ||||||
|  | from supervisor.addons.const import AddonBackupMode | ||||||
|  |  | ||||||
| from ..const import ( | from ..const import ( | ||||||
|     ATTR_ADVANCED, |     ATTR_ADVANCED, | ||||||
| @@ -12,6 +13,9 @@ from ..const import ( | |||||||
|     ATTR_ARCH, |     ATTR_ARCH, | ||||||
|     ATTR_AUDIO, |     ATTR_AUDIO, | ||||||
|     ATTR_AUTH_API, |     ATTR_AUTH_API, | ||||||
|  |     ATTR_BACKUP_EXCLUDE, | ||||||
|  |     ATTR_BACKUP_POST, | ||||||
|  |     ATTR_BACKUP_PRE, | ||||||
|     ATTR_BOOT, |     ATTR_BOOT, | ||||||
|     ATTR_DESCRIPTON, |     ATTR_DESCRIPTON, | ||||||
|     ATTR_DEVICES, |     ATTR_DEVICES, | ||||||
| @@ -31,7 +35,9 @@ from ..const import ( | |||||||
|     ATTR_HOST_PID, |     ATTR_HOST_PID, | ||||||
|     ATTR_IMAGE, |     ATTR_IMAGE, | ||||||
|     ATTR_INGRESS, |     ATTR_INGRESS, | ||||||
|  |     ATTR_INGRESS_STREAM, | ||||||
|     ATTR_INIT, |     ATTR_INIT, | ||||||
|  |     ATTR_JOURNALD, | ||||||
|     ATTR_KERNEL_MODULES, |     ATTR_KERNEL_MODULES, | ||||||
|     ATTR_LEGACY, |     ATTR_LEGACY, | ||||||
|     ATTR_LOCATON, |     ATTR_LOCATON, | ||||||
| @@ -45,16 +51,17 @@ 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, | ||||||
|     ATTR_SLUG, |     ATTR_SLUG, | ||||||
|     ATTR_SNAPSHOT_EXCLUDE, |  | ||||||
|     ATTR_STAGE, |     ATTR_STAGE, | ||||||
|     ATTR_STARTUP, |     ATTR_STARTUP, | ||||||
|     ATTR_STDIN, |     ATTR_STDIN, | ||||||
|     ATTR_TIMEOUT, |     ATTR_TIMEOUT, | ||||||
|     ATTR_TMPFS, |     ATTR_TMPFS, | ||||||
|  |     ATTR_TRANSLATIONS, | ||||||
|     ATTR_UART, |     ATTR_UART, | ||||||
|     ATTR_UDEV, |     ATTR_UDEV, | ||||||
|     ATTR_URL, |     ATTR_URL, | ||||||
| @@ -71,10 +78,12 @@ from ..const import ( | |||||||
|     AddonStartup, |     AddonStartup, | ||||||
| ) | ) | ||||||
| from ..coresys import CoreSys, CoreSysAttributes | from ..coresys import CoreSys, CoreSysAttributes | ||||||
|  | from ..docker.const import Capabilities | ||||||
|  | from .const import ATTR_BACKUP | ||||||
| from .options import AddonOptions, UiOptions | from .options import AddonOptions, UiOptions | ||||||
| from .validate import RE_SERVICE, RE_VOLUME | from .validate import RE_SERVICE, RE_VOLUME | ||||||
|  |  | ||||||
| Data = Dict[str, Any] | Data = dict[str, Any] | ||||||
|  |  | ||||||
|  |  | ||||||
| class AddonModel(CoreSysAttributes, ABC): | class AddonModel(CoreSysAttributes, ABC): | ||||||
| @@ -106,7 +115,7 @@ class AddonModel(CoreSysAttributes, ABC): | |||||||
|         return self._available(self.data) |         return self._available(self.data) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def options(self) -> Dict[str, Any]: |     def options(self) -> dict[str, Any]: | ||||||
|         """Return options with local changes.""" |         """Return options with local changes.""" | ||||||
|         return self.data[ATTR_OPTIONS] |         return self.data[ATTR_OPTIONS] | ||||||
|  |  | ||||||
| @@ -131,7 +140,7 @@ class AddonModel(CoreSysAttributes, ABC): | |||||||
|         return self.slug.replace("_", "-") |         return self.slug.replace("_", "-") | ||||||
|  |  | ||||||
|     @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.""" | ||||||
|         return [] |         return [] | ||||||
|  |  | ||||||
| @@ -175,14 +184,18 @@ class AddonModel(CoreSysAttributes, ABC): | |||||||
|             return None |             return None | ||||||
|  |  | ||||||
|         # Return data |         # Return data | ||||||
|         with readme.open("r") as readme_file: |         return readme.read_text(encoding="utf-8") | ||||||
|             return readme_file.read() |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def repository(self) -> str: |     def repository(self) -> str: | ||||||
|         """Return repository of add-on.""" |         """Return repository of add-on.""" | ||||||
|         return self.data[ATTR_REPOSITORY] |         return self.data[ATTR_REPOSITORY] | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def translations(self) -> dict: | ||||||
|  |         """Return add-on translations.""" | ||||||
|  |         return self.data[ATTR_TRANSLATIONS] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def latest_version(self) -> AwesomeVersion: |     def latest_version(self) -> AwesomeVersion: | ||||||
|         """Return latest version of add-on.""" |         """Return latest version of add-on.""" | ||||||
| @@ -214,7 +227,7 @@ class AddonModel(CoreSysAttributes, ABC): | |||||||
|         return self.data[ATTR_STAGE] |         return self.data[ATTR_STAGE] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def services_role(self) -> Dict[str, str]: |     def services_role(self) -> dict[str, str]: | ||||||
|         """Return dict of services with rights.""" |         """Return dict of services with rights.""" | ||||||
|         services_list = self.data.get(ATTR_SERVICES, []) |         services_list = self.data.get(ATTR_SERVICES, []) | ||||||
|  |  | ||||||
| @@ -227,17 +240,17 @@ class AddonModel(CoreSysAttributes, ABC): | |||||||
|         return services |         return services | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def discovery(self) -> List[str]: |     def discovery(self) -> list[str]: | ||||||
|         """Return list of discoverable components/platforms.""" |         """Return list of discoverable components/platforms.""" | ||||||
|         return self.data.get(ATTR_DISCOVERY, []) |         return self.data.get(ATTR_DISCOVERY, []) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def ports_description(self) -> Optional[Dict[str, str]]: |     def ports_description(self) -> Optional[dict[str, str]]: | ||||||
|         """Return descriptions of ports.""" |         """Return descriptions of ports.""" | ||||||
|         return self.data.get(ATTR_PORTS_DESCRIPTION) |         return self.data.get(ATTR_PORTS_DESCRIPTION) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def ports(self) -> Optional[Dict[str, Optional[int]]]: |     def ports(self) -> Optional[dict[str, Optional[int]]]: | ||||||
|         """Return ports of add-on.""" |         """Return ports of add-on.""" | ||||||
|         return self.data.get(ATTR_PORTS) |         return self.data.get(ATTR_PORTS) | ||||||
|  |  | ||||||
| @@ -297,17 +310,17 @@ class AddonModel(CoreSysAttributes, ABC): | |||||||
|         return self.data[ATTR_HOST_DBUS] |         return self.data[ATTR_HOST_DBUS] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def static_devices(self) -> List[Path]: |     def static_devices(self) -> list[Path]: | ||||||
|         """Return static devices of add-on.""" |         """Return static devices of add-on.""" | ||||||
|         return [Path(node) for node in self.data.get(ATTR_DEVICES, [])] |         return [Path(node) for node in self.data.get(ATTR_DEVICES, [])] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def environment(self) -> Optional[Dict[str, str]]: |     def environment(self) -> Optional[dict[str, str]]: | ||||||
|         """Return environment of add-on.""" |         """Return environment of add-on.""" | ||||||
|         return self.data.get(ATTR_ENVIRONMENT) |         return self.data.get(ATTR_ENVIRONMENT) | ||||||
|  |  | ||||||
|     @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, []) | ||||||
|  |  | ||||||
| @@ -346,9 +359,24 @@ class AddonModel(CoreSysAttributes, ABC): | |||||||
|         return self.data[ATTR_HASSIO_ROLE] |         return self.data[ATTR_HASSIO_ROLE] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def snapshot_exclude(self) -> List[str]: |     def backup_exclude(self) -> list[str]: | ||||||
|         """Return Exclude list for snapshot.""" |         """Return Exclude list for backup.""" | ||||||
|         return self.data.get(ATTR_SNAPSHOT_EXCLUDE, []) |         return self.data.get(ATTR_BACKUP_EXCLUDE, []) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def backup_pre(self) -> Optional[str]: | ||||||
|  |         """Return pre-backup command.""" | ||||||
|  |         return self.data.get(ATTR_BACKUP_PRE) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def backup_post(self) -> Optional[str]: | ||||||
|  |         """Return post-backup command.""" | ||||||
|  |         return self.data.get(ATTR_BACKUP_POST) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def backup_mode(self) -> AddonBackupMode: | ||||||
|  |         """Return if backup is hot/cold.""" | ||||||
|  |         return self.data[ATTR_BACKUP] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def default_init(self) -> bool: |     def default_init(self) -> bool: | ||||||
| @@ -370,6 +398,11 @@ class AddonModel(CoreSysAttributes, ABC): | |||||||
|         """Return True if the add-on access support ingress.""" |         """Return True if the add-on access support ingress.""" | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def ingress_stream(self) -> bool: | ||||||
|  |         """Return True if post requests to ingress should be streamed.""" | ||||||
|  |         return self.data[ATTR_INGRESS_STREAM] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def with_gpio(self) -> bool: |     def with_gpio(self) -> bool: | ||||||
|         """Return True if the add-on access to GPIO interface.""" |         """Return True if the add-on access to GPIO interface.""" | ||||||
| @@ -395,6 +428,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.""" | ||||||
| @@ -456,12 +494,12 @@ class AddonModel(CoreSysAttributes, ABC): | |||||||
|         return self.path_documentation.exists() |         return self.path_documentation.exists() | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def supported_arch(self) -> List[str]: |     def supported_arch(self) -> list[str]: | ||||||
|         """Return list of supported arch.""" |         """Return list of supported arch.""" | ||||||
|         return self.data[ATTR_ARCH] |         return self.data[ATTR_ARCH] | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def supported_machine(self) -> List[str]: |     def supported_machine(self) -> list[str]: | ||||||
|         """Return list of supported machine.""" |         """Return list of supported machine.""" | ||||||
|         return self.data.get(ATTR_MACHINE, []) |         return self.data.get(ATTR_MACHINE, []) | ||||||
|  |  | ||||||
| @@ -476,7 +514,7 @@ class AddonModel(CoreSysAttributes, ABC): | |||||||
|         return ATTR_IMAGE not in self.data |         return ATTR_IMAGE not in self.data | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def map_volumes(self) -> Dict[str, str]: |     def map_volumes(self) -> dict[str, str]: | ||||||
|         """Return a dict of {volume: policy} from add-on.""" |         """Return a dict of {volume: policy} from add-on.""" | ||||||
|         volumes = {} |         volumes = {} | ||||||
|         for volume in self.data[ATTR_MAP]: |         for volume in self.data[ATTR_MAP]: | ||||||
| @@ -518,16 +556,16 @@ class AddonModel(CoreSysAttributes, ABC): | |||||||
|         return Path(self.path_location, "apparmor.txt") |         return Path(self.path_location, "apparmor.txt") | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def schema(self) -> vol.Schema: |     def schema(self) -> AddonOptions: | ||||||
|         """Create a schema for add-on options.""" |         """Return Addon options validation object.""" | ||||||
|         raw_schema = self.data[ATTR_SCHEMA] |         raw_schema = self.data[ATTR_SCHEMA] | ||||||
|  |  | ||||||
|         if isinstance(raw_schema, bool): |         if isinstance(raw_schema, bool): | ||||||
|             raw_schema = {} |             raw_schema = {} | ||||||
|         return vol.Schema(vol.All(dict, AddonOptions(self.coresys, raw_schema))) |  | ||||||
|  |         return 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[any, any]]]: | ||||||
|         """Create a UI schema for add-on options.""" |         """Create a UI schema for add-on options.""" | ||||||
|         raw_schema = self.data[ATTR_SCHEMA] |         raw_schema = self.data[ATTR_SCHEMA] | ||||||
|  |  | ||||||
| @@ -535,6 +573,11 @@ class AddonModel(CoreSysAttributes, ABC): | |||||||
|             return None |             return None | ||||||
|         return UiOptions(self.coresys)(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.""" | ||||||
|         if not isinstance(other, AddonModel): |         if not isinstance(other, AddonModel): | ||||||
| @@ -579,9 +622,9 @@ class AddonModel(CoreSysAttributes, ABC): | |||||||
|         """Uninstall this add-on.""" |         """Uninstall this add-on.""" | ||||||
|         return self.sys_addons.uninstall(self.slug) |         return self.sys_addons.uninstall(self.slug) | ||||||
|  |  | ||||||
|     def update(self) -> Awaitable[None]: |     def update(self, backup: Optional[bool] = False) -> Awaitable[None]: | ||||||
|         """Update this add-on.""" |         """Update this add-on.""" | ||||||
|         return self.sys_addons.update(self.slug) |         return self.sys_addons.update(self.slug, backup=backup) | ||||||
|  |  | ||||||
|     def rebuild(self) -> Awaitable[None]: |     def rebuild(self) -> Awaitable[None]: | ||||||
|         """Rebuild this add-on.""" |         """Rebuild this add-on.""" | ||||||
|   | |||||||
| @@ -1,8 +1,9 @@ | |||||||
| """Add-on Options / UI rendering.""" | """Add-on Options / UI rendering.""" | ||||||
|  | import hashlib | ||||||
| import logging | import logging | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| import re | import re | ||||||
| from typing import Any, Dict, List, Set, Union | from typing import Any, Union | ||||||
|  |  | ||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
|  |  | ||||||
| @@ -58,14 +59,20 @@ class AddonOptions(CoreSysAttributes): | |||||||
|     """Validate Add-ons Options.""" |     """Validate Add-ons Options.""" | ||||||
|  |  | ||||||
|     def __init__( |     def __init__( | ||||||
|         self, |         self, coresys: CoreSys, raw_schema: dict[str, Any], name: str, slug: str | ||||||
|         coresys: CoreSys, |  | ||||||
|         raw_schema: Dict[str, Any], |  | ||||||
|     ): |     ): | ||||||
|         """Validate schema.""" |         """Validate schema.""" | ||||||
|         self.coresys: CoreSys = coresys |         self.coresys: CoreSys = coresys | ||||||
|         self.raw_schema: Dict[str, Any] = raw_schema |         self.raw_schema: dict[str, Any] = raw_schema | ||||||
|         self.devices: Set[Device] = set() |         self.devices: set[Device] = set() | ||||||
|  |         self.pwned: set[str] = set() | ||||||
|  |         self._name = name | ||||||
|  |         self._slug = slug | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def validate(self) -> vol.Schema: | ||||||
|  |         """Create a schema for add-on options.""" | ||||||
|  |         return vol.Schema(vol.All(dict, self)) | ||||||
|  |  | ||||||
|     def __call__(self, struct): |     def __call__(self, struct): | ||||||
|         """Create schema validator for add-ons options.""" |         """Create schema validator for add-ons options.""" | ||||||
| @@ -75,7 +82,12 @@ class AddonOptions(CoreSysAttributes): | |||||||
|         for key, value in struct.items(): |         for key, value in struct.items(): | ||||||
|             # Ignore unknown options / remove from list |             # Ignore unknown options / remove from list | ||||||
|             if key not in self.raw_schema: |             if key not in self.raw_schema: | ||||||
|                 _LOGGER.warning("Unknown options %s", key) |                 _LOGGER.warning( | ||||||
|  |                     "Option '%s' does not exist in the schema for %s (%s)", | ||||||
|  |                     key, | ||||||
|  |                     self._name, | ||||||
|  |                     self._slug, | ||||||
|  |                 ) | ||||||
|                 continue |                 continue | ||||||
|  |  | ||||||
|             typ = self.raw_schema[key] |             typ = self.raw_schema[key] | ||||||
| @@ -90,7 +102,9 @@ class AddonOptions(CoreSysAttributes): | |||||||
|                     # normal value |                     # normal value | ||||||
|                     options[key] = self._single_validate(typ, value, key) |                     options[key] = self._single_validate(typ, value, key) | ||||||
|             except (IndexError, KeyError): |             except (IndexError, KeyError): | ||||||
|                 raise vol.Invalid(f"Type error for {key}") from None |                 raise vol.Invalid( | ||||||
|  |                     f"Type error for option '{key}' in {self._name} ({self._slug})" | ||||||
|  |                 ) from None | ||||||
|  |  | ||||||
|         self._check_missing_options(self.raw_schema, options, "root") |         self._check_missing_options(self.raw_schema, options, "root") | ||||||
|         return options |         return options | ||||||
| @@ -100,20 +114,26 @@ class AddonOptions(CoreSysAttributes): | |||||||
|         """Validate a single element.""" |         """Validate a single element.""" | ||||||
|         # if required argument |         # if required argument | ||||||
|         if value is None: |         if value is None: | ||||||
|             raise vol.Invalid(f"Missing required option '{key}'") from None |             raise vol.Invalid( | ||||||
|  |                 f"Missing required option '{key}' in {self._name} ({self._slug})" | ||||||
|  |             ) from None | ||||||
|  |  | ||||||
|         # Lookup secret |         # Lookup secret | ||||||
|         if str(value).startswith("!secret "): |         if str(value).startswith("!secret "): | ||||||
|             secret: str = value.partition(" ")[2] |             secret: str = value.partition(" ")[2] | ||||||
|             value = self.sys_homeassistant.secrets.get(secret) |             value = self.sys_homeassistant.secrets.get(secret) | ||||||
|             if value is None: |             if value is None: | ||||||
|                 raise vol.Invalid(f"Unknown secret {secret}") from None |                 raise vol.Invalid( | ||||||
|  |                     f"Unknown secret '{secret}' in {self._name} ({self._slug})" | ||||||
|  |                 ) from None | ||||||
|  |  | ||||||
|         # parse extend data from type |         # parse extend data from type | ||||||
|         match = RE_SCHEMA_ELEMENT.match(typ) |         match = RE_SCHEMA_ELEMENT.match(typ) | ||||||
|  |  | ||||||
|         if not match: |         if not match: | ||||||
|             raise vol.Invalid(f"Unknown type {typ}") from None |             raise vol.Invalid( | ||||||
|  |                 f"Unknown type '{typ}' in {self._name} ({self._slug})" | ||||||
|  |             ) from None | ||||||
|  |  | ||||||
|         # prepare range |         # prepare range | ||||||
|         range_args = {} |         range_args = {} | ||||||
| @@ -123,6 +143,8 @@ class AddonOptions(CoreSysAttributes): | |||||||
|                 range_args[group_name[2:]] = float(group_value) |                 range_args[group_name[2:]] = float(group_value) | ||||||
|  |  | ||||||
|         if typ.startswith(_STR) or typ.startswith(_PASSWORD): |         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) |             return vol.All(str(value), vol.Range(**range_args))(value) | ||||||
|         elif typ.startswith(_INT): |         elif typ.startswith(_INT): | ||||||
|             return vol.All(vol.Coerce(int), vol.Range(**range_args))(value) |             return vol.All(vol.Coerce(int), vol.Range(**range_args))(value) | ||||||
| @@ -144,7 +166,9 @@ class AddonOptions(CoreSysAttributes): | |||||||
|             try: |             try: | ||||||
|                 device = self.sys_hardware.get_by_path(Path(value)) |                 device = self.sys_hardware.get_by_path(Path(value)) | ||||||
|             except HardwareNotFound: |             except HardwareNotFound: | ||||||
|                 raise vol.Invalid(f"Device {value} does not exists!") from None |                 raise vol.Invalid( | ||||||
|  |                     f"Device '{value}' does not exist in {self._name} ({self._slug})" | ||||||
|  |                 ) from None | ||||||
|  |  | ||||||
|             # Have filter |             # Have filter | ||||||
|             if match.group("filter"): |             if match.group("filter"): | ||||||
| @@ -152,22 +176,26 @@ class AddonOptions(CoreSysAttributes): | |||||||
|                 device_filter = _create_device_filter(str_filter) |                 device_filter = _create_device_filter(str_filter) | ||||||
|                 if device not in self.sys_hardware.filter_devices(**device_filter): |                 if device not in self.sys_hardware.filter_devices(**device_filter): | ||||||
|                     raise vol.Invalid( |                     raise vol.Invalid( | ||||||
|                         f"Device {value} don't match the filter {str_filter}!" |                         f"Device '{value}' don't match the filter {str_filter}! in {self._name} ({self._slug})" | ||||||
|                     ) |                     ) | ||||||
|  |  | ||||||
|             # Device valid |             # Device valid | ||||||
|             self.devices.add(device) |             self.devices.add(device) | ||||||
|             return str(device.path) |             return str(device.path) | ||||||
|  |  | ||||||
|         raise vol.Invalid(f"Fatal error for {key} type {typ}") from None |         raise vol.Invalid( | ||||||
|  |             f"Fatal error for option '{key}' with type '{typ}' in {self._name} ({self._slug})" | ||||||
|  |         ) from None | ||||||
|  |  | ||||||
|     def _nested_validate_list(self, typ: Any, data_list: List[Any], key: str): |     def _nested_validate_list(self, typ: Any, data_list: list[Any], key: str): | ||||||
|         """Validate nested items.""" |         """Validate nested items.""" | ||||||
|         options = [] |         options = [] | ||||||
|  |  | ||||||
|         # Make sure it is a list |         # Make sure it is a list | ||||||
|         if not isinstance(data_list, list): |         if not isinstance(data_list, list): | ||||||
|             raise vol.Invalid(f"Invalid list for {key}") from None |             raise vol.Invalid( | ||||||
|  |                 f"Invalid list for option '{key}' in {self._name} ({self._slug})" | ||||||
|  |             ) from None | ||||||
|  |  | ||||||
|         # Process list |         # Process list | ||||||
|         for element in data_list: |         for element in data_list: | ||||||
| @@ -181,20 +209,24 @@ class AddonOptions(CoreSysAttributes): | |||||||
|         return options |         return options | ||||||
|  |  | ||||||
|     def _nested_validate_dict( |     def _nested_validate_dict( | ||||||
|         self, typ: Dict[Any, Any], data_dict: Dict[Any, Any], key: str |         self, typ: dict[Any, Any], data_dict: dict[Any, Any], key: str | ||||||
|     ): |     ): | ||||||
|         """Validate nested items.""" |         """Validate nested items.""" | ||||||
|         options = {} |         options = {} | ||||||
|  |  | ||||||
|         # Make sure it is a dict |         # Make sure it is a dict | ||||||
|         if not isinstance(data_dict, dict): |         if not isinstance(data_dict, dict): | ||||||
|             raise vol.Invalid(f"Invalid dict for {key}") from None |             raise vol.Invalid( | ||||||
|  |                 f"Invalid dict for option '{key}' in {self._name} ({self._slug})" | ||||||
|  |             ) from None | ||||||
|  |  | ||||||
|         # Process dict |         # Process dict | ||||||
|         for c_key, c_value in data_dict.items(): |         for c_key, c_value in data_dict.items(): | ||||||
|             # Ignore unknown options / remove from list |             # Ignore unknown options / remove from list | ||||||
|             if c_key not in typ: |             if c_key not in typ: | ||||||
|                 _LOGGER.warning("Unknown options %s", c_key) |                 _LOGGER.warning( | ||||||
|  |                     "Unknown option '%s' for %s (%s)", c_key, self._name, self._slug | ||||||
|  |                 ) | ||||||
|                 continue |                 continue | ||||||
|  |  | ||||||
|             # Nested? |             # Nested? | ||||||
| @@ -209,14 +241,23 @@ class AddonOptions(CoreSysAttributes): | |||||||
|         return options |         return options | ||||||
|  |  | ||||||
|     def _check_missing_options( |     def _check_missing_options( | ||||||
|         self, origin: Dict[Any, Any], exists: Dict[Any, Any], root: str |         self, origin: dict[Any, Any], exists: dict[Any, Any], root: str | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         """Check if all options are exists.""" |         """Check if all options are exists.""" | ||||||
|         missing = set(origin) - set(exists) |         missing = set(origin) - set(exists) | ||||||
|         for miss_opt in missing: |         for miss_opt in missing: | ||||||
|             if isinstance(origin[miss_opt], str) and origin[miss_opt].endswith("?"): |             miss_schema = origin[miss_opt] | ||||||
|  |  | ||||||
|  |             # If its a list then value in list decides if its optional like ["str?"] | ||||||
|  |             if isinstance(miss_schema, list) and len(miss_schema) > 0: | ||||||
|  |                 miss_schema = miss_schema[0] | ||||||
|  |  | ||||||
|  |             if isinstance(miss_schema, str) and miss_schema.endswith("?"): | ||||||
|                 continue |                 continue | ||||||
|             raise vol.Invalid(f"Missing option {miss_opt} in {root}") from None |  | ||||||
|  |             raise vol.Invalid( | ||||||
|  |                 f"Missing option '{miss_opt}' in {root} in {self._name} ({self._slug})" | ||||||
|  |             ) from None | ||||||
|  |  | ||||||
|  |  | ||||||
| class UiOptions(CoreSysAttributes): | class UiOptions(CoreSysAttributes): | ||||||
| @@ -226,9 +267,9 @@ class UiOptions(CoreSysAttributes): | |||||||
|         """Initialize UI option render.""" |         """Initialize UI option render.""" | ||||||
|         self.coresys = coresys |         self.coresys = coresys | ||||||
|  |  | ||||||
|     def __call__(self, raw_schema: Dict[str, Any]) -> List[Dict[str, Any]]: |     def __call__(self, raw_schema: dict[str, Any]) -> list[dict[str, Any]]: | ||||||
|         """Generate UI schema.""" |         """Generate UI schema.""" | ||||||
|         ui_schema: List[Dict[str, Any]] = [] |         ui_schema: list[dict[str, Any]] = [] | ||||||
|  |  | ||||||
|         # read options |         # read options | ||||||
|         for key, value in raw_schema.items(): |         for key, value in raw_schema.items(): | ||||||
| @@ -246,13 +287,13 @@ class UiOptions(CoreSysAttributes): | |||||||
|  |  | ||||||
|     def _single_ui_option( |     def _single_ui_option( | ||||||
|         self, |         self, | ||||||
|         ui_schema: List[Dict[str, Any]], |         ui_schema: list[dict[str, Any]], | ||||||
|         value: str, |         value: str, | ||||||
|         key: str, |         key: str, | ||||||
|         multiple: bool = False, |         multiple: bool = False, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         """Validate a single element.""" |         """Validate a single element.""" | ||||||
|         ui_node: Dict[str, Union[str, bool, float, List[str]]] = {"name": key} |         ui_node: dict[str, Union[str, bool, float, list[str]]] = {"name": key} | ||||||
|  |  | ||||||
|         # If multiple |         # If multiple | ||||||
|         if multiple: |         if multiple: | ||||||
| @@ -324,8 +365,8 @@ class UiOptions(CoreSysAttributes): | |||||||
|  |  | ||||||
|     def _nested_ui_list( |     def _nested_ui_list( | ||||||
|         self, |         self, | ||||||
|         ui_schema: List[Dict[str, Any]], |         ui_schema: list[dict[str, Any]], | ||||||
|         option_list: List[Any], |         option_list: list[Any], | ||||||
|         key: str, |         key: str, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         """UI nested list items.""" |         """UI nested list items.""" | ||||||
| @@ -342,8 +383,8 @@ class UiOptions(CoreSysAttributes): | |||||||
|  |  | ||||||
|     def _nested_ui_dict( |     def _nested_ui_dict( | ||||||
|         self, |         self, | ||||||
|         ui_schema: List[Dict[str, Any]], |         ui_schema: list[dict[str, Any]], | ||||||
|         option_dict: Dict[str, Any], |         option_dict: dict[str, Any], | ||||||
|         key: str, |         key: str, | ||||||
|         multiple: bool = False, |         multiple: bool = False, | ||||||
|     ) -> None: |     ) -> None: | ||||||
| @@ -367,7 +408,7 @@ class UiOptions(CoreSysAttributes): | |||||||
|         ui_schema.append(ui_node) |         ui_schema.append(ui_node) | ||||||
|  |  | ||||||
|  |  | ||||||
| def _create_device_filter(str_filter: str) -> Dict[str, Any]: | def _create_device_filter(str_filter: str) -> dict[str, Any]: | ||||||
|     """Generate device Filter.""" |     """Generate device Filter.""" | ||||||
|     raw_filter = dict(value.split("=") for value in str_filter.split(";")) |     raw_filter = dict(value.split("=") for value in str_filter.split(";")) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 ( | ||||||
|         privilege in addon.privileged |         any( | ||||||
|         for privilege in ( |             privilege in addon.privileged | ||||||
|             PRIVILEGED_NET_ADMIN, |             for privilege in ( | ||||||
|             PRIVILEGED_SYS_ADMIN, |                 Capabilities.NET_ADMIN, | ||||||
|             PRIVILEGED_SYS_RAWIO, |                 Capabilities.SYS_ADMIN, | ||||||
|             PRIVILEGED_SYS_PTRACE, |                 Capabilities.SYS_RAWIO, | ||||||
|             PRIVILEGED_SYS_MODULE, |                 Capabilities.SYS_PTRACE, | ||||||
|             PRIVILEGED_DAC_READ_SEARCH, |                 Capabilities.SYS_MODULE, | ||||||
|  |                 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,11 +2,13 @@ | |||||||
| import logging | import logging | ||||||
| import re | import re | ||||||
| import secrets | import secrets | ||||||
| from typing import Any, Dict | from typing import Any | ||||||
| import uuid | import uuid | ||||||
|  |  | ||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
|  |  | ||||||
|  | from supervisor.addons.const import AddonBackupMode | ||||||
|  |  | ||||||
| from ..const import ( | from ..const import ( | ||||||
|     ARCH_ALL, |     ARCH_ALL, | ||||||
|     ATTR_ACCESS_TOKEN, |     ATTR_ACCESS_TOKEN, | ||||||
| @@ -19,8 +21,12 @@ from ..const import ( | |||||||
|     ATTR_AUDIO_OUTPUT, |     ATTR_AUDIO_OUTPUT, | ||||||
|     ATTR_AUTH_API, |     ATTR_AUTH_API, | ||||||
|     ATTR_AUTO_UPDATE, |     ATTR_AUTO_UPDATE, | ||||||
|  |     ATTR_BACKUP_EXCLUDE, | ||||||
|  |     ATTR_BACKUP_POST, | ||||||
|  |     ATTR_BACKUP_PRE, | ||||||
|     ATTR_BOOT, |     ATTR_BOOT, | ||||||
|     ATTR_BUILD_FROM, |     ATTR_BUILD_FROM, | ||||||
|  |     ATTR_CONFIGURATION, | ||||||
|     ATTR_DESCRIPTON, |     ATTR_DESCRIPTON, | ||||||
|     ATTR_DEVICES, |     ATTR_DEVICES, | ||||||
|     ATTR_DEVICETREE, |     ATTR_DEVICETREE, | ||||||
| @@ -42,9 +48,12 @@ from ..const import ( | |||||||
|     ATTR_INGRESS_ENTRY, |     ATTR_INGRESS_ENTRY, | ||||||
|     ATTR_INGRESS_PANEL, |     ATTR_INGRESS_PANEL, | ||||||
|     ATTR_INGRESS_PORT, |     ATTR_INGRESS_PORT, | ||||||
|  |     ATTR_INGRESS_STREAM, | ||||||
|     ATTR_INGRESS_TOKEN, |     ATTR_INGRESS_TOKEN, | ||||||
|     ATTR_INIT, |     ATTR_INIT, | ||||||
|  |     ATTR_JOURNALD, | ||||||
|     ATTR_KERNEL_MODULES, |     ATTR_KERNEL_MODULES, | ||||||
|  |     ATTR_LABELS, | ||||||
|     ATTR_LEGACY, |     ATTR_LEGACY, | ||||||
|     ATTR_LOCATON, |     ATTR_LOCATON, | ||||||
|     ATTR_MACHINE, |     ATTR_MACHINE, | ||||||
| @@ -59,11 +68,11 @@ 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, | ||||||
|     ATTR_SLUG, |     ATTR_SLUG, | ||||||
|     ATTR_SNAPSHOT_EXCLUDE, |  | ||||||
|     ATTR_SQUASH, |     ATTR_SQUASH, | ||||||
|     ATTR_STAGE, |     ATTR_STAGE, | ||||||
|     ATTR_STARTUP, |     ATTR_STARTUP, | ||||||
| @@ -72,6 +81,7 @@ from ..const import ( | |||||||
|     ATTR_SYSTEM, |     ATTR_SYSTEM, | ||||||
|     ATTR_TIMEOUT, |     ATTR_TIMEOUT, | ||||||
|     ATTR_TMPFS, |     ATTR_TMPFS, | ||||||
|  |     ATTR_TRANSLATIONS, | ||||||
|     ATTR_UART, |     ATTR_UART, | ||||||
|     ATTR_UDEV, |     ATTR_UDEV, | ||||||
|     ATTR_URL, |     ATTR_URL, | ||||||
| @@ -82,7 +92,6 @@ from ..const import ( | |||||||
|     ATTR_VIDEO, |     ATTR_VIDEO, | ||||||
|     ATTR_WATCHDOG, |     ATTR_WATCHDOG, | ||||||
|     ATTR_WEBUI, |     ATTR_WEBUI, | ||||||
|     PRIVILEGED_ALL, |  | ||||||
|     ROLE_ALL, |     ROLE_ALL, | ||||||
|     ROLE_DEFAULT, |     ROLE_DEFAULT, | ||||||
|     AddonBoot, |     AddonBoot, | ||||||
| @@ -91,6 +100,7 @@ from ..const import ( | |||||||
|     AddonState, |     AddonState, | ||||||
| ) | ) | ||||||
| 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_image, |     docker_image, | ||||||
|     docker_ports, |     docker_ports, | ||||||
| @@ -100,6 +110,7 @@ from ..validate import ( | |||||||
|     uuid_match, |     uuid_match, | ||||||
|     version_tag, |     version_tag, | ||||||
| ) | ) | ||||||
|  | from .const import ATTR_BACKUP | ||||||
| from .options import RE_SCHEMA_ELEMENT | from .options import RE_SCHEMA_ELEMENT | ||||||
|  |  | ||||||
| _LOGGER: logging.Logger = logging.getLogger(__name__) | _LOGGER: logging.Logger = logging.getLogger(__name__) | ||||||
| @@ -117,6 +128,7 @@ SCHEMA_ELEMENT = vol.Match(RE_SCHEMA_ELEMENT) | |||||||
| RE_MACHINE = re.compile( | RE_MACHINE = re.compile( | ||||||
|     r"^!?(?:" |     r"^!?(?:" | ||||||
|     r"|intel-nuc" |     r"|intel-nuc" | ||||||
|  |     r"|generic-x86-64" | ||||||
|     r"|odroid-c2" |     r"|odroid-c2" | ||||||
|     r"|odroid-c4" |     r"|odroid-c4" | ||||||
|     r"|odroid-n2" |     r"|odroid-n2" | ||||||
| @@ -136,10 +148,38 @@ RE_MACHINE = re.compile( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _warn_addon_config(config: dict[str, Any]): | ||||||
|  |     """Warn about miss configs.""" | ||||||
|  |     name = config.get(ATTR_NAME) | ||||||
|  |     if not name: | ||||||
|  |         raise vol.Invalid("Invalid Add-on config!") | ||||||
|  |  | ||||||
|  |     if config.get(ATTR_FULL_ACCESS, False) and ( | ||||||
|  |         config.get(ATTR_DEVICES) | ||||||
|  |         or config.get(ATTR_UART) | ||||||
|  |         or config.get(ATTR_USB) | ||||||
|  |         or config.get(ATTR_GPIO) | ||||||
|  |     ): | ||||||
|  |         _LOGGER.warning( | ||||||
|  |             "Add-on have full device access, and selective device access in the configuration. Please report this to the maintainer of %s", | ||||||
|  |             name, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     if config.get(ATTR_BACKUP, AddonBackupMode.HOT) == AddonBackupMode.COLD and ( | ||||||
|  |         config.get(ATTR_BACKUP_POST) or config.get(ATTR_BACKUP_PRE) | ||||||
|  |     ): | ||||||
|  |         _LOGGER.warning( | ||||||
|  |             "Add-on which only support COLD backups trying to use post/pre commands. Please report this to the maintainer of %s", | ||||||
|  |             name, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
| def _migrate_addon_config(protocol=False): | def _migrate_addon_config(protocol=False): | ||||||
|     """Migrate addon config.""" |     """Migrate addon config.""" | ||||||
|  |  | ||||||
|     def _migrate(config: Dict[str, Any]): |     def _migrate(config: dict[str, Any]): | ||||||
|         name = config.get(ATTR_NAME) |         name = config.get(ATTR_NAME) | ||||||
|         if not name: |         if not name: | ||||||
|             raise vol.Invalid("Invalid Add-on config!") |             raise vol.Invalid("Invalid Add-on config!") | ||||||
| @@ -185,6 +225,23 @@ def _migrate_addon_config(protocol=False): | |||||||
|                 ) |                 ) | ||||||
|             config[ATTR_TMPFS] = True |             config[ATTR_TMPFS] = True | ||||||
|  |  | ||||||
|  |         # 2021-06 "snapshot" renamed to "backup" | ||||||
|  |         for entry in ( | ||||||
|  |             "snapshot_exclude", | ||||||
|  |             "snapshot_post", | ||||||
|  |             "snapshot_pre", | ||||||
|  |             "snapshot", | ||||||
|  |         ): | ||||||
|  |             if entry in config: | ||||||
|  |                 new_entry = entry.replace("snapshot", "backup") | ||||||
|  |                 config[new_entry] = config.pop(entry) | ||||||
|  |                 _LOGGER.warning( | ||||||
|  |                     "Add-on config '%s' is deprecated, '%s' should be used instead. Please report this to the maintainer of %s", | ||||||
|  |                     entry, | ||||||
|  |                     new_entry, | ||||||
|  |                     name, | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|         return config |         return config | ||||||
|  |  | ||||||
|     return _migrate |     return _migrate | ||||||
| @@ -210,7 +267,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema( | |||||||
|         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( |         vol.Optional(ATTR_WATCHDOG): vol.Match( | ||||||
|             r"^(?:https?|\[PROTO:\w+\]|tcp):\/\/\[HOST\]:\[PORT:\d+\].*$" |             r"^(?:https?|\[PROTO:\w+\]|tcp):\/\/\[HOST\]:(\[PORT:\d+\]|\d+).*$" | ||||||
|         ), |         ), | ||||||
|         vol.Optional(ATTR_WEBUI): vol.Match( |         vol.Optional(ATTR_WEBUI): vol.Match( | ||||||
|             r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$" |             r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$" | ||||||
| @@ -220,10 +277,11 @@ _SCHEMA_ADDON_CONFIG = vol.Schema( | |||||||
|             network_port, vol.Equal(0) |             network_port, vol.Equal(0) | ||||||
|         ), |         ), | ||||||
|         vol.Optional(ATTR_INGRESS_ENTRY): str, |         vol.Optional(ATTR_INGRESS_ENTRY): str, | ||||||
|  |         vol.Optional(ATTR_INGRESS_STREAM, default=False): vol.Boolean(), | ||||||
|         vol.Optional(ATTR_PANEL_ICON, default="mdi:puzzle"): str, |         vol.Optional(ATTR_PANEL_ICON, default="mdi:puzzle"): str, | ||||||
|         vol.Optional(ATTR_PANEL_TITLE): 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(version_tag), |         vol.Optional(ATTR_HOMEASSISTANT): 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(), | ||||||
| @@ -233,7 +291,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema( | |||||||
|         vol.Optional(ATTR_TMPFS, default=False): vol.Boolean(), |         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*"): 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(), | ||||||
| @@ -243,6 +301,7 @@ _SCHEMA_ADDON_CONFIG = vol.Schema( | |||||||
|         vol.Optional(ATTR_UART, 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(), | ||||||
| @@ -252,7 +311,12 @@ _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): [str], |         vol.Optional(ATTR_BACKUP_EXCLUDE): [str], | ||||||
|  |         vol.Optional(ATTR_BACKUP_PRE): str, | ||||||
|  |         vol.Optional(ATTR_BACKUP_POST): str, | ||||||
|  |         vol.Optional(ATTR_BACKUP, default=AddonBackupMode.HOT): vol.Coerce( | ||||||
|  |             AddonBackupMode | ||||||
|  |         ), | ||||||
|         vol.Optional(ATTR_OPTIONS, default={}): dict, |         vol.Optional(ATTR_OPTIONS, default={}): dict, | ||||||
|         vol.Optional(ATTR_SCHEMA, default={}): vol.Any( |         vol.Optional(ATTR_SCHEMA, default={}): vol.Any( | ||||||
|             vol.Schema( |             vol.Schema( | ||||||
| @@ -275,11 +339,14 @@ _SCHEMA_ADDON_CONFIG = vol.Schema( | |||||||
|         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), _SCHEMA_ADDON_CONFIG) | 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 | ||||||
| @@ -289,9 +356,25 @@ SCHEMA_BUILD_CONFIG = vol.Schema( | |||||||
|             {vol.In(ARCH_ALL): vol.Match(RE_DOCKER_IMAGE_BUILD)} |             {vol.In(ARCH_ALL): vol.Match(RE_DOCKER_IMAGE_BUILD)} | ||||||
|         ), |         ), | ||||||
|         vol.Optional(ATTR_SQUASH, default=False): vol.Boolean(), |         vol.Optional(ATTR_SQUASH, default=False): vol.Boolean(), | ||||||
|         vol.Optional(ATTR_ARGS, default=dict): vol.Schema( |         vol.Optional(ATTR_ARGS, default=dict): vol.Schema({str: str}), | ||||||
|             {vol.Coerce(str): vol.Coerce(str)} |         vol.Optional(ATTR_LABELS, default=dict): vol.Schema({str: str}), | ||||||
|         ), |     }, | ||||||
|  |     extra=vol.REMOVE_EXTRA, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | SCHEMA_TRANSLATION_CONFIGURATION = vol.Schema( | ||||||
|  |     { | ||||||
|  |         vol.Required(ATTR_NAME): str, | ||||||
|  |         vol.Optional(ATTR_DESCRIPTON): vol.Maybe(str), | ||||||
|  |     }, | ||||||
|  |     extra=vol.REMOVE_EXTRA, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | SCHEMA_ADDON_TRANSLATIONS = vol.Schema( | ||||||
|  |     { | ||||||
|  |         vol.Optional(ATTR_CONFIGURATION): {str: SCHEMA_TRANSLATION_CONFIGURATION}, | ||||||
|  |         vol.Optional(ATTR_NETWORK): {str: str}, | ||||||
|     }, |     }, | ||||||
|     extra=vol.REMOVE_EXTRA, |     extra=vol.REMOVE_EXTRA, | ||||||
| ) | ) | ||||||
| @@ -318,13 +401,15 @@ SCHEMA_ADDON_USER = vol.Schema( | |||||||
|     extra=vol.REMOVE_EXTRA, |     extra=vol.REMOVE_EXTRA, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| SCHEMA_ADDON_SYSTEM = vol.All( | SCHEMA_ADDON_SYSTEM = vol.All( | ||||||
|     _migrate_addon_config(), |     _migrate_addon_config(), | ||||||
|     _SCHEMA_ADDON_CONFIG.extend( |     _SCHEMA_ADDON_CONFIG.extend( | ||||||
|         { |         { | ||||||
|             vol.Required(ATTR_LOCATON): str, |             vol.Required(ATTR_LOCATON): str, | ||||||
|             vol.Required(ATTR_REPOSITORY): str, |             vol.Required(ATTR_REPOSITORY): str, | ||||||
|  |             vol.Required(ATTR_TRANSLATIONS, default=dict): { | ||||||
|  |                 str: SCHEMA_ADDON_TRANSLATIONS | ||||||
|  |             }, | ||||||
|         } |         } | ||||||
|     ), |     ), | ||||||
| ) | ) | ||||||
| @@ -339,7 +424,7 @@ SCHEMA_ADDONS_FILE = vol.Schema( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| SCHEMA_ADDON_SNAPSHOT = vol.Schema( | SCHEMA_ADDON_BACKUP = 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, | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ from ..coresys import CoreSys, CoreSysAttributes | |||||||
| from .addons import APIAddons | from .addons import APIAddons | ||||||
| from .audio import APIAudio | from .audio import APIAudio | ||||||
| from .auth import APIAuth | from .auth import APIAuth | ||||||
|  | from .backups import APIBackups | ||||||
| from .cli import APICli | from .cli import APICli | ||||||
| from .discovery import APIDiscovery | from .discovery import APIDiscovery | ||||||
| from .dns import APICoreDNS | from .dns import APICoreDNS | ||||||
| @@ -19,15 +20,16 @@ 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 .jobs import APIJobs | ||||||
|  | from .middleware.security import SecurityMiddleware | ||||||
| from .multicast import APIMulticast | from .multicast import APIMulticast | ||||||
| from .network import APINetwork | from .network import APINetwork | ||||||
| from .observer import APIObserver | from .observer import APIObserver | ||||||
| from .os import APIOS | from .os import APIOS | ||||||
| from .proxy import APIProxy | from .proxy import APIProxy | ||||||
| from .resolution import APIResoulution | from .resolution import APIResoulution | ||||||
| from .security import SecurityMiddleware | from .security import APISecurity | ||||||
| from .services import APIServices | from .services import APIServices | ||||||
| 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__) | ||||||
| @@ -60,6 +62,7 @@ class RestAPI(CoreSysAttributes): | |||||||
|         self._register_addons() |         self._register_addons() | ||||||
|         self._register_audio() |         self._register_audio() | ||||||
|         self._register_auth() |         self._register_auth() | ||||||
|  |         self._register_backups() | ||||||
|         self._register_cli() |         self._register_cli() | ||||||
|         self._register_discovery() |         self._register_discovery() | ||||||
|         self._register_dns() |         self._register_dns() | ||||||
| @@ -78,8 +81,9 @@ class RestAPI(CoreSysAttributes): | |||||||
|         self._register_proxy() |         self._register_proxy() | ||||||
|         self._register_resolution() |         self._register_resolution() | ||||||
|         self._register_services() |         self._register_services() | ||||||
|         self._register_snapshots() |  | ||||||
|         self._register_supervisor() |         self._register_supervisor() | ||||||
|  |         self._register_store() | ||||||
|  |         self._register_security() | ||||||
|  |  | ||||||
|         await self.start() |         await self.start() | ||||||
|  |  | ||||||
| @@ -141,6 +145,20 @@ class RestAPI(CoreSysAttributes): | |||||||
|                 web.get("/os/info", api_os.info), |                 web.get("/os/info", api_os.info), | ||||||
|                 web.post("/os/update", api_os.update), |                 web.post("/os/update", api_os.update), | ||||||
|                 web.post("/os/config/sync", api_os.config_sync), |                 web.post("/os/config/sync", api_os.config_sync), | ||||||
|  |                 web.post("/os/datadisk/move", api_os.migrate_data), | ||||||
|  |                 web.get("/os/datadisk/list", api_os.list_data), | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def _register_security(self) -> None: | ||||||
|  |         """Register Security functions.""" | ||||||
|  |         api_security = APISecurity() | ||||||
|  |         api_security.coresys = self.coresys | ||||||
|  |  | ||||||
|  |         self.webapp.add_routes( | ||||||
|  |             [ | ||||||
|  |                 web.get("/security/info", api_security.info), | ||||||
|  |                 web.post("/security/options", api_security.options), | ||||||
|             ] |             ] | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -207,7 +225,6 @@ class RestAPI(CoreSysAttributes): | |||||||
|             [ |             [ | ||||||
|                 web.get("/hardware/info", api_hardware.info), |                 web.get("/hardware/info", api_hardware.info), | ||||||
|                 web.get("/hardware/audio", api_hardware.audio), |                 web.get("/hardware/audio", api_hardware.audio), | ||||||
|                 web.post("/hardware/trigger", api_hardware.trigger), |  | ||||||
|             ] |             ] | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -226,6 +243,10 @@ class RestAPI(CoreSysAttributes): | |||||||
|         self.webapp.add_routes( |         self.webapp.add_routes( | ||||||
|             [ |             [ | ||||||
|                 web.get("/resolution/info", api_resolution.info), |                 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( |                 web.post( | ||||||
|                     "/resolution/suggestion/{suggestion}", |                     "/resolution/suggestion/{suggestion}", | ||||||
|                     api_resolution.apply_suggestion, |                     api_resolution.apply_suggestion, | ||||||
| @@ -238,6 +259,7 @@ class RestAPI(CoreSysAttributes): | |||||||
|                     "/resolution/issue/{issue}", |                     "/resolution/issue/{issue}", | ||||||
|                     api_resolution.dismiss_issue, |                     api_resolution.dismiss_issue, | ||||||
|                 ), |                 ), | ||||||
|  |                 web.post("/resolution/healthcheck", api_resolution.healthcheck), | ||||||
|             ] |             ] | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -262,6 +284,10 @@ class RestAPI(CoreSysAttributes): | |||||||
|  |  | ||||||
|         self.webapp.add_routes( |         self.webapp.add_routes( | ||||||
|             [ |             [ | ||||||
|  |                 web.get( | ||||||
|  |                     "/supervisor/available_updates", api_supervisor.available_updates | ||||||
|  |                 ), | ||||||
|  |                 web.post("/refresh_updates", api_supervisor.reload), | ||||||
|                 web.get("/supervisor/ping", api_supervisor.ping), |                 web.get("/supervisor/ping", api_supervisor.ping), | ||||||
|                 web.get("/supervisor/info", api_supervisor.info), |                 web.get("/supervisor/info", api_supervisor.info), | ||||||
|                 web.get("/supervisor/stats", api_supervisor.stats), |                 web.get("/supervisor/stats", api_supervisor.stats), | ||||||
| @@ -338,12 +364,10 @@ 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( |                 web.post( | ||||||
|                     "/addons/{addon}/options/validate", api_addons.options_validate |                     "/addons/{addon}/options/validate", api_addons.options_validate | ||||||
| @@ -375,30 +399,41 @@ class RestAPI(CoreSysAttributes): | |||||||
|             ] |             ] | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def _register_snapshots(self) -> None: |     def _register_backups(self) -> None: | ||||||
|         """Register snapshots functions.""" |         """Register backups functions.""" | ||||||
|         api_snapshots = APISnapshots() |         api_backups = APIBackups() | ||||||
|         api_snapshots.coresys = self.coresys |         api_backups.coresys = self.coresys | ||||||
|  |  | ||||||
|         self.webapp.add_routes( |         self.webapp.add_routes( | ||||||
|             [ |             [ | ||||||
|                 web.get("/snapshots", api_snapshots.list), |                 web.get("/snapshots", api_backups.list), | ||||||
|                 web.post("/snapshots/reload", api_snapshots.reload), |                 web.post("/snapshots/reload", api_backups.reload), | ||||||
|                 web.post("/snapshots/new/full", api_snapshots.snapshot_full), |                 web.post("/snapshots/new/full", api_backups.backup_full), | ||||||
|                 web.post("/snapshots/new/partial", api_snapshots.snapshot_partial), |                 web.post("/snapshots/new/partial", api_backups.backup_partial), | ||||||
|                 web.post("/snapshots/new/upload", api_snapshots.upload), |                 web.post("/snapshots/new/upload", api_backups.upload), | ||||||
|                 web.get("/snapshots/{snapshot}/info", api_snapshots.info), |                 web.get("/snapshots/{slug}/info", api_backups.info), | ||||||
|                 web.delete("/snapshots/{snapshot}", api_snapshots.remove), |                 web.delete("/snapshots/{slug}", api_backups.remove), | ||||||
|  |                 web.post("/snapshots/{slug}/restore/full", api_backups.restore_full), | ||||||
|                 web.post( |                 web.post( | ||||||
|                     "/snapshots/{snapshot}/restore/full", api_snapshots.restore_full |                     "/snapshots/{slug}/restore/partial", | ||||||
|  |                     api_backups.restore_partial, | ||||||
|                 ), |                 ), | ||||||
|  |                 web.get("/snapshots/{slug}/download", api_backups.download), | ||||||
|  |                 web.post("/snapshots/{slug}/remove", api_backups.remove), | ||||||
|  |                 # June 2021: /snapshots was renamed to /backups | ||||||
|  |                 web.get("/backups", api_backups.list), | ||||||
|  |                 web.post("/backups/reload", api_backups.reload), | ||||||
|  |                 web.post("/backups/new/full", api_backups.backup_full), | ||||||
|  |                 web.post("/backups/new/partial", api_backups.backup_partial), | ||||||
|  |                 web.post("/backups/new/upload", api_backups.upload), | ||||||
|  |                 web.get("/backups/{slug}/info", api_backups.info), | ||||||
|  |                 web.delete("/backups/{slug}", api_backups.remove), | ||||||
|  |                 web.post("/backups/{slug}/restore/full", api_backups.restore_full), | ||||||
|                 web.post( |                 web.post( | ||||||
|                     "/snapshots/{snapshot}/restore/partial", |                     "/backups/{slug}/restore/partial", | ||||||
|                     api_snapshots.restore_partial, |                     api_backups.restore_partial, | ||||||
|                 ), |                 ), | ||||||
|                 web.get("/snapshots/{snapshot}/download", api_snapshots.download), |                 web.get("/backups/{slug}/download", api_backups.download), | ||||||
|                 # Old, remove at end of 2020 |  | ||||||
|                 web.post("/snapshots/{snapshot}/remove", api_snapshots.remove), |  | ||||||
|             ] |             ] | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -469,6 +504,46 @@ class RestAPI(CoreSysAttributes): | |||||||
|             ] |             ] | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     def _register_store(self) -> None: | ||||||
|  |         """Register store endpoints.""" | ||||||
|  |         api_store = APIStore() | ||||||
|  |         api_store.coresys = self.coresys | ||||||
|  |  | ||||||
|  |         self.webapp.add_routes( | ||||||
|  |             [ | ||||||
|  |                 web.get("/store", api_store.store_info), | ||||||
|  |                 web.get("/store/addons", api_store.addons_list), | ||||||
|  |                 web.get("/store/addons/{addon}", api_store.addons_addon_info), | ||||||
|  |                 web.get("/store/addons/{addon}/{version}", api_store.addons_addon_info), | ||||||
|  |                 web.post( | ||||||
|  |                     "/store/addons/{addon}/install", api_store.addons_addon_install | ||||||
|  |                 ), | ||||||
|  |                 web.post( | ||||||
|  |                     "/store/addons/{addon}/install/{version}", | ||||||
|  |                     api_store.addons_addon_install, | ||||||
|  |                 ), | ||||||
|  |                 web.post("/store/addons/{addon}/update", api_store.addons_addon_update), | ||||||
|  |                 web.post( | ||||||
|  |                     "/store/addons/{addon}/update/{version}", | ||||||
|  |                     api_store.addons_addon_update, | ||||||
|  |                 ), | ||||||
|  |                 web.post("/store/reload", api_store.reload), | ||||||
|  |                 web.get("/store/repositories", api_store.repositories_list), | ||||||
|  |                 web.get( | ||||||
|  |                     "/store/repositories/{repository}", | ||||||
|  |                     api_store.repositories_repository_info, | ||||||
|  |                 ), | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Reroute from legacy | ||||||
|  |         self.webapp.add_routes( | ||||||
|  |             [ | ||||||
|  |                 web.post("/addons/{addon}/install", api_store.addons_addon_install), | ||||||
|  |                 web.post("/addons/{addon}/update", api_store.addons_addon_update), | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def _register_panel(self) -> None: |     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") | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| """Init file for Supervisor Home Assistant RESTful API.""" | """Init file for Supervisor Home Assistant RESTful API.""" | ||||||
| import asyncio | import asyncio | ||||||
| import logging | import logging | ||||||
| from typing import Any, Awaitable, Dict, List | from typing import Any, Awaitable | ||||||
|  |  | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
| @@ -71,6 +71,7 @@ from ..const import ( | |||||||
|     ATTR_OPTIONS, |     ATTR_OPTIONS, | ||||||
|     ATTR_PRIVILEGED, |     ATTR_PRIVILEGED, | ||||||
|     ATTR_PROTECTED, |     ATTR_PROTECTED, | ||||||
|  |     ATTR_PWNED, | ||||||
|     ATTR_RATING, |     ATTR_RATING, | ||||||
|     ATTR_REPOSITORIES, |     ATTR_REPOSITORIES, | ||||||
|     ATTR_REPOSITORY, |     ATTR_REPOSITORY, | ||||||
| @@ -82,6 +83,7 @@ from ..const import ( | |||||||
|     ATTR_STARTUP, |     ATTR_STARTUP, | ||||||
|     ATTR_STATE, |     ATTR_STATE, | ||||||
|     ATTR_STDIN, |     ATTR_STDIN, | ||||||
|  |     ATTR_TRANSLATIONS, | ||||||
|     ATTR_UART, |     ATTR_UART, | ||||||
|     ATTR_UDEV, |     ATTR_UDEV, | ||||||
|     ATTR_UPDATE_AVAILABLE, |     ATTR_UPDATE_AVAILABLE, | ||||||
| @@ -102,13 +104,13 @@ from ..const import ( | |||||||
| ) | ) | ||||||
| from ..coresys import CoreSysAttributes | from ..coresys import CoreSysAttributes | ||||||
| from ..docker.stats import DockerStats | from ..docker.stats import DockerStats | ||||||
| from ..exceptions import APIError, APIForbidden | from ..exceptions import APIError, APIForbidden, PwnedError, PwnedSecret | ||||||
| from ..validate import docker_ports | from ..validate import docker_ports | ||||||
| from .utils import api_process, api_process_raw, api_validate | from .utils import api_process, api_process_raw, api_validate, json_loads | ||||||
|  |  | ||||||
| _LOGGER: logging.Logger = logging.getLogger(__name__) | _LOGGER: logging.Logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)}) | SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): str}) | ||||||
|  |  | ||||||
| # pylint: disable=no-value-for-parameter | # pylint: disable=no-value-for-parameter | ||||||
| SCHEMA_OPTIONS = vol.Schema( | SCHEMA_OPTIONS = vol.Schema( | ||||||
| @@ -116,8 +118,8 @@ SCHEMA_OPTIONS = vol.Schema( | |||||||
|         vol.Optional(ATTR_BOOT): vol.Coerce(AddonBoot), |         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(str), | ||||||
|         vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)), |         vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(str), | ||||||
|         vol.Optional(ATTR_INGRESS_PANEL): vol.Boolean(), |         vol.Optional(ATTR_INGRESS_PANEL): vol.Boolean(), | ||||||
|         vol.Optional(ATTR_WATCHDOG): vol.Boolean(), |         vol.Optional(ATTR_WATCHDOG): vol.Boolean(), | ||||||
|     } |     } | ||||||
| @@ -154,7 +156,7 @@ class APIAddons(CoreSysAttributes): | |||||||
|         return addon |         return addon | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def list(self, request: web.Request) -> Dict[str, Any]: |     async def list(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return all add-ons or repositories.""" |         """Return all add-ons or repositories.""" | ||||||
|         data_addons = [ |         data_addons = [ | ||||||
|             { |             { | ||||||
| @@ -171,6 +173,7 @@ class APIAddons(CoreSysAttributes): | |||||||
|                 ATTR_INSTALLED: addon.is_installed, |                 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, | ||||||
| @@ -198,7 +201,7 @@ class APIAddons(CoreSysAttributes): | |||||||
|         await asyncio.shield(self.sys_store.reload()) |         await asyncio.shield(self.sys_store.reload()) | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def info(self, request: web.Request) -> Dict[str, Any]: |     async def info(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return add-on information.""" |         """Return add-on information.""" | ||||||
|         addon: AnyAddon = self._extract_addon(request) |         addon: AnyAddon = self._extract_addon(request) | ||||||
|  |  | ||||||
| @@ -264,6 +267,7 @@ class APIAddons(CoreSysAttributes): | |||||||
|             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, | ||||||
| @@ -305,7 +309,7 @@ class APIAddons(CoreSysAttributes): | |||||||
|  |  | ||||||
|         # Extend schema with add-on specific validation |         # Extend schema with add-on specific validation | ||||||
|         addon_schema = SCHEMA_OPTIONS.extend( |         addon_schema = SCHEMA_OPTIONS.extend( | ||||||
|             {vol.Optional(ATTR_OPTIONS): vol.Any(None, addon.schema)} |             {vol.Optional(ATTR_OPTIONS): vol.Maybe(addon.schema)} | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         # Validate/Process Body |         # Validate/Process Body | ||||||
| @@ -334,13 +338,39 @@ class APIAddons(CoreSysAttributes): | |||||||
|     async def options_validate(self, request: web.Request) -> None: |     async def options_validate(self, request: web.Request) -> None: | ||||||
|         """Validate user options for add-on.""" |         """Validate user options for add-on.""" | ||||||
|         addon = self._extract_addon_installed(request) |         addon = self._extract_addon_installed(request) | ||||||
|         data = {ATTR_MESSAGE: "", ATTR_VALID: True} |         data = {ATTR_MESSAGE: "", ATTR_VALID: True, ATTR_PWNED: False} | ||||||
|  |  | ||||||
|  |         options = await request.json(loads=json_loads) or addon.options | ||||||
|  |  | ||||||
|  |         # Validate config | ||||||
|  |         options_schema = addon.schema | ||||||
|         try: |         try: | ||||||
|             addon.schema(addon.options) |             options_schema.validate(options) | ||||||
|         except vol.Invalid as ex: |         except vol.Invalid as ex: | ||||||
|             data[ATTR_MESSAGE] = humanize_error(addon.options, ex) |             data[ATTR_MESSAGE] = humanize_error(options, ex) | ||||||
|             data[ATTR_VALID] = False |             data[ATTR_VALID] = False | ||||||
|  |  | ||||||
|  |         if not self.sys_security.pwned: | ||||||
|  |             return data | ||||||
|  |  | ||||||
|  |         # Pwned check | ||||||
|  |         for secret in options_schema.pwned: | ||||||
|  |             try: | ||||||
|  |                 await self.sys_security.verify_secret(secret) | ||||||
|  |                 continue | ||||||
|  |             except PwnedSecret: | ||||||
|  |                 data[ATTR_PWNED] = True | ||||||
|  |             except PwnedError: | ||||||
|  |                 data[ATTR_PWNED] = None | ||||||
|  |             break | ||||||
|  |  | ||||||
|  |         if self.sys_security.force and data[ATTR_PWNED] in (None, True): | ||||||
|  |             data[ATTR_VALID] = False | ||||||
|  |             if data[ATTR_PWNED] is None: | ||||||
|  |                 data[ATTR_MESSAGE] = "Error happening on pwned secrets check!" | ||||||
|  |             else: | ||||||
|  |                 data[ATTR_MESSAGE] = "Add-on uses pwned secrets!" | ||||||
|  |  | ||||||
|         return data |         return data | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
| @@ -349,10 +379,12 @@ class APIAddons(CoreSysAttributes): | |||||||
|         slug: str = request.match_info.get("addon") |         slug: str = request.match_info.get("addon") | ||||||
|         if slug != "self": |         if slug != "self": | ||||||
|             raise APIForbidden("This can be only read by the Add-on itself!") |             raise APIForbidden("This can be only read by the Add-on itself!") | ||||||
|  |  | ||||||
|         addon = self._extract_addon_installed(request) |         addon = self._extract_addon_installed(request) | ||||||
|  |  | ||||||
|  |         # Lookup/reload secrets | ||||||
|  |         await self.sys_homeassistant.secrets.reload() | ||||||
|         try: |         try: | ||||||
|             return addon.schema(addon.options) |             return addon.schema.validate(addon.options) | ||||||
|         except vol.Invalid: |         except vol.Invalid: | ||||||
|             raise APIError("Invalid configuration data for the add-on") from None |             raise APIError("Invalid configuration data for the add-on") from None | ||||||
|  |  | ||||||
| @@ -360,7 +392,7 @@ class APIAddons(CoreSysAttributes): | |||||||
|     async def security(self, request: web.Request) -> None: |     async def security(self, request: web.Request) -> None: | ||||||
|         """Store security options for add-on.""" |         """Store security options for add-on.""" | ||||||
|         addon = self._extract_addon_installed(request) |         addon = self._extract_addon_installed(request) | ||||||
|         body: Dict[str, Any] = await api_validate(SCHEMA_SECURITY, request) |         body: dict[str, Any] = await api_validate(SCHEMA_SECURITY, request) | ||||||
|  |  | ||||||
|         if ATTR_PROTECTED in body: |         if ATTR_PROTECTED in body: | ||||||
|             _LOGGER.warning("Changing protected flag for %s!", addon.slug) |             _LOGGER.warning("Changing protected flag for %s!", addon.slug) | ||||||
| @@ -369,7 +401,7 @@ class APIAddons(CoreSysAttributes): | |||||||
|         addon.save_persist() |         addon.save_persist() | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def stats(self, request: web.Request) -> Dict[str, Any]: |     async def stats(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return resource information.""" |         """Return resource information.""" | ||||||
|         addon = self._extract_addon_installed(request) |         addon = self._extract_addon_installed(request) | ||||||
|  |  | ||||||
| @@ -386,12 +418,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.""" | ||||||
| @@ -410,12 +436,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.""" | ||||||
| @@ -485,6 +505,6 @@ class APIAddons(CoreSysAttributes): | |||||||
|         await asyncio.shield(addon.write_stdin(data)) |         await asyncio.shield(addon.write_stdin(data)) | ||||||
|  |  | ||||||
|  |  | ||||||
| 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()] | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| """Init file for Supervisor Audio RESTful API.""" | """Init file for Supervisor Audio RESTful API.""" | ||||||
| import asyncio | import asyncio | ||||||
| import logging | import logging | ||||||
| from typing import Any, Awaitable, Dict | from typing import Any, Awaitable | ||||||
|  |  | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
| import attr | import attr | ||||||
| @@ -56,10 +56,10 @@ SCHEMA_MUTE = vol.Schema( | |||||||
|     } |     } | ||||||
| ) | ) | ||||||
|  |  | ||||||
| SCHEMA_DEFAULT = vol.Schema({vol.Required(ATTR_NAME): vol.Coerce(str)}) | SCHEMA_DEFAULT = vol.Schema({vol.Required(ATTR_NAME): str}) | ||||||
|  |  | ||||||
| SCHEMA_PROFILE = vol.Schema( | SCHEMA_PROFILE = vol.Schema( | ||||||
|     {vol.Required(ATTR_CARD): vol.Coerce(str), vol.Required(ATTR_NAME): vol.Coerce(str)} |     {vol.Required(ATTR_CARD): str, vol.Required(ATTR_NAME): str} | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -67,7 +67,7 @@ class APIAudio(CoreSysAttributes): | |||||||
|     """Handle RESTful API for Audio functions.""" |     """Handle RESTful API for Audio functions.""" | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def info(self, request: web.Request) -> Dict[str, Any]: |     async def info(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return Audio information.""" |         """Return Audio information.""" | ||||||
|         return { |         return { | ||||||
|             ATTR_VERSION: self.sys_plugins.audio.version, |             ATTR_VERSION: self.sys_plugins.audio.version, | ||||||
| @@ -89,7 +89,7 @@ class APIAudio(CoreSysAttributes): | |||||||
|         } |         } | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def stats(self, request: web.Request) -> Dict[str, Any]: |     async def stats(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return resource information.""" |         """Return resource information.""" | ||||||
|         stats = await self.sys_plugins.audio.stats() |         stats = await self.sys_plugins.audio.stats() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| """Init file for Supervisor auth/SSO RESTful API.""" | """Init file for Supervisor auth/SSO RESTful API.""" | ||||||
| import asyncio | import asyncio | ||||||
| import logging | import logging | ||||||
| from typing import Dict |  | ||||||
|  |  | ||||||
| from aiohttp import BasicAuth, web | from aiohttp import BasicAuth, web | ||||||
| from aiohttp.hdrs import AUTHORIZATION, CONTENT_TYPE, WWW_AUTHENTICATE | from aiohttp.hdrs import AUTHORIZATION, CONTENT_TYPE, WWW_AUTHENTICATE | ||||||
| @@ -24,12 +23,12 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) | |||||||
|  |  | ||||||
| SCHEMA_PASSWORD_RESET = vol.Schema( | SCHEMA_PASSWORD_RESET = vol.Schema( | ||||||
|     { |     { | ||||||
|         vol.Required(ATTR_USERNAME): vol.Coerce(str), |         vol.Required(ATTR_USERNAME): str, | ||||||
|         vol.Required(ATTR_PASSWORD): vol.Coerce(str), |         vol.Required(ATTR_PASSWORD): str, | ||||||
|     } |     } | ||||||
| ) | ) | ||||||
|  |  | ||||||
| REALM_HEADER: Dict[str, str] = { | REALM_HEADER: dict[str, str] = { | ||||||
|     WWW_AUTHENTICATE: 'Basic realm="Home Assistant Authentication"' |     WWW_AUTHENTICATE: 'Basic realm="Home Assistant Authentication"' | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -46,7 +45,7 @@ class APIAuth(CoreSysAttributes): | |||||||
|         return self.sys_auth.check_login(addon, auth.login, auth.password) |         return self.sys_auth.check_login(addon, auth.login, auth.password) | ||||||
|  |  | ||||||
|     def _process_dict( |     def _process_dict( | ||||||
|         self, request: web.Request, addon: Addon, data: Dict[str, str] |         self, request: web.Request, addon: Addon, data: dict[str, str] | ||||||
|     ) -> bool: |     ) -> bool: | ||||||
|         """Process login with dict data. |         """Process login with dict data. | ||||||
|  |  | ||||||
| @@ -86,7 +85,7 @@ class APIAuth(CoreSysAttributes): | |||||||
|     @api_process |     @api_process | ||||||
|     async def reset(self, request: web.Request) -> None: |     async def reset(self, request: web.Request) -> None: | ||||||
|         """Process reset password request.""" |         """Process reset password request.""" | ||||||
|         body: Dict[str, str] = await api_validate(SCHEMA_PASSWORD_RESET, request) |         body: dict[str, str] = await api_validate(SCHEMA_PASSWORD_RESET, request) | ||||||
|         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]) | ||||||
|         ) |         ) | ||||||
|   | |||||||
							
								
								
									
										218
									
								
								supervisor/api/backups.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								supervisor/api/backups.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,218 @@ | |||||||
|  | """Backups RESTful API.""" | ||||||
|  | import asyncio | ||||||
|  | import logging | ||||||
|  | from pathlib import Path | ||||||
|  | import re | ||||||
|  | from tempfile import TemporaryDirectory | ||||||
|  |  | ||||||
|  | from aiohttp import web | ||||||
|  | from aiohttp.hdrs import CONTENT_DISPOSITION | ||||||
|  | import voluptuous as vol | ||||||
|  |  | ||||||
|  | from ..backups.validate import ALL_FOLDERS | ||||||
|  | from ..const import ( | ||||||
|  |     ATTR_ADDONS, | ||||||
|  |     ATTR_BACKUPS, | ||||||
|  |     ATTR_CONTENT, | ||||||
|  |     ATTR_DATE, | ||||||
|  |     ATTR_FOLDERS, | ||||||
|  |     ATTR_HOMEASSISTANT, | ||||||
|  |     ATTR_NAME, | ||||||
|  |     ATTR_PASSWORD, | ||||||
|  |     ATTR_PROTECTED, | ||||||
|  |     ATTR_REPOSITORIES, | ||||||
|  |     ATTR_SIZE, | ||||||
|  |     ATTR_SLUG, | ||||||
|  |     ATTR_TYPE, | ||||||
|  |     ATTR_VERSION, | ||||||
|  |     CONTENT_TYPE_TAR, | ||||||
|  | ) | ||||||
|  | from ..coresys import CoreSysAttributes | ||||||
|  | from ..exceptions import APIError | ||||||
|  | from .utils import api_process, api_validate | ||||||
|  |  | ||||||
|  | _LOGGER: logging.Logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  | RE_SLUGIFY_NAME = re.compile(r"[^A-Za-z0-9]+") | ||||||
|  |  | ||||||
|  | # pylint: disable=no-value-for-parameter | ||||||
|  | SCHEMA_RESTORE_PARTIAL = vol.Schema( | ||||||
|  |     { | ||||||
|  |         vol.Optional(ATTR_PASSWORD): vol.Maybe(str), | ||||||
|  |         vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(), | ||||||
|  |         vol.Optional(ATTR_ADDONS): vol.All([str], vol.Unique()), | ||||||
|  |         vol.Optional(ATTR_FOLDERS): vol.All([vol.In(ALL_FOLDERS)], vol.Unique()), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | SCHEMA_RESTORE_FULL = vol.Schema({vol.Optional(ATTR_PASSWORD): vol.Maybe(str)}) | ||||||
|  |  | ||||||
|  | SCHEMA_BACKUP_FULL = vol.Schema( | ||||||
|  |     { | ||||||
|  |         vol.Optional(ATTR_NAME): str, | ||||||
|  |         vol.Optional(ATTR_PASSWORD): vol.Maybe(str), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend( | ||||||
|  |     { | ||||||
|  |         vol.Optional(ATTR_ADDONS): vol.All([str], vol.Unique()), | ||||||
|  |         vol.Optional(ATTR_FOLDERS): vol.All([vol.In(ALL_FOLDERS)], vol.Unique()), | ||||||
|  |         vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class APIBackups(CoreSysAttributes): | ||||||
|  |     """Handle RESTful API for backups functions.""" | ||||||
|  |  | ||||||
|  |     def _extract_slug(self, request): | ||||||
|  |         """Return backup, throw an exception if it doesn't exist.""" | ||||||
|  |         backup = self.sys_backups.get(request.match_info.get("slug")) | ||||||
|  |         if not backup: | ||||||
|  |             raise APIError("Backup does not exist") | ||||||
|  |         return backup | ||||||
|  |  | ||||||
|  |     @api_process | ||||||
|  |     async def list(self, request): | ||||||
|  |         """Return backup list.""" | ||||||
|  |         data_backups = [] | ||||||
|  |         for backup in self.sys_backups.list_backups: | ||||||
|  |             data_backups.append( | ||||||
|  |                 { | ||||||
|  |                     ATTR_SLUG: backup.slug, | ||||||
|  |                     ATTR_NAME: backup.name, | ||||||
|  |                     ATTR_DATE: backup.date, | ||||||
|  |                     ATTR_TYPE: backup.sys_type, | ||||||
|  |                     ATTR_SIZE: backup.size, | ||||||
|  |                     ATTR_PROTECTED: backup.protected, | ||||||
|  |                     ATTR_CONTENT: { | ||||||
|  |                         ATTR_HOMEASSISTANT: backup.homeassistant_version is not None, | ||||||
|  |                         ATTR_ADDONS: backup.addon_list, | ||||||
|  |                         ATTR_FOLDERS: backup.folders, | ||||||
|  |                     }, | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         if request.path == "/snapshots": | ||||||
|  |             # Kept for backwards compability | ||||||
|  |             return {"snapshots": data_backups} | ||||||
|  |  | ||||||
|  |         return {ATTR_BACKUPS: data_backups} | ||||||
|  |  | ||||||
|  |     @api_process | ||||||
|  |     async def reload(self, request): | ||||||
|  |         """Reload backup list.""" | ||||||
|  |         await asyncio.shield(self.sys_backups.reload()) | ||||||
|  |         return True | ||||||
|  |  | ||||||
|  |     @api_process | ||||||
|  |     async def info(self, request): | ||||||
|  |         """Return backup info.""" | ||||||
|  |         backup = self._extract_slug(request) | ||||||
|  |  | ||||||
|  |         data_addons = [] | ||||||
|  |         for addon_data in backup.addons: | ||||||
|  |             data_addons.append( | ||||||
|  |                 { | ||||||
|  |                     ATTR_SLUG: addon_data[ATTR_SLUG], | ||||||
|  |                     ATTR_NAME: addon_data[ATTR_NAME], | ||||||
|  |                     ATTR_VERSION: addon_data[ATTR_VERSION], | ||||||
|  |                     ATTR_SIZE: addon_data[ATTR_SIZE], | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         return { | ||||||
|  |             ATTR_SLUG: backup.slug, | ||||||
|  |             ATTR_TYPE: backup.sys_type, | ||||||
|  |             ATTR_NAME: backup.name, | ||||||
|  |             ATTR_DATE: backup.date, | ||||||
|  |             ATTR_SIZE: backup.size, | ||||||
|  |             ATTR_PROTECTED: backup.protected, | ||||||
|  |             ATTR_HOMEASSISTANT: backup.homeassistant_version, | ||||||
|  |             ATTR_ADDONS: data_addons, | ||||||
|  |             ATTR_REPOSITORIES: backup.repositories, | ||||||
|  |             ATTR_FOLDERS: backup.folders, | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     @api_process | ||||||
|  |     async def backup_full(self, request): | ||||||
|  |         """Create full backup.""" | ||||||
|  |         body = await api_validate(SCHEMA_BACKUP_FULL, request) | ||||||
|  |         backup = await asyncio.shield(self.sys_backups.do_backup_full(**body)) | ||||||
|  |  | ||||||
|  |         if backup: | ||||||
|  |             return {ATTR_SLUG: backup.slug} | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |     @api_process | ||||||
|  |     async def backup_partial(self, request): | ||||||
|  |         """Create a partial backup.""" | ||||||
|  |         body = await api_validate(SCHEMA_BACKUP_PARTIAL, request) | ||||||
|  |         backup = await asyncio.shield(self.sys_backups.do_backup_partial(**body)) | ||||||
|  |  | ||||||
|  |         if backup: | ||||||
|  |             return {ATTR_SLUG: backup.slug} | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |     @api_process | ||||||
|  |     async def restore_full(self, request): | ||||||
|  |         """Full restore of a backup.""" | ||||||
|  |         backup = self._extract_slug(request) | ||||||
|  |         body = await api_validate(SCHEMA_RESTORE_FULL, request) | ||||||
|  |  | ||||||
|  |         return await asyncio.shield(self.sys_backups.do_restore_full(backup, **body)) | ||||||
|  |  | ||||||
|  |     @api_process | ||||||
|  |     async def restore_partial(self, request): | ||||||
|  |         """Partial restore a backup.""" | ||||||
|  |         backup = self._extract_slug(request) | ||||||
|  |         body = await api_validate(SCHEMA_RESTORE_PARTIAL, request) | ||||||
|  |  | ||||||
|  |         return await asyncio.shield(self.sys_backups.do_restore_partial(backup, **body)) | ||||||
|  |  | ||||||
|  |     @api_process | ||||||
|  |     async def remove(self, request): | ||||||
|  |         """Remove a backup.""" | ||||||
|  |         backup = self._extract_slug(request) | ||||||
|  |         return self.sys_backups.remove(backup) | ||||||
|  |  | ||||||
|  |     async def download(self, request): | ||||||
|  |         """Download a backup file.""" | ||||||
|  |         backup = self._extract_slug(request) | ||||||
|  |  | ||||||
|  |         _LOGGER.info("Downloading backup %s", backup.slug) | ||||||
|  |         response = web.FileResponse(backup.tarfile) | ||||||
|  |         response.content_type = CONTENT_TYPE_TAR | ||||||
|  |         response.headers[ | ||||||
|  |             CONTENT_DISPOSITION | ||||||
|  |         ] = f"attachment; filename={RE_SLUGIFY_NAME.sub('_', backup.name)}.tar" | ||||||
|  |         return response | ||||||
|  |  | ||||||
|  |     @api_process | ||||||
|  |     async def upload(self, request): | ||||||
|  |         """Upload a backup file.""" | ||||||
|  |         with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp_dir: | ||||||
|  |             tar_file = Path(temp_dir, "backup.tar") | ||||||
|  |             reader = await request.multipart() | ||||||
|  |             contents = await reader.next() | ||||||
|  |             try: | ||||||
|  |                 with tar_file.open("wb") as backup: | ||||||
|  |                     while True: | ||||||
|  |                         chunk = await contents.read_chunk() | ||||||
|  |                         if not chunk: | ||||||
|  |                             break | ||||||
|  |                         backup.write(chunk) | ||||||
|  |  | ||||||
|  |             except OSError as err: | ||||||
|  |                 _LOGGER.error("Can't write new backup file: %s", err) | ||||||
|  |                 return False | ||||||
|  |  | ||||||
|  |             except asyncio.CancelledError: | ||||||
|  |                 return False | ||||||
|  |  | ||||||
|  |             backup = await asyncio.shield(self.sys_backups.import_backup(tar_file)) | ||||||
|  |  | ||||||
|  |         if backup: | ||||||
|  |             return {ATTR_SLUG: backup.slug} | ||||||
|  |         return False | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| """Init file for Supervisor HA cli RESTful API.""" | """Init file for Supervisor HA cli RESTful API.""" | ||||||
| import asyncio | import asyncio | ||||||
| import logging | import logging | ||||||
| from typing import Any, Dict | from typing import Any | ||||||
|  |  | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
| @@ -32,7 +32,7 @@ class APICli(CoreSysAttributes): | |||||||
|     """Handle RESTful API for HA Cli functions.""" |     """Handle RESTful API for HA Cli functions.""" | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def info(self, request: web.Request) -> Dict[str, Any]: |     async def info(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return HA cli information.""" |         """Return HA cli information.""" | ||||||
|         return { |         return { | ||||||
|             ATTR_VERSION: self.sys_plugins.cli.version, |             ATTR_VERSION: self.sys_plugins.cli.version, | ||||||
| @@ -41,7 +41,7 @@ class APICli(CoreSysAttributes): | |||||||
|         } |         } | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def stats(self, request: web.Request) -> Dict[str, Any]: |     async def stats(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return resource information.""" |         """Return resource information.""" | ||||||
|         stats = await self.sys_plugins.cli.stats() |         stats = await self.sys_plugins.cli.stats() | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								supervisor/api/const.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								supervisor/api/const.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | """Const for API.""" | ||||||
|  |  | ||||||
|  | ATTR_AGENT_VERSION = "agent_version" | ||||||
|  | ATTR_BOOT_TIMESTAMP = "boot_timestamp" | ||||||
|  | ATTR_DATA_DISK = "data_disk" | ||||||
|  | ATTR_DEVICE = "device" | ||||||
|  | ATTR_DT_SYNCHRONIZED = "dt_synchronized" | ||||||
|  | ATTR_DT_UTC = "dt_utc" | ||||||
|  | ATTR_STARTUP_TIME = "startup_time" | ||||||
|  | ATTR_USE_NTP = "use_ntp" | ||||||
|  | ATTR_USE_RTC = "use_rtc" | ||||||
|  | ATTR_APPARMOR_VERSION = "apparmor_version" | ||||||
|  | ATTR_PANEL_PATH = "panel_path" | ||||||
|  | ATTR_UPDATE_TYPE = "update_type" | ||||||
|  | ATTR_AVAILABLE_UPDATES = "available_updates" | ||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| """Init file for Supervisor DNS RESTful API.""" | """Init file for Supervisor DNS RESTful API.""" | ||||||
| import asyncio | import asyncio | ||||||
| import logging | import logging | ||||||
| from typing import Any, Awaitable, Dict | from typing import Any, Awaitable | ||||||
|  |  | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
| @@ -40,7 +40,7 @@ class APICoreDNS(CoreSysAttributes): | |||||||
|     """Handle RESTful API for DNS functions.""" |     """Handle RESTful API for DNS functions.""" | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def info(self, request: web.Request) -> Dict[str, Any]: |     async def info(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return DNS information.""" |         """Return DNS information.""" | ||||||
|         return { |         return { | ||||||
|             ATTR_VERSION: self.sys_plugins.dns.version, |             ATTR_VERSION: self.sys_plugins.dns.version, | ||||||
| @@ -63,7 +63,7 @@ class APICoreDNS(CoreSysAttributes): | |||||||
|         self.sys_plugins.dns.save_data() |         self.sys_plugins.dns.save_data() | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def stats(self, request: web.Request) -> Dict[str, Any]: |     async def stats(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return resource information.""" |         """Return resource information.""" | ||||||
|         stats = await self.sys_plugins.dns.stats() |         stats = await self.sys_plugins.dns.stats() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| """Init file for Supervisor Home Assistant RESTful API.""" | """Init file for Supervisor Home Assistant RESTful API.""" | ||||||
| import logging | import logging | ||||||
| from typing import Any, Dict | from typing import Any | ||||||
|  |  | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
| @@ -21,7 +21,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) | |||||||
|  |  | ||||||
| SCHEMA_DOCKER_REGISTRY = vol.Schema( | SCHEMA_DOCKER_REGISTRY = vol.Schema( | ||||||
|     { |     { | ||||||
|         vol.Coerce(str): { |         str: { | ||||||
|             vol.Required(ATTR_USERNAME): str, |             vol.Required(ATTR_USERNAME): str, | ||||||
|             vol.Required(ATTR_PASSWORD): str, |             vol.Required(ATTR_PASSWORD): str, | ||||||
|         } |         } | ||||||
| @@ -33,7 +33,7 @@ class APIDocker(CoreSysAttributes): | |||||||
|     """Handle RESTful API for Docker configuration.""" |     """Handle RESTful API for Docker configuration.""" | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def registries(self, request) -> Dict[str, Any]: |     async def registries(self, request) -> dict[str, Any]: | ||||||
|         """Return the list of registries.""" |         """Return the list of registries.""" | ||||||
|         data_registries = {} |         data_registries = {} | ||||||
|         for hostname, registry in self.sys_docker.config.registries.items(): |         for hostname, registry in self.sys_docker.config.registries.items(): | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| """Init file for Supervisor hardware RESTful API.""" | """Init file for Supervisor hardware RESTful API.""" | ||||||
| import logging | import logging | ||||||
| from typing import Any, Awaitable, Dict | from typing import Any | ||||||
|  |  | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
|  |  | ||||||
| @@ -19,7 +19,7 @@ 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]: | def device_struct(device: Device) -> dict[str, Any]: | ||||||
|     """Return a dict with information of a interface to be used in th API.""" |     """Return a dict with information of a interface to be used in th API.""" | ||||||
|     return { |     return { | ||||||
|         ATTR_NAME: device.name, |         ATTR_NAME: device.name, | ||||||
| @@ -35,7 +35,7 @@ class APIHardware(CoreSysAttributes): | |||||||
|     """Handle RESTful API for hardware functions.""" |     """Handle RESTful API for hardware functions.""" | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     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_DEVICES: [ |             ATTR_DEVICES: [ | ||||||
| @@ -44,7 +44,7 @@ class APIHardware(CoreSysAttributes): | |||||||
|         } |         } | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def audio(self, request: web.Request) -> Dict[str, Any]: |     async def audio(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Show pulse audio profiles.""" |         """Show pulse audio profiles.""" | ||||||
|         return { |         return { | ||||||
|             ATTR_AUDIO: { |             ATTR_AUDIO: { | ||||||
| @@ -58,8 +58,3 @@ class APIHardware(CoreSysAttributes): | |||||||
|                 }, |                 }, | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     @api_process |  | ||||||
|     async def trigger(self, request: web.Request) -> Awaitable[None]: |  | ||||||
|         """Trigger a udev device reload.""" |  | ||||||
|         _LOGGER.debug("Ignoring DEPRECATED hardware trigger function call.") |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| """Init file for Supervisor Home Assistant RESTful API.""" | """Init file for Supervisor Home Assistant RESTful API.""" | ||||||
| import asyncio | import asyncio | ||||||
| import logging | import logging | ||||||
| from typing import Any, Awaitable, Dict | from typing import Any, Awaitable | ||||||
|  |  | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
| @@ -10,6 +10,7 @@ from ..const import ( | |||||||
|     ATTR_ARCH, |     ATTR_ARCH, | ||||||
|     ATTR_AUDIO_INPUT, |     ATTR_AUDIO_INPUT, | ||||||
|     ATTR_AUDIO_OUTPUT, |     ATTR_AUDIO_OUTPUT, | ||||||
|  |     ATTR_BACKUP, | ||||||
|     ATTR_BLK_READ, |     ATTR_BLK_READ, | ||||||
|     ATTR_BLK_WRITE, |     ATTR_BLK_WRITE, | ||||||
|     ATTR_BOOT, |     ATTR_BOOT, | ||||||
| @@ -43,25 +44,30 @@ _LOGGER: logging.Logger = logging.getLogger(__name__) | |||||||
| SCHEMA_OPTIONS = vol.Schema( | SCHEMA_OPTIONS = vol.Schema( | ||||||
|     { |     { | ||||||
|         vol.Optional(ATTR_BOOT): vol.Boolean(), |         vol.Optional(ATTR_BOOT): vol.Boolean(), | ||||||
|         vol.Optional(ATTR_IMAGE): docker_image, |         vol.Optional(ATTR_IMAGE): vol.Maybe(docker_image), | ||||||
|         vol.Optional(ATTR_PORT): network_port, |         vol.Optional(ATTR_PORT): network_port, | ||||||
|         vol.Optional(ATTR_SSL): vol.Boolean(), |         vol.Optional(ATTR_SSL): vol.Boolean(), | ||||||
|         vol.Optional(ATTR_WATCHDOG): vol.Boolean(), |         vol.Optional(ATTR_WATCHDOG): vol.Boolean(), | ||||||
|         vol.Optional(ATTR_WAIT_BOOT): vol.All(vol.Coerce(int), vol.Range(min=60)), |         vol.Optional(ATTR_WAIT_BOOT): vol.All(vol.Coerce(int), vol.Range(min=60)), | ||||||
|         vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(vol.Coerce(str)), |         vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(str), | ||||||
|         vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)), |         vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(str), | ||||||
|         vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)), |         vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(str), | ||||||
|     } |     } | ||||||
| ) | ) | ||||||
|  |  | ||||||
| SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag}) | SCHEMA_UPDATE = vol.Schema( | ||||||
|  |     { | ||||||
|  |         vol.Optional(ATTR_VERSION): version_tag, | ||||||
|  |         vol.Optional(ATTR_BACKUP): bool, | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class APIHomeAssistant(CoreSysAttributes): | class APIHomeAssistant(CoreSysAttributes): | ||||||
|     """Handle RESTful API for Home Assistant functions.""" |     """Handle RESTful API for Home Assistant functions.""" | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def info(self, request: web.Request) -> Dict[str, Any]: |     async def info(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return host information.""" |         """Return host information.""" | ||||||
|         return { |         return { | ||||||
|             ATTR_VERSION: self.sys_homeassistant.version, |             ATTR_VERSION: self.sys_homeassistant.version, | ||||||
| @@ -117,7 +123,7 @@ class APIHomeAssistant(CoreSysAttributes): | |||||||
|         self.sys_homeassistant.save_data() |         self.sys_homeassistant.save_data() | ||||||
|  |  | ||||||
|     @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.core.stats() |         stats = await self.sys_homeassistant.core.stats() | ||||||
|         if not stats: |         if not stats: | ||||||
| @@ -137,10 +143,14 @@ class APIHomeAssistant(CoreSysAttributes): | |||||||
|     @api_process |     @api_process | ||||||
|     async def update(self, request: web.Request) -> None: |     async def update(self, request: web.Request) -> None: | ||||||
|         """Update Home Assistant.""" |         """Update Home Assistant.""" | ||||||
|         body = await api_validate(SCHEMA_VERSION, request) |         body = await api_validate(SCHEMA_UPDATE, request) | ||||||
|         version = body.get(ATTR_VERSION, self.sys_homeassistant.latest_version) |  | ||||||
|  |  | ||||||
|         await asyncio.shield(self.sys_homeassistant.core.update(version)) |         await asyncio.shield( | ||||||
|  |             self.sys_homeassistant.core.update( | ||||||
|  |                 version=body.get(ATTR_VERSION, self.sys_homeassistant.latest_version), | ||||||
|  |                 backup=body.get(ATTR_BACKUP), | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     def stop(self, request: web.Request) -> Awaitable[None]: |     def stop(self, request: web.Request) -> Awaitable[None]: | ||||||
|   | |||||||
| @@ -21,14 +21,25 @@ from ..const import ( | |||||||
|     ATTR_OPERATING_SYSTEM, |     ATTR_OPERATING_SYSTEM, | ||||||
|     ATTR_SERVICES, |     ATTR_SERVICES, | ||||||
|     ATTR_STATE, |     ATTR_STATE, | ||||||
|  |     ATTR_TIMEZONE, | ||||||
|     CONTENT_TYPE_BINARY, |     CONTENT_TYPE_BINARY, | ||||||
| ) | ) | ||||||
| from ..coresys import CoreSysAttributes | from ..coresys import CoreSysAttributes | ||||||
|  | from .const import ( | ||||||
|  |     ATTR_AGENT_VERSION, | ||||||
|  |     ATTR_APPARMOR_VERSION, | ||||||
|  |     ATTR_BOOT_TIMESTAMP, | ||||||
|  |     ATTR_DT_SYNCHRONIZED, | ||||||
|  |     ATTR_DT_UTC, | ||||||
|  |     ATTR_STARTUP_TIME, | ||||||
|  |     ATTR_USE_NTP, | ||||||
|  |     ATTR_USE_RTC, | ||||||
|  | ) | ||||||
| from .utils import api_process, api_process_raw, api_validate | from .utils import api_process, api_process_raw, api_validate | ||||||
|  |  | ||||||
| SERVICE = "service" | SERVICE = "service" | ||||||
|  |  | ||||||
| SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): vol.Coerce(str)}) | SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): str}) | ||||||
|  |  | ||||||
|  |  | ||||||
| class APIHost(CoreSysAttributes): | class APIHost(CoreSysAttributes): | ||||||
| @@ -38,6 +49,8 @@ class APIHost(CoreSysAttributes): | |||||||
|     async def info(self, request): |     async def info(self, request): | ||||||
|         """Return host information.""" |         """Return host information.""" | ||||||
|         return { |         return { | ||||||
|  |             ATTR_AGENT_VERSION: self.sys_dbus.agent.version, | ||||||
|  |             ATTR_APPARMOR_VERSION: self.sys_host.apparmor.version, | ||||||
|             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_DEPLOYMENT: self.sys_host.info.deployment, |             ATTR_DEPLOYMENT: self.sys_host.info.deployment, | ||||||
| @@ -49,6 +62,13 @@ class APIHost(CoreSysAttributes): | |||||||
|             ATTR_HOSTNAME: self.sys_host.info.hostname, |             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, |             ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system, | ||||||
|  |             ATTR_TIMEZONE: self.sys_host.info.timezone, | ||||||
|  |             ATTR_DT_UTC: self.sys_host.info.dt_utc, | ||||||
|  |             ATTR_DT_SYNCHRONIZED: self.sys_host.info.dt_synchronized, | ||||||
|  |             ATTR_USE_NTP: self.sys_host.info.use_ntp, | ||||||
|  |             ATTR_USE_RTC: self.sys_host.info.use_rtc, | ||||||
|  |             ATTR_STARTUP_TIME: self.sys_host.info.startup_time, | ||||||
|  |             ATTR_BOOT_TIMESTAMP: self.sys_host.info.boot_timestamp, | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| """Init file for Supervisor info RESTful API.""" | """Init file for Supervisor info RESTful API.""" | ||||||
| import logging | import logging | ||||||
| from typing import Any, Dict | from typing import Any | ||||||
|  |  | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
|  |  | ||||||
| @@ -31,12 +31,12 @@ class APIInfo(CoreSysAttributes): | |||||||
|     """Handle RESTful API for info functions.""" |     """Handle RESTful API for info functions.""" | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def info(self, request: web.Request) -> Dict[str, Any]: |     async def info(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Show system info.""" |         """Show system info.""" | ||||||
|         return { |         return { | ||||||
|             ATTR_SUPERVISOR: self.sys_supervisor.version, |             ATTR_SUPERVISOR: self.sys_supervisor.version, | ||||||
|             ATTR_HOMEASSISTANT: self.sys_homeassistant.version, |             ATTR_HOMEASSISTANT: self.sys_homeassistant.version, | ||||||
|             ATTR_HASSOS: self.sys_hassos.version, |             ATTR_HASSOS: self.sys_os.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_OPERATING_SYSTEM: self.sys_host.info.operating_system, | ||||||
| @@ -48,5 +48,5 @@ class APIInfo(CoreSysAttributes): | |||||||
|             ATTR_SUPPORTED: self.sys_core.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_config.timezone, |             ATTR_TIMEZONE: self.sys_timezone, | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -2,10 +2,10 @@ | |||||||
| import asyncio | import asyncio | ||||||
| from ipaddress import ip_address | from ipaddress import ip_address | ||||||
| import logging | import logging | ||||||
| from typing import Any, Dict, Union | from typing import Any, Union | ||||||
|  |  | ||||||
| import aiohttp | import aiohttp | ||||||
| from aiohttp import hdrs, web | from aiohttp import ClientTimeout, hdrs, web | ||||||
| from aiohttp.web_exceptions import ( | from aiohttp.web_exceptions import ( | ||||||
|     HTTPBadGateway, |     HTTPBadGateway, | ||||||
|     HTTPServiceUnavailable, |     HTTPServiceUnavailable, | ||||||
| @@ -25,10 +25,9 @@ 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, api_validate | from .utils import api_process, api_validate, require_home_assistant | ||||||
|  |  | ||||||
| _LOGGER: logging.Logger = logging.getLogger(__name__) | _LOGGER: logging.Logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -50,17 +49,12 @@ 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}" | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def panels(self, request: web.Request) -> Dict[str, Any]: |     async def panels(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Create a list of panel data.""" |         """Create a list of panel data.""" | ||||||
|         addons = {} |         addons = {} | ||||||
|         for addon in self.sys_ingress.addons: |         for addon in self.sys_ingress.addons: | ||||||
| @@ -74,18 +68,16 @@ class APIIngress(CoreSysAttributes): | |||||||
|         return {ATTR_PANELS: addons} |         return {ATTR_PANELS: addons} | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def create_session(self, request: web.Request) -> Dict[str, Any]: |     @require_home_assistant | ||||||
|  |     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 |     @api_process | ||||||
|     async def validate_session(self, request: web.Request) -> Dict[str, Any]: |     @require_home_assistant | ||||||
|  |     async def validate_session(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Validate session and extending how long it's valid for.""" |         """Validate session and extending how long it's valid for.""" | ||||||
|         self._check_ha_access(request) |  | ||||||
|  |  | ||||||
|         data = await api_validate(VALIDATE_SESSION_DATA, request) |         data = await api_validate(VALIDATE_SESSION_DATA, request) | ||||||
|  |  | ||||||
|         # Check Ingress Session |         # Check Ingress Session | ||||||
| @@ -93,11 +85,11 @@ class APIIngress(CoreSysAttributes): | |||||||
|             _LOGGER.warning("No valid ingress session %s", data[ATTR_SESSION]) |             _LOGGER.warning("No valid ingress session %s", data[ATTR_SESSION]) | ||||||
|             raise HTTPUnauthorized() |             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) | ||||||
| @@ -170,9 +162,18 @@ class APIIngress(CoreSysAttributes): | |||||||
|     ) -> Union[web.Response, web.StreamResponse]: |     ) -> Union[web.Response, web.StreamResponse]: | ||||||
|         """Ingress route for request.""" |         """Ingress route for request.""" | ||||||
|         url = self._create_url(addon, path) |         url = self._create_url(addon, path) | ||||||
|         data = await request.read() |  | ||||||
|         source_header = _init_header(request, addon) |         source_header = _init_header(request, addon) | ||||||
|  |  | ||||||
|  |         # Passing the raw stream breaks requests for some webservers | ||||||
|  |         # since we just need it for POST requests really, for all other methods | ||||||
|  |         # we read the bytes and pass that to the request to the add-on | ||||||
|  |         # add-ons needs to add support with that in the configuration | ||||||
|  |         data = ( | ||||||
|  |             request.content | ||||||
|  |             if request.method == "POST" and addon.ingress_stream | ||||||
|  |             else await request.read() | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         async with self.sys_websession.request( |         async with self.sys_websession.request( | ||||||
|             request.method, |             request.method, | ||||||
|             url, |             url, | ||||||
| @@ -180,6 +181,7 @@ class APIIngress(CoreSysAttributes): | |||||||
|             params=request.query, |             params=request.query, | ||||||
|             allow_redirects=False, |             allow_redirects=False, | ||||||
|             data=data, |             data=data, | ||||||
|  |             timeout=ClientTimeout(total=None), | ||||||
|         ) as result: |         ) as result: | ||||||
|             headers = _response_header(result) |             headers = _response_header(result) | ||||||
|  |  | ||||||
| @@ -218,7 +220,7 @@ class APIIngress(CoreSysAttributes): | |||||||
|  |  | ||||||
| def _init_header( | def _init_header( | ||||||
|     request: web.Request, addon: str |     request: web.Request, addon: str | ||||||
| ) -> Union[CIMultiDict, Dict[str, str]]: | ) -> Union[CIMultiDict, dict[str, str]]: | ||||||
|     """Create initial header.""" |     """Create initial header.""" | ||||||
|     headers = {} |     headers = {} | ||||||
|  |  | ||||||
| @@ -227,6 +229,7 @@ def _init_header( | |||||||
|         if name in ( |         if name in ( | ||||||
|             hdrs.CONTENT_LENGTH, |             hdrs.CONTENT_LENGTH, | ||||||
|             hdrs.CONTENT_ENCODING, |             hdrs.CONTENT_ENCODING, | ||||||
|  |             hdrs.TRANSFER_ENCODING, | ||||||
|             hdrs.SEC_WEBSOCKET_EXTENSIONS, |             hdrs.SEC_WEBSOCKET_EXTENSIONS, | ||||||
|             hdrs.SEC_WEBSOCKET_PROTOCOL, |             hdrs.SEC_WEBSOCKET_PROTOCOL, | ||||||
|             hdrs.SEC_WEBSOCKET_VERSION, |             hdrs.SEC_WEBSOCKET_VERSION, | ||||||
| @@ -245,7 +248,7 @@ def _init_header( | |||||||
|     return headers |     return headers | ||||||
|  |  | ||||||
|  |  | ||||||
| def _response_header(response: aiohttp.ClientResponse) -> Dict[str, str]: | def _response_header(response: aiohttp.ClientResponse) -> dict[str, str]: | ||||||
|     """Create response header.""" |     """Create response header.""" | ||||||
|     headers = {} |     headers = {} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| """Init file for Supervisor Jobs RESTful API.""" | """Init file for Supervisor Jobs RESTful API.""" | ||||||
| import logging | import logging | ||||||
| from typing import Any, Dict | from typing import Any | ||||||
|  |  | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
| @@ -20,7 +20,7 @@ class APIJobs(CoreSysAttributes): | |||||||
|     """Handle RESTful API for OS functions.""" |     """Handle RESTful API for OS functions.""" | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def info(self, request: web.Request) -> Dict[str, Any]: |     async def info(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return JobManager information.""" |         """Return JobManager information.""" | ||||||
|         return { |         return { | ||||||
|             ATTR_IGNORE_CONDITIONS: self.sys_jobs.ignore_conditions, |             ATTR_IGNORE_CONDITIONS: self.sys_jobs.ignore_conditions, | ||||||
| @@ -36,6 +36,8 @@ class APIJobs(CoreSysAttributes): | |||||||
|  |  | ||||||
|         self.sys_jobs.save_data() |         self.sys_jobs.save_data() | ||||||
|  |  | ||||||
|  |         await self.sys_resolution.evaluate.evaluate_system() | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def reset(self, request: web.Request) -> None: |     async def reset(self, request: web.Request) -> None: | ||||||
|         """Reset options for JobManager.""" |         """Reset options for JobManager.""" | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								supervisor/api/middleware/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/middleware/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | """API middleware for aiohttp.""" | ||||||
							
								
								
									
										208
									
								
								supervisor/api/middleware/security.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								supervisor/api/middleware/security.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,208 @@ | |||||||
|  | """Handle security part of this API.""" | ||||||
|  | import logging | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | from aiohttp.web import Request, RequestHandler, Response, middleware | ||||||
|  | from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized | ||||||
|  |  | ||||||
|  | from ...const import ( | ||||||
|  |     REQUEST_FROM, | ||||||
|  |     ROLE_ADMIN, | ||||||
|  |     ROLE_BACKUP, | ||||||
|  |     ROLE_DEFAULT, | ||||||
|  |     ROLE_HOMEASSISTANT, | ||||||
|  |     ROLE_MANAGER, | ||||||
|  |     CoreState, | ||||||
|  | ) | ||||||
|  | from ...coresys import CoreSys, CoreSysAttributes | ||||||
|  | from ..utils import api_return_error, excract_supervisor_token | ||||||
|  |  | ||||||
|  | _LOGGER: logging.Logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  | # fmt: off | ||||||
|  |  | ||||||
|  | # Block Anytime | ||||||
|  | BLACKLIST = re.compile( | ||||||
|  |     r"^(?:" | ||||||
|  |     r"|/homeassistant/api/hassio/.*" | ||||||
|  |     r"|/core/api/hassio/.*" | ||||||
|  |     r")$" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # Free to call or have own security concepts | ||||||
|  | NO_SECURITY_CHECK = re.compile( | ||||||
|  |     r"^(?:" | ||||||
|  |     r"|/homeassistant/api/.*" | ||||||
|  |     r"|/homeassistant/websocket" | ||||||
|  |     r"|/core/api/.*" | ||||||
|  |     r"|/core/websocket" | ||||||
|  |     r"|/supervisor/ping" | ||||||
|  |     r")$" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # Observer allow API calls | ||||||
|  | OBSERVER_CHECK = re.compile( | ||||||
|  |     r"^(?:" | ||||||
|  |     r"|/.+/info" | ||||||
|  |     r")$" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # Can called by every add-on | ||||||
|  | ADDONS_API_BYPASS = re.compile( | ||||||
|  |     r"^(?:" | ||||||
|  |     r"|/addons/self/(?!security|update)[^/]+" | ||||||
|  |     r"|/addons/self/options/config" | ||||||
|  |     r"|/info" | ||||||
|  |     r"|/services.*" | ||||||
|  |     r"|/discovery.*" | ||||||
|  |     r"|/auth" | ||||||
|  |     r")$" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # Policy role add-on API access | ||||||
|  | ADDONS_ROLE_ACCESS = { | ||||||
|  |     ROLE_DEFAULT: re.compile( | ||||||
|  |         r"^(?:" | ||||||
|  |         r"|/.+/info" | ||||||
|  |         r")$" | ||||||
|  |     ), | ||||||
|  |     ROLE_HOMEASSISTANT: re.compile( | ||||||
|  |         r"^(?:" | ||||||
|  |         r"|/.+/info" | ||||||
|  |         r"|/core/.+" | ||||||
|  |         r"|/homeassistant/.+" | ||||||
|  |         r")$" | ||||||
|  |     ), | ||||||
|  |     ROLE_BACKUP: re.compile( | ||||||
|  |         r"^(?:" | ||||||
|  |         r"|/.+/info" | ||||||
|  |         r"|/backups.*" | ||||||
|  |         r"|/snapshots.*" | ||||||
|  |         r")$" | ||||||
|  |     ), | ||||||
|  |     ROLE_MANAGER: re.compile( | ||||||
|  |         r"^(?:" | ||||||
|  |         r"|/.+/info" | ||||||
|  |         r"|/addons(?:/[^/]+/(?!security).+|/reload)?" | ||||||
|  |         r"|/audio/.+" | ||||||
|  |         r"|/auth/cache" | ||||||
|  |         r"|/cli/.+" | ||||||
|  |         r"|/core/.+" | ||||||
|  |         r"|/dns/.+" | ||||||
|  |         r"|/docker/.+" | ||||||
|  |         r"|/jobs/.+" | ||||||
|  |         r"|/hardware/.+" | ||||||
|  |         r"|/hassos/.+" | ||||||
|  |         r"|/homeassistant/.+" | ||||||
|  |         r"|/host/.+" | ||||||
|  |         r"|/multicast/.+" | ||||||
|  |         r"|/network/.+" | ||||||
|  |         r"|/observer/.+" | ||||||
|  |         r"|/os/.+" | ||||||
|  |         r"|/resolution/.+" | ||||||
|  |         r"|/backups.*" | ||||||
|  |         r"|/snapshots.*" | ||||||
|  |         r"|/store.*" | ||||||
|  |         r"|/supervisor/.+" | ||||||
|  |         r"|/security/.+" | ||||||
|  |         r")$" | ||||||
|  |     ), | ||||||
|  |     ROLE_ADMIN: re.compile( | ||||||
|  |         r".*" | ||||||
|  |     ), | ||||||
|  | } | ||||||
|  |  | ||||||
|  | # fmt: on | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SecurityMiddleware(CoreSysAttributes): | ||||||
|  |     """Security middleware functions.""" | ||||||
|  |  | ||||||
|  |     def __init__(self, coresys: CoreSys): | ||||||
|  |         """Initialize security middleware.""" | ||||||
|  |         self.coresys: CoreSys = coresys | ||||||
|  |  | ||||||
|  |     @middleware | ||||||
|  |     async def system_validation( | ||||||
|  |         self, request: Request, handler: RequestHandler | ||||||
|  |     ) -> Response: | ||||||
|  |         """Check if core is ready to response.""" | ||||||
|  |         if self.sys_core.state not in ( | ||||||
|  |             CoreState.STARTUP, | ||||||
|  |             CoreState.RUNNING, | ||||||
|  |             CoreState.FREEZE, | ||||||
|  |         ): | ||||||
|  |             return api_return_error( | ||||||
|  |                 message=f"System is not ready with state: {self.sys_core.state.value}" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         return await handler(request) | ||||||
|  |  | ||||||
|  |     @middleware | ||||||
|  |     async def token_validation( | ||||||
|  |         self, request: Request, handler: RequestHandler | ||||||
|  |     ) -> Response: | ||||||
|  |         """Check security access of this layer.""" | ||||||
|  |         request_from = None | ||||||
|  |         supervisor_token = excract_supervisor_token(request) | ||||||
|  |  | ||||||
|  |         # Blacklist | ||||||
|  |         if BLACKLIST.match(request.path): | ||||||
|  |             _LOGGER.error("%s is blacklisted!", request.path) | ||||||
|  |             raise HTTPForbidden() | ||||||
|  |  | ||||||
|  |         # Ignore security check | ||||||
|  |         if NO_SECURITY_CHECK.match(request.path): | ||||||
|  |             _LOGGER.debug("Passthrough %s", request.path) | ||||||
|  |             return await handler(request) | ||||||
|  |  | ||||||
|  |         # Not token | ||||||
|  |         if not supervisor_token: | ||||||
|  |             _LOGGER.warning("No API token provided for %s", request.path) | ||||||
|  |             raise HTTPUnauthorized() | ||||||
|  |  | ||||||
|  |         # Home-Assistant | ||||||
|  |         if supervisor_token == self.sys_homeassistant.supervisor_token: | ||||||
|  |             _LOGGER.debug("%s access from Home Assistant", request.path) | ||||||
|  |             request_from = self.sys_homeassistant | ||||||
|  |  | ||||||
|  |         # Host | ||||||
|  |         if supervisor_token == self.sys_plugins.cli.supervisor_token: | ||||||
|  |             _LOGGER.debug("%s access from Host", request.path) | ||||||
|  |             request_from = self.sys_host | ||||||
|  |  | ||||||
|  |         # Observer | ||||||
|  |         if supervisor_token == self.sys_plugins.observer.supervisor_token: | ||||||
|  |             if not OBSERVER_CHECK.match(request.path): | ||||||
|  |                 _LOGGER.warning("%s invalid Observer access", request.path) | ||||||
|  |                 raise HTTPForbidden() | ||||||
|  |             _LOGGER.debug("%s access from Observer", request.path) | ||||||
|  |             request_from = self.sys_plugins.observer | ||||||
|  |  | ||||||
|  |         # Add-on | ||||||
|  |         addon = None | ||||||
|  |         if supervisor_token and not request_from: | ||||||
|  |             addon = self.sys_addons.from_token(supervisor_token) | ||||||
|  |  | ||||||
|  |         # Check Add-on API access | ||||||
|  |         if addon and ADDONS_API_BYPASS.match(request.path): | ||||||
|  |             _LOGGER.debug("Passthrough %s from %s", request.path, addon.slug) | ||||||
|  |             request_from = addon | ||||||
|  |         elif addon and addon.access_hassio_api: | ||||||
|  |             # Check Role | ||||||
|  |             if ADDONS_ROLE_ACCESS[addon.hassio_role].match(request.path): | ||||||
|  |                 _LOGGER.info("%s access from %s", request.path, addon.slug) | ||||||
|  |                 request_from = addon | ||||||
|  |             else: | ||||||
|  |                 _LOGGER.warning("%s no role for %s", request.path, addon.slug) | ||||||
|  |         elif addon: | ||||||
|  |             _LOGGER.warning( | ||||||
|  |                 "%s missing API permission for %s", addon.slug, request.path | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         if request_from: | ||||||
|  |             request[REQUEST_FROM] = request_from | ||||||
|  |             return await handler(request) | ||||||
|  |  | ||||||
|  |         _LOGGER.error("Invalid token for access %s", request.path) | ||||||
|  |         raise HTTPForbidden() | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| """Init file for Supervisor Multicast RESTful API.""" | """Init file for Supervisor Multicast RESTful API.""" | ||||||
| import asyncio | import asyncio | ||||||
| import logging | import logging | ||||||
| from typing import Any, Awaitable, Dict | from typing import Any, Awaitable | ||||||
|  |  | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
| @@ -34,7 +34,7 @@ class APIMulticast(CoreSysAttributes): | |||||||
|     """Handle RESTful API for Multicast functions.""" |     """Handle RESTful API for Multicast functions.""" | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def info(self, request: web.Request) -> Dict[str, Any]: |     async def info(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return Multicast information.""" |         """Return Multicast information.""" | ||||||
|         return { |         return { | ||||||
|             ATTR_VERSION: self.sys_plugins.multicast.version, |             ATTR_VERSION: self.sys_plugins.multicast.version, | ||||||
| @@ -43,7 +43,7 @@ class APIMulticast(CoreSysAttributes): | |||||||
|         } |         } | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def stats(self, request: web.Request) -> Dict[str, Any]: |     async def stats(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return resource information.""" |         """Return resource information.""" | ||||||
|         stats = await self.sys_plugins.multicast.stats() |         stats = await self.sys_plugins.multicast.stats() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| """REST API for network.""" | """REST API for network.""" | ||||||
| import asyncio | import asyncio | ||||||
| from ipaddress import ip_address, ip_interface | from ipaddress import ip_address, ip_interface | ||||||
| from typing import Any, Awaitable, Dict | from typing import Any, Awaitable | ||||||
|  |  | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
| import attr | import attr | ||||||
| @@ -82,7 +82,7 @@ SCHEMA_UPDATE = vol.Schema( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def ipconfig_struct(config: IpConfig) -> Dict[str, Any]: | def ipconfig_struct(config: IpConfig) -> dict[str, Any]: | ||||||
|     """Return a dict with information about ip configuration.""" |     """Return a dict with information about ip configuration.""" | ||||||
|     return { |     return { | ||||||
|         ATTR_METHOD: config.method, |         ATTR_METHOD: config.method, | ||||||
| @@ -92,7 +92,7 @@ def ipconfig_struct(config: IpConfig) -> Dict[str, Any]: | |||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| def wifi_struct(config: WifiConfig) -> Dict[str, Any]: | def wifi_struct(config: WifiConfig) -> dict[str, Any]: | ||||||
|     """Return a dict with information about wifi configuration.""" |     """Return a dict with information about wifi configuration.""" | ||||||
|     return { |     return { | ||||||
|         ATTR_MODE: config.mode, |         ATTR_MODE: config.mode, | ||||||
| @@ -102,7 +102,7 @@ def wifi_struct(config: WifiConfig) -> Dict[str, Any]: | |||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| def vlan_struct(config: VlanConfig) -> Dict[str, Any]: | def vlan_struct(config: VlanConfig) -> dict[str, Any]: | ||||||
|     """Return a dict with information about VLAN configuration.""" |     """Return a dict with information about VLAN configuration.""" | ||||||
|     return { |     return { | ||||||
|         ATTR_ID: config.id, |         ATTR_ID: config.id, | ||||||
| @@ -110,7 +110,7 @@ def vlan_struct(config: VlanConfig) -> Dict[str, Any]: | |||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| def interface_struct(interface: Interface) -> Dict[str, Any]: | def interface_struct(interface: Interface) -> dict[str, Any]: | ||||||
|     """Return a dict with information of a interface to be used in th API.""" |     """Return a dict with information of a interface to be used in th API.""" | ||||||
|     return { |     return { | ||||||
|         ATTR_INTERFACE: interface.name, |         ATTR_INTERFACE: interface.name, | ||||||
| @@ -125,7 +125,7 @@ def interface_struct(interface: Interface) -> Dict[str, Any]: | |||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| def accesspoint_struct(accesspoint: AccessPoint) -> Dict[str, Any]: | def accesspoint_struct(accesspoint: AccessPoint) -> dict[str, Any]: | ||||||
|     """Return a dict for AccessPoint.""" |     """Return a dict for AccessPoint.""" | ||||||
|     return { |     return { | ||||||
|         ATTR_MODE: accesspoint.mode, |         ATTR_MODE: accesspoint.mode, | ||||||
| @@ -158,7 +158,7 @@ class APINetwork(CoreSysAttributes): | |||||||
|         raise APIError(f"Interface {name} does not exist") from None |         raise APIError(f"Interface {name} does not exist") from None | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def info(self, request: web.Request) -> Dict[str, Any]: |     async def info(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return network information.""" |         """Return network information.""" | ||||||
|         return { |         return { | ||||||
|             ATTR_INTERFACES: [ |             ATTR_INTERFACES: [ | ||||||
| @@ -176,7 +176,7 @@ class APINetwork(CoreSysAttributes): | |||||||
|         } |         } | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def interface_info(self, request: web.Request) -> Dict[str, Any]: |     async def interface_info(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return network information for a interface.""" |         """Return network information for a interface.""" | ||||||
|         interface = self._get_interface(request.match_info.get(ATTR_INTERFACE)) |         interface = self._get_interface(request.match_info.get(ATTR_INTERFACE)) | ||||||
|  |  | ||||||
| @@ -223,7 +223,7 @@ class APINetwork(CoreSysAttributes): | |||||||
|         return asyncio.shield(self.sys_host.network.update()) |         return asyncio.shield(self.sys_host.network.update()) | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def scan_accesspoints(self, request: web.Request) -> Dict[str, Any]: |     async def scan_accesspoints(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Scan and return a list of available networks.""" |         """Scan and return a list of available networks.""" | ||||||
|         interface = self._get_interface(request.match_info.get(ATTR_INTERFACE)) |         interface = self._get_interface(request.match_info.get(ATTR_INTERFACE)) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| """Init file for Supervisor Observer RESTful API.""" | """Init file for Supervisor Observer RESTful API.""" | ||||||
| import asyncio | import asyncio | ||||||
| import logging | import logging | ||||||
| from typing import Any, Dict | from typing import Any | ||||||
|  |  | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
| @@ -33,7 +33,7 @@ class APIObserver(CoreSysAttributes): | |||||||
|     """Handle RESTful API for Observer functions.""" |     """Handle RESTful API for Observer functions.""" | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def info(self, request: web.Request) -> Dict[str, Any]: |     async def info(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return HA Observer information.""" |         """Return HA Observer information.""" | ||||||
|         return { |         return { | ||||||
|             ATTR_HOST: str(self.sys_docker.network.observer), |             ATTR_HOST: str(self.sys_docker.network.observer), | ||||||
| @@ -43,7 +43,7 @@ class APIObserver(CoreSysAttributes): | |||||||
|         } |         } | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def stats(self, request: web.Request) -> Dict[str, Any]: |     async def stats(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return resource information.""" |         """Return resource information.""" | ||||||
|         stats = await self.sys_plugins.observer.stats() |         stats = await self.sys_plugins.observer.stats() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| """Init file for Supervisor HassOS RESTful API.""" | """Init file for Supervisor HassOS RESTful API.""" | ||||||
| import asyncio | import asyncio | ||||||
| import logging | import logging | ||||||
| from typing import Any, Awaitable, Dict | from pathlib import Path | ||||||
|  | from typing import Any, Awaitable | ||||||
|  |  | ||||||
| from aiohttp import web | from aiohttp import web | ||||||
| import voluptuous as vol | import voluptuous as vol | ||||||
| @@ -9,42 +10,60 @@ import voluptuous as vol | |||||||
| from ..const import ( | from ..const import ( | ||||||
|     ATTR_BOARD, |     ATTR_BOARD, | ||||||
|     ATTR_BOOT, |     ATTR_BOOT, | ||||||
|  |     ATTR_DEVICES, | ||||||
|     ATTR_UPDATE_AVAILABLE, |     ATTR_UPDATE_AVAILABLE, | ||||||
|     ATTR_VERSION, |     ATTR_VERSION, | ||||||
|     ATTR_VERSION_LATEST, |     ATTR_VERSION_LATEST, | ||||||
| ) | ) | ||||||
| from ..coresys import CoreSysAttributes | from ..coresys import CoreSysAttributes | ||||||
| from ..validate import version_tag | from ..validate import version_tag | ||||||
|  | from .const import ATTR_DATA_DISK, ATTR_DEVICE | ||||||
| from .utils import api_process, api_validate | from .utils import api_process, api_validate | ||||||
|  |  | ||||||
| _LOGGER: logging.Logger = logging.getLogger(__name__) | _LOGGER: logging.Logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag}) | SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag}) | ||||||
|  | SCHEMA_DISK = vol.Schema({vol.Required(ATTR_DEVICE): vol.All(str, vol.Coerce(Path))}) | ||||||
|  |  | ||||||
|  |  | ||||||
| class APIOS(CoreSysAttributes): | class APIOS(CoreSysAttributes): | ||||||
|     """Handle RESTful API for OS functions.""" |     """Handle RESTful API for OS functions.""" | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def info(self, request: web.Request) -> Dict[str, Any]: |     async def info(self, request: web.Request) -> dict[str, Any]: | ||||||
|         """Return OS information.""" |         """Return OS information.""" | ||||||
|         return { |         return { | ||||||
|             ATTR_VERSION: self.sys_hassos.version, |             ATTR_VERSION: self.sys_os.version, | ||||||
|             ATTR_VERSION_LATEST: self.sys_hassos.latest_version, |             ATTR_VERSION_LATEST: self.sys_os.latest_version, | ||||||
|             ATTR_UPDATE_AVAILABLE: self.sys_hassos.need_update, |             ATTR_UPDATE_AVAILABLE: self.sys_os.need_update, | ||||||
|             ATTR_BOARD: self.sys_hassos.board, |             ATTR_BOARD: self.sys_os.board, | ||||||
|             ATTR_BOOT: self.sys_dbus.rauc.boot_slot, |             ATTR_BOOT: self.sys_dbus.rauc.boot_slot, | ||||||
|  |             ATTR_DATA_DISK: self.sys_os.datadisk.disk_used, | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     async def update(self, request: web.Request) -> None: |     async def update(self, request: web.Request) -> None: | ||||||
|         """Update OS.""" |         """Update OS.""" | ||||||
|         body = await api_validate(SCHEMA_VERSION, request) |         body = await api_validate(SCHEMA_VERSION, request) | ||||||
|         version = body.get(ATTR_VERSION, self.sys_hassos.latest_version) |         version = body.get(ATTR_VERSION, self.sys_os.latest_version) | ||||||
|  |  | ||||||
|         await asyncio.shield(self.sys_hassos.update(version)) |         await asyncio.shield(self.sys_os.update(version)) | ||||||
|  |  | ||||||
|     @api_process |     @api_process | ||||||
|     def config_sync(self, request: web.Request) -> Awaitable[None]: |     def config_sync(self, request: web.Request) -> Awaitable[None]: | ||||||
|         """Trigger config reload on OS.""" |         """Trigger config reload on OS.""" | ||||||
|         return asyncio.shield(self.sys_hassos.config_sync()) |         return asyncio.shield(self.sys_os.config_sync()) | ||||||
|  |  | ||||||
|  |     @api_process | ||||||
|  |     async def migrate_data(self, request: web.Request) -> None: | ||||||
|  |         """Trigger data disk migration on Host.""" | ||||||
|  |         body = await api_validate(SCHEMA_DISK, request) | ||||||
|  |  | ||||||
|  |         await asyncio.shield(self.sys_os.datadisk.migrate_disk(body[ATTR_DEVICE])) | ||||||
|  |  | ||||||
|  |     @api_process | ||||||
|  |     async def list_data(self, request: web.Request) -> dict[str, Any]: | ||||||
|  |         """Return possible data targets.""" | ||||||
|  |         return { | ||||||
|  |             ATTR_DEVICES: self.sys_os.datadisk.available_disks, | ||||||
|  |         } | ||||||
|   | |||||||
| @@ -1,9 +1,16 @@ | |||||||
|  |  | ||||||
| try { | function loadES5() { | ||||||
|   new Function("import('/api/hassio/app/frontend_latest/entrypoint.b537a8c0.js')")(); |  | ||||||
| } catch (err) { |  | ||||||
|   var el = document.createElement('script'); |   var el = document.createElement('script'); | ||||||
|   el.src = '/api/hassio/app/frontend_es5/entrypoint.f493e22d.js'; |   el.src = '/api/hassio/app/frontend_es5/entrypoint.5d40ff8b.js'; | ||||||
|   document.body.appendChild(el); |   document.body.appendChild(el); | ||||||
| } | } | ||||||
|  | if (/.*Version\/(?:11|12)(?:\.\d+)*.*Safari\//.test(navigator.userAgent)) { | ||||||
|  |     loadES5(); | ||||||
|  | } else { | ||||||
|  |   try { | ||||||
|  |     new Function("import('/api/hassio/app/frontend_latest/entrypoint.f09e9f8e.js')")(); | ||||||
|  |   } catch (err) { | ||||||
|  |     loadES5(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|    |    | ||||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								supervisor/api/panel/frontend_es5/12dbe34e.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/panel/frontend_es5/12dbe34e.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/12dbe34e.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/12dbe34e.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								supervisor/api/panel/frontend_es5/2dbdaab4.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/panel/frontend_es5/2dbdaab4.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/2dbdaab4.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/2dbdaab4.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										2
									
								
								supervisor/api/panel/frontend_es5/33b00c5f.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								supervisor/api/panel/frontend_es5/33b00c5f.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/33b00c5f.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/33b00c5f.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								supervisor/api/panel/frontend_es5/39b0c1a9.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/panel/frontend_es5/39b0c1a9.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/39b0c1a9.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/39b0c1a9.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								supervisor/api/panel/frontend_es5/3a2e08bf.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/panel/frontend_es5/3a2e08bf.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/3a2e08bf.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/3a2e08bf.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								supervisor/api/panel/frontend_es5/450f1129.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/panel/frontend_es5/450f1129.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/450f1129.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/450f1129.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								supervisor/api/panel/frontend_es5/4a274bef.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/panel/frontend_es5/4a274bef.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/4a274bef.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/4a274bef.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								supervisor/api/panel/frontend_es5/64ef8d49.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/panel/frontend_es5/64ef8d49.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/64ef8d49.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/64ef8d49.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										2
									
								
								supervisor/api/panel/frontend_es5/69e58998.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								supervisor/api/panel/frontend_es5/69e58998.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | /*! js-yaml 4.1.0 https://github.com/nodeca/js-yaml @license MIT */ | ||||||
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/69e58998.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/69e58998.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								supervisor/api/panel/frontend_es5/6b0926eb.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/panel/frontend_es5/6b0926eb.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | !function(){"use strict";var r,t,n={5425:function(r,t,n){var e=n(93217);n(58556);function o(r,t){return function(r){if(Array.isArray(r))return r}(r)||function(r,t){var n=null==r?null:"undefined"!=typeof Symbol&&r[Symbol.iterator]||r["@@iterator"];if(null==n)return;var e,o,u=[],i=!0,a=!1;try{for(n=n.call(r);!(i=(e=n.next()).done)&&(u.push(e.value),!t||u.length!==t);i=!0);}catch(f){a=!0,o=f}finally{try{i||null==n.return||n.return()}finally{if(a)throw o}}return u}(r,t)||function(r,t){if(!r)return;if("string"==typeof r)return u(r,t);var n=Object.prototype.toString.call(r).slice(8,-1);"Object"===n&&r.constructor&&(n=r.constructor.name);if("Map"===n||"Set"===n)return Array.from(r);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return u(r,t)}(r,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function u(r,t){(null==t||t>r.length)&&(t=r.length);for(var n=0,e=new Array(t);n<t;n++)e[n]=r[n];return e}var i={filterData:function(r,t,n){return n=n.toUpperCase(),r.filter((function(r){return Object.entries(t).some((function(t){var e=o(t,2),u=e[0],i=e[1];return!(!i.filterable||!String(i.filterKey?r[i.valueColumn||u][i.filterKey]:r[i.valueColumn||u]).toUpperCase().includes(n))}))}))},sortData:function(r,t,n,e){return r.sort((function(r,o){var u=1;"desc"===n&&(u=-1);var i=t.filterKey?r[t.valueColumn||e][t.filterKey]:r[t.valueColumn||e],a=t.filterKey?o[t.valueColumn||e][t.filterKey]:o[t.valueColumn||e];return"string"==typeof i&&(i=i.toUpperCase()),"string"==typeof a&&(a=a.toUpperCase()),void 0===i&&void 0!==a?1:void 0===a&&void 0!==i?-1:i<a?-1*u:i>a?1*u:0}))}};(0,e.Jj)(i)}},e={};function o(r){var t=e[r];if(void 0!==t)return t.exports;var u=e[r]={exports:{}};return n[r](u,u.exports,o),u.exports}o.m=n,o.x=function(){var r=o.O(void 0,[191],(function(){return o(5425)}));return r=o.O(r)},r=[],o.O=function(t,n,e,u){if(!n){var i=1/0;for(c=0;c<r.length;c++){n=r[c][0],e=r[c][1],u=r[c][2];for(var a=!0,f=0;f<n.length;f++)(!1&u||i>=u)&&Object.keys(o.O).every((function(r){return o.O[r](n[f])}))?n.splice(f--,1):(a=!1,u<i&&(i=u));if(a){r.splice(c--,1);var l=e();void 0!==l&&(t=l)}}return t}u=u||0;for(var c=r.length;c>0&&r[c-1][2]>u;c--)r[c]=r[c-1];r[c]=[n,e,u]},o.n=function(r){var t=r&&r.__esModule?function(){return r.default}:function(){return r};return o.d(t,{a:t}),t},o.d=function(r,t){for(var n in t)o.o(t,n)&&!o.o(r,n)&&Object.defineProperty(r,n,{enumerable:!0,get:t[n]})},o.f={},o.e=function(r){return Promise.all(Object.keys(o.f).reduce((function(t,n){return o.f[n](r,t),t}),[]))},o.u=function(r){return"2dbdaab4.js"},o.o=function(r,t){return Object.prototype.hasOwnProperty.call(r,t)},o.p="/api/hassio/app/frontend_es5/",function(){var r={477:1,425:1};o.f.i=function(t,n){r[t]||importScripts(o.p+o.u(t))};var t=self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[],n=t.push.bind(t);t.push=function(t){var e=t[0],u=t[1],i=t[2];for(var a in u)o.o(u,a)&&(o.m[a]=u[a]);for(i&&i(o);e.length;)r[e.pop()]=1;n(t)}}(),t=o.x,o.x=function(){return o.e(191).then(t)};o.x()}(); | ||||||
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/6b0926eb.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/6b0926eb.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								supervisor/api/panel/frontend_es5/6d91c010.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/panel/frontend_es5/6d91c010.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/6d91c010.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/6d91c010.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								supervisor/api/panel/frontend_es5/72c04451.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/panel/frontend_es5/72c04451.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/72c04451.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/72c04451.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								supervisor/api/panel/frontend_es5/75af0819.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/panel/frontend_es5/75af0819.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/75af0819.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/75af0819.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								supervisor/api/panel/frontend_es5/8066b1de.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/panel/frontend_es5/8066b1de.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/8066b1de.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/8066b1de.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								supervisor/api/panel/frontend_es5/94873099.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/panel/frontend_es5/94873099.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | !function(){"use strict";var n,t,e={14971:function(n,t,e){var r,o,i=e(93217),u=e(9902),a=e.n(u),f=(e(58556),e(62173)),c=function(n,t,e){if("input"===n){if("type"===t&&"checkbox"===e||"checked"===t||"disabled"===t)return;return""}},s={renderMarkdown:function(n,t){var e,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return r||(r=Object.assign({},(0,f.getDefaultWhiteList)(),{input:["type","disabled","checked"],"ha-icon":["icon"],"ha-svg-icon":["path"]})),i.allowSvg?(o||(o=Object.assign({},r,{svg:["xmlns","height","width"],path:["transform","stroke","d"],img:["src"]})),e=o):e=r,(0,f.filterXSS)(a()(n,t),{whiteList:e,onTagAttr:c})}};(0,i.Jj)(s)}},r={};function o(n){var t=r[n];if(void 0!==t)return t.exports;var i=r[n]={exports:{}};return e[n].call(i.exports,i,i.exports,o),i.exports}o.m=e,o.x=function(){var n=o.O(void 0,[191,468],(function(){return o(14971)}));return n=o.O(n)},n=[],o.O=function(t,e,r,i){if(!e){var u=1/0;for(s=0;s<n.length;s++){e=n[s][0],r=n[s][1],i=n[s][2];for(var a=!0,f=0;f<e.length;f++)(!1&i||u>=i)&&Object.keys(o.O).every((function(n){return o.O[n](e[f])}))?e.splice(f--,1):(a=!1,i<u&&(u=i));if(a){n.splice(s--,1);var c=r();void 0!==c&&(t=c)}}return t}i=i||0;for(var s=n.length;s>0&&n[s-1][2]>i;s--)n[s]=n[s-1];n[s]=[e,r,i]},o.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return o.d(t,{a:t}),t},o.d=function(n,t){for(var e in t)o.o(t,e)&&!o.o(n,e)&&Object.defineProperty(n,e,{enumerable:!0,get:t[e]})},o.f={},o.e=function(n){return Promise.all(Object.keys(o.f).reduce((function(t,e){return o.f[e](n,t),t}),[]))},o.u=function(n){return{191:"2dbdaab4",468:"4a274bef"}[n]+".js"},o.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)},o.p="/api/hassio/app/frontend_es5/",function(){var n={971:1};o.f.i=function(t,e){n[t]||importScripts(o.p+o.u(t))};var t=self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[],e=t.push.bind(t);t.push=function(t){var r=t[0],i=t[1],u=t[2];for(var a in i)o.o(i,a)&&(o.m[a]=i[a]);for(u&&u(o);r.length;)n[r.pop()]=1;e(t)}}(),t=o.x,o.x=function(){return Promise.all([o.e(191),o.e(468)]).then(t)};o.x()}(); | ||||||
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/94873099.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/94873099.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								supervisor/api/panel/frontend_es5/a5a3a80a.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/panel/frontend_es5/a5a3a80a.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/a5a3a80a.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/a5a3a80a.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								supervisor/api/panel/frontend_es5/be03a8d0.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/panel/frontend_es5/be03a8d0.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/be03a8d0.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/be03a8d0.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								supervisor/api/panel/frontend_es5/c6564c6f.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								supervisor/api/panel/frontend_es5/c6564c6f.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/c6564c6f.js.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								supervisor/api/panel/frontend_es5/c6564c6f.js.gz
									
									
									
									
									
										Normal file
									
								
							
										
											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