Add manual_only option to addon boot config (#5272)

* Add manual_forced option to addon boot config

* Include client library in pull request template

* Add boot_config to api output so frontend can use it

* `manual_forced` to `manual_only`
This commit is contained in:
Mike Degatano 2024-08-27 11:59:52 -04:00 committed by GitHub
parent 91a8fae9b5
commit 0177cd9528
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 78 additions and 6 deletions

View File

@ -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)
<!--
Thank you for contributing <3
@ -67,3 +70,5 @@ If API endpoints of add-on configuration are added/changed:
[dev-checklist]: https://developers.home-assistant.io/docs/en/development_checklist.html
[docs-repository]: https://github.com/home-assistant/developers.home-assistant
[cli-repository]: https://github.com/home-assistant/cli
[client-library-repository]: https://github.com/home-assistant-libs/python-supervisor-client/

View File

@ -57,6 +57,7 @@ from ..const import (
ATTR_WATCHDOG,
DNS_SUFFIX,
AddonBoot,
AddonBootConfig,
AddonStartup,
AddonState,
BusEvent,
@ -311,7 +312,9 @@ class Addon(AddonModel):
@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_ONLY:
return super().boot
return self.persist.get(ATTR_BOOT, super().boot)
@boot.setter

View File

@ -83,6 +83,7 @@ from ..const import (
SECURITY_DISABLE,
SECURITY_PROFILE,
AddonBoot,
AddonBootConfig,
AddonStage,
AddonStartup,
)
@ -150,10 +151,15 @@ class AddonModel(JobGroup, ABC):
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."""

View File

@ -98,6 +98,7 @@ from ..const import (
ROLE_ALL,
ROLE_DEFAULT,
AddonBoot,
AddonBootConfig,
AddonStage,
AddonStartup,
AddonState,
@ -321,7 +322,9 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
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),

View File

@ -98,6 +98,7 @@ from ..const import (
ATTR_WEBUI,
REQUEST_FROM,
AddonBoot,
AddonBootConfig,
)
from ..coresys import CoreSysAttributes
from ..docker.stats import DockerStats
@ -109,7 +110,7 @@ from ..exceptions import (
PwnedSecret,
)
from ..validate import docker_ports
from .const import ATTR_REMOVE_CONFIG, ATTR_SIGNED
from .const import ATTR_BOOT_CONFIG, ATTR_REMOVE_CONFIG, ATTR_SIGNED
from .utils import api_process, api_validate, json_loads
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -217,6 +218,7 @@ class APIAddons(CoreSysAttributes):
ATTR_VERSION_LATEST: addon.latest_version,
ATTR_PROTECTED: addon.protected,
ATTR_RATING: rating_security(addon),
ATTR_BOOT_CONFIG: addon.boot_config,
ATTR_BOOT: addon.boot,
ATTR_OPTIONS: addon.options,
ATTR_SCHEMA: addon.schema_ui,
@ -300,6 +302,10 @@ class APIAddons(CoreSysAttributes):
if ATTR_OPTIONS in body:
addon.options = body[ATTR_OPTIONS]
if ATTR_BOOT in body:
if addon.boot_config == AddonBootConfig.MANUAL_ONLY:
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]

View File

@ -17,6 +17,7 @@ ATTR_APPARMOR_VERSION = "apparmor_version"
ATTR_ATTRIBUTES = "attributes"
ATTR_AVAILABLE_UPDATES = "available_updates"
ATTR_BACKGROUND = "background"
ATTR_BOOT_CONFIG = "boot_config"
ATTR_BOOT_SLOT = "boot_slot"
ATTR_BOOT_SLOTS = "boot_slots"
ATTR_BOOT_TIMESTAMP = "boot_timestamp"

View File

@ -382,12 +382,27 @@ ROLE_ADMIN = "admin"
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_ONLY = "manual_only"
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_ONLY:
return AddonBoot.MANUAL
return None
class AddonStartup(StrEnum):
"""Startup types of Add-on."""

View File

@ -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_only_boot(coresys: CoreSys, install_addon_example: Addon):
"""Test an addon with manual only boot mode."""
assert install_addon_example.boot_config == "manual_only"
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"

View File

@ -346,3 +346,23 @@ 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_manual_only_invalid(
api_client: TestClient, install_addon_example: Addon
):
"""Test changing boot mode is invalid if set to manual only."""
install_addon_example.data["ingress"] = False
resp = await api_client.get("/addons/local_example/info")
assert resp.status == 200
body = await resp.json()
assert body["data"]["boot"] == "manual"
assert body["data"]["boot_config"] == "manual_only"
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_only so it cannot be changed"
)

View File

@ -23,3 +23,4 @@ ingress_port: 0
breaking_versions:
- test
- 1.0
boot: manual_only