mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-10-11 12:49:32 +00:00
Compare commits
1 Commits
2024.06.2
...
need-updat
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b7c53d9e40 |
8
.github/workflows/builder.yml
vendored
8
.github/workflows/builder.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
requirements: ${{ steps.requirements.outputs.changed }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
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.7
|
||||
uses: actions/checkout@v4.1.6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: actions/checkout@v4.1.7
|
||||
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.7
|
||||
uses: actions/checkout@v4.1.6
|
||||
|
||||
- name: Build the Supervisor
|
||||
if: needs.init.outputs.publish != 'true'
|
||||
|
20
.github/workflows/ci.yaml
vendored
20
.github/workflows/ci.yaml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
name: Prepare Python dependencies
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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,7 +288,7 @@ jobs:
|
||||
name: Run tests Python ${{ needs.prepare.outputs.python-version }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.1.6
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
id: python
|
||||
@@ -346,7 +346,7 @@ jobs:
|
||||
needs: ["pytest", "prepare"]
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.1.6
|
||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
id: python
|
||||
@@ -373,4 +373,4 @@ jobs:
|
||||
coverage report
|
||||
coverage xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4.5.0
|
||||
uses: codecov/codecov-action@v4.4.1
|
||||
|
2
.github/workflows/release-drafter.yml
vendored
2
.github/workflows/release-drafter.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
name: Release Drafter
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.1.6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
2
.github/workflows/sentry.yaml
vendored
2
.github/workflows/sentry.yaml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
uses: actions/checkout@v4.1.6
|
||||
- name: Sentry Release
|
||||
uses: getsentry/action-release@v1.7.0
|
||||
env:
|
||||
|
@@ -22,8 +22,8 @@ pyudev==0.24.3
|
||||
PyYAML==6.0.1
|
||||
requests==2.32.3
|
||||
securetar==2024.2.1
|
||||
sentry-sdk==2.6.0
|
||||
setuptools==70.1.0
|
||||
sentry-sdk==2.5.1
|
||||
setuptools==70.0.0
|
||||
voluptuous==0.14.2
|
||||
dbus-fast==2.21.3
|
||||
typing_extensions==4.12.2
|
||||
|
@@ -6,7 +6,7 @@ pytest-asyncio==0.23.6
|
||||
pytest-cov==5.0.0
|
||||
pytest-timeout==2.3.1
|
||||
pytest==8.2.2
|
||||
ruff==0.4.10
|
||||
ruff==0.4.8
|
||||
time-machine==2.14.1
|
||||
typing_extensions==4.12.2
|
||||
urllib3==2.2.2
|
||||
urllib3==2.2.1
|
||||
|
@@ -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]:
|
||||
|
@@ -174,6 +174,17 @@ class Tasks(CoreSysAttributes):
|
||||
self._cache[HASS_WATCHDOG_API_FAILURES] = 0
|
||||
return
|
||||
|
||||
# Give up after 5 reanimation failures in a row. Supervisor cannot fix this issue.
|
||||
reanimate_fails = self._cache.get(HASS_WATCHDOG_REANIMATE_FAILURES, 0)
|
||||
if reanimate_fails >= HASS_WATCHDOG_MAX_REANIMATE_ATTEMPTS:
|
||||
if reanimate_fails == HASS_WATCHDOG_MAX_REANIMATE_ATTEMPTS:
|
||||
_LOGGER.critical(
|
||||
"Watchdog cannot reanimate Home Assistant Core, failed all %s attempts.",
|
||||
reanimate_fails,
|
||||
)
|
||||
self._cache[HASS_WATCHDOG_REANIMATE_FAILURES] += 1
|
||||
return
|
||||
|
||||
# Init cache data
|
||||
api_fails = self._cache.get(HASS_WATCHDOG_API_FAILURES, 0)
|
||||
|
||||
@@ -184,38 +195,16 @@ class Tasks(CoreSysAttributes):
|
||||
_LOGGER.warning("Watchdog missed an Home Assistant Core API response.")
|
||||
return
|
||||
|
||||
# After 5 reanimation attempts switch to safe mode. If that fails, give up
|
||||
reanimate_fails = self._cache.get(HASS_WATCHDOG_REANIMATE_FAILURES, 0)
|
||||
if reanimate_fails > HASS_WATCHDOG_MAX_REANIMATE_ATTEMPTS:
|
||||
return
|
||||
|
||||
if safe_mode := reanimate_fails == HASS_WATCHDOG_MAX_REANIMATE_ATTEMPTS:
|
||||
_LOGGER.critical(
|
||||
"Watchdog cannot reanimate Home Assistant Core, failed all %s attempts. Restarting into safe mode",
|
||||
reanimate_fails,
|
||||
)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Watchdog missed %s Home Assistant Core API responses in a row. Restarting Home Assistant Core!",
|
||||
HASS_WATCHDOG_MAX_API_ATTEMPTS,
|
||||
)
|
||||
|
||||
_LOGGER.error(
|
||||
"Watchdog missed %s Home Assistant Core API responses in a row. Restarting Home Assistant Core API!",
|
||||
HASS_WATCHDOG_MAX_API_ATTEMPTS,
|
||||
)
|
||||
try:
|
||||
if safe_mode:
|
||||
await self.sys_homeassistant.core.rebuild(safe_mode=True)
|
||||
else:
|
||||
await self.sys_homeassistant.core.restart()
|
||||
await self.sys_homeassistant.core.restart()
|
||||
except HomeAssistantError as err:
|
||||
if reanimate_fails == 0 or safe_mode:
|
||||
_LOGGER.error("Home Assistant watchdog reanimation failed!")
|
||||
if reanimate_fails == 0:
|
||||
capture_exception(err)
|
||||
|
||||
if safe_mode:
|
||||
_LOGGER.critical(
|
||||
"Safe mode restart failed. Watchdog cannot bring Home Assistant online."
|
||||
)
|
||||
else:
|
||||
_LOGGER.error("Home Assistant watchdog reanimation failed!")
|
||||
|
||||
self._cache[HASS_WATCHDOG_REANIMATE_FAILURES] = reanimate_fails + 1
|
||||
else:
|
||||
self._cache[HASS_WATCHDOG_REANIMATE_FAILURES] = 0
|
||||
|
@@ -26,7 +26,7 @@ class FixupAddonExecuteRemove(FixupBase):
|
||||
# Remove addon
|
||||
_LOGGER.info("Remove addon: %s", reference)
|
||||
try:
|
||||
await addon.uninstall(remove_config=False)
|
||||
addon.uninstall()
|
||||
except AddonsError as err:
|
||||
_LOGGER.error("Could not remove %s due to %s", reference, err)
|
||||
raise ResolutionFixupError() from None
|
||||
|
@@ -8,13 +8,11 @@ from .const import StoreType
|
||||
|
||||
URL_COMMUNITY_ADDONS = "https://github.com/hassio-addons/repository"
|
||||
URL_ESPHOME = "https://github.com/esphome/home-assistant-addon"
|
||||
URL_MUSIC_ASSISTANT = "https://github.com/music-assistant/home-assistant-addon"
|
||||
BUILTIN_REPOSITORIES = {
|
||||
StoreType.CORE,
|
||||
StoreType.LOCAL,
|
||||
URL_COMMUNITY_ADDONS,
|
||||
URL_ESPHOME,
|
||||
URL_MUSIC_ASSISTANT,
|
||||
}
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
|
@@ -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
|
||||
|
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Music Assistant",
|
||||
"url": "https://github.com/music-assistant/core",
|
||||
"maintainer": "Music Assistant <marcelveldt@users.noreply.github.com>"
|
||||
}
|
@@ -46,7 +46,7 @@ async def test_watchdog_homeassistant_api(
|
||||
restart.assert_called_once()
|
||||
assert "Watchdog missed an Home Assistant Core API response." not in caplog.text
|
||||
assert (
|
||||
"Watchdog missed 2 Home Assistant Core API responses in a row. Restarting Home Assistant Core!"
|
||||
"Watchdog missed 2 Home Assistant Core API responses in a row. Restarting Home Assistant Core API!"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
@@ -109,48 +109,31 @@ async def test_watchdog_homeassistant_api_reanimation_limit(
|
||||
HomeAssistantAPI, "check_api_state", return_value=False
|
||||
), patch.object(
|
||||
HomeAssistantCore, "restart", side_effect=(err := HomeAssistantError())
|
||||
) as restart, patch.object(
|
||||
HomeAssistantCore, "rebuild", side_effect=err
|
||||
) as rebuild:
|
||||
) as restart:
|
||||
for _ in range(5):
|
||||
await tasks._watchdog_homeassistant_api()
|
||||
restart.assert_not_called()
|
||||
|
||||
await tasks._watchdog_homeassistant_api()
|
||||
restart.assert_called_once_with()
|
||||
restart.assert_called_once()
|
||||
assert "Home Assistant watchdog reanimation failed!" in caplog.text
|
||||
|
||||
rebuild.assert_not_called()
|
||||
restart.reset_mock()
|
||||
|
||||
capture_exception.assert_called_once_with(err)
|
||||
|
||||
# Next time it should try safe mode
|
||||
caplog.clear()
|
||||
await tasks._watchdog_homeassistant_api()
|
||||
rebuild.assert_not_called()
|
||||
|
||||
await tasks._watchdog_homeassistant_api()
|
||||
|
||||
rebuild.assert_called_once_with(safe_mode=True)
|
||||
restart.assert_not_called()
|
||||
assert "Watchdog missed an Home Assistant Core API response." not in caplog.text
|
||||
assert "Watchdog found a problem with Home Assistant API!" not in caplog.text
|
||||
assert (
|
||||
"Watchdog cannot reanimate Home Assistant Core, failed all 5 attempts. Restarting into safe mode"
|
||||
in caplog.text
|
||||
)
|
||||
assert (
|
||||
"Safe mode restart failed. Watchdog cannot bring Home Assistant online."
|
||||
"Watchdog cannot reanimate Home Assistant Core, failed all 5 attempts."
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
# After safe mode has failed too, no more restart attempts
|
||||
rebuild.reset_mock()
|
||||
caplog.clear()
|
||||
await tasks._watchdog_homeassistant_api()
|
||||
assert "Watchdog missed an Home Assistant Core API response." in caplog.text
|
||||
|
||||
caplog.clear()
|
||||
await tasks._watchdog_homeassistant_api()
|
||||
restart.assert_not_called()
|
||||
assert not caplog.text
|
||||
restart.assert_not_called()
|
||||
rebuild.assert_not_called()
|
||||
|
@@ -163,7 +163,6 @@ async def test_preinstall_valid_repository(
|
||||
assert store_manager.get("local").validate()
|
||||
assert store_manager.get("a0d7b954").validate()
|
||||
assert store_manager.get("5c53de3b").validate()
|
||||
assert store_manager.get("d5369777").validate()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("use_update", [True, False])
|
||||
|
@@ -39,11 +39,11 @@ async def test_default_load(coresys: CoreSys):
|
||||
):
|
||||
await store_manager.load()
|
||||
|
||||
assert len(store_manager.all) == 5
|
||||
assert len(store_manager.all) == 4
|
||||
assert isinstance(store_manager.get("core"), Repository)
|
||||
assert isinstance(store_manager.get("local"), Repository)
|
||||
|
||||
assert len(store_manager.repository_urls) == 3
|
||||
assert len(store_manager.repository_urls) == 2
|
||||
assert (
|
||||
"https://github.com/hassio-addons/repository" in store_manager.repository_urls
|
||||
)
|
||||
@@ -51,11 +51,6 @@ async def test_default_load(coresys: CoreSys):
|
||||
"https://github.com/esphome/home-assistant-addon"
|
||||
in store_manager.repository_urls
|
||||
)
|
||||
assert (
|
||||
"https://github.com/music-assistant/home-assistant-addon"
|
||||
in store_manager.repository_urls
|
||||
)
|
||||
# NOTE: When adding new stores, make sure to add it to tests/fixtures/addons/git/
|
||||
assert refresh_cache_calls == {"local_ssh", "local_example", "core_samba"}
|
||||
|
||||
|
||||
@@ -82,11 +77,11 @@ async def test_load_with_custom_repository(coresys: CoreSys):
|
||||
):
|
||||
await store_manager.load()
|
||||
|
||||
assert len(store_manager.all) == 6
|
||||
assert len(store_manager.all) == 5
|
||||
assert isinstance(store_manager.get("core"), Repository)
|
||||
assert isinstance(store_manager.get("local"), Repository)
|
||||
|
||||
assert len(store_manager.repository_urls) == 4
|
||||
assert len(store_manager.repository_urls) == 3
|
||||
assert (
|
||||
"https://github.com/hassio-addons/repository" in store_manager.repository_urls
|
||||
)
|
||||
@@ -94,10 +89,6 @@ async def test_load_with_custom_repository(coresys: CoreSys):
|
||||
"https://github.com/esphome/home-assistant-addon"
|
||||
in store_manager.repository_urls
|
||||
)
|
||||
assert (
|
||||
"https://github.com/music-assistant/home-assistant-addon"
|
||||
in store_manager.repository_urls
|
||||
)
|
||||
assert "http://example.com" in store_manager.repository_urls
|
||||
|
||||
|
||||
@@ -114,11 +105,11 @@ async def test_load_from_core_config(coresys: CoreSys):
|
||||
), patch("pathlib.Path.exists", return_value=True):
|
||||
await coresys.store.load()
|
||||
|
||||
assert len(coresys.store.all) == 6
|
||||
assert len(coresys.store.all) == 5
|
||||
assert isinstance(coresys.store.get("core"), Repository)
|
||||
assert isinstance(coresys.store.get("local"), Repository)
|
||||
|
||||
assert len(coresys.store.repository_urls) == 4
|
||||
assert len(coresys.store.repository_urls) == 3
|
||||
assert (
|
||||
"https://github.com/hassio-addons/repository" in coresys.store.repository_urls
|
||||
)
|
||||
@@ -126,10 +117,6 @@ async def test_load_from_core_config(coresys: CoreSys):
|
||||
"https://github.com/esphome/home-assistant-addon"
|
||||
in coresys.store.repository_urls
|
||||
)
|
||||
assert (
|
||||
"https://github.com/music-assistant/home-assistant-addon"
|
||||
in coresys.store.repository_urls
|
||||
)
|
||||
assert "http://example.com" in coresys.store.repository_urls
|
||||
|
||||
assert coresys.config.addons_repositories == []
|
||||
@@ -256,12 +243,12 @@ async def test_install_unavailable_addon(
|
||||
async def test_reload(coresys: CoreSys):
|
||||
"""Test store reload."""
|
||||
await coresys.store.load()
|
||||
assert len(coresys.store.all) == 5
|
||||
assert len(coresys.store.all) == 4
|
||||
|
||||
with patch.object(GitRepo, "pull") as git_pull:
|
||||
await coresys.store.reload()
|
||||
|
||||
assert git_pull.call_count == 4
|
||||
assert git_pull.call_count == 3
|
||||
|
||||
|
||||
async def test_addon_version_timestamp(coresys: CoreSys, install_addon_example: Addon):
|
||||
|
@@ -49,13 +49,12 @@ async def test_repository_validate(repo_list: list[str], valid: bool):
|
||||
"""Test repository list validate."""
|
||||
if valid:
|
||||
processed = repositories(repo_list)
|
||||
assert len(processed) == 5
|
||||
assert len(processed) == 4
|
||||
assert set(repositories(repo_list)) == {
|
||||
"core",
|
||||
"local",
|
||||
"https://github.com/hassio-addons/repository",
|
||||
"https://github.com/esphome/home-assistant-addon",
|
||||
"https://github.com/music-assistant/home-assistant-addon",
|
||||
}
|
||||
else:
|
||||
with pytest.raises(Invalid):
|
||||
|
Reference in New Issue
Block a user