From a39a07cb50f010a6298da164b7411b4bce2df1b7 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Mon, 26 Aug 2024 19:50:50 +0000 Subject: [PATCH 1/4] Add manual_forced option to addon boot config --- supervisor/addons/addon.py | 5 ++++- supervisor/addons/model.py | 10 ++++++++-- supervisor/addons/validate.py | 5 ++++- supervisor/api/addons.py | 5 +++++ supervisor/const.py | 15 +++++++++++++++ tests/addons/test_addon.py | 12 ++++++++++++ tests/api/test_addons.py | 13 +++++++++++++ tests/fixtures/addons/local/example/config.yaml | 1 + 8 files changed, 62 insertions(+), 4 deletions(-) diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index 8ac72e56538..54901a9977d 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -57,6 +57,7 @@ ATTR_WATCHDOG, DNS_SUFFIX, AddonBoot, + AddonBootConfig, AddonStartup, AddonState, BusEvent, @@ -311,7 +312,9 @@ def options(self, value: dict[str, Any] | None) -> None: @property def boot(self) -> AddonBoot: - """Return boot config with prio local settings.""" + """Return boot config with prio local settings unless config is forced.""" + if self.boot_config == AddonBootConfig.MANUAL_FORCED: + return super().boot return self.persist.get(ATTR_BOOT, super().boot) @boot.setter diff --git a/supervisor/addons/model.py b/supervisor/addons/model.py index 2d400c00c29..8a53224ca19 100644 --- a/supervisor/addons/model.py +++ b/supervisor/addons/model.py @@ -83,6 +83,7 @@ SECURITY_DISABLE, SECURITY_PROFILE, AddonBoot, + AddonBootConfig, AddonStage, AddonStartup, ) @@ -150,10 +151,15 @@ def options(self) -> dict[str, Any]: return self.data[ATTR_OPTIONS] @property - def boot(self) -> AddonBoot: - """Return boot config with prio local settings.""" + def boot_config(self) -> AddonBootConfig: + """Return boot config.""" return self.data[ATTR_BOOT] + @property + def boot(self) -> AddonBoot: + """Return boot config with prio local settings unless config is forced.""" + return AddonBoot(self.data[ATTR_BOOT]) + @property def auto_update(self) -> bool | None: """Return if auto update is enable.""" diff --git a/supervisor/addons/validate.py b/supervisor/addons/validate.py index c096860e8bf..a177ac9d215 100644 --- a/supervisor/addons/validate.py +++ b/supervisor/addons/validate.py @@ -98,6 +98,7 @@ ROLE_ALL, ROLE_DEFAULT, AddonBoot, + AddonBootConfig, AddonStage, AddonStartup, AddonState, @@ -321,7 +322,9 @@ def _migrate(config: dict[str, Any]): vol.Optional(ATTR_STARTUP, default=AddonStartup.APPLICATION): vol.Coerce( AddonStartup ), - vol.Optional(ATTR_BOOT, default=AddonBoot.AUTO): vol.Coerce(AddonBoot), + vol.Optional(ATTR_BOOT, default=AddonBootConfig.AUTO): vol.Coerce( + AddonBootConfig + ), vol.Optional(ATTR_INIT, default=True): vol.Boolean(), vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(), vol.Optional(ATTR_STAGE, default=AddonStage.STABLE): vol.Coerce(AddonStage), diff --git a/supervisor/api/addons.py b/supervisor/api/addons.py index 83b6ad42c2e..dc98282dd00 100644 --- a/supervisor/api/addons.py +++ b/supervisor/api/addons.py @@ -98,6 +98,7 @@ ATTR_WEBUI, REQUEST_FROM, AddonBoot, + AddonBootConfig, ) from ..coresys import CoreSysAttributes from ..docker.stats import DockerStats @@ -300,6 +301,10 @@ async def options(self, request: web.Request) -> None: if ATTR_OPTIONS in body: addon.options = body[ATTR_OPTIONS] if ATTR_BOOT in body: + if addon.boot_config == AddonBootConfig.MANUAL_FORCED: + raise APIError( + f"Addon {addon.slug} boot option is set to {addon.boot_config} so it cannot be changed" + ) addon.boot = body[ATTR_BOOT] if ATTR_AUTO_UPDATE in body: addon.auto_update = body[ATTR_AUTO_UPDATE] diff --git a/supervisor/const.py b/supervisor/const.py index b1181af2af8..f8b55b22390 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -382,12 +382,27 @@ ROLE_ALL = [ROLE_DEFAULT, ROLE_HOMEASSISTANT, ROLE_BACKUP, ROLE_MANAGER, ROLE_ADMIN] +class AddonBootConfig(StrEnum): + """Boot mode config for the add-on.""" + + AUTO = "auto" + MANUAL = "manual" + MANUAL_FORCED = "manual_forced" + + class AddonBoot(StrEnum): """Boot mode for the add-on.""" AUTO = "auto" MANUAL = "manual" + @classmethod + def _missing_(cls, value: str) -> Self | None: + """Convert 'forced' config values to their counterpart.""" + if value == AddonBootConfig.MANUAL_FORCED: + return AddonBoot.MANUAL + return None + class AddonStartup(StrEnum): """Startup types of Add-on.""" diff --git a/tests/addons/test_addon.py b/tests/addons/test_addon.py index f67d6267d09..d04764da0b3 100644 --- a/tests/addons/test_addon.py +++ b/tests/addons/test_addon.py @@ -691,6 +691,7 @@ async def test_local_example_install( mock_aarch64_arch_supported: None, ): """Test install of an addon.""" + coresys.hardware.disk.get_disk_free_space = lambda x: 5000 assert not ( data_dir := tmp_supervisor_data / "addons" / "data" / "local_example" ).exists() @@ -883,3 +884,14 @@ async def test_addon_load_succeeds_with_docker_errors( caplog.clear() await install_addon_ssh.load() assert "Unknown error with test/amd64-addon-ssh:9.2.1" in caplog.text + + +async def test_addon_manual_forced_boot(coresys: CoreSys, install_addon_example: Addon): + """Test an addon with manual forced boot mode.""" + assert install_addon_example.boot_config == "manual_forced" + assert install_addon_example.boot == "manual" + + # Users cannot change boot mode of an addon with manual forced so changing boot isn't realistic + # However boot mode can change on update and user may have set auto before, ensure it is ignored + install_addon_example.boot = "auto" + assert install_addon_example.boot == "manual" diff --git a/tests/api/test_addons.py b/tests/api/test_addons.py index ad8e5d00d2e..645fa95069c 100644 --- a/tests/api/test_addons.py +++ b/tests/api/test_addons.py @@ -346,3 +346,16 @@ async def test_api_addon_system_managed( body = await resp.json() assert body["data"]["system_managed"] is False assert body["data"]["system_managed_config_entry"] is None + + +async def test_addon_options_boot_mode_forced_invalid( + api_client: TestClient, install_addon_example: Addon +): + """Test changing boot mode is invalid if set to manual forced.""" + resp = await api_client.post("/addons/local_example/options", json={"boot": "auto"}) + assert resp.status == 400 + body = await resp.json() + assert ( + body["message"] + == "Addon local_example boot option is set to manual_forced so it cannot be changed" + ) diff --git a/tests/fixtures/addons/local/example/config.yaml b/tests/fixtures/addons/local/example/config.yaml index 8a269b562ef..3ca0c1561cd 100644 --- a/tests/fixtures/addons/local/example/config.yaml +++ b/tests/fixtures/addons/local/example/config.yaml @@ -23,3 +23,4 @@ ingress_port: 0 breaking_versions: - test - 1.0 +boot: manual_forced From 8d547df45ebcfb538cc9bf8ceed629d60d3f4d66 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Mon, 26 Aug 2024 19:58:46 +0000 Subject: [PATCH 2/4] Include client library in pull request template --- .github/PULL_REQUEST_TEMPLATE.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0a3480a4c84..27f17d8ddaa 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -38,6 +38,7 @@ - This PR is related to issue: - Link to documentation pull request: - Link to cli pull request: +- Link to client library pull request: ## Checklist @@ -55,9 +56,11 @@ - [ ] The code has been formatted using Ruff (`ruff format supervisor tests`) - [ ] Tests have been added to verify that the new code works. -If API endpoints of add-on configuration are added/changed: +If API endpoints or add-on configuration are added/changed: - [ ] Documentation added/updated for [developers.home-assistant.io][docs-repository] +- [ ] [CLI][cli-repository] updated (if necessary) +- [ ] [Client library][client-library-repository] updated (if necessary)