Compare commits

...

89 Commits

Author SHA1 Message Date
Mike Degatano
b7c53d9e40 No update available if update cannot be installed on system 2024-06-12 15:58:30 -04:00
dependabot[bot]
b684c8673e Bump sentry-sdk from 2.5.0 to 2.5.1 (#5130)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.5.0 to 2.5.1.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.5.0...2.5.1)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 09:01:06 +02:00
dependabot[bot]
547f42439d Bump typing-extensions from 4.12.1 to 4.12.2 (#5129)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 08:49:42 +02:00
dependabot[bot]
c51ceb000f Bump sentry-sdk from 2.4.0 to 2.5.0 (#5126)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-07 09:19:18 +02:00
dependabot[bot]
4cbede1bc8 Bump pylint from 3.2.2 to 3.2.3 (#5127)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-07 09:01:27 +02:00
dependabot[bot]
5eac8c7780 Bump ruff from 0.4.7 to 0.4.8 (#5125)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.7 to 0.4.8.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.4.7...v0.4.8)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-06 08:37:25 +02:00
Mike Degatano
ab78d87304 Add safe mode option to core rebuild (#5120)
* Add safe mode option to core rebuild

* Adding logging for increased traceability
2024-06-05 15:44:07 -04:00
dependabot[bot]
09166e3867 Bump cryptography from 42.0.7 to 42.0.8 (#5121)
Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.7 to 42.0.8.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.7...42.0.8)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-05 15:38:13 -04:00
dependabot[bot]
8a5c813cdd Bump sentry-sdk from 2.3.1 to 2.4.0 (#5123)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.3.1 to 2.4.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.3.1...2.4.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-05 15:37:41 -04:00
dependabot[bot]
4200622f43 Bump pytest from 8.2.1 to 8.2.2 (#5122)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-05 08:43:01 +02:00
Mike Degatano
c4452a85b4 Fix addon in wrong state after restore (#5111)
* Fix addon in wrong state after restore

* Do not stop docker monitor for a shutdown
2024-06-04 16:17:43 +02:00
Mike Degatano
e57de4a3c1 Add uninstall addon suggestion to detached_addon_removed (#5105) 2024-06-03 10:38:34 -04:00
dependabot[bot]
9fd2c91c55 Bump ruff from 0.4.6 to 0.4.7 (#5116)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-03 10:29:23 +02:00
dependabot[bot]
fbd70013a8 Bump typing-extensions from 4.12.0 to 4.12.1 (#5117)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-03 09:54:10 +02:00
dependabot[bot]
8d18f3e66e Bump requests from 2.32.2 to 2.32.3 (#5115)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-30 08:52:20 +02:00
Richard P
5f5754e860 Add Exception handling when processing udev devices (#5088)
* Add Exception handling to UDEV reading and parsing

Khadas VIM4 UDEV returns something that python crapps its pants about. The exception just allows it to continue.

* Add an exception print

Added an exception print for the times things go bad.

* Split exception handling

The exception is not fatal when parsing error happens on one node. print it and continue.

* cleanups

* swapped functions

device.device_node   function bails very badly!  It raises no exceptions to the top but complains and errors

* Update supervisor/hardware/manager.py

Co-authored-by: Stefan Agner <stefan@agner.ch>

* Update supervisor/hardware/manager.py

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
Co-authored-by: Pascal Vizeli <pvizeli@syshack.ch>
2024-05-29 15:33:15 -04:00
dependabot[bot]
974c882b9a Bump docker/login-action from 3.1.0 to 3.2.0 (#5114)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-29 08:50:25 +02:00
dependabot[bot]
a9ea90096b Bump ruff from 0.4.5 to 0.4.6 (#5113)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-29 08:26:23 +02:00
dependabot[bot]
45c72c426e Bump coverage from 7.5.2 to 7.5.3 (#5112)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-29 08:26:10 +02:00
dependabot[bot]
4e5b75fe19 Bump coverage from 7.5.1 to 7.5.2 (#5109)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-27 08:37:29 +02:00
dependabot[bot]
3cd617e68f --- (#5099)
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-24 09:33:15 +02:00
dependabot[bot]
ddff02f73b Bump docker from 7.0.0 to 7.1.0 (#5106)
Bumps [docker](https://github.com/docker/docker-py) from 7.0.0 to 7.1.0.
- [Release notes](https://github.com/docker/docker-py/releases)
- [Commits](https://github.com/docker/docker-py/compare/7.0.0...7.1.0)

---
updated-dependencies:
- dependency-name: docker
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-24 08:52:21 +02:00
dependabot[bot]
b59347b3d3 Bump typing-extensions from 4.11.0 to 4.12.0 (#5107)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-24 08:32:48 +02:00
dependabot[bot]
1dc769076f Bump sentry-sdk from 2.2.1 to 2.3.1 (#5108)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-24 08:25:53 +02:00
Mike Degatano
f150a19c0f Create issue for detached addons (#5084)
* Create issue for detached addons

* Separate issues into missing and removed
2024-05-23 09:36:59 +02:00
dependabot[bot]
c4bc1e3824 Bump ruff from 0.4.4 to 0.4.5 (#5103)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-23 08:33:52 +02:00
Mike Degatano
eca99b69db Max retries for auto applying addon image fixup (#5051)
* Max retries for auto applying addon image fixup

* Update supervisor/resolution/fixups/addon_execute_repair.py

Co-authored-by: Stefan Agner <stefan@agner.ch>

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
2024-05-22 11:54:02 +02:00
dependabot[bot]
043af72847 Bump setuptools from 69.5.1 to 70.0.0 (#5100)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-22 09:33:26 +02:00
dependabot[bot]
05c7b6c639 Bump sentry-sdk from 2.2.0 to 2.2.1 (#5101)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-22 08:16:31 +02:00
dependabot[bot]
3385c99f1f --- (#5095)
updated-dependencies:
- dependency-name: dbus-fast
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-21 17:06:19 +02:00
dependabot[bot]
895117f857 --- (#5094)
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-21 17:05:23 +02:00
dependabot[bot]
9e3135e2de --- (#5093)
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-21 17:05:00 +02:00
Stefan Agner
9a1c517437 Pin Python requets package (#5097)
Make sure Python requests package is pinned to a known version. This is
required by docker-py for instance. This fixes current CI builds.
2024-05-21 14:52:24 +02:00
Mike Degatano
c0c0c4b7ad Fix doc and changelog API response for orphaned addons (#5082)
* Fix doc and changelog API response for orphaned addons

* Use correct terminology in tests
2024-05-21 09:02:01 +02:00
dependabot[bot]
be6e39fed0 Bump pylint from 3.1.1 to 3.2.1 (#5090)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-20 09:30:42 +02:00
dependabot[bot]
b384921ee0 Bump pytest from 8.2.0 to 8.2.1 (#5089)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-20 09:29:26 +02:00
dependabot[bot]
0d05a6eae3 Bump sentry-sdk from 2.1.1 to 2.2.0 (#5085)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-17 15:19:01 +02:00
dependabot[bot]
430aef68c6 Bump actions/checkout from 4.1.5 to 4.1.6 (#5086)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-17 10:40:37 +02:00
Stefan Agner
eac6070e12 Don't process hardware events when landing page is running (#5079) 2024-05-15 08:56:37 +02:00
dependabot[bot]
6693b7c2e6 Bump codecov/codecov-action from 4.3.1 to 4.4.0 (#5080)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-15 08:49:07 +02:00
dependabot[bot]
7898c3e433 Bump pylint from 3.1.0 to 3.1.1 (#5078)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-14 10:11:19 +02:00
dependabot[bot]
420ecd064e Bump pyudev from 0.24.1 to 0.24.3 (#5074)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-13 09:29:49 +02:00
dependabot[bot]
4289be53f8 Bump pre-commit from 3.7.0 to 3.7.1 (#5075)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-13 09:14:14 +02:00
dependabot[bot]
29b41b564e Bump ruff from 0.4.3 to 0.4.4 (#5072)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.3 to 0.4.4.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.4.3...v0.4.4)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-10 12:48:18 +02:00
dependabot[bot]
998eb69583 Bump dbus-fast from 2.21.1 to 2.21.2 (#5071)
Bumps [dbus-fast](https://github.com/bluetooth-devices/dbus-fast) from 2.21.1 to 2.21.2.
- [Release notes](https://github.com/bluetooth-devices/dbus-fast/releases)
- [Changelog](https://github.com/Bluetooth-Devices/dbus-fast/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bluetooth-devices/dbus-fast/compare/v2.21.1...v2.21.2)

---
updated-dependencies:
- dependency-name: dbus-fast
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-10 12:48:09 +02:00
Stefan Agner
8ebc097ff4 Revert "Bump orjson from 3.9.15 to 3.10.3 (#5057)" (#5068)
This reverts commit 71e91328f1.

It is not proven that the segfaults seen in Core are resolved. Since
we are shipping a bugfix release, conservatively revert back to 3.9.15.
2024-05-07 16:02:34 +02:00
Mike Degatano
c05984ca49 Fix no changelog API response (#5064)
* Fix no changelog API response

* Add comment reasoning HTTP 200 for no changelog

Co-authored-by: Stefan Agner <stefan@agner.ch>

* Apply suggestions from code review

Co-authored-by: Jan Čermák <sairon@users.noreply.github.com>

---------

Co-authored-by: Jan Čermák <sairon@users.noreply.github.com>
Co-authored-by: Stefan Agner <stefan@agner.ch>
2024-05-07 10:41:16 +02:00
dependabot[bot]
1a700c3013 Bump sentry-sdk from 2.0.1 to 2.1.1 (#5067)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.0.1 to 2.1.1.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.0.1...2.1.1)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 10:17:12 +02:00
dependabot[bot]
a9c92cdec8 Bump cryptography from 42.0.6 to 42.0.7 (#5066)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 09:23:56 +02:00
dependabot[bot]
da8b938d5b Bump actions/checkout from 4.1.4 to 4.1.5 (#5065) 2024-05-07 08:26:00 +02:00
dependabot[bot]
71e91328f1 Bump orjson from 3.9.15 to 3.10.3 (#5057)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 22:46:12 +02:00
dependabot[bot]
6356be4c52 Bump jinja2 from 3.1.3 to 3.1.4 (#5061)
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 11:14:01 +02:00
dependabot[bot]
e26e5440b6 Bump ruff from 0.4.2 to 0.4.3 (#5060)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.2 to 0.4.3.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.4.2...v0.4.3)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 11:13:04 +02:00
dependabot[bot]
fecfbd1a3e Bump coverage from 7.5.0 to 7.5.1 (#5059)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.5.0 to 7.5.1.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.5.0...7.5.1)

---
updated-dependencies:
- dependency-name: coverage
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 11:12:49 +02:00
dependabot[bot]
c00d6dfc76 Bump cryptography from 42.0.5 to 42.0.6 (#5058) 2024-05-06 10:00:57 +02:00
dependabot[bot]
85be66d90d Bump codecov/codecov-action from 4.3.0 to 4.3.1 (#5054)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-02 09:02:54 +02:00
Mike Degatano
1ac506b391 Skip udisks listener on failure to connect (#5049) 2024-05-01 10:50:48 +02:00
dependabot[bot]
f7738b77de Bump pytest from 8.1.1 to 8.2.0 (#5046)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.1.1 to 8.2.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.1.1...8.2.0)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-30 15:55:34 -04:00
Mike Degatano
824037bb7d Bump pytest-asyncio to 0.23.6 (#5048) 2024-04-30 15:46:44 -04:00
Stefan Agner
221292ad14 Mark issues in FixupBase abstract (#5033)
Since #5024 all fixups have an associated issue. Generally, it is
generally better to have an issue for every fixup so that things
can be mapped to repairs in Core easily. Let's mark the issues property
as abstract to indicate subclasses are required to implement it.
2024-04-29 16:24:58 -04:00
dependabot[bot]
16f8c75e9f Bump sentry-sdk from 1.45.0 to 2.0.1 (#5047)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 17:33:36 +02:00
dependabot[bot]
90a37079f1 Bump ruff from 0.4.1 to 0.4.2 (#5043)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-26 10:07:04 +02:00
J. Nick Koston
798092af5e Revert orjson to 3.9.15 due to segmentation faults (#5041)
https://github.com/ijl/orjson/issues/479
2024-04-25 17:48:48 +02:00
Jan Čermák
2a622a929d Limit reporting of errors in Supervisor logs fallback (#5040)
We do not need to capture HostNotSupported errors to Sentry. The only
possible code path this error might come from is where the Journal
Gateway Daemon socket is unavailable, which is already reported as an
"Unsupported system" repair.
2024-04-25 10:44:21 +02:00
dependabot[bot]
ca8eeaa68c Bump actions/download-artifact from 4.1.6 to 4.1.7 (#5039)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.1.6 to 4.1.7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4.1.6...v4.1.7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 09:25:11 +02:00
dependabot[bot]
d1b8ac1249 Bump actions/checkout from 4.1.3 to 4.1.4 (#5038)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.3 to 4.1.4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4.1.3...v4.1.4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 09:15:34 +02:00
dependabot[bot]
3f629c4d60 Bump coverage from 7.4.4 to 7.5.0 (#5037)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-24 09:52:18 +02:00
dependabot[bot]
3fa910e68b Bump actions/download-artifact from 4.1.5 to 4.1.6 (#5034)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.1.5 to 4.1.6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4.1.5...v4.1.6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 09:06:04 +02:00
dependabot[bot]
e3cf2989c9 Bump actions/upload-artifact from 4.3.2 to 4.3.3 (#5035)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.2 to 4.3.3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.3.2...v4.3.3)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 09:05:50 +02:00
dependabot[bot]
136b2f402d Bump dirhash from 0.3.0 to 0.4.0 (#5036)
Bumps [dirhash](https://github.com/andhus/dirhash-python) from 0.3.0 to 0.4.0.
- [Release notes](https://github.com/andhus/dirhash-python/releases)
- [Changelog](https://github.com/andhus/dirhash-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/andhus/dirhash-python/compare/v0.3.0...v0.4.0)

---
updated-dependencies:
- dependency-name: dirhash
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 09:05:06 +02:00
Mike Degatano
8d18d2d9c6 Use signals to recognize new disks immediately (#5023)
* Use signals to recognize new disks immediately

* Add test for disabled data disk issue

* Add mock of UDisks2 base service to test

* Apply suggestions from code review

* Shutdown manager first to avoid potential race conditions

* Update tests/dbus_service_mocks/udisks2.py

Co-authored-by: Jan Čermák <sairon@users.noreply.github.com>

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
Co-authored-by: Jan Čermák <sairon@users.noreply.github.com>
2024-04-22 16:35:03 +02:00
Mike Degatano
f18213361a Add issues field to create full backup suggestion (#5024) 2024-04-22 09:58:22 +02:00
Jan Čermák
18d9d32bca Fix Supervisor logs fallback (#5022)
Supervisor logs fallback in get_supervisor_logs didn't work properly
because the exception was caught in api_process_raw instead. This was
not discovered in tests because the side effect raised OSError, which
isn't handled there.

To address that, I split the advanced_logs to two functions, one being a
wrapped API handler, one being plain function returning response without
any additional error handling. The tests now check for both cases of
errors (HassioError and random generic Python error).

Refs #5021
2024-04-22 09:42:12 +02:00
dependabot[bot]
1246e429c9 Bump ruff from 0.4.0 to 0.4.1 (#5032)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.0 to 0.4.1.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.4.0...v0.4.1)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-22 09:14:49 +02:00
dependabot[bot]
77bc46bc37 Bump actions/checkout from 4.1.2 to 4.1.3 (#5031)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-22 08:50:53 +02:00
dependabot[bot]
ce16963c94 Bump actions/upload-artifact from 4.3.1 to 4.3.2 (#5025)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.1 to 4.3.2.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.3.1...v4.3.2)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-19 09:41:14 +02:00
dependabot[bot]
a70e8cfe58 Bump actions/download-artifact from 4.1.4 to 4.1.5 (#5026)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.1.4 to 4.1.5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4.1.4...v4.1.5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-19 09:41:06 +02:00
dependabot[bot]
ba922a1aaa Bump ruff from 0.3.7 to 0.4.0 (#5027)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.3.7 to 0.4.0.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.3.7...v0.4.0)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-19 09:40:19 +02:00
dependabot[bot]
b09230a884 Bump aiohttp from 3.9.4 to 3.9.5 (#5020)
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.9.4 to 3.9.5.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.4...v3.9.5)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-17 09:01:18 +02:00
dependabot[bot]
f1cb9ca08e Bump orjson from 3.9.15 to 3.10.1 (#5019)
Bumps [orjson](https://github.com/ijl/orjson) from 3.9.15 to 3.10.1.
- [Release notes](https://github.com/ijl/orjson/releases)
- [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ijl/orjson/compare/3.9.15...3.10.1)

---
updated-dependencies:
- dependency-name: orjson
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-17 08:59:30 +02:00
Mike Degatano
06513e88c6 Allow restarting core in safe mode (#5017) 2024-04-17 08:54:56 +02:00
dependabot[bot]
b4a79bd068 Bump setuptools from 69.2.0 to 69.5.1 (#5018) 2024-04-15 08:58:39 +02:00
Mike Degatano
dfd8fe84e0 Mount manager reload mounts all failed mounts (#5014)
* Mount manager reload mounts all failed mounts

* Remove invalid part of mount manager reload test
2024-04-12 12:02:29 +02:00
J. Nick Koston
4857c2e243 Bump aiohttp to 3.9.4 (#4899) 2024-04-12 08:51:07 +02:00
dependabot[bot]
7d384f6160 Bump ruff from 0.3.5 to 0.3.7 (#5015)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-12 08:45:52 +02:00
Mike Degatano
672a7621f9 Adopt a disabled data disk (#5010) 2024-04-11 13:53:19 -04:00
Mike Degatano
f0e2fb3f57 Addon load should not fail due to docker error (#5011) 2024-04-11 15:06:57 +02:00
dependabot[bot]
8c3a520512 Bump sentry-sdk from 1.44.1 to 1.45.0 (#5012)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 1.44.1 to 1.45.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/1.44.1...1.45.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-11 08:56:48 +02:00
dependabot[bot]
22e50d56db Bump sigstore/cosign-installer from 3.4.0 to 3.5.0 (#5013)
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.4.0...v3.5.0)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-11 08:55:50 +02:00
58 changed files with 1579 additions and 153 deletions

View File

@@ -53,7 +53,7 @@ jobs:
requirements: ${{ steps.requirements.outputs.changed }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
@@ -92,7 +92,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.6
with:
fetch-depth: 0
@@ -131,7 +131,7 @@ jobs:
- name: Install Cosign
if: needs.init.outputs.publish == 'true'
uses: sigstore/cosign-installer@v3.4.0
uses: sigstore/cosign-installer@v3.5.0
with:
cosign-release: "v2.2.3"
@@ -149,7 +149,7 @@ jobs:
- name: Login to GitHub Container Registry
if: needs.init.outputs.publish == 'true'
uses: docker/login-action@v3.1.0
uses: docker/login-action@v3.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -178,7 +178,7 @@ jobs:
steps:
- name: Checkout the repository
if: needs.init.outputs.publish == 'true'
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.6
- name: Initialize git
if: needs.init.outputs.publish == 'true'
@@ -203,7 +203,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.6
- name: Build the Supervisor
if: needs.init.outputs.publish != 'true'

View File

@@ -25,7 +25,7 @@ jobs:
name: Prepare Python dependencies
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.6
- name: Set up Python
id: python
uses: actions/setup-python@v5.1.0
@@ -67,7 +67,7 @@ jobs:
needs: prepare
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.6
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@v5.1.0
id: python
@@ -110,7 +110,7 @@ jobs:
needs: prepare
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.6
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@v5.1.0
id: python
@@ -153,7 +153,7 @@ jobs:
needs: prepare
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.6
- name: Register hadolint problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
@@ -168,7 +168,7 @@ jobs:
needs: prepare
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.6
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@v5.1.0
id: python
@@ -212,7 +212,7 @@ jobs:
needs: prepare
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.6
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@v5.1.0
id: python
@@ -256,7 +256,7 @@ jobs:
needs: prepare
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.6
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@v5.1.0
id: python
@@ -288,14 +288,14 @@ jobs:
name: Run tests Python ${{ needs.prepare.outputs.python-version }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.6
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@v5.1.0
id: python
with:
python-version: ${{ needs.prepare.outputs.python-version }}
- name: Install Cosign
uses: sigstore/cosign-installer@v3.4.0
uses: sigstore/cosign-installer@v3.5.0
with:
cosign-release: "v2.2.3"
- name: Restore Python virtual environment
@@ -335,7 +335,7 @@ jobs:
-o console_output_style=count \
tests
- name: Upload coverage artifact
uses: actions/upload-artifact@v4.3.1
uses: actions/upload-artifact@v4.3.3
with:
name: coverage-${{ matrix.python-version }}
path: .coverage
@@ -346,7 +346,7 @@ jobs:
needs: ["pytest", "prepare"]
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.6
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@v5.1.0
id: python
@@ -365,7 +365,7 @@ jobs:
echo "Failed to restore Python virtual environment from cache"
exit 1
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.4
uses: actions/download-artifact@v4.1.7
- name: Combine coverage results
run: |
. venv/bin/activate
@@ -373,4 +373,4 @@ jobs:
coverage report
coverage xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4.3.0
uses: codecov/codecov-action@v4.4.1

View File

@@ -11,7 +11,7 @@ jobs:
name: Release Drafter
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.6
with:
fetch-depth: 0

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.2
uses: actions/checkout@v4.1.6
- name: Sentry Release
uses: getsentry/action-release@v1.7.0
env:

View File

@@ -1,5 +1,5 @@
aiodns==3.2.0
aiohttp==3.9.3
aiohttp==3.9.5
aiohttp-fast-url-dispatcher==0.3.0
atomicwrites-homeassistant==1.4.1
attrs==23.2.0
@@ -8,22 +8,23 @@ brotli==1.1.0
ciso8601==2.3.1
colorlog==6.8.2
cpe==1.2.1
cryptography==42.0.5
cryptography==42.0.8
debugpy==1.8.1
deepmerge==1.1.1
dirhash==0.3.0
docker==7.0.0
dirhash==0.4.0
docker==7.1.0
faust-cchardet==2.1.19
gitpython==3.1.43
jinja2==3.1.3
jinja2==3.1.4
orjson==3.9.15
pulsectl==24.4.0
pyudev==0.24.1
pyudev==0.24.3
PyYAML==6.0.1
requests==2.32.3
securetar==2024.2.1
sentry-sdk==1.44.1
setuptools==69.2.0
sentry-sdk==2.5.1
setuptools==70.0.0
voluptuous==0.14.2
dbus-fast==2.21.1
typing_extensions==4.11.0
dbus-fast==2.21.3
typing_extensions==4.12.2
zlib-fast==0.2.0

View File

@@ -1,12 +1,12 @@
coverage==7.4.4
pre-commit==3.7.0
pylint==3.1.0
coverage==7.5.3
pre-commit==3.7.1
pylint==3.2.3
pytest-aiohttp==1.0.5
pytest-asyncio==0.23.5
pytest-asyncio==0.23.6
pytest-cov==5.0.0
pytest-timeout==2.3.1
pytest==8.1.1
ruff==0.3.5
pytest==8.2.2
ruff==0.4.8
time-machine==2.14.1
typing_extensions==4.11.0
typing_extensions==4.12.2
urllib3==2.2.1

View File

@@ -203,7 +203,7 @@ class Addon(AddonModel):
await self.instance.check_image(self.version, default_image, self.arch)
except DockerError:
_LOGGER.info("No %s addon Docker image %s found", self.slug, self.image)
with suppress(AddonsError):
with suppress(DockerError):
await self.instance.install(self.version, default_image, arch=self.arch)
self.persist[ATTR_IMAGE] = default_image
@@ -285,9 +285,13 @@ class Addon(AddonModel):
@property
def need_update(self) -> bool:
"""Return True if an update is available."""
if self.is_detached:
if self.is_detached or self.version == self.latest_version:
return False
return self.version != self.latest_version
with suppress(AddonsNotSupportedError):
self._validate_availability(self.data_store)
return True
return False
@property
def dns(self) -> list[str]:
@@ -1343,11 +1347,11 @@ class Addon(AddonModel):
)
raise AddonsError() from err
finally:
# Is add-on loaded
if not self.loaded:
await self.load()
finally:
# Run add-on
if data[ATTR_STATE] == AddonState.STARTED:
wait_for_start = await self.start()

View File

@@ -9,7 +9,7 @@ from aiohttp_fast_url_dispatcher import FastUrlDispatcher, attach_fast_url_dispa
from ..const import AddonState
from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import APIAddonNotInstalled
from ..exceptions import APIAddonNotInstalled, HostNotSupportedError
from ..utils.sentry import capture_exception
from .addons import APIAddons
from .audio import APIAudio
@@ -401,7 +401,7 @@ class RestAPI(CoreSysAttributes):
async def get_supervisor_logs(*args, **kwargs):
try:
return await self._api_host.advanced_logs(
return await self._api_host.advanced_logs_handler(
*args, identifier="hassio_supervisor", **kwargs
)
except Exception as err: # pylint: disable=broad-exception-caught
@@ -410,7 +410,10 @@ class RestAPI(CoreSysAttributes):
_LOGGER.exception(
"Failed to get supervisor logs using advanced_logs API"
)
capture_exception(err)
if not isinstance(err, HostNotSupportedError):
# No need to capture HostNotSupportedError to Sentry, the cause
# is known and reported to the user using the resolution center.
capture_exception(err)
return await api_supervisor.logs(*args, **kwargs)
self.webapp.add_routes(
@@ -694,7 +697,6 @@ class RestAPI(CoreSysAttributes):
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.get("/store/addons/{addon}/icon", api_store.addons_addon_icon),
web.get("/store/addons/{addon}/logo", api_store.addons_addon_logo),
web.get(
@@ -716,6 +718,8 @@ class RestAPI(CoreSysAttributes):
"/store/addons/{addon}/update/{version}",
api_store.addons_addon_update,
),
# Must be below others since it has a wildcard in resource path
web.get("/store/addons/{addon}/{version}", api_store.addons_addon_info),
web.post("/store/reload", api_store.reload),
web.get("/store/repositories", api_store.repositories_list),
web.get(

View File

@@ -53,6 +53,7 @@ ATTR_PANEL_PATH = "panel_path"
ATTR_REMOVABLE = "removable"
ATTR_REMOVE_CONFIG = "remove_config"
ATTR_REVISION = "revision"
ATTR_SAFE_MODE = "safe_mode"
ATTR_SEAT = "seat"
ATTR_SIGNED = "signed"
ATTR_STARTUP_TIME = "startup_time"

View File

@@ -16,7 +16,7 @@ from ..const import (
ATTR_SYSTEM,
)
from ..coresys import CoreSysAttributes
from ..dbus.udisks2 import UDisks2
from ..dbus.udisks2 import UDisks2Manager
from ..dbus.udisks2.block import UDisks2Block
from ..dbus.udisks2.drive import UDisks2Drive
from ..hardware.data import Device
@@ -72,7 +72,7 @@ def filesystem_struct(fs_block: UDisks2Block) -> dict[str, Any]:
}
def drive_struct(udisks2: UDisks2, drive: UDisks2Drive) -> dict[str, Any]:
def drive_struct(udisks2: UDisks2Manager, drive: UDisks2Drive) -> dict[str, Any]:
"""Return a dict with information of a disk to be used in the API."""
return {
ATTR_VENDOR: drive.vendor,

View File

@@ -36,6 +36,7 @@ from ..const import (
from ..coresys import CoreSysAttributes
from ..exceptions import APIError
from ..validate import docker_image, network_port, version_tag
from .const import ATTR_SAFE_MODE
from .utils import api_process, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__)
@@ -62,6 +63,12 @@ SCHEMA_UPDATE = vol.Schema(
}
)
SCHEMA_RESTART = vol.Schema(
{
vol.Optional(ATTR_SAFE_MODE, default=False): vol.Boolean(),
}
)
class APIHomeAssistant(CoreSysAttributes):
"""Handle RESTful API for Home Assistant functions."""
@@ -166,14 +173,22 @@ class APIHomeAssistant(CoreSysAttributes):
return asyncio.shield(self.sys_homeassistant.core.start())
@api_process
def restart(self, request: web.Request) -> Awaitable[None]:
async def restart(self, request: web.Request) -> None:
"""Restart Home Assistant."""
return asyncio.shield(self.sys_homeassistant.core.restart())
body = await api_validate(SCHEMA_RESTART, request)
await asyncio.shield(
self.sys_homeassistant.core.restart(safe_mode=body[ATTR_SAFE_MODE])
)
@api_process
def rebuild(self, request: web.Request) -> Awaitable[None]:
async def rebuild(self, request: web.Request) -> None:
"""Rebuild Home Assistant."""
return asyncio.shield(self.sys_homeassistant.core.rebuild())
body = await api_validate(SCHEMA_RESTART, request)
await asyncio.shield(
self.sys_homeassistant.core.rebuild(safe_mode=body[ATTR_SAFE_MODE])
)
@api_process
async def check(self, request: web.Request) -> None:

View File

@@ -1,4 +1,5 @@
"""Init file for Supervisor host RESTful API."""
import asyncio
from contextlib import suppress
import logging
@@ -163,8 +164,7 @@ class APIHost(CoreSysAttributes):
raise APIError() from err
return possible_offset
@api_process_raw(CONTENT_TYPE_TEXT, error_type=CONTENT_TYPE_TEXT)
async def advanced_logs(
async def advanced_logs_handler(
self, request: web.Request, identifier: str | None = None, follow: bool = False
) -> web.StreamResponse:
"""Return systemd-journald logs."""
@@ -218,3 +218,10 @@ class APIHost(CoreSysAttributes):
"Connection reset when trying to fetch data from systemd-journald."
) from ex
return response
@api_process_raw(CONTENT_TYPE_TEXT, error_type=CONTENT_TYPE_TEXT)
async def advanced_logs(
self, request: web.Request, identifier: str | None = None, follow: bool = False
) -> web.StreamResponse:
"""Return systemd-journald logs. Wrapped as standard API handler."""
return await self.advanced_logs_handler(request, identifier, follow)

View File

@@ -249,9 +249,14 @@ class APIStore(CoreSysAttributes):
@api_process_raw(CONTENT_TYPE_TEXT)
async def addons_addon_changelog(self, request: web.Request) -> str:
"""Return changelog from add-on."""
addon = self._extract_addon(request)
# Frontend can't handle error response here, need to return 200 and error as text for now
try:
addon = self._extract_addon(request)
except APIError as err:
return str(err)
if not addon.with_changelog:
raise APIError(f"No changelog found for add-on {addon.slug}!")
return f"No changelog found for add-on {addon.slug}!"
with addon.path_changelog.open("r") as changelog:
return changelog.read()
@@ -259,9 +264,14 @@ class APIStore(CoreSysAttributes):
@api_process_raw(CONTENT_TYPE_TEXT)
async def addons_addon_documentation(self, request: web.Request) -> str:
"""Return documentation from add-on."""
addon = self._extract_addon(request)
# Frontend can't handle error response here, need to return 200 and error as text for now
try:
addon = self._extract_addon(request)
except APIError as err:
return str(err)
if not addon.with_documentation:
raise APIError(f"No documentation found for add-on {addon.slug}!")
return f"No documentation found for add-on {addon.slug}!"
with addon.path_documentation.open("r") as documentation:
return documentation.read()

View File

@@ -345,9 +345,6 @@ class Core(CoreSysAttributes):
if self.state == CoreState.RUNNING:
self.state = CoreState.SHUTDOWN
# Stop docker monitoring
await self.sys_docker.unload()
# Shutdown Application Add-ons, using Home Assistant API
await self.sys_addons.shutdown(AddonStartup.APPLICATION)

View File

@@ -61,7 +61,8 @@ DBUS_OBJECT_RESOLVED = "/org/freedesktop/resolve1"
DBUS_OBJECT_SETTINGS = "/org/freedesktop/NetworkManager/Settings"
DBUS_OBJECT_SYSTEMD = "/org/freedesktop/systemd1"
DBUS_OBJECT_TIMEDATE = "/org/freedesktop/timedate1"
DBUS_OBJECT_UDISKS2 = "/org/freedesktop/UDisks2/Manager"
DBUS_OBJECT_UDISKS2 = "/org/freedesktop/UDisks2"
DBUS_OBJECT_UDISKS2_MANAGER = "/org/freedesktop/UDisks2/Manager"
DBUS_ATTR_ACTIVE_ACCESSPOINT = "ActiveAccessPoint"
DBUS_ATTR_ACTIVE_CONNECTION = "ActiveConnection"

View File

@@ -17,7 +17,7 @@ from .rauc import Rauc
from .resolved import Resolved
from .systemd import Systemd
from .timedate import TimeDate
from .udisks2 import UDisks2
from .udisks2 import UDisks2Manager
_LOGGER: logging.Logger = logging.getLogger(__name__)
@@ -37,7 +37,7 @@ class DBusManager(CoreSysAttributes):
self._agent: OSAgent = OSAgent()
self._timedate: TimeDate = TimeDate()
self._resolved: Resolved = Resolved()
self._udisks2: UDisks2 = UDisks2()
self._udisks2: UDisks2Manager = UDisks2Manager()
self._bus: MessageBus | None = None
@property
@@ -81,7 +81,7 @@ class DBusManager(CoreSysAttributes):
return self._resolved
@property
def udisks2(self) -> UDisks2:
def udisks2(self) -> UDisks2Manager:
"""Return the udisks2 interface."""
return self._udisks2

View File

@@ -15,12 +15,15 @@ from ...exceptions import (
from ..const import (
DBUS_ATTR_SUPPORTED_FILESYSTEMS,
DBUS_ATTR_VERSION,
DBUS_IFACE_BLOCK,
DBUS_IFACE_DRIVE,
DBUS_IFACE_UDISKS2_MANAGER,
DBUS_NAME_UDISKS2,
DBUS_OBJECT_BASE,
DBUS_OBJECT_UDISKS2,
DBUS_OBJECT_UDISKS2_MANAGER,
)
from ..interface import DBusInterfaceProxy, dbus_property
from ..interface import DBusInterface, DBusInterfaceProxy, dbus_property
from ..utils import dbus_connected
from .block import UDisks2Block
from .const import UDISKS2_DEFAULT_OPTIONS
@@ -30,7 +33,15 @@ from .drive import UDisks2Drive
_LOGGER: logging.Logger = logging.getLogger(__name__)
class UDisks2(DBusInterfaceProxy):
class UDisks2(DBusInterface):
"""Handle D-Bus interface for UDisks2 root object."""
name: str = DBUS_NAME_UDISKS2
bus_name: str = DBUS_NAME_UDISKS2
object_path: str = DBUS_OBJECT_UDISKS2
class UDisks2Manager(DBusInterfaceProxy):
"""Handle D-Bus interface for UDisks2.
http://storaged.org/doc/udisks2-api/latest/
@@ -38,22 +49,36 @@ class UDisks2(DBusInterfaceProxy):
name: str = DBUS_NAME_UDISKS2
bus_name: str = DBUS_NAME_UDISKS2
object_path: str = DBUS_OBJECT_UDISKS2
object_path: str = DBUS_OBJECT_UDISKS2_MANAGER
properties_interface: str = DBUS_IFACE_UDISKS2_MANAGER
_block_devices: dict[str, UDisks2Block] = {}
_drives: dict[str, UDisks2Drive] = {}
def __init__(self):
"""Initialize object."""
super().__init__()
self.udisks2_object_manager = UDisks2()
async def connect(self, bus: MessageBus):
"""Connect to D-Bus."""
try:
await super().connect(bus)
await self.udisks2_object_manager.connect(bus)
except DBusError:
_LOGGER.warning("Can't connect to udisks2")
except (DBusServiceUnkownError, DBusInterfaceError):
_LOGGER.warning(
"No udisks2 support on the host. Host control has been disabled."
)
else:
# Register for signals on devices added/removed
self.udisks2_object_manager.dbus.object_manager.on_interfaces_added(
self._interfaces_added
)
self.udisks2_object_manager.dbus.object_manager.on_interfaces_removed(
self._interfaces_removed
)
@dbus_connected
async def update(self, changed: dict[str, Any] | None = None) -> None:
@@ -161,11 +186,47 @@ class UDisks2(DBusInterfaceProxy):
]
)
async def _interfaces_added(
self, object_path: str, properties: dict[str, dict[str, Any]]
) -> None:
"""Interfaces added to a UDisks2 object."""
if object_path in self._block_devices:
await self._block_devices[object_path].update()
return
if object_path in self._drives:
await self._drives[object_path].update()
return
if DBUS_IFACE_BLOCK in properties:
self._block_devices[object_path] = await UDisks2Block.new(
object_path, self.dbus.bus
)
return
if DBUS_IFACE_DRIVE in properties:
self._drives[object_path] = await UDisks2Drive.new(
object_path, self.dbus.bus
)
async def _interfaces_removed(
self, object_path: str, interfaces: list[str]
) -> None:
"""Interfaces removed from a UDisks2 object."""
if object_path in self._block_devices and DBUS_IFACE_BLOCK in interfaces:
self._block_devices[object_path].shutdown()
del self._block_devices[object_path]
return
if object_path in self._drives and DBUS_IFACE_DRIVE in interfaces:
self._drives[object_path].shutdown()
del self._drives[object_path]
def shutdown(self) -> None:
"""Shutdown the object and disconnect from D-Bus.
This method is irreversible.
"""
self.udisks2_object_manager.shutdown()
for block_device in self.block_devices:
block_device.shutdown()
for drive in self.drives:

View File

@@ -103,7 +103,13 @@ class HardwareManager(CoreSysAttributes):
# Exctract all devices
for device in self._udev.list_devices():
# Skip devices without mapping
if not device.device_node or self.helper.hide_virtual_device(device):
try:
if not device.device_node or self.helper.hide_virtual_device(device):
continue
except UnicodeDecodeError as err:
# Some udev properties have an unkown/different encoding. This is a general
# problem with pyudev, see https://github.com/pyudev/pyudev/pull/230
_LOGGER.warning("Ignoring udev device due to error: %s", err)
continue
self._devices[device.sys_name] = Device.import_udev(device)

View File

@@ -1,6 +1,7 @@
"""Constants for homeassistant."""
from datetime import timedelta
from enum import StrEnum
from pathlib import PurePath
from awesomeversion import AwesomeVersion
@@ -12,6 +13,7 @@ WATCHDOG_RETRY_SECONDS = 10
WATCHDOG_MAX_ATTEMPTS = 5
WATCHDOG_THROTTLE_PERIOD = timedelta(minutes=30)
WATCHDOG_THROTTLE_MAX_CALLS = 10
SAFE_MODE_FILENAME = PurePath("safe-mode")
CLOSING_STATES = [
CoreState.SHUTDOWN,

View File

@@ -35,6 +35,7 @@ from ..utils import convert_to_ascii
from ..utils.sentry import capture_exception
from .const import (
LANDINGPAGE,
SAFE_MODE_FILENAME,
WATCHDOG_MAX_ATTEMPTS,
WATCHDOG_RETRY_SECONDS,
WATCHDOG_THROTTLE_MAX_CALLS,
@@ -362,8 +363,15 @@ class HomeAssistantCore(JobGroup):
limit=JobExecutionLimit.GROUP_ONCE,
on_condition=HomeAssistantJobError,
)
async def restart(self) -> None:
async def restart(self, *, safe_mode: bool = False) -> None:
"""Restart Home Assistant Docker."""
# Create safe mode marker file if necessary
if safe_mode:
_LOGGER.debug("Creating safe mode marker file.")
await self.sys_run_in_executor(
(self.sys_config.path_homeassistant / SAFE_MODE_FILENAME).touch
)
try:
await self.instance.restart()
except DockerError as err:
@@ -376,8 +384,15 @@ class HomeAssistantCore(JobGroup):
limit=JobExecutionLimit.GROUP_ONCE,
on_condition=HomeAssistantJobError,
)
async def rebuild(self) -> None:
async def rebuild(self, *, safe_mode: bool = False) -> None:
"""Rebuild Home Assistant Docker container."""
# Create safe mode marker file if necessary
if safe_mode:
_LOGGER.debug("Creating safe mode marker file.")
await self.sys_run_in_executor(
(self.sys_config.path_homeassistant / SAFE_MODE_FILENAME).touch
)
with suppress(DockerError):
await self.instance.stop()
await self.start()

View File

@@ -48,7 +48,7 @@ from ..utils import remove_folder
from ..utils.common import FileConfiguration
from ..utils.json import read_json_file, write_json_file
from .api import HomeAssistantAPI
from .const import ATTR_OVERRIDE_IMAGE, WSType
from .const import ATTR_OVERRIDE_IMAGE, LANDINGPAGE, WSType
from .core import HomeAssistantCore
from .secrets import HomeAssistantSecrets
from .validate import SCHEMA_HASS_CONFIG
@@ -328,6 +328,7 @@ class HomeAssistant(FileConfiguration, CoreSysAttributes):
if (
not self.sys_hardware.policy.is_match_cgroup(PolicyGroup.UART, device)
or not self.version
or self.version == LANDINGPAGE
or self.version < "2021.9.0"
):
return

View File

@@ -150,15 +150,10 @@ class MountManager(FileConfiguration, CoreSysAttributes):
*[mount.update() for mount in mounts], return_exceptions=True
)
# Try to reload any newly failed mounts and report issues if failure persists
new_failures = [
mounts[i]
for i in range(len(mounts))
if results[i] is not True
and mounts[i].failed_issue not in self.sys_resolution.issues
]
# Try to reload failed mounts and report issues if failure persists
failures = [mounts[i] for i in range(len(mounts)) if results[i] is not True]
await self._mount_errors_to_issues(
new_failures, [mount.reload() for mount in new_failures]
failures, [self.reload_mount(mount.name) for mount in failures]
)
async def _mount_errors_to_issues(
@@ -170,6 +165,8 @@ class MountManager(FileConfiguration, CoreSysAttributes):
for i in range(len(errors)): # pylint: disable=consider-using-enumerate
if not errors[i]:
continue
if mounts[i].failed_issue in self.sys_resolution.issues:
continue
if not isinstance(errors[i], MountError):
capture_exception(errors[i])

View File

@@ -342,20 +342,23 @@ class Mount(CoreSysAttributes, ABC):
"Mount %s is not mounted, mounting instead of reloading", self.name
)
await self.mount()
return
except DBusError as err:
raise MountError(
f"Could not reload mount {self.name} due to: {err!s}", _LOGGER.error
) from err
else:
if await self._update_unit():
await self._update_state_await(not_state=UnitActiveState.ACTIVATING)
if await self._update_unit():
await self._update_state_await(not_state=UnitActiveState.ACTIVATING)
if not await self.is_mounted():
raise MountActivationError(
f"Reloading {self.name} did not succeed. Check host logs for errors from mount or systemd unit {self.unit_name} for details.",
_LOGGER.error,
)
if not await self.is_mounted():
raise MountActivationError(
f"Reloading {self.name} did not succeed. Check host logs for errors from mount or systemd unit {self.unit_name} for details.",
_LOGGER.error,
)
# If it is mounted now, dismiss corresponding issue if present
if self.failed_issue in self.sys_resolution.issues:
self.sys_resolution.dismiss_issue(self.failed_issue)
class NetworkMount(Mount, ABC):

View File

@@ -1,6 +1,7 @@
"""Constants for OS."""
FILESYSTEM_LABEL_DATA_DISK = "hassos-data"
FILESYSTEM_LABEL_DISABLED_DATA_DISK = "hassos-data-dis"
FILESYSTEM_LABEL_OLD_DATA_DISK = "hassos-data-old"
PARTITION_NAME_EXTERNAL_DATA_DISK = "hassos-data-external"
PARTITION_NAME_OLD_EXTERNAL_DATA_DISK = "hassos-data-external-old"

View File

@@ -1,14 +1,16 @@
"""Home Assistant Operating-System DataDisk."""
import asyncio
from contextlib import suppress
from dataclasses import dataclass
import logging
from pathlib import Path
from typing import Final
from typing import Any, Final
from awesomeversion import AwesomeVersion
from ..coresys import CoreSys, CoreSysAttributes
from ..dbus.const import DBUS_ATTR_ID_LABEL, DBUS_IFACE_BLOCK
from ..dbus.udisks2.block import UDisks2Block
from ..dbus.udisks2.const import FormatType
from ..dbus.udisks2.drive import UDisks2Drive
@@ -22,8 +24,12 @@ from ..exceptions import (
)
from ..jobs.const import JobCondition, JobExecutionLimit
from ..jobs.decorator import Job
from ..resolution.checks.disabled_data_disk import CheckDisabledDataDisk
from ..resolution.checks.multiple_data_disks import CheckMultipleDataDisks
from ..utils.sentry import capture_exception
from .const import (
FILESYSTEM_LABEL_DATA_DISK,
FILESYSTEM_LABEL_DISABLED_DATA_DISK,
PARTITION_NAME_EXTERNAL_DATA_DISK,
PARTITION_NAME_OLD_EXTERNAL_DATA_DISK,
)
@@ -157,6 +163,16 @@ class DataDisk(CoreSysAttributes):
return available
@property
def check_multiple_data_disks(self) -> CheckMultipleDataDisks:
"""Resolution center check for multiple data disks."""
return self.sys_resolution.check.get("multiple_data_disks")
@property
def check_disabled_data_disk(self) -> CheckDisabledDataDisk:
"""Resolution center check for disabled data disk."""
return self.sys_resolution.check.get("disabled_data_disk")
def _get_block_devices_for_drive(self, drive: UDisks2Drive) -> list[UDisks2Block]:
"""Get block devices for a drive."""
return [
@@ -172,6 +188,14 @@ class DataDisk(CoreSysAttributes):
if self.sys_dbus.agent.version >= AwesomeVersion("1.2.0"):
await self.sys_dbus.agent.datadisk.reload_device()
# Register for signals on devices added/removed
self.sys_dbus.udisks2.udisks2_object_manager.dbus.object_manager.on_interfaces_added(
self._udisks2_interface_added
)
self.sys_dbus.udisks2.udisks2_object_manager.dbus.object_manager.on_interfaces_removed(
self._udisks2_interface_removed
)
@Job(
name="data_disk_migrate",
conditions=[JobCondition.HAOS, JobCondition.OS_AGENT, JobCondition.HEALTHY],
@@ -348,3 +372,54 @@ class DataDisk(CoreSysAttributes):
"New data partition prepared on device %s", partition_block.device
)
return partition_block
async def _udisks2_interface_added(
self, _: str, properties: dict[str, dict[str, Any]]
):
"""If a data disk is added, trigger the resolution check."""
if (
DBUS_IFACE_BLOCK not in properties
or DBUS_ATTR_ID_LABEL not in properties[DBUS_IFACE_BLOCK]
):
return
if (
properties[DBUS_IFACE_BLOCK][DBUS_ATTR_ID_LABEL]
== FILESYSTEM_LABEL_DATA_DISK
):
check = self.check_multiple_data_disks
elif (
properties[DBUS_IFACE_BLOCK][DBUS_ATTR_ID_LABEL]
== FILESYSTEM_LABEL_DISABLED_DATA_DISK
):
check = self.check_disabled_data_disk
else:
return
# Delay briefly before running check to allow data updates to occur
await asyncio.sleep(0.1)
await check()
async def _udisks2_interface_removed(self, _: str, interfaces: list[str]):
"""If affected by a data disk issue, re-check on removal of a block device."""
if DBUS_IFACE_BLOCK not in interfaces:
return
if any(
issue.type == self.check_multiple_data_disks.issue
and issue.context == self.check_multiple_data_disks.context
for issue in self.sys_resolution.issues
):
check = self.check_multiple_data_disks
elif any(
issue.type == self.check_disabled_data_disk.issue
and issue.context == self.check_disabled_data_disk.context
for issue in self.sys_resolution.issues
):
check = self.check_disabled_data_disk
else:
return
# Delay briefly before running check to allow data updates to occur
await asyncio.sleep(0.1)
await check()

View File

@@ -0,0 +1,49 @@
"""Helpers to check for detached addons due to repo misisng."""
from ...const import CoreState
from ...coresys import CoreSys
from ..const import ContextType, IssueType
from .base import CheckBase
def setup(coresys: CoreSys) -> CheckBase:
"""Check setup function."""
return CheckDetachedAddonMissing(coresys)
class CheckDetachedAddonMissing(CheckBase):
"""CheckDetachedAddonMissing class for check."""
async def run_check(self) -> None:
"""Run check if not affected by issue."""
for addon in self.sys_addons.installed:
if (
addon.is_detached
and addon.repository not in self.sys_store.repositories
):
self.sys_resolution.create_issue(
IssueType.DETACHED_ADDON_MISSING,
ContextType.ADDON,
reference=addon.slug,
)
async def approve_check(self, reference: str | None = None) -> bool:
"""Approve check if it is affected by issue."""
return (
addon := self.sys_addons.get(reference, local_only=True)
) and addon.is_detached
@property
def issue(self) -> IssueType:
"""Return a IssueType enum."""
return IssueType.DETACHED_ADDON_MISSING
@property
def context(self) -> ContextType:
"""Return a ContextType enum."""
return ContextType.ADDON
@property
def states(self) -> list[CoreState]:
"""Return a list of valid states when this check can run."""
return [CoreState.SETUP]

View File

@@ -0,0 +1,47 @@
"""Helpers to check for detached addons due to removal from repo."""
from ...const import CoreState
from ...coresys import CoreSys
from ..const import ContextType, IssueType, SuggestionType
from .base import CheckBase
def setup(coresys: CoreSys) -> CheckBase:
"""Check setup function."""
return CheckDetachedAddonRemoved(coresys)
class CheckDetachedAddonRemoved(CheckBase):
"""CheckDetachedAddonRemoved class for check."""
async def run_check(self) -> None:
"""Run check if not affected by issue."""
for addon in self.sys_addons.installed:
if addon.is_detached and addon.repository in self.sys_store.repositories:
self.sys_resolution.create_issue(
IssueType.DETACHED_ADDON_REMOVED,
ContextType.ADDON,
reference=addon.slug,
suggestions=[SuggestionType.EXECUTE_REMOVE],
)
async def approve_check(self, reference: str | None = None) -> bool:
"""Approve check if it is affected by issue."""
return (
addon := self.sys_addons.get(reference, local_only=True)
) and addon.is_detached
@property
def issue(self) -> IssueType:
"""Return a IssueType enum."""
return IssueType.DETACHED_ADDON_REMOVED
@property
def context(self) -> ContextType:
"""Return a ContextType enum."""
return ContextType.ADDON
@property
def states(self) -> list[CoreState]:
"""Return a list of valid states when this check can run."""
return [CoreState.SETUP]

View File

@@ -0,0 +1,63 @@
"""Helpers to check for a disabled data disk."""
from pathlib import Path
from ...const import CoreState
from ...coresys import CoreSys
from ...dbus.udisks2.block import UDisks2Block
from ...dbus.udisks2.data import DeviceSpecification
from ...os.const import FILESYSTEM_LABEL_DISABLED_DATA_DISK
from ..const import ContextType, IssueType, SuggestionType
from .base import CheckBase
def setup(coresys: CoreSys) -> CheckBase:
"""Check setup function."""
return CheckDisabledDataDisk(coresys)
class CheckDisabledDataDisk(CheckBase):
"""CheckDisabledDataDisk class for check."""
async def run_check(self) -> None:
"""Run check if not affected by issue."""
for block_device in self.sys_dbus.udisks2.block_devices:
if self._is_disabled_data_disk(block_device):
self.sys_resolution.create_issue(
IssueType.DISABLED_DATA_DISK,
ContextType.SYSTEM,
reference=block_device.device.as_posix(),
suggestions=[
SuggestionType.RENAME_DATA_DISK,
SuggestionType.ADOPT_DATA_DISK,
],
)
async def approve_check(self, reference: str | None = None) -> bool:
"""Approve check if it is affected by issue."""
resolved = await self.sys_dbus.udisks2.resolve_device(
DeviceSpecification(path=Path(reference))
)
return resolved and self._is_disabled_data_disk(resolved[0])
def _is_disabled_data_disk(self, block_device: UDisks2Block) -> bool:
"""Return true if filesystem block device has name indicating it was disabled by OS."""
return (
block_device.filesystem
and block_device.id_label == FILESYSTEM_LABEL_DISABLED_DATA_DISK
)
@property
def issue(self) -> IssueType:
"""Return a IssueType enum."""
return IssueType.DISABLED_DATA_DISK
@property
def context(self) -> ContextType:
"""Return a ContextType enum."""
return ContextType.SYSTEM
@property
def states(self) -> list[CoreState]:
"""Return a list of valid states when this check can run."""
return [CoreState.RUNNING, CoreState.SETUP]

View File

@@ -73,6 +73,9 @@ class IssueType(StrEnum):
CORRUPT_DOCKER = "corrupt_docker"
CORRUPT_REPOSITORY = "corrupt_repository"
CORRUPT_FILESYSTEM = "corrupt_filesystem"
DETACHED_ADDON_MISSING = "detached_addon_missing"
DETACHED_ADDON_REMOVED = "detached_addon_removed"
DISABLED_DATA_DISK = "disabled_data_disk"
DNS_LOOP = "dns_loop"
DNS_SERVER_FAILED = "dns_server_failed"
DNS_SERVER_IPV6_ERROR = "dns_server_ipv6_error"

View File

@@ -0,0 +1,52 @@
"""Helpers to fix addon issue by removing it."""
import logging
from ...coresys import CoreSys
from ...exceptions import AddonsError, ResolutionFixupError
from ..const import ContextType, IssueType, SuggestionType
from .base import FixupBase
_LOGGER: logging.Logger = logging.getLogger(__name__)
def setup(coresys: CoreSys) -> FixupBase:
"""Check setup function."""
return FixupAddonExecuteRemove(coresys)
class FixupAddonExecuteRemove(FixupBase):
"""Storage class for fixup."""
async def process_fixup(self, reference: str | None = None) -> None:
"""Initialize the fixup class."""
if not (addon := self.sys_addons.get(reference, local_only=True)):
_LOGGER.info("Addon %s already removed", reference)
return
# Remove addon
_LOGGER.info("Remove addon: %s", reference)
try:
addon.uninstall()
except AddonsError as err:
_LOGGER.error("Could not remove %s due to %s", reference, err)
raise ResolutionFixupError() from None
@property
def suggestion(self) -> SuggestionType:
"""Return a SuggestionType enum."""
return SuggestionType.EXECUTE_REMOVE
@property
def context(self) -> ContextType:
"""Return a ContextType enum."""
return ContextType.ADDON
@property
def issues(self) -> list[IssueType]:
"""Return a IssueType enum list."""
return [IssueType.DETACHED_ADDON_REMOVED]
@property
def auto(self) -> bool:
"""Return if a fixup can be apply as auto fix."""
return False

View File

@@ -7,6 +7,7 @@ from ..const import ContextType, IssueType, SuggestionType
from .base import FixupBase
_LOGGER: logging.Logger = logging.getLogger(__name__)
MAX_AUTO_ATTEMPTS = 5
def setup(coresys: CoreSys) -> FixupBase:
@@ -17,6 +18,11 @@ def setup(coresys: CoreSys) -> FixupBase:
class FixupAddonExecuteRepair(FixupBase):
"""Storage class for fixup."""
def __init__(self, coresys: CoreSys) -> None:
"""Initialize the add-on execute repair fixup class."""
super().__init__(coresys)
self.attempts = 0
async def process_fixup(self, reference: str | None = None) -> None:
"""Pull the addons image."""
addon = self.sys_addons.get(reference, local_only=True)
@@ -34,6 +40,7 @@ class FixupAddonExecuteRepair(FixupBase):
return
_LOGGER.info("Installing image for addon %s")
self.attempts += 1
await addon.instance.install(addon.version)
@property
@@ -54,4 +61,4 @@ class FixupAddonExecuteRepair(FixupBase):
@property
def auto(self) -> bool:
"""Return if a fixup can be apply as auto fix."""
return True
return self.attempts < MAX_AUTO_ATTEMPTS

View File

@@ -58,9 +58,9 @@ class FixupBase(ABC, CoreSysAttributes):
"""Return a ContextType enum."""
@property
@abstractmethod
def issues(self) -> list[IssueType]:
"""Return a IssueType enum list."""
return []
@property
def auto(self) -> bool:

View File

@@ -6,7 +6,7 @@ from pathlib import Path
from ...coresys import CoreSys
from ...dbus.udisks2.data import DeviceSpecification
from ...exceptions import DBusError, HostError, ResolutionFixupError
from ...os.const import FILESYSTEM_LABEL_OLD_DATA_DISK
from ...os.const import FILESYSTEM_LABEL_DATA_DISK, FILESYSTEM_LABEL_OLD_DATA_DISK
from ..const import ContextType, IssueType, SuggestionType
from .base import FixupBase
@@ -23,8 +23,10 @@ class FixupSystemAdoptDataDisk(FixupBase):
async def process_fixup(self, reference: str | None = None) -> None:
"""Initialize the fixup class."""
if not await self.sys_dbus.udisks2.resolve_device(
DeviceSpecification(path=Path(reference))
if not (
new_resolved := await self.sys_dbus.udisks2.resolve_device(
DeviceSpecification(path=Path(reference))
)
):
_LOGGER.info(
"Data disk at %s with name conflict was removed, skipping adopt",
@@ -36,16 +38,30 @@ class FixupSystemAdoptDataDisk(FixupBase):
if (
not current
or not (
resolved := await self.sys_dbus.udisks2.resolve_device(
current_resolved := await self.sys_dbus.udisks2.resolve_device(
DeviceSpecification(path=current)
)
)
or not resolved[0].filesystem
or not current_resolved[0].filesystem
):
raise ResolutionFixupError(
"Cannot resolve current data disk for rename", _LOGGER.error
)
if new_resolved[0].id_label != FILESYSTEM_LABEL_DATA_DISK:
_LOGGER.info(
"Renaming disabled data disk at %s to %s to activate it",
reference,
FILESYSTEM_LABEL_DATA_DISK,
)
try:
await new_resolved[0].filesystem.set_label(FILESYSTEM_LABEL_DATA_DISK)
except DBusError as err:
raise ResolutionFixupError(
f"Could not rename filesystem at {reference}: {err!s}",
_LOGGER.error,
) from err
_LOGGER.info(
"Renaming current data disk at %s to %s so new data disk at %s becomes primary ",
self.sys_dbus.agent.datadisk.current_device,
@@ -53,7 +69,9 @@ class FixupSystemAdoptDataDisk(FixupBase):
reference,
)
try:
await resolved[0].filesystem.set_label(FILESYSTEM_LABEL_OLD_DATA_DISK)
await current_resolved[0].filesystem.set_label(
FILESYSTEM_LABEL_OLD_DATA_DISK
)
except DBusError as err:
raise ResolutionFixupError(
f"Could not rename filesystem at {current.as_posix()}: {err!s}",
@@ -87,4 +105,4 @@ class FixupSystemAdoptDataDisk(FixupBase):
@property
def issues(self) -> list[IssueType]:
"""Return a IssueType enum list."""
return [IssueType.MULTIPLE_DATA_DISKS]
return [IssueType.DISABLED_DATA_DISK, IssueType.MULTIPLE_DATA_DISKS]

View File

@@ -2,7 +2,7 @@
import logging
from ...coresys import CoreSys
from ..const import ContextType, SuggestionType
from ..const import ContextType, IssueType, SuggestionType
from .base import FixupBase
_LOGGER: logging.Logger = logging.getLogger(__name__)
@@ -21,6 +21,11 @@ class FixupSystemCreateFullBackup(FixupBase):
_LOGGER.info("Creating a full backup")
await self.sys_backups.do_backup_full()
@property
def issues(self) -> list[IssueType]:
"""Return a IssueType enum list."""
return [IssueType.NO_CURRENT_BACKUP]
@property
def suggestion(self) -> SuggestionType:
"""Return a SuggestionType enum."""

View File

@@ -66,4 +66,4 @@ class FixupSystemRenameDataDisk(FixupBase):
@property
def issues(self) -> list[IssueType]:
"""Return a IssueType enum list."""
return [IssueType.MULTIPLE_DATA_DISKS]
return [IssueType.DISABLED_DATA_DISK, IssueType.MULTIPLE_DATA_DISKS]

View File

@@ -37,6 +37,7 @@ from .sentry import capture_exception
_LOGGER: logging.Logger = logging.getLogger(__name__)
DBUS_INTERFACE_OBJECT_MANAGER: str = "org.freedesktop.DBus.ObjectManager"
DBUS_INTERFACE_PROPERTIES: str = "org.freedesktop.DBus.Properties"
DBUS_METHOD_GETALL: str = "org.freedesktop.DBus.Properties.GetAll"
@@ -196,6 +197,13 @@ class DBus:
return None
return DBusCallWrapper(self, DBUS_INTERFACE_PROPERTIES)
@property
def object_manager(self) -> DBusCallWrapper | None:
"""Get object manager proxy interface."""
if DBUS_INTERFACE_OBJECT_MANAGER not in self._proxies:
return None
return DBusCallWrapper(self, DBUS_INTERFACE_OBJECT_MANAGER)
async def get_properties(self, interface: str) -> dict[str, Any]:
"""Read all properties from interface."""
if not self.properties:

View File

@@ -817,3 +817,33 @@ async def test_addon_loads_missing_image(
)
assert coresys.docker.images.build.call_args.kwargs["platform"] == "linux/amd64"
assert install_addon_ssh.image == "local/amd64-addon-ssh"
async def test_addon_load_succeeds_with_docker_errors(
coresys: CoreSys,
install_addon_ssh: Addon,
container: MagicMock,
caplog: pytest.LogCaptureFixture,
mock_amd64_arch_supported,
):
"""Docker errors while building/pulling an image during load should not raise and fail setup."""
# Build env invalid failure
coresys.docker.images.get.side_effect = ImageNotFound("missing")
caplog.clear()
await install_addon_ssh.load()
assert "Invalid build environment" in caplog.text
# Image build failure
coresys.docker.images.build.side_effect = DockerException()
caplog.clear()
with patch("pathlib.Path.is_file", return_value=True):
await install_addon_ssh.load()
assert "Can't build local/amd64-addon-ssh:9.2.1" in caplog.text
# Image pull failure
install_addon_ssh.data["image"] = "test/amd64-addon-ssh"
coresys.docker.images.build.reset_mock(side_effect=True)
coresys.docker.images.pull.side_effect = DockerException()
caplog.clear()
await install_addon_ssh.load()
assert "Unknown error with test/amd64-addon-ssh:9.2.1" in caplog.text

View File

@@ -4,6 +4,8 @@ import asyncio
from unittest.mock import MagicMock, PropertyMock, patch
from aiohttp.test_utils import TestClient
from awesomeversion import AwesomeVersion
import pytest
from supervisor.addons.addon import Addon
from supervisor.addons.build import AddonBuild
@@ -285,3 +287,37 @@ async def test_api_addon_uninstall_remove_config(
assert resp.status == 200
assert not coresys.addons.get("local_example", local_only=True)
assert not test_folder.exists()
async def test_api_update_available_validates_version(
api_client: TestClient,
coresys: CoreSys,
install_addon_example: Addon,
caplog: pytest.LogCaptureFixture,
tmp_supervisor_data,
path_extern,
):
"""Test update available field is only true if user can update to latest version."""
install_addon_example.data["ingress"] = False
install_addon_example.data_store["version"] = "1.3.0"
caplog.clear()
resp = await api_client.get("/addons/local_example/info")
assert resp.status == 200
result = await resp.json()
assert result["data"]["version"] == "1.2.0"
assert result["data"]["version_latest"] == "1.3.0"
assert result["data"]["update_available"] is True
# If new version can't be installed due to HA version, then no update is available
coresys.homeassistant.version = AwesomeVersion("2024.04.0")
install_addon_example.data_store["homeassistant"] = "2024.06.0"
resp = await api_client.get("/addons/local_example/info")
assert resp.status == 200
result = await resp.json()
assert result["data"]["version"] == "1.2.0"
assert result["data"]["version_latest"] == "1.3.0"
assert result["data"]["update_available"] is False
assert "Add-on local_example not supported" not in caplog.text

View File

@@ -1,11 +1,14 @@
"""Test homeassistant api."""
from pathlib import Path
from unittest.mock import MagicMock, patch
from aiohttp.test_utils import TestClient
from awesomeversion import AwesomeVersion
import pytest
from supervisor.coresys import CoreSys
from supervisor.homeassistant.core import HomeAssistantCore
from supervisor.homeassistant.module import HomeAssistant
from tests.api import common_test_api_advanced_logs
@@ -92,3 +95,50 @@ async def test_api_set_image(api_client: TestClient, coresys: CoreSys):
coresys.homeassistant.image == "ghcr.io/home-assistant/qemux86-64-homeassistant"
)
assert coresys.homeassistant.override_image is False
async def test_api_restart(
api_client: TestClient,
container: MagicMock,
tmp_supervisor_data: Path,
):
"""Test restarting homeassistant."""
safe_mode_marker = tmp_supervisor_data / "homeassistant" / "safe-mode"
with patch.object(HomeAssistantCore, "_block_till_run"):
await api_client.post("/homeassistant/restart")
container.restart.assert_called_once()
assert not safe_mode_marker.exists()
with patch.object(HomeAssistantCore, "_block_till_run"):
await api_client.post("/homeassistant/restart", json={"safe_mode": True})
assert container.restart.call_count == 2
assert safe_mode_marker.exists()
async def test_api_rebuild(
api_client: TestClient,
coresys: CoreSys,
container: MagicMock,
tmp_supervisor_data: Path,
path_extern,
):
"""Test rebuilding homeassistant."""
coresys.homeassistant.version = AwesomeVersion("2023.09.0")
safe_mode_marker = tmp_supervisor_data / "homeassistant" / "safe-mode"
with patch.object(HomeAssistantCore, "_block_till_run"):
await api_client.post("/homeassistant/rebuild")
assert container.remove.call_count == 2
container.start.assert_called_once()
assert not safe_mode_marker.exists()
with patch.object(HomeAssistantCore, "_block_till_run"):
await api_client.post("/homeassistant/rebuild", json={"safe_mode": True})
assert container.remove.call_count == 4
assert container.start.call_count == 2
assert safe_mode_marker.exists()

View File

@@ -1,6 +1,7 @@
"""Test Store API."""
import asyncio
from pathlib import Path
from unittest.mock import MagicMock, PropertyMock, patch
from aiohttp.test_utils import TestClient
@@ -8,6 +9,7 @@ import pytest
from supervisor.addons.addon import Addon
from supervisor.arch import CpuArch
from supervisor.config import CoreConfig
from supervisor.const import AddonState
from supervisor.coresys import CoreSys
from supervisor.docker.addon import DockerAddon
@@ -188,3 +190,91 @@ async def test_api_store_update_healthcheck(
assert resp.status == 200
await _container_events_task
@pytest.mark.parametrize("resource", ["store/addons", "addons"])
async def test_api_store_addons_no_changelog(
api_client: TestClient, coresys: CoreSys, store_addon: AddonStore, resource: str
):
"""Test /store/addons/{addon}/changelog REST API.
Currently the frontend expects a valid body even in the error case. Make sure that is
what the API returns.
"""
assert store_addon.with_changelog is False
resp = await api_client.get(f"/{resource}/{store_addon.slug}/changelog")
assert resp.status == 200
result = await resp.text()
assert result == "No changelog found for add-on test_store_addon!"
@pytest.mark.parametrize("resource", ["store/addons", "addons"])
async def test_api_detached_addon_changelog(
api_client: TestClient,
coresys: CoreSys,
install_addon_ssh: Addon,
tmp_supervisor_data: Path,
resource: str,
):
"""Test /store/addons/{addon}/changelog for an detached addon.
Currently the frontend expects a valid body even in the error case. Make sure that is
what the API returns.
"""
(addons_dir := tmp_supervisor_data / "addons" / "local").mkdir()
with patch.object(
CoreConfig, "path_addons_local", new=PropertyMock(return_value=addons_dir)
):
await coresys.store.load()
assert install_addon_ssh.is_detached is True
assert install_addon_ssh.with_changelog is False
resp = await api_client.get(f"/{resource}/{install_addon_ssh.slug}/changelog")
assert resp.status == 200
result = await resp.text()
assert result == "Addon local_ssh with version latest does not exist in the store"
@pytest.mark.parametrize("resource", ["store/addons", "addons"])
async def test_api_store_addons_no_documentation(
api_client: TestClient, coresys: CoreSys, store_addon: AddonStore, resource: str
):
"""Test /store/addons/{addon}/documentation REST API.
Currently the frontend expects a valid body even in the error case. Make sure that is
what the API returns.
"""
assert store_addon.with_documentation is False
resp = await api_client.get(f"/{resource}/{store_addon.slug}/documentation")
assert resp.status == 200
result = await resp.text()
assert result == "No documentation found for add-on test_store_addon!"
@pytest.mark.parametrize("resource", ["store/addons", "addons"])
async def test_api_detached_addon_documentation(
api_client: TestClient,
coresys: CoreSys,
install_addon_ssh: Addon,
tmp_supervisor_data: Path,
resource: str,
):
"""Test /store/addons/{addon}/changelog for an detached addon.
Currently the frontend expects a valid body even in the error case. Make sure that is
what the API returns.
"""
(addons_dir := tmp_supervisor_data / "addons" / "local").mkdir()
with patch.object(
CoreConfig, "path_addons_local", new=PropertyMock(return_value=addons_dir)
):
await coresys.store.load()
assert install_addon_ssh.is_detached is True
assert install_addon_ssh.with_documentation is False
resp = await api_client.get(f"/{resource}/{install_addon_ssh.slug}/documentation")
assert resp.status == 200
result = await resp.text()
assert result == "Addon local_ssh with version latest does not exist in the store"

View File

@@ -6,7 +6,12 @@ from aiohttp.test_utils import TestClient
import pytest
from supervisor.coresys import CoreSys
from supervisor.exceptions import StoreGitError, StoreNotFound
from supervisor.exceptions import (
HassioError,
HostNotSupportedError,
StoreGitError,
StoreNotFound,
)
from supervisor.store.repository import Repository
from tests.api import common_test_api_advanced_logs
@@ -160,7 +165,7 @@ async def test_api_supervisor_fallback(
api_client: TestClient, journald_logs: MagicMock, docker_logs: MagicMock
):
"""Check that supervisor logs read from container logs if reading from journald gateway fails badly."""
journald_logs.side_effect = OSError("Something bad happened!")
journald_logs.side_effect = HassioError("Something bad happened!")
with patch("supervisor.api._LOGGER.exception") as logger:
resp = await api_client.get("/supervisor/logs")
@@ -176,6 +181,40 @@ async def test_api_supervisor_fallback(
b"\x1b[36m22-10-11 14:04:23 DEBUG (MainThread) [supervisor.utils.dbus] D-Bus call - org.freedesktop.DBus.Properties.call_get_all on /io/hass/os/AppArmor\x1b[0m",
]
journald_logs.reset_mock()
# also check generic Python error
journald_logs.side_effect = OSError("Something bad happened!")
with patch("supervisor.api._LOGGER.exception") as logger:
resp = await api_client.get("/supervisor/logs")
logger.assert_called_once_with(
"Failed to get supervisor logs using advanced_logs API"
)
assert resp.status == 200
assert resp.content_type == "text/plain"
async def test_api_supervisor_fallback_log_capture(
api_client: TestClient, journald_logs: MagicMock, docker_logs: MagicMock
):
"""Check that Sentry log capture is executed only for unexpected errors."""
journald_logs.side_effect = HostNotSupportedError(
"No systemd-journal-gatewayd Unix socket available!"
)
with patch("supervisor.api.capture_exception") as capture_exception:
await api_client.get("/supervisor/logs")
capture_exception.assert_not_called()
journald_logs.reset_mock()
journald_logs.side_effect = HassioError("Something bad happened!")
with patch("supervisor.api.capture_exception") as capture_exception:
await api_client.get("/supervisor/logs")
capture_exception.assert_called_once()
async def test_api_supervisor_reload(api_client: TestClient):
"""Test supervisor reload."""

View File

@@ -1750,3 +1750,40 @@ async def test_reload_error(
assert "Could not list backups" in caplog.text
assert coresys.core.healthy is healthy_expected
async def test_monitoring_after_full_restore(
coresys: CoreSys, full_backup_mock, install_addon_ssh, container
):
"""Test monitoring of addon state still works after full restore."""
coresys.core.state = CoreState.RUNNING
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
coresys.homeassistant.core.start = AsyncMock(return_value=None)
coresys.homeassistant.core.stop = AsyncMock(return_value=None)
coresys.homeassistant.core.update = AsyncMock(return_value=None)
manager = BackupManager(coresys)
backup_instance = full_backup_mock.return_value
assert await manager.do_restore_full(backup_instance)
backup_instance.restore_addons.assert_called_once_with([TEST_ADDON_SLUG])
assert coresys.core.state == CoreState.RUNNING
coresys.docker.unload.assert_not_called()
async def test_monitoring_after_partial_restore(
coresys: CoreSys, partial_backup_mock, install_addon_ssh, container
):
"""Test monitoring of addon state still works after full restore."""
coresys.core.state = CoreState.RUNNING
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
manager = BackupManager(coresys)
backup_instance = partial_backup_mock.return_value
assert await manager.do_restore_partial(backup_instance, addons=[TEST_ADDON_SLUG])
backup_instance.restore_addons.assert_called_once_with([TEST_ADDON_SLUG])
assert coresys.core.state == CoreState.RUNNING
coresys.docker.unload.assert_not_called()

View File

@@ -229,6 +229,7 @@ async def fixture_udisks2_services(
],
"udisks2_loop": None,
"udisks2_manager": None,
"udisks2": None,
"udisks2_partition_table": [
"/org/freedesktop/UDisks2/block_devices/mmcblk1",
"/org/freedesktop/UDisks2/block_devices/sda",

View File

@@ -144,5 +144,9 @@ async def test_set_label(
filesystem_sda1_service.SetLabel.calls.clear()
await sda1.set_label("test")
assert filesystem_sda1_service.SetLabel.calls == [
("test", {"auth.no_user_interaction": Variant("b", True)})
(
"/org/freedesktop/UDisks2/block_devices/sda1",
"test",
{"auth.no_user_interaction": Variant("b", True)},
)
]

View File

@@ -1,5 +1,6 @@
"""Test UDisks2 Manager interface."""
import asyncio
from pathlib import Path
from awesomeversion import AwesomeVersion
@@ -7,13 +8,14 @@ from dbus_fast import Variant
from dbus_fast.aio.message_bus import MessageBus
import pytest
from supervisor.dbus.udisks2 import UDisks2
from supervisor.dbus.udisks2 import UDisks2Manager
from supervisor.dbus.udisks2.const import PartitionTableType
from supervisor.dbus.udisks2.data import DeviceSpecification
from supervisor.exceptions import DBusNotConnectedError, DBusObjectError
from tests.common import mock_dbus_services
from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.udisks2 import UDisks2 as UDisks2Service
from tests.dbus_service_mocks.udisks2_manager import (
UDisks2Manager as UDisks2ManagerService,
)
@@ -27,12 +29,20 @@ async def fixture_udisks2_manager_service(
yield udisks2_services["udisks2_manager"]
@pytest.fixture(name="udisks2_service")
async def fixture_udisks2_service(
udisks2_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
) -> UDisks2Service:
"""Mock UDisks2 base service."""
yield udisks2_services["udisks2"]
async def test_udisks2_manager_info(
udisks2_manager_service: UDisks2ManagerService, dbus_session_bus: MessageBus
):
"""Test udisks2 manager dbus connection."""
udisks2_manager_service.GetBlockDevices.calls.clear()
udisks2 = UDisks2()
udisks2 = UDisks2Manager()
assert udisks2.supported_filesystems is None
@@ -95,6 +105,7 @@ async def test_update_checks_devices_and_drives(dbus_session_bus: MessageBus):
"""Test update rechecks block devices and drives correctly."""
mocked = await mock_dbus_services(
{
"udisks2": None,
"udisks2_manager": None,
"udisks2_block": [
"/org/freedesktop/UDisks2/block_devices/sda",
@@ -115,7 +126,7 @@ async def test_update_checks_devices_and_drives(dbus_session_bus: MessageBus):
"/org/freedesktop/UDisks2/block_devices/sdb",
]
udisks2 = UDisks2()
udisks2 = UDisks2Manager()
await udisks2.connect(dbus_session_bus)
assert len(udisks2.block_devices) == 3
@@ -214,7 +225,7 @@ async def test_get_block_device(
udisks2_manager_service: UDisks2ManagerService, dbus_session_bus: MessageBus
):
"""Test get block device by object path."""
udisks2 = UDisks2()
udisks2 = UDisks2Manager()
with pytest.raises(DBusNotConnectedError):
udisks2.get_block_device("/org/freedesktop/UDisks2/block_devices/sda1")
@@ -234,7 +245,7 @@ async def test_get_drive(
udisks2_manager_service: UDisks2ManagerService, dbus_session_bus: MessageBus
):
"""Test get drive by object path."""
udisks2 = UDisks2()
udisks2 = UDisks2Manager()
with pytest.raises(DBusNotConnectedError):
udisks2.get_drive("/org/freedesktop/UDisks2/drives/BJTD4R_0x97cde291")
@@ -253,7 +264,7 @@ async def test_resolve_device(
):
"""Test resolve device."""
udisks2_manager_service.ResolveDevice.calls.clear()
udisks2 = UDisks2()
udisks2 = UDisks2Manager()
with pytest.raises(DBusNotConnectedError):
await udisks2.resolve_device(DeviceSpecification(path=Path("/dev/sda1")))
@@ -269,3 +280,52 @@ async def test_resolve_device(
{"auth.no_user_interaction": Variant("b", True)},
)
]
async def test_block_devices_add_remove_signals(
udisks2_service: UDisks2Service, dbus_session_bus: MessageBus
):
"""Test signals processed for added and removed block devices."""
udisks2 = UDisks2Manager()
await udisks2.connect(dbus_session_bus)
assert any(
device
for device in udisks2.block_devices
if device.object_path == "/org/freedesktop/UDisks2/block_devices/zram1"
)
udisks2_service.InterfacesRemoved(
"/org/freedesktop/UDisks2/block_devices/zram1",
["org.freedesktop.UDisks2.Block"],
)
await udisks2_service.ping()
assert not any(
device
for device in udisks2.block_devices
if device.object_path == "/org/freedesktop/UDisks2/block_devices/zram1"
)
udisks2_service.InterfacesAdded(
"/org/freedesktop/UDisks2/block_devices/zram1",
{
"org.freedesktop.UDisks2.Block": {
"Device": Variant("ay", b"/dev/zram1"),
"PreferredDevice": Variant("ay", b"/dev/zram1"),
"DeviceNumber": Variant("t", 64769),
"Id": Variant("s", ""),
"IdUsage": Variant("s", ""),
"IdType": Variant("s", ""),
"IdVersion": Variant("s", ""),
"IdLabel": Variant("s", ""),
"IdUUID": Variant("s", ""),
}
},
)
await udisks2_service.ping()
await asyncio.sleep(0.1)
assert any(
device
for device in udisks2.block_devices
if device.object_path == "/org/freedesktop/UDisks2/block_devices/zram1"
)

View File

@@ -9,32 +9,6 @@ from dbus_fast.aio.message_bus import MessageBus
from dbus_fast.service import ServiceInterface, method
def dbus_method(name: str = None, disabled: bool = False):
"""Make DBus method with call tracking.
Identical to dbus_fast.service.method wrapper except all calls to it are tracked.
Can then test that methods with no output were called or the right arguments were
used if the output is static.
"""
orig_decorator = method(name=name, disabled=disabled)
@no_type_check_decorator
def decorator(func):
calls: list[list[Any]] = []
@wraps(func)
def track_calls(self, *args):
calls.append(args)
return func(self, *args)
wrapped = orig_decorator(track_calls)
wrapped.__dict__["calls"] = calls
return wrapped
return decorator
class DBusServiceMock(ServiceInterface):
"""Base dbus service mock."""
@@ -66,3 +40,32 @@ class DBusServiceMock(ServiceInterface):
# So in general we sleep(0) after to clear the new task
if sleep:
await asyncio.sleep(0)
def dbus_method(name: str = None, disabled: bool = False, track_obj_path: bool = False):
"""Make DBus method with call tracking.
Identical to dbus_fast.service.method wrapper except all calls to it are tracked.
Can then test that methods with no output were called or the right arguments were
used if the output is static.
"""
orig_decorator = method(name=name, disabled=disabled)
@no_type_check_decorator
def decorator(func):
calls: list[list[Any]] = []
@wraps(func)
def track_calls(self: DBusServiceMock, *args):
if track_obj_path:
calls.append((self.object_path, *args))
else:
calls.append(args)
return func(self, *args)
wrapped = orig_decorator(track_calls)
wrapped.__dict__["calls"] = calls
return wrapped
return decorator

View File

@@ -0,0 +1,41 @@
"""Mock of base UDisks2 service."""
from dbus_fast import Variant
from dbus_fast.service import signal
from .base import DBusServiceMock, dbus_method
BUS_NAME = "org.freedesktop.UDisks2"
def setup(object_path: str | None = None) -> DBusServiceMock:
"""Create dbus mock object."""
return UDisks2()
class UDisks2(DBusServiceMock):
"""UDisks2 base object mock.
gdbus introspect --system --dest org.freedesktop.UDisks2 --object-path /org/freedesktop/UDisks2
"""
interface = "org.freedesktop.DBus.ObjectManager"
object_path = "/org/freedesktop/UDisks2"
response_get_managed_objects: dict[str, dict[str, dict[str, Variant]]] = {}
@dbus_method()
def GetManagedObjects(self) -> "a{oa{sa{sv}}}":
"""Do GetManagedObjects method."""
return self.response_get_managed_objects
@signal()
def InterfacesAdded(
self, object_path: str, interfaces_and_properties: dict[str, dict[str, Variant]]
) -> "oa{sa{sv}}":
"""Signal interfaces added."""
return [object_path, interfaces_and_properties]
@signal()
def InterfacesRemoved(self, object_path: str, interfaces: list[str]) -> "oas":
"""Signal interfaces removed."""
return [object_path, interfaces]

View File

@@ -83,7 +83,7 @@ class Filesystem(DBusServiceMock):
"""Get Size."""
return self.fixture.Size
@dbus_method()
@dbus_method(track_obj_path=True)
def SetLabel(self, label: "s", options: "a{sv}") -> None:
"""Do SetLabel method."""

View File

@@ -32,7 +32,9 @@ class UDisks2Manager(DBusServiceMock):
"/org/freedesktop/UDisks2/block_devices/sdb1",
"/org/freedesktop/UDisks2/block_devices/zram1",
]
resolved_devices = ["/org/freedesktop/UDisks2/block_devices/sda1"]
resolved_devices: list[list[str]] | list[str] = [
"/org/freedesktop/UDisks2/block_devices/sda1"
]
@dbus_property(access=PropertyAccess.READ)
def Version(self) -> "s":
@@ -98,4 +100,8 @@ class UDisks2Manager(DBusServiceMock):
@dbus_method()
def ResolveDevice(self, devspec: "a{sv}", options: "a{sv}") -> "ao":
"""Do ResolveDevice method."""
if len(self.resolved_devices) > 0 and isinstance(
self.resolved_devices[0], list
):
return self.resolved_devices.pop(0)
return self.resolved_devices

View File

@@ -595,10 +595,6 @@ async def test_reload_mounts(
assert len(coresys.resolution.suggestions_for_issue(mount.failed_issue)) == 2
assert len(systemd_service.ReloadOrRestartUnit.calls) == 1
# This shouldn't reload the mount again since this isn't a new failure
await coresys.mounts.reload()
assert len(systemd_service.ReloadOrRestartUnit.calls) == 1
# This should now remove the issue from the list
systemd_unit_service.active_state = "active"
await coresys.mounts.reload()
@@ -608,6 +604,49 @@ async def test_reload_mounts(
assert not coresys.resolution.suggestions_for_issue(mount.failed_issue)
async def test_reload_mounts_attempts_initial_mount(
coresys: CoreSys, all_dbus_services: dict[str, DBusServiceMock], mount: Mount
):
"""Test reloading mounts attempts initial mount."""
systemd_service: SystemdService = all_dbus_services["systemd"]
systemd_service.StartTransientUnit.calls.clear()
systemd_service.response_get_unit = [
ERROR_NO_UNIT,
"/org/freedesktop/systemd1/unit/tmp_2dyellow_2emount",
ERROR_NO_UNIT,
ERROR_NO_UNIT,
"/org/freedesktop/systemd1/unit/tmp_2dyellow_2emount",
]
systemd_service.response_reload_or_restart_unit = ERROR_NO_UNIT
coresys.mounts.bound_mounts[0].emergency = True
await coresys.mounts.reload()
assert systemd_service.StartTransientUnit.calls == [
(
"mnt-data-supervisor-mounts-media_test.mount",
"fail",
[
["Options", Variant("s", "soft,timeo=200")],
["Type", Variant("s", "nfs")],
["Description", Variant("s", "Supervisor nfs mount: media_test")],
["What", Variant("s", "media.local:/media")],
],
[],
),
(
"mnt-data-supervisor-media-media_test.mount",
"fail",
[
["Options", Variant("s", "bind")],
["Description", Variant("s", "Supervisor bind mount: bind_media_test")],
["What", Variant("s", "/mnt/data/supervisor/mounts/media_test")],
],
[],
),
]
@pytest.mark.parametrize("os_available", ["9.5"], indirect=True)
async def test_mounting_not_supported(
coresys: CoreSys,

View File

@@ -1,4 +1,6 @@
"""Test OS API."""
import asyncio
from dataclasses import replace
from pathlib import PosixPath
from unittest.mock import patch
@@ -6,16 +8,20 @@ from unittest.mock import patch
from dbus_fast import DBusError, ErrorType, Variant
import pytest
from supervisor.const import CoreState
from supervisor.core import Core
from supervisor.coresys import CoreSys
from supervisor.exceptions import HassOSDataDiskError, HassOSError
from supervisor.os.data_disk import Disk
from supervisor.resolution.const import ContextType, IssueType
from supervisor.resolution.data import Issue
from tests.common import mock_dbus_services
from tests.dbus_service_mocks.agent_datadisk import DataDisk as DataDiskService
from tests.dbus_service_mocks.agent_system import System as SystemService
from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.logind import Logind as LogindService
from tests.dbus_service_mocks.udisks2 import UDisks2 as UDisks2Service
from tests.dbus_service_mocks.udisks2_block import Block as BlockService
from tests.dbus_service_mocks.udisks2_filesystem import Filesystem as FilesystemService
from tests.dbus_service_mocks.udisks2_partition import Partition as PartitionService
@@ -313,3 +319,107 @@ async def test_datadisk_wipe_errors(
assert system_service.ScheduleWipeDevice.calls == [()]
assert logind_service.Reboot.calls == [(False,)]
async def test_multiple_datadisk_add_remove_signals(
coresys: CoreSys,
udisks2_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
os_available,
):
"""Test multiple data disk issue created/removed on signal."""
udisks2_service: UDisks2Service = udisks2_services["udisks2"]
sdb1_block: BlockService = udisks2_services["udisks2_block"][
"/org/freedesktop/UDisks2/block_devices/sdb1"
]
await coresys.os.datadisk.load()
coresys.core.state = CoreState.RUNNING
assert coresys.resolution.issues == []
assert coresys.resolution.suggestions == []
sdb1_block.fixture = replace(sdb1_block.fixture, IdLabel="hassos-data")
udisks2_service.InterfacesAdded(
"/org/freedesktop/UDisks2/block_devices/sdb1",
{
"org.freedesktop.UDisks2.Block": {
"Device": Variant("ay", b"/dev/sdb1"),
"PreferredDevice": Variant("ay", b"/dev/sdb1"),
"DeviceNumber": Variant("t", 2065),
"Id": Variant("s", ""),
"IdUsage": Variant("s", ""),
"IdType": Variant("s", ""),
"IdVersion": Variant("s", ""),
"IdLabel": Variant("s", "hassos-data"),
"IdUUID": Variant("s", ""),
}
},
)
await udisks2_service.ping()
await asyncio.sleep(0.2)
assert (
Issue(IssueType.MULTIPLE_DATA_DISKS, ContextType.SYSTEM, reference="/dev/sdb1")
in coresys.resolution.issues
)
udisks2_service.InterfacesRemoved(
"/org/freedesktop/UDisks2/block_devices/sdb1",
["org.freedesktop.UDisks2.Block", "org.freedesktop.UDisks2.Filesystem"],
)
await udisks2_service.ping()
await asyncio.sleep(0.2)
assert coresys.resolution.issues == []
async def test_disabled_datadisk_add_remove_signals(
coresys: CoreSys,
udisks2_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
os_available,
):
"""Test disabled data disk issue created/removed on signal."""
udisks2_service: UDisks2Service = udisks2_services["udisks2"]
sdb1_block: BlockService = udisks2_services["udisks2_block"][
"/org/freedesktop/UDisks2/block_devices/sdb1"
]
await coresys.os.datadisk.load()
coresys.core.state = CoreState.RUNNING
assert coresys.resolution.issues == []
assert coresys.resolution.suggestions == []
sdb1_block.fixture = replace(sdb1_block.fixture, IdLabel="hassos-data-dis")
udisks2_service.InterfacesAdded(
"/org/freedesktop/UDisks2/block_devices/sdb1",
{
"org.freedesktop.UDisks2.Block": {
"Device": Variant("ay", b"/dev/sdb1"),
"PreferredDevice": Variant("ay", b"/dev/sdb1"),
"DeviceNumber": Variant("t", 2065),
"Id": Variant("s", ""),
"IdUsage": Variant("s", ""),
"IdType": Variant("s", ""),
"IdVersion": Variant("s", ""),
"IdLabel": Variant("s", "hassos-data-dis"),
"IdUUID": Variant("s", ""),
}
},
)
await udisks2_service.ping()
await asyncio.sleep(0.2)
assert (
Issue(IssueType.DISABLED_DATA_DISK, ContextType.SYSTEM, reference="/dev/sdb1")
in coresys.resolution.issues
)
udisks2_service.InterfacesRemoved(
"/org/freedesktop/UDisks2/block_devices/sdb1",
["org.freedesktop.UDisks2.Block", "org.freedesktop.UDisks2.Filesystem"],
)
await udisks2_service.ping()
await asyncio.sleep(0.2)
assert coresys.resolution.issues == []

View File

@@ -0,0 +1,86 @@
"""Test check for detached addons due to repo missing."""
from unittest.mock import patch
from supervisor.addons.addon import Addon
from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.resolution.checks.detached_addon_missing import (
CheckDetachedAddonMissing,
)
from supervisor.resolution.const import ContextType, IssueType
async def test_base(coresys: CoreSys):
"""Test check basics."""
detached_addon_missing = CheckDetachedAddonMissing(coresys)
assert detached_addon_missing.slug == "detached_addon_missing"
assert detached_addon_missing.enabled
async def test_check(coresys: CoreSys, install_addon_ssh: Addon):
"""Test check for detached addons."""
detached_addon_missing = CheckDetachedAddonMissing(coresys)
coresys.core.state = CoreState.SETUP
await detached_addon_missing()
assert len(coresys.resolution.issues) == 0
# Mock test addon was been installed from a now non-existent store
install_addon_ssh.slug = "abc123_ssh"
coresys.addons.data.system["abc123_ssh"] = coresys.addons.data.system["local_ssh"]
coresys.addons.local["abc123_ssh"] = coresys.addons.local["local_ssh"]
install_addon_ssh.data["repository"] = "abc123"
await detached_addon_missing()
assert len(coresys.resolution.issues) == 1
assert coresys.resolution.issues[0].type is IssueType.DETACHED_ADDON_MISSING
assert coresys.resolution.issues[0].context is ContextType.ADDON
assert coresys.resolution.issues[0].reference == install_addon_ssh.slug
assert len(coresys.resolution.suggestions) == 0
async def test_approve(coresys: CoreSys, install_addon_ssh: Addon):
"""Test approve existing detached addon issues."""
detached_addon_missing = CheckDetachedAddonMissing(coresys)
coresys.core.state = CoreState.SETUP
assert (
await detached_addon_missing.approve_check(reference=install_addon_ssh.slug)
is False
)
# Mock test addon was been installed from a now non-existent store
install_addon_ssh.slug = "abc123_ssh"
coresys.addons.data.system["abc123_ssh"] = coresys.addons.data.system["local_ssh"]
coresys.addons.local["abc123_ssh"] = coresys.addons.local["local_ssh"]
install_addon_ssh.data["repository"] = "abc123"
assert (
await detached_addon_missing.approve_check(reference=install_addon_ssh.slug)
is True
)
async def test_did_run(coresys: CoreSys):
"""Test that the check ran as expected."""
detached_addon_missing = CheckDetachedAddonMissing(coresys)
should_run = detached_addon_missing.states
should_not_run = [state for state in CoreState if state not in should_run]
assert should_run == [CoreState.SETUP]
assert len(should_not_run) != 0
with patch.object(
CheckDetachedAddonMissing, "run_check", return_value=None
) as check:
for state in should_run:
coresys.core.state = state
await detached_addon_missing()
check.assert_called_once()
check.reset_mock()
for state in should_not_run:
coresys.core.state = state
await detached_addon_missing()
check.assert_not_called()
check.reset_mock()

View File

@@ -0,0 +1,97 @@
"""Test check for detached addons due to removal from repo."""
from pathlib import Path
from unittest.mock import PropertyMock, patch
from supervisor.addons.addon import Addon
from supervisor.config import CoreConfig
from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.resolution.checks.detached_addon_removed import (
CheckDetachedAddonRemoved,
)
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
async def test_base(coresys: CoreSys):
"""Test check basics."""
detached_addon_removed = CheckDetachedAddonRemoved(coresys)
assert detached_addon_removed.slug == "detached_addon_removed"
assert detached_addon_removed.enabled
async def test_check(
coresys: CoreSys, install_addon_ssh: Addon, tmp_supervisor_data: Path
):
"""Test check for detached addons."""
detached_addon_removed = CheckDetachedAddonRemoved(coresys)
coresys.core.state = CoreState.SETUP
await detached_addon_removed()
assert len(coresys.resolution.issues) == 0
assert len(coresys.resolution.suggestions) == 0
(addons_dir := tmp_supervisor_data / "addons" / "local").mkdir()
with patch.object(
CoreConfig, "path_addons_local", new=PropertyMock(return_value=addons_dir)
):
await coresys.store.load()
await detached_addon_removed()
assert len(coresys.resolution.issues) == 1
assert coresys.resolution.issues[0].type is IssueType.DETACHED_ADDON_REMOVED
assert coresys.resolution.issues[0].context is ContextType.ADDON
assert coresys.resolution.issues[0].reference == install_addon_ssh.slug
assert len(coresys.resolution.suggestions) == 1
assert coresys.resolution.suggestions[0].type is SuggestionType.EXECUTE_REMOVE
assert coresys.resolution.suggestions[0].context is ContextType.ADDON
assert coresys.resolution.suggestions[0].reference == install_addon_ssh.slug
async def test_approve(
coresys: CoreSys, install_addon_ssh: Addon, tmp_supervisor_data: Path
):
"""Test approve existing detached addon issues."""
detached_addon_removed = CheckDetachedAddonRemoved(coresys)
coresys.core.state = CoreState.SETUP
assert (
await detached_addon_removed.approve_check(reference=install_addon_ssh.slug)
is False
)
(addons_dir := tmp_supervisor_data / "addons" / "local").mkdir()
with patch.object(
CoreConfig, "path_addons_local", new=PropertyMock(return_value=addons_dir)
):
await coresys.store.load()
assert (
await detached_addon_removed.approve_check(reference=install_addon_ssh.slug)
is True
)
async def test_did_run(coresys: CoreSys):
"""Test that the check ran as expected."""
detached_addon_removed = CheckDetachedAddonRemoved(coresys)
should_run = detached_addon_removed.states
should_not_run = [state for state in CoreState if state not in should_run]
assert should_run == [CoreState.SETUP]
assert len(should_not_run) != 0
with patch.object(
CheckDetachedAddonRemoved, "run_check", return_value=None
) as check:
for state in should_run:
coresys.core.state = state
await detached_addon_removed()
check.assert_called_once()
check.reset_mock()
for state in should_not_run:
coresys.core.state = state
await detached_addon_removed()
check.assert_not_called()
check.reset_mock()

View File

@@ -0,0 +1,99 @@
"""Test check for disabled data disk."""
# pylint: disable=import-error
from dataclasses import replace
from unittest.mock import patch
import pytest
from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.resolution.checks.disabled_data_disk import CheckDisabledDataDisk
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
from supervisor.resolution.data import Issue, Suggestion
from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.udisks2_block import Block as BlockService
@pytest.fixture(name="sda1_block_service")
async def fixture_sda1_block_service(
udisks2_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
) -> BlockService:
"""Return sda1 block service."""
yield udisks2_services["udisks2_block"][
"/org/freedesktop/UDisks2/block_devices/sda1"
]
async def test_base(coresys: CoreSys):
"""Test check basics."""
disabled_data_disk = CheckDisabledDataDisk(coresys)
assert disabled_data_disk.slug == "disabled_data_disk"
assert disabled_data_disk.enabled
async def test_check(coresys: CoreSys, sda1_block_service: BlockService):
"""Test check."""
disabled_data_disk = CheckDisabledDataDisk(coresys)
coresys.core.state = CoreState.RUNNING
await disabled_data_disk.run_check()
assert len(coresys.resolution.issues) == 0
assert len(coresys.resolution.suggestions) == 0
sda1_block_service.emit_properties_changed({"IdLabel": "hassos-data-dis"})
await sda1_block_service.ping()
await disabled_data_disk.run_check()
assert coresys.resolution.issues == [
Issue(IssueType.DISABLED_DATA_DISK, ContextType.SYSTEM, reference="/dev/sda1")
]
assert coresys.resolution.suggestions == [
Suggestion(
SuggestionType.RENAME_DATA_DISK, ContextType.SYSTEM, reference="/dev/sda1"
),
Suggestion(
SuggestionType.ADOPT_DATA_DISK, ContextType.SYSTEM, reference="/dev/sda1"
),
]
async def test_approve(coresys: CoreSys, sda1_block_service: BlockService):
"""Test approve."""
disabled_data_disk = CheckDisabledDataDisk(coresys)
coresys.core.state = CoreState.RUNNING
assert not await disabled_data_disk.approve_check(reference="/dev/sda1")
sda1_block_service.fixture = replace(
sda1_block_service.fixture, IdLabel="hassos-data-dis"
)
assert await disabled_data_disk.approve_check(reference="/dev/sda1")
async def test_did_run(coresys: CoreSys):
"""Test that the check ran as expected."""
disabled_data_disk = CheckDisabledDataDisk(coresys)
should_run = disabled_data_disk.states
should_not_run = [state for state in CoreState if state not in should_run]
assert len(should_run) != 0
assert len(should_not_run) != 0
with patch(
"supervisor.resolution.checks.disabled_data_disk.CheckDisabledDataDisk.run_check",
return_value=None,
) as check:
for state in should_run:
coresys.core.state = state
await disabled_data_disk()
check.assert_called_once()
check.reset_mock()
for state in should_not_run:
coresys.core.state = state
await disabled_data_disk()
check.assert_not_called()
check.reset_mock()

View File

@@ -0,0 +1,34 @@
"""Test evaluation base."""
from unittest.mock import patch
from supervisor.addons.addon import Addon
from supervisor.coresys import CoreSys
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
from supervisor.resolution.data import Issue, Suggestion
from supervisor.resolution.fixups.addon_execute_remove import FixupAddonExecuteRemove
async def test_fixup(coresys: CoreSys, install_addon_ssh: Addon):
"""Test fixup."""
addon_execute_remove = FixupAddonExecuteRemove(coresys)
assert addon_execute_remove.auto is False
coresys.resolution.suggestions = Suggestion(
SuggestionType.EXECUTE_REMOVE,
ContextType.ADDON,
reference=install_addon_ssh.slug,
)
coresys.resolution.issues = Issue(
IssueType.DETACHED_ADDON_REMOVED,
ContextType.ADDON,
reference=install_addon_ssh.slug,
)
with patch.object(Addon, "uninstall") as uninstall:
await addon_execute_remove()
assert uninstall.called
assert len(coresys.resolution.suggestions) == 0
assert len(coresys.resolution.issues) == 0

View File

@@ -3,12 +3,14 @@
from unittest.mock import MagicMock, patch
from docker.errors import NotFound
import pytest
from supervisor.addons.addon import Addon
from supervisor.coresys import CoreSys
from supervisor.docker.addon import DockerAddon
from supervisor.docker.interface import DockerInterface
from supervisor.docker.manager import DockerAPI
from supervisor.exceptions import DockerError
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
from supervisor.resolution.fixups.addon_execute_repair import FixupAddonExecuteRepair
@@ -35,6 +37,30 @@ async def test_fixup(docker: DockerAPI, coresys: CoreSys, install_addon_ssh: Add
assert not coresys.resolution.suggestions
async def test_fixup_max_auto_attempts(
docker: DockerAPI, coresys: CoreSys, install_addon_ssh: Addon
):
"""Test fixup stops being auto-applied after 5 failures."""
docker.images.get.side_effect = NotFound("missing")
install_addon_ssh.data["image"] = "test_image"
addon_execute_repair = FixupAddonExecuteRepair(coresys)
coresys.resolution.create_issue(
IssueType.MISSING_IMAGE,
ContextType.ADDON,
reference="local_ssh",
suggestions=[SuggestionType.EXECUTE_REPAIR],
)
with patch.object(DockerInterface, "install", side_effect=DockerError):
for _ in range(5):
assert addon_execute_repair.auto is True
with pytest.raises(DockerError):
await addon_execute_repair()
assert addon_execute_repair.auto is False
async def test_fixup_no_addon(coresys: CoreSys):
"""Test fixup dismisses if addon is missing."""
addon_execute_repair = FixupAddonExecuteRepair(coresys)

View File

@@ -1,5 +1,7 @@
"""Test system fixup adopt data disk."""
from dataclasses import dataclass, replace
from dbus_fast import DBusError, ErrorType, Variant
import pytest
@@ -10,20 +12,34 @@ from supervisor.resolution.fixups.system_adopt_data_disk import FixupSystemAdopt
from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.logind import Logind as LogindService
from tests.dbus_service_mocks.udisks2_block import Block as BlockService
from tests.dbus_service_mocks.udisks2_filesystem import Filesystem as FilesystemService
from tests.dbus_service_mocks.udisks2_manager import (
UDisks2Manager as UDisks2ManagerService,
)
@pytest.fixture(name="sda1_filesystem_service")
async def fixture_sda1_filesystem_service(
@dataclass
class FSDevice:
"""Filesystem device services."""
block_service: BlockService
filesystem_service: FilesystemService
@pytest.fixture(name="sda1_device")
async def fixture_sda1_device(
udisks2_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
) -> FilesystemService:
"""Return sda1 filesystem service."""
return udisks2_services["udisks2_filesystem"][
"/org/freedesktop/UDisks2/block_devices/sda1"
]
) -> FSDevice:
"""Return sda1 services."""
return FSDevice(
udisks2_services["udisks2_block"][
"/org/freedesktop/UDisks2/block_devices/sda1"
],
udisks2_services["udisks2_filesystem"][
"/org/freedesktop/UDisks2/block_devices/sda1"
],
)
@pytest.fixture(name="mmcblk1p3_filesystem_service")
@@ -54,6 +70,7 @@ async def fixture_logind_service(
async def test_fixup(
coresys: CoreSys,
sda1_device: FSDevice,
mmcblk1p3_filesystem_service: FilesystemService,
udisks2_service: UDisks2ManagerService,
logind_service: LogindService,
@@ -61,6 +78,9 @@ async def test_fixup(
"""Test fixup."""
mmcblk1p3_filesystem_service.SetLabel.calls.clear()
logind_service.Reboot.calls.clear()
sda1_device.block_service.fixture = replace(
sda1_device.block_service.fixture, IdLabel="hassos-data"
)
system_adopt_data_disk = FixupSystemAdoptDataDisk(coresys)
assert not system_adopt_data_disk.auto
@@ -72,13 +92,18 @@ async def test_fixup(
IssueType.MULTIPLE_DATA_DISKS, ContextType.SYSTEM, reference="/dev/sda1"
)
udisks2_service.resolved_devices = [
"/org/freedesktop/UDisks2/block_devices/mmcblk1p3"
["/org/freedesktop/UDisks2/block_devices/sda1"],
["/org/freedesktop/UDisks2/block_devices/mmcblk1p3"],
]
await system_adopt_data_disk()
assert mmcblk1p3_filesystem_service.SetLabel.calls == [
("hassos-data-old", {"auth.no_user_interaction": Variant("b", True)})
(
"/org/freedesktop/UDisks2/block_devices/mmcblk1p3",
"hassos-data-old",
{"auth.no_user_interaction": Variant("b", True)},
)
]
assert len(coresys.resolution.suggestions) == 0
assert len(coresys.resolution.issues) == 0
@@ -118,6 +143,7 @@ async def test_fixup_device_removed(
async def test_fixup_reboot_failed(
coresys: CoreSys,
sda1_device: FSDevice,
mmcblk1p3_filesystem_service: FilesystemService,
udisks2_service: UDisks2ManagerService,
logind_service: LogindService,
@@ -126,6 +152,9 @@ async def test_fixup_reboot_failed(
"""Test fixup when reboot fails."""
mmcblk1p3_filesystem_service.SetLabel.calls.clear()
logind_service.side_effect_reboot = DBusError(ErrorType.SERVICE_ERROR, "error")
sda1_device.block_service.fixture = replace(
sda1_device.block_service.fixture, IdLabel="hassos-data"
)
system_adopt_data_disk = FixupSystemAdoptDataDisk(coresys)
assert not system_adopt_data_disk.auto
@@ -137,13 +166,18 @@ async def test_fixup_reboot_failed(
IssueType.MULTIPLE_DATA_DISKS, ContextType.SYSTEM, reference="/dev/sda1"
)
udisks2_service.resolved_devices = [
"/org/freedesktop/UDisks2/block_devices/mmcblk1p3"
["/org/freedesktop/UDisks2/block_devices/sda1"],
["/org/freedesktop/UDisks2/block_devices/mmcblk1p3"],
]
await system_adopt_data_disk()
assert mmcblk1p3_filesystem_service.SetLabel.calls == [
("hassos-data-old", {"auth.no_user_interaction": Variant("b", True)})
(
"/org/freedesktop/UDisks2/block_devices/mmcblk1p3",
"hassos-data-old",
{"auth.no_user_interaction": Variant("b", True)},
)
]
assert len(coresys.resolution.suggestions) == 1
assert (
@@ -156,3 +190,50 @@ async def test_fixup_reboot_failed(
in coresys.resolution.issues
)
assert "Could not reboot host to finish data disk adoption" in caplog.text
async def test_fixup_disabled_data_disk(
coresys: CoreSys,
sda1_device: FSDevice,
mmcblk1p3_filesystem_service: FilesystemService,
udisks2_service: UDisks2ManagerService,
logind_service: LogindService,
):
"""Test fixup for activating a disabled data disk."""
mmcblk1p3_filesystem_service.SetLabel.calls.clear()
logind_service.Reboot.calls.clear()
sda1_device.block_service.fixture = replace(
sda1_device.block_service.fixture, IdLabel="hassos-data-dis"
)
system_adopt_data_disk = FixupSystemAdoptDataDisk(coresys)
assert not system_adopt_data_disk.auto
coresys.resolution.suggestions = Suggestion(
SuggestionType.ADOPT_DATA_DISK, ContextType.SYSTEM, reference="/dev/sda1"
)
coresys.resolution.issues = Issue(
IssueType.DISABLED_DATA_DISK, ContextType.SYSTEM, reference="/dev/sda1"
)
udisks2_service.resolved_devices = [
["/org/freedesktop/UDisks2/block_devices/sda1"],
["/org/freedesktop/UDisks2/block_devices/mmcblk1p3"],
]
await system_adopt_data_disk()
assert mmcblk1p3_filesystem_service.SetLabel.calls == [
(
"/org/freedesktop/UDisks2/block_devices/sda1",
"hassos-data",
{"auth.no_user_interaction": Variant("b", True)},
),
(
"/org/freedesktop/UDisks2/block_devices/mmcblk1p3",
"hassos-data-old",
{"auth.no_user_interaction": Variant("b", True)},
),
]
assert len(coresys.resolution.suggestions) == 0
assert len(coresys.resolution.issues) == 0
assert logind_service.Reboot.calls == [(False,)]

View File

@@ -52,7 +52,11 @@ async def test_fixup(coresys: CoreSys, sda1_filesystem_service: FilesystemServic
await system_rename_data_disk()
assert sda1_filesystem_service.SetLabel.calls == [
("hassos-data-old", {"auth.no_user_interaction": Variant("b", True)})
(
"/org/freedesktop/UDisks2/block_devices/sda1",
"hassos-data-old",
{"auth.no_user_interaction": Variant("b", True)},
)
]
assert len(coresys.resolution.suggestions) == 0
assert len(coresys.resolution.issues) == 0