Compare commits

..

8 Commits

Author SHA1 Message Date
ludeeus
e415923553 Trigger backup sync when backup is complete 2024-10-16 04:54:29 +00:00
dependabot[bot]
95c638991d Bump coverage from 7.6.2 to 7.6.3 (#5351) 2024-10-14 08:16:23 +02:00
Stefan Agner
e2ada42001 Fix log follow mode without range header (#5347) 2024-10-11 19:54:53 +02:00
dependabot[bot]
22e50b4ace Bump aiohttp from 3.10.9 to 3.10.10 (#5345)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-11 10:31:09 +02:00
dependabot[bot]
334484de7f Bump debugpy from 1.8.6 to 1.8.7 (#5346)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-11 10:12:26 +02:00
Stefan Agner
180a7c3990 Throttle connectivity check on connectivity issue (#5342)
* Throttle connectivity check on connectivity issue

If Supervisor detects a connectivity issue, currenlty every function
which requires internet get delayed by 10s due to the connectivity
check. This especially slows down initial startup when there are
connectivity issues. It is unlikely to resolve immeaditly, so throttle
the connectivity check to check every 30s.

* Fix pytest

* Reset throttle in test and refactor helper

* CodeRabbit suggestion

---------

Co-authored-by: Mike Degatano <michael.degatano@gmail.com>
2024-10-10 22:57:16 +02:00
dependabot[bot]
d5f33de808 Bump coverage from 7.6.1 to 7.6.2 (#5344)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.6.1 to 7.6.2.
- [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.6.1...7.6.2)

---
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-10-10 09:46:41 +02:00
dependabot[bot]
6539f0df6f Bump actions/upload-artifact from 4.4.2 to 4.4.3 (#5343)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.4.2 to 4.4.3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.4.2...v4.4.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-10-10 09:46:18 +02:00
14 changed files with 108 additions and 15 deletions

View File

@@ -335,7 +335,7 @@ jobs:
-o console_output_style=count \
tests
- name: Upload coverage artifact
uses: actions/upload-artifact@v4.4.2
uses: actions/upload-artifact@v4.4.3
with:
name: coverage-${{ matrix.python-version }}
path: .coverage

View File

@@ -1,5 +1,5 @@
aiodns==3.2.0
aiohttp==3.10.9
aiohttp==3.10.10
atomicwrites-homeassistant==1.4.1
attrs==24.2.0
awesomeversion==24.6.0
@@ -8,7 +8,7 @@ ciso8601==2.3.1
colorlog==6.8.2
cpe==1.3.1
cryptography==43.0.1
debugpy==1.8.6
debugpy==1.8.7
deepmerge==2.0
dirhash==0.5.0
docker==7.1.0

View File

@@ -1,4 +1,4 @@
coverage==7.6.1
coverage==7.6.3
pre-commit==4.0.1
pylint==3.3.1
pytest-aiohttp==1.0.5

View File

@@ -239,11 +239,13 @@ class APIHost(CoreSysAttributes):
# return 2 lines at minimum.
lines = max(2, lines)
# entries=cursor[[:num_skip]:num_entries]
range_header = f"entries=:-{lines-1}:{lines}"
range_header = f"entries=:-{lines-1}:{'' if follow else lines}"
elif RANGE in request.headers:
range_header = request.headers.get(RANGE)
else:
range_header = f"entries=:-{DEFAULT_LINES-1}:{DEFAULT_LINES}"
range_header = (
f"entries=:-{DEFAULT_LINES-1}:{'' if follow else DEFAULT_LINES}"
)
async with self.sys_host.logs.journald_logs(
params=params, range_header=range_header, accept=LogFormat.JOURNAL

View File

@@ -10,7 +10,10 @@ from pathlib import Path
from ..addons.addon import Addon
from ..const import (
ATTR_DATA,
ATTR_DAYS_UNTIL_STALE,
ATTR_SLUG,
ATTR_TYPE,
FILE_HASSIO_BACKUPS,
FOLDER_HOMEASSISTANT,
CoreState,
@@ -21,7 +24,9 @@ from ..exceptions import (
BackupInvalidError,
BackupJobError,
BackupMountDownError,
HomeAssistantWSError,
)
from ..homeassistant.const import WSType
from ..jobs.const import JOB_GROUP_BACKUP_MANAGER, JobCondition, JobExecutionLimit
from ..jobs.decorator import Job
from ..jobs.job_group import JobGroup
@@ -299,6 +304,18 @@ class BackupManager(FileConfiguration, JobGroup):
# Ignore exceptions from waiting for addon startup, addon errors handled elsewhere
await asyncio.gather(*addon_start_tasks, return_exceptions=True)
try:
await self.sys_homeassistant.websocket.async_send_command(
{
ATTR_TYPE: WSType.BACKUP_SYNC,
ATTR_DATA: {
ATTR_SLUG: backup.slug,
},
},
)
except HomeAssistantWSError as err:
_LOGGER.error("Can't send backup sync to Home Assistant: %s", err)
return backup
finally:
self.sys_core.state = CoreState.RUNNING

View File

@@ -32,6 +32,7 @@ class WSType(StrEnum):
SUPERVISOR_EVENT = "supervisor/event"
BACKUP_START = "backup/start"
BACKUP_END = "backup/end"
BACKUP_SYNC = "backup/sync"
class WSEvent(StrEnum):

View File

@@ -34,6 +34,7 @@ MIN_VERSION = {
WSType.SUPERVISOR_EVENT: "2021.2.4",
WSType.BACKUP_START: "2022.1.0",
WSType.BACKUP_END: "2022.1.0",
WSType.BACKUP_SYNC: "2024.10.99",
}
_LOGGER: logging.Logger = logging.getLogger(__name__)

View File

@@ -41,7 +41,7 @@ def _check_connectivity_throttle_period(coresys: CoreSys, *_) -> timedelta:
if coresys.supervisor.connectivity:
return timedelta(minutes=10)
return timedelta()
return timedelta(seconds=30)
class Supervisor(CoreSysAttributes):

View File

@@ -7,6 +7,7 @@ from aiohttp.test_utils import TestClient
from supervisor.host.const import LogFormat
DEFAULT_LOG_RANGE = "entries=:-99:100"
DEFAULT_LOG_RANGE_FOLLOW = "entries=:-99:"
async def common_test_api_advanced_logs(
@@ -34,7 +35,7 @@ async def common_test_api_advanced_logs(
journald_logs.assert_called_once_with(
params={"SYSLOG_IDENTIFIER": syslog_identifier, "follow": ""},
range_header=DEFAULT_LOG_RANGE,
range_header=DEFAULT_LOG_RANGE_FOLLOW,
accept=LogFormat.JOURNAL,
)
@@ -62,6 +63,6 @@ async def common_test_api_advanced_logs(
"_BOOT_ID": "ccc",
"follow": "",
},
range_header=DEFAULT_LOG_RANGE,
range_header=DEFAULT_LOG_RANGE_FOLLOW,
accept=LogFormat.JOURNAL,
)

View File

@@ -15,6 +15,7 @@ from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.systemd import Systemd as SystemdService
DEFAULT_RANGE = "entries=:-99:100"
DEFAULT_RANGE_FOLLOW = "entries=:-99:"
# pylint: disable=protected-access
@@ -233,7 +234,7 @@ async def test_advanced_logs(
"SYSLOG_IDENTIFIER": coresys.host.logs.default_identifiers,
"follow": "",
},
range_header=DEFAULT_RANGE,
range_header=DEFAULT_RANGE_FOLLOW,
accept=LogFormat.JOURNAL,
)

View File

@@ -1169,6 +1169,40 @@ async def test_backup_progress(
]
async def test_backup_sync_notification(
coresys: CoreSys,
install_addon_ssh: Addon,
container: MagicMock,
ha_ws_client: AsyncMock,
tmp_supervisor_data,
path_extern,
):
"""Test backup sync notification."""
container.status = "running"
install_addon_ssh.path_data.mkdir()
coresys.core.state = CoreState.RUNNING
ha_ws_client.ha_version = AwesomeVersion("9999.9.9")
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
ha_ws_client.async_send_command.reset_mock()
partial_backup: Backup = await coresys.backups.do_backup_partial()
await asyncio.sleep(0)
messages = [
call.args[0]
for call in ha_ws_client.async_send_command.call_args_list
if call.args[0]["type"] == WSType.BACKUP_SYNC
]
assert messages == [
{
"data": {
"slug": partial_backup.slug,
},
"type": WSType.BACKUP_SYNC,
},
]
async def test_restore_progress(
request: pytest.FixtureRequest,
coresys: CoreSys,

View File

@@ -1,12 +1,15 @@
"""Common test functions."""
from datetime import datetime
from importlib import import_module
from inspect import getclosurevars
import json
from pathlib import Path
from typing import Any
from dbus_fast.aio.message_bus import MessageBus
from supervisor.jobs.decorator import Job
from supervisor.resolution.validate import get_valid_modules
from supervisor.utils.yaml import read_yaml_file
@@ -82,3 +85,17 @@ async def mock_dbus_services(
services[module] = service_module.setup(to_mock[module]).export(bus)
return services
def get_job_decorator(func) -> Job:
"""Get Job object of decorated function."""
# Access the closure of the wrapper function
job = getclosurevars(func).nonlocals["self"]
if not isinstance(job, Job):
raise TypeError(f"{func.__qualname__} is not a Job")
return job
def reset_last_call(func, group: str | None = None) -> None:
"""Reset last call for a function using the Job decorator."""
get_job_decorator(func).set_last_call(datetime.min, group)

View File

@@ -26,8 +26,11 @@ from supervisor.jobs.job_group import JobGroup
from supervisor.os.manager import OSManager
from supervisor.plugins.audio import PluginAudio
from supervisor.resolution.const import UnhealthyReason
from supervisor.supervisor import Supervisor
from supervisor.utils.dt import utcnow
from tests.common import reset_last_call
async def test_healthy(coresys: CoreSys, caplog: pytest.LogCaptureFixture):
"""Test the healty decorator."""
@@ -73,6 +76,7 @@ async def test_internet(
):
"""Test the internet decorator."""
coresys.core.state = CoreState.RUNNING
reset_last_call(Supervisor.check_connectivity)
class TestClass:
"""Test class."""

View File

@@ -1,6 +1,6 @@
"""Test supervisor object."""
from datetime import datetime
from datetime import datetime, timedelta
import errno
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
@@ -8,6 +8,7 @@ from aiohttp import ClientTimeout
from aiohttp.client_exceptions import ClientError
from awesomeversion import AwesomeVersion
import pytest
from time_machine import travel
from supervisor.const import UpdateChannel
from supervisor.coresys import CoreSys
@@ -22,6 +23,8 @@ from supervisor.resolution.const import ContextType, IssueType
from supervisor.resolution.data import Issue
from supervisor.supervisor import Supervisor
from tests.common import reset_last_call
@pytest.fixture(name="websession", scope="function")
async def fixture_webession(coresys: CoreSys) -> AsyncMock:
@@ -58,21 +61,33 @@ async def test_connectivity_check(
assert supervisor_unthrottled.connectivity is connectivity
@pytest.mark.parametrize("side_effect,call_count", [(ClientError(), 3), (None, 1)])
@pytest.mark.parametrize(
"side_effect,call_interval,throttled",
[
(None, timedelta(minutes=5), True),
(None, timedelta(minutes=15), False),
(ClientError(), timedelta(seconds=20), True),
(ClientError(), timedelta(seconds=40), False),
],
)
async def test_connectivity_check_throttling(
coresys: CoreSys,
websession: AsyncMock,
side_effect: Exception | None,
call_count: int,
call_interval: timedelta,
throttled: bool,
):
"""Test connectivity check throttled when checks succeed."""
coresys.supervisor.connectivity = None
websession.head.side_effect = side_effect
for _ in range(3):
reset_last_call(Supervisor.check_connectivity)
with travel(datetime.now(), tick=False) as traveller:
await coresys.supervisor.check_connectivity()
traveller.shift(call_interval)
await coresys.supervisor.check_connectivity()
assert websession.head.call_count == call_count
assert websession.head.call_count == (1 if throttled else 2)
async def test_update_failed(coresys: CoreSys, capture_exception: Mock):