From 9d78949d73b93b182a45e9e445df159c98dc4dbc Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Tue, 31 Oct 2023 21:46:21 -0400 Subject: [PATCH] Addon methods interfacing with docker are job groups --- supervisor/addons/__init__.py | 148 ++------------------ supervisor/addons/addon.py | 250 ++++++++++++++++++++++++++++++++-- supervisor/addons/model.py | 18 +-- supervisor/api/addons.py | 4 +- supervisor/api/store.py | 4 +- supervisor/backups/backup.py | 14 +- supervisor/backups/manager.py | 4 +- supervisor/misc/tasks.py | 2 +- tests/addons/test_addon.py | 6 +- tests/addons/test_manager.py | 6 +- 10 files changed, 269 insertions(+), 187 deletions(-) diff --git a/supervisor/addons/__init__.py b/supervisor/addons/__init__.py index 8398902c76b..96cf097996d 100644 --- a/supervisor/addons/__init__.py +++ b/supervisor/addons/__init__.py @@ -17,8 +17,8 @@ DockerAPIError, DockerError, DockerNotFound, + HassioError, HomeAssistantAPIError, - HostAppArmorError, ) from ..jobs.decorator import Job, JobCondition from ..resolution.const import ContextType, IssueType, SuggestionType @@ -119,8 +119,8 @@ async def boot(self, stage: AddonStartup) -> None: ): addon.boot = AddonBoot.MANUAL addon.save_persist() - except Exception as err: # pylint: disable=broad-except - capture_exception(err) + except HassioError: + pass # These are already handled else: continue @@ -169,36 +169,7 @@ async def install(self, slug: str) -> None: store.validate_availability() - self.data.install(store) - addon = Addon(self.coresys, slug) - await addon.load() - - if not addon.path_data.is_dir(): - _LOGGER.info( - "Creating Home Assistant add-on data folder %s", addon.path_data - ) - addon.path_data.mkdir() - - if addon.addon_config_used and not addon.path_config.is_dir(): - _LOGGER.info( - "Creating Home Assistant add-on config folder %s", addon.path_config - ) - addon.path_config.mkdir() - - # Setup/Fix AppArmor profile - await addon.install_apparmor() - - try: - await addon.instance.install(store.version, store.image, arch=addon.arch) - except DockerError as err: - self.data.uninstall(addon) - raise AddonsError() from err - - self.local[slug] = addon - - # Reload ingress tokens - if addon.with_ingress: - await self.sys_ingress.reload() + await Addon(self.coresys, slug).install() _LOGGER.info("Add-on '%s' successfully installed", slug) @@ -207,51 +178,8 @@ async def uninstall(self, slug: str) -> None: if slug not in self.local: _LOGGER.warning("Add-on %s is not installed", slug) return - addon = self.local[slug] - - try: - await addon.instance.remove() - except DockerError as err: - raise AddonsError() from err - - addon.state = AddonState.UNKNOWN - await addon.unload() - - # Cleanup audio settings - if addon.path_pulse.exists(): - with suppress(OSError): - addon.path_pulse.unlink() - - # Cleanup AppArmor profile - with suppress(HostAppArmorError): - await addon.uninstall_apparmor() - - # Cleanup Ingress panel from sidebar - if addon.ingress_panel: - addon.ingress_panel = False - with suppress(HomeAssistantAPIError): - await self.sys_ingress.update_hass_panel(addon) - - # Cleanup Ingress dynamic port assignment - if addon.with_ingress: - self.sys_create_task(self.sys_ingress.reload()) - self.sys_ingress.del_dynamic_port(slug) - - # Cleanup discovery data - for message in self.sys_discovery.list_messages: - if message.addon != addon.slug: - continue - self.sys_discovery.remove(message) - - # Cleanup services data - for service in self.sys_services.list_services: - if addon.slug not in service.active: - continue - service.del_service_data(addon) - - self.data.uninstall(addon) - self.local.pop(slug) + await self.local[slug].uninstall() _LOGGER.info("Add-on '%s' successfully removed", slug) @@ -262,10 +190,10 @@ async def uninstall(self, slug: str) -> None: ) async def update( self, slug: str, backup: bool | None = False - ) -> Awaitable[None] | None: + ) -> asyncio.Task | None: """Update add-on. - Returns a coroutine that completes when addon has state 'started' (see addon.start) + Returns a Task that completes when addon has state 'started' (see addon.start) if addon is started after update. Else nothing is returned. """ self.sys_jobs.current.reference = slug @@ -293,41 +221,7 @@ async def update( addons=[addon.slug], ) - # Update instance - old_image = addon.image - # Cache data to prevent races with other updates to global - store = store.clone() - - try: - await addon.instance.update(store.version, store.image) - except DockerError as err: - raise AddonsError() from err - - # Stop the addon if running - if (last_state := addon.state) in {AddonState.STARTED, AddonState.STARTUP}: - await addon.stop() - - try: - _LOGGER.info("Add-on '%s' successfully updated", slug) - self.data.update(store) - - # Cleanup - with suppress(DockerError): - await addon.instance.cleanup( - old_image=old_image, image=store.image, version=store.version - ) - - # Setup/Fix AppArmor profile - await addon.install_apparmor() - - finally: - # restore state. Return awaitable for caller if no exception - out = ( - await addon.start() - if last_state in {AddonState.STARTED, AddonState.STARTUP} - else None - ) - return out + return await addon.update() @Job( name="addon_manager_rebuild", @@ -338,10 +232,10 @@ async def update( ], on_condition=AddonsJobError, ) - async def rebuild(self, slug: str) -> Awaitable[None] | None: + async def rebuild(self, slug: str) -> asyncio.Task | None: """Perform a rebuild of local build add-on. - Returns a coroutine that completes when addon has state 'started' (see addon.start) + Returns a Task that completes when addon has state 'started' (see addon.start) if addon is started after rebuild. Else nothing is returned. """ self.sys_jobs.current.reference = slug @@ -366,23 +260,7 @@ async def rebuild(self, slug: str) -> Awaitable[None] | None: "Can't rebuild a image based add-on", _LOGGER.error ) - # remove docker container but not addon config - last_state: AddonState = addon.state - try: - await addon.instance.remove() - await addon.instance.install(addon.version) - except DockerError as err: - raise AddonsError() from err - - self.data.update(store) - _LOGGER.info("Add-on '%s' successfully rebuilt", slug) - - # restore state - return ( - await addon.start() - if last_state in [AddonState.STARTED, AddonState.STARTUP] - else None - ) + return await addon.rebuild() @Job( name="addon_manager_restore", @@ -395,10 +273,10 @@ async def rebuild(self, slug: str) -> Awaitable[None] | None: ) async def restore( self, slug: str, tar_file: tarfile.TarFile - ) -> Awaitable[None] | None: + ) -> asyncio.Task | None: """Restore state of an add-on. - Returns a coroutine that completes when addon has state 'started' (see addon.start) + Returns a Task that completes when addon has state 'started' (see addon.start) if addon is started after restore. Else nothing is returned. """ self.sys_jobs.current.reference = slug diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index eaba6007dee..d7481de9c14 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -65,12 +65,14 @@ AddonsNotSupportedError, ConfigurationFileError, DockerError, + HomeAssistantAPIError, HostAppArmorError, ) from ..hardware.data import Device from ..homeassistant.const import WSEvent, WSType from ..jobs.const import JobExecutionLimit from ..jobs.decorator import Job +from ..store.addon import AddonStore from ..utils import check_port from ..utils.apparmor import adjust_profile from ..utils.json import read_json_file, write_json_file @@ -200,6 +202,11 @@ def data_store(self) -> Data: """Return add-on data from store.""" return self.sys_store.data.addons.get(self.slug, self.data) + @property + def addon_store(self) -> AddonStore | None: + """Return store representation of addon.""" + return self.sys_addons.store.get(self.slug) + @property def persist(self) -> Data: """Return add-on data/config.""" @@ -575,6 +582,11 @@ async def write_options(self) -> None: raise AddonConfigurationError() + @Job( + name="addon_unload", + limit=JobExecutionLimit.GROUP_ONCE, + on_condition=AddonsJobError, + ) async def unload(self) -> None: """Unload add-on and remove data.""" if self._startup_task: @@ -594,6 +606,177 @@ async def unload(self) -> None: _LOGGER.info("Removing add-on config folder %s", self.path_config) await remove_data(self.path_config) + @Job( + name="addon_install", + limit=JobExecutionLimit.GROUP_ONCE, + on_condition=AddonsJobError, + ) + async def install(self) -> None: + """Install and setup this addon.""" + self.sys_addons.data.install(self.addon_store) + await self.load() + + if not self.path_data.is_dir(): + _LOGGER.info( + "Creating Home Assistant add-on data folder %s", self.path_data + ) + self.path_data.mkdir() + + if self.addon_config_used and not self.path_config.is_dir(): + _LOGGER.info( + "Creating Home Assistant add-on config folder %s", self.path_config + ) + self.path_config.mkdir() + + # Setup/Fix AppArmor profile + await self.install_apparmor() + + # Install image + try: + await self.instance.install( + self.latest_version, self.addon_store.image, arch=self.arch + ) + except DockerError as err: + self.sys_addons.data.uninstall(self) + raise AddonsError() from err + + # Add to addon manager + self.sys_addons.local[self.slug] = self + + # Reload ingress tokens + if self.with_ingress: + await self.sys_ingress.reload() + + @Job( + name="addon_uninstall", + limit=JobExecutionLimit.GROUP_ONCE, + on_condition=AddonsJobError, + ) + async def uninstall(self) -> None: + """Uninstall and cleanup this addon.""" + try: + await self.instance.remove() + except DockerError as err: + raise AddonsError() from err + + self.state = AddonState.UNKNOWN + + await self.unload() + + # Cleanup audio settings + if self.path_pulse.exists(): + with suppress(OSError): + self.path_pulse.unlink() + + # Cleanup AppArmor profile + with suppress(HostAppArmorError): + await self.uninstall_apparmor() + + # Cleanup Ingress panel from sidebar + if self.ingress_panel: + self.ingress_panel = False + with suppress(HomeAssistantAPIError): + await self.sys_ingress.update_hass_panel(self) + + # Cleanup Ingress dynamic port assignment + if self.with_ingress: + self.sys_create_task(self.sys_ingress.reload()) + self.sys_ingress.del_dynamic_port(self.slug) + + # Cleanup discovery data + for message in self.sys_discovery.list_messages: + if message.addon != self.slug: + continue + self.sys_discovery.remove(message) + + # Cleanup services data + for service in self.sys_services.list_services: + if self.slug not in service.active: + continue + service.del_service_data(self) + + # Remove from addon manager + self.sys_addons.data.uninstall(self) + self.sys_addons.local.pop(self.slug) + + @Job( + name="addon_update", + limit=JobExecutionLimit.GROUP_ONCE, + on_condition=AddonsJobError, + ) + async def update(self) -> asyncio.Task | None: + """Update this addon to latest version. + + Returns a Task that completes when addon has state 'started' (see start) + if it was running. Else nothing is returned. + """ + old_image = self.image + # Cache data to prevent races with other updates to global + store = self.addon_store.clone() + + try: + await self.instance.update(store.version, store.image) + except DockerError as err: + raise AddonsError() from err + + # Stop the addon if running + if (last_state := self.state) in {AddonState.STARTED, AddonState.STARTUP}: + await self.stop() + + try: + _LOGGER.info("Add-on '%s' successfully updated", self.slug) + self.sys_addons.data.update(store) + + # Cleanup + with suppress(DockerError): + await self.instance.cleanup( + old_image=old_image, image=store.image, version=store.version + ) + + # Setup/Fix AppArmor profile + await self.install_apparmor() + + finally: + # restore state. Return Task for caller if no exception + out = ( + await self.start() + if last_state in {AddonState.STARTED, AddonState.STARTUP} + else None + ) + return out + + @Job( + name="addon_rebuild", + limit=JobExecutionLimit.GROUP_ONCE, + on_condition=AddonsJobError, + ) + async def rebuild(self) -> asyncio.Task | None: + """Rebuild this addons container and image. + + Returns a Task that completes when addon has state 'started' (see start) + if it was running. Else nothing is returned. + """ + last_state: AddonState = self.state + try: + # remove docker container but not addon config + try: + await self.instance.remove() + await self.instance.install(self.version) + except DockerError as err: + raise AddonsError() from err + + self.sys_addons.data.update(self.addon_store) + _LOGGER.info("Add-on '%s' successfully rebuilt", self.slug) + + finally: + # restore state + out = ( + await self.start() + if last_state in [AddonState.STARTED, AddonState.STARTUP] + else None + ) + return out + def write_pulse(self) -> None: """Write asound config to file and return True on success.""" pulse_config = self.sys_plugins.audio.pulse_client( @@ -689,16 +872,21 @@ async def _wait_for_startup(self) -> None: finally: self._startup_task = None - async def start(self) -> Awaitable[None]: + @Job( + name="addon_start", + limit=JobExecutionLimit.GROUP_ONCE, + on_condition=AddonsJobError, + ) + async def start(self) -> asyncio.Task: """Set options and start add-on. - Returns a coroutine that completes when addon has state 'started'. + Returns a Task that completes when addon has state 'started'. For addons with a healthcheck, that is when they become healthy or unhealthy. Addons without a healthcheck have state 'started' immediately. """ if await self.instance.is_running(): _LOGGER.warning("%s is already running!", self.slug) - return self._wait_for_startup() + return self.sys_create_task(self._wait_for_startup()) # Access Token self.persist[ATTR_ACCESS_TOKEN] = secrets.token_hex(56) @@ -719,8 +907,13 @@ async def start(self) -> Awaitable[None]: self.state = AddonState.ERROR raise AddonsError() from err - return self._wait_for_startup() + return self.sys_create_task(self._wait_for_startup()) + @Job( + name="addon_stop", + limit=JobExecutionLimit.GROUP_ONCE, + on_condition=AddonsJobError, + ) async def stop(self) -> None: """Stop add-on.""" self._manual_stop = True @@ -730,10 +923,15 @@ async def stop(self) -> None: self.state = AddonState.ERROR raise AddonsError() from err - async def restart(self) -> Awaitable[None]: + @Job( + name="addon_restart", + limit=JobExecutionLimit.GROUP_ONCE, + on_condition=AddonsJobError, + ) + async def restart(self) -> asyncio.Task: """Restart add-on. - Returns a coroutine that completes when addon has state 'started' (see start). + Returns a Task that completes when addon has state 'started' (see start). """ with suppress(AddonsError): await self.stop() @@ -760,6 +958,11 @@ async def stats(self) -> DockerStats: except DockerError as err: raise AddonsError() from err + @Job( + name="addon_write_stdin", + limit=JobExecutionLimit.GROUP_ONCE, + on_condition=AddonsJobError, + ) async def write_stdin(self, data) -> None: """Write data to add-on stdin.""" if not self.with_stdin: @@ -789,7 +992,11 @@ async def _backup_command(self, command: str) -> None: _LOGGER.error, ) from err - @Job(name="addon_begin_backup") + @Job( + name="addon_begin_backup", + limit=JobExecutionLimit.GROUP_ONCE, + on_condition=AddonsJobError, + ) async def begin_backup(self) -> bool: """Execute pre commands or stop addon if necessary. @@ -807,11 +1014,15 @@ async def begin_backup(self) -> bool: return True - @Job(name="addon_end_backup") - async def end_backup(self) -> Awaitable[None] | None: + @Job( + name="addon_end_backup", + limit=JobExecutionLimit.GROUP_ONCE, + on_condition=AddonsJobError, + ) + async def end_backup(self) -> asyncio.Task | None: """Execute post commands or restart addon if necessary. - Returns a coroutine that completes when addon has state 'started' (see start) + Returns a Task that completes when addon has state 'started' (see start) for cold backup. Else nothing is returned. """ if self.backup_mode is AddonBackupMode.COLD: @@ -822,11 +1033,15 @@ async def end_backup(self) -> Awaitable[None] | None: await self._backup_command(self.backup_post) return None - @Job(name="addon_backup") - async def backup(self, tar_file: tarfile.TarFile) -> Awaitable[None] | None: + @Job( + name="addon_backup", + limit=JobExecutionLimit.GROUP_ONCE, + on_condition=AddonsJobError, + ) + async def backup(self, tar_file: tarfile.TarFile) -> asyncio.Task | None: """Backup state of an add-on. - Returns a coroutine that completes when addon has state 'started' (see start) + Returns a Task that completes when addon has state 'started' (see start) for cold backup. Else nothing is returned. """ wait_for_start: Awaitable[None] | None = None @@ -905,10 +1120,15 @@ def _write_tarfile(): _LOGGER.info("Finish backup for addon %s", self.slug) return wait_for_start - async def restore(self, tar_file: tarfile.TarFile) -> Awaitable[None] | None: + @Job( + name="addon_restore", + limit=JobExecutionLimit.GROUP_ONCE, + on_condition=AddonsJobError, + ) + async def restore(self, tar_file: tarfile.TarFile) -> asyncio.Task | None: """Restore state of an add-on. - Returns a coroutine that completes when addon has state 'started' (see start) + Returns a Task that completes when addon has state 'started' (see start) if addon is started after restore. Else nothing is returned. """ wait_for_start: Awaitable[None] | None = None diff --git a/supervisor/addons/model.py b/supervisor/addons/model.py index a0f347c8fa5..a7302519173 100644 --- a/supervisor/addons/model.py +++ b/supervisor/addons/model.py @@ -1,7 +1,7 @@ """Init file for Supervisor add-ons.""" from abc import ABC, abstractmethod from collections import defaultdict -from collections.abc import Awaitable, Callable +from collections.abc import Callable from contextlib import suppress import logging from pathlib import Path @@ -669,19 +669,3 @@ def _image(self, config) -> str: # local build return f"{config[ATTR_REPOSITORY]}/{self.sys_arch.default}-addon-{config[ATTR_SLUG]}" - - def install(self) -> Awaitable[None]: - """Install this add-on.""" - return self.sys_addons.install(self.slug) - - def uninstall(self) -> Awaitable[None]: - """Uninstall this add-on.""" - return self.sys_addons.uninstall(self.slug) - - def update(self, backup: bool | None = False) -> Awaitable[Awaitable[None] | None]: - """Update this add-on.""" - return self.sys_addons.update(self.slug, backup=backup) - - def rebuild(self) -> Awaitable[Awaitable[None] | None]: - """Rebuild this add-on.""" - return self.sys_addons.rebuild(self.slug) diff --git a/supervisor/api/addons.py b/supervisor/api/addons.py index 942f188d7a1..1022d9c0cc6 100644 --- a/supervisor/api/addons.py +++ b/supervisor/api/addons.py @@ -388,7 +388,7 @@ async def stats(self, request: web.Request) -> dict[str, Any]: def uninstall(self, request: web.Request) -> Awaitable[None]: """Uninstall add-on.""" addon = self._extract_addon(request) - return asyncio.shield(addon.uninstall()) + return asyncio.shield(self.sys_addons.uninstall(addon.slug)) @api_process async def start(self, request: web.Request) -> None: @@ -414,7 +414,7 @@ async def restart(self, request: web.Request) -> None: async def rebuild(self, request: web.Request) -> None: """Rebuild local build add-on.""" addon = self._extract_addon(request) - if start_task := await asyncio.shield(addon.rebuild()): + if start_task := await asyncio.shield(self.sys_addons.rebuild(addon.slug)): await start_task @api_process_raw(CONTENT_TYPE_BINARY) diff --git a/supervisor/api/store.py b/supervisor/api/store.py index 1bc7ebb3e68..5fb6fae5d77 100644 --- a/supervisor/api/store.py +++ b/supervisor/api/store.py @@ -199,7 +199,7 @@ async def addons_list(self, request: web.Request) -> dict[str, Any]: def addons_addon_install(self, request: web.Request) -> Awaitable[None]: """Install add-on.""" addon = self._extract_addon(request) - return asyncio.shield(addon.install()) + return asyncio.shield(self.sys_addons.install(addon.slug)) @api_process async def addons_addon_update(self, request: web.Request) -> None: @@ -211,7 +211,7 @@ async def addons_addon_update(self, request: web.Request) -> None: body = await api_validate(SCHEMA_UPDATE, request) if start_task := await asyncio.shield( - addon.update(backup=body.get(ATTR_BACKUP)) + self.sys_addons.update(addon.slug, backup=body.get(ATTR_BACKUP)) ): await start_task diff --git a/supervisor/backups/backup.py b/supervisor/backups/backup.py index 3c2e2ce7f3b..1182a048067 100644 --- a/supervisor/backups/backup.py +++ b/supervisor/backups/backup.py @@ -349,14 +349,14 @@ def _create_backup(): finally: self._tmp.cleanup() - async def store_addons(self, addon_list: list[str]) -> list[Awaitable[None]]: + async def store_addons(self, addon_list: list[str]) -> list[asyncio.Task]: """Add a list of add-ons into backup. - For each addon that needs to be started after backup, returns a task which + For each addon that needs to be started after backup, returns a Task which completes when that addon has state 'started' (see addon.start). """ - async def _addon_save(addon: Addon) -> Awaitable[None] | None: + async def _addon_save(addon: Addon) -> asyncio.Task | None: """Task to store an add-on into backup.""" tar_name = f"{addon.slug}.tar{'.gz' if self.compressed else ''}" addon_file = SecureTarFile( @@ -388,7 +388,7 @@ async def _addon_save(addon: Addon) -> Awaitable[None] | None: # Save Add-ons sequential # avoid issue on slow IO - start_tasks: list[Awaitable[None]] = [] + start_tasks: list[asyncio.Task] = [] for addon in addon_list: try: if start_task := await _addon_save(addon): @@ -398,10 +398,10 @@ async def _addon_save(addon: Addon) -> Awaitable[None] | None: return start_tasks - async def restore_addons(self, addon_list: list[str]) -> list[Awaitable[None]]: + async def restore_addons(self, addon_list: list[str]) -> list[asyncio.Task]: """Restore a list add-on from backup.""" - async def _addon_restore(addon_slug: str) -> Awaitable[None] | None: + async def _addon_restore(addon_slug: str) -> asyncio.Task | None: """Task to restore an add-on into backup.""" tar_name = f"{addon_slug}.tar{'.gz' if self.compressed else ''}" addon_file = SecureTarFile( @@ -425,7 +425,7 @@ async def _addon_restore(addon_slug: str) -> Awaitable[None] | None: # Save Add-ons sequential # avoid issue on slow IO - start_tasks: list[Awaitable[None]] = [] + start_tasks: list[asyncio.Task] = [] for slug in addon_list: try: if start_task := await _addon_restore(slug): diff --git a/supervisor/backups/manager.py b/supervisor/backups/manager.py index ba1cf583d47..764fc984c56 100644 --- a/supervisor/backups/manager.py +++ b/supervisor/backups/manager.py @@ -406,7 +406,7 @@ async def _do_restore( # Remove Add-on because it's not a part of the new env # Do it sequential avoid issue on slow IO try: - await addon.uninstall() + await self.sys_addons.uninstall(addon.slug) except AddonsError: _LOGGER.warning("Can't uninstall Add-on %s", addon.slug) @@ -614,7 +614,7 @@ async def _thaw_all( await self.sys_homeassistant.end_backup() self._change_stage(BackupJobStage.ADDONS) - addon_start_tasks: list[Awaitable[None]] = [ + addon_start_tasks: list[asyncio.Task] = [ task for task in await asyncio.gather( *[addon.end_backup() for addon in running_addons] diff --git a/supervisor/misc/tasks.py b/supervisor/misc/tasks.py index d7a665e1d5e..4333821c5ed 100644 --- a/supervisor/misc/tasks.py +++ b/supervisor/misc/tasks.py @@ -103,7 +103,7 @@ async def _update_addons(self): # avoid issue on slow IO _LOGGER.info("Add-on auto update process %s", addon.slug) try: - if start_task := await addon.update(backup=True): + if start_task := await self.sys_addons.update(addon.slug, backup=True): start_tasks.append(start_task) except AddonsError: _LOGGER.error("Can't auto update Add-on %s", addon.slug) diff --git a/tests/addons/test_addon.py b/tests/addons/test_addon.py index 647d8a91232..0efd089d7e0 100644 --- a/tests/addons/test_addon.py +++ b/tests/addons/test_addon.py @@ -267,7 +267,7 @@ async def test_install_update_fails_if_out_of_date( with pytest.raises(AddonsJobError): await coresys.addons.install(TEST_ADDON_SLUG) with pytest.raises(AddonsJobError): - await install_addon_ssh.update() + await coresys.addons.update(TEST_ADDON_SLUG) with patch.object( type(coresys.plugins.audio), "need_update", new=PropertyMock(return_value=True) @@ -277,7 +277,7 @@ async def test_install_update_fails_if_out_of_date( with pytest.raises(AddonsJobError): await coresys.addons.install(TEST_ADDON_SLUG) with pytest.raises(AddonsJobError): - await install_addon_ssh.update() + await coresys.addons.update(TEST_ADDON_SLUG) async def test_listeners_removed_on_uninstall( @@ -342,7 +342,7 @@ async def test_start_wait_healthcheck( await asyncio.sleep(0) assert install_addon_ssh.state == AddonState.STOPPED - start_task = asyncio.create_task(await install_addon_ssh.start()) + start_task = await install_addon_ssh.start() assert start_task _fire_test_event(coresys, f"addon_{TEST_ADDON_SLUG}", ContainerState.RUNNING) diff --git a/tests/addons/test_manager.py b/tests/addons/test_manager.py index ea55e66f7d3..5aa657af778 100644 --- a/tests/addons/test_manager.py +++ b/tests/addons/test_manager.py @@ -65,7 +65,7 @@ async def test_image_added_removed_on_update( with patch.object(DockerInterface, "install") as install, patch.object( DockerAddon, "_build" ) as build: - await install_addon_ssh.update() + await coresys.addons.update(TEST_ADDON_SLUG) build.assert_not_called() install.assert_called_once_with( AwesomeVersion("10.0.0"), "test/amd64-my-ssh-addon", False, None @@ -85,7 +85,7 @@ async def test_image_added_removed_on_update( with patch.object(DockerInterface, "install") as install, patch.object( DockerAddon, "_build" ) as build: - await install_addon_ssh.update() + await coresys.addons.update(TEST_ADDON_SLUG) build.assert_called_once_with(AwesomeVersion("11.0.0")) install.assert_not_called() @@ -299,7 +299,7 @@ async def test_start_wait_cancel_on_uninstall( await asyncio.sleep(0) assert install_addon_ssh.state == AddonState.STOPPED - start_task = asyncio.create_task(await install_addon_ssh.start()) + start_task = await install_addon_ssh.start() assert start_task coresys.bus.fire_event(