mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-08-13 00:12:57 +01:00
5ea8a48e1b3cd556f19dfd3a87ac24d61676b7a2
295
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c01efa05a5 |
Rename resolution addon issue types and check slugs to app-based naming (#7098)
* Rename resolution addon issue types and check slugs to app-based naming Renames the following for consistency with the new apps terminology: - Issue types: deprecated_addon -> deprecated_app, deprecated_arch_addon -> deprecated_arch_app, detached_addon_missing -> detached_app_missing, detached_addon_removed -> detached_app_removed - Check slugs: addon_pwned -> app_pwned, deprecated_addon -> deprecated_app, deprecated_arch_addon -> deprecated_arch_app, detached_addon_missing -> detached_app_missing, detached_addon_removed -> detached_app_removed Full backward compatibility is maintained: - REST API V1 (root) returns legacy names and accepts legacy slugs - REST API V2 (/v2) returns and accepts new names only - WebSocket events use legacy names unless SUPERVISOR_WEBSOCKET_V2_API feature flag is enabled - resolution.json files with legacy check slugs are automatically migrated to new names on load Fixes #7029 * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Address review nits for resolution compatibility maps * Update supervisor/resolution/const.py --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Stefan Agner <stefan@agner.ch> |
||
|
|
c5ea8e174e |
Gracefully handle lost systemd-journal-gatewayd connections (#7106)
* Gracefully end log stream when journal gateway connection is lost When systemd-journal-gatewayd is stopped while a client follows logs (e.g. on host reboot with the log viewer open), aiohttp raises ClientPayloadError and advanced_logs_handler converted it to an APIError. For the /supervisor/logs endpoints this got logged as an unexpected error with a full traceback and captured to Sentry on every occurrence (#7103, SUPERVISOR-1FHT). Once the streaming response has started, an error response can no longer be delivered anyway, so treat a lost connection to systemd-journal-gatewayd like a client-side disconnect and end the stream gracefully. The APIError is still raised when the connection is lost before any data was sent to the client. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Treat unreachable systemd-journal-gatewayd as a known API error Manual testing of the previous commit (killing systemd-journal-gatewayd while Supervisor is running) showed that every log API request hitting the dead gateway logs an "Unexpected error during API call" traceback and captures HostServiceError to Sentry (SUPERVISOR-K8C), in addition to the ERROR already logged at the raise site in journald_logs(). Make HostServiceError inherit from APIError as well, following the HostContainerLogEpochError precedent, so the api_process decorators return a plain 400 response without the redundant traceback and Sentry capture. Also treat it like HostNotSupportedError in the supervisor logs fallback wrapper: fall back to Docker container logs with a warning instead of an exception log plus Sentry capture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Use dedicated exception for journal gateway connection errors HostServiceError is also raised by ServiceManager for systemd units called through the API (e.g. /os/config/sync), where inheriting from APIError would hide genuine service breakage from Sentry. Introduce HostJournalGatewaydConnectionError subclassing HostServiceError and APIError, and raise it for the gatewayd connection failure only, as suggested in the PR review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3dea234189 |
Add v1/v2 terminology shim for host disk usage response (#7100)
Implement API versioned response terminology for GET /host/disks/default/usage: - V2 returns app terminology as-is (apps_data/apps_config) - V1 applies compatibility shim and returns legacy addon terminology (addons_data/addons_config) Also adds tests to verify: - v1 keeps legacy IDs - v2 keeps app IDs - disk usage endpoint behavior remains stable across max_depth scenarios Fixes #7030 |
||
|
|
f8ba166ed2 |
Enter STOPPING state before Supervisor restart API response (#7085)
* Enter STOPPING state before Supervisor restart API response The Supervisor accepts new API requests in the window after responding to /supervisor/restart: restart() schedules Core.stop() as a task and returns immediately, so the state only changes to stopping after the restart response has been sent. A request accepted in that window keeps running while the Supervisor shuts down, and when stop() tears down the API server after the 10 second stage 1 timeout the connection is dropped without a response - the client sees EOF instead of an error. This is what made the CI restore step flaky (see #7084 for the CI-side fix): the restore request landed on the old, dying instance. Make restart() transition to STOPPING before returning, via a new Core.begin_stop() which contains the transition part previously at the start of Core.stop(). The system validation middleware already rejects requests outside of STARTUP/RUNNING/FREEZE, so any request arriving after the restart response now gets a clear "System is not ready" error instead of possibly being accepted and killed. Since the state can now already be STOPPING when stop() runs, its re-entry guard is changed from a state check to an explicit flag. Supervisor.update() schedules the same stop task but is left unchanged: entering STOPPING before update() returns would suppress the final job progress event to Home Assistant (WebSocket messages are dropped in CLOSING_STATES), regressing update progress reporting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Allow stop() retry when begin_stop() fails Address review feedback: stop() set _stop_initiated before awaiting begin_stop(), so an exception or cancellation during the state transition (e.g. a failing config write in _update_last_boot) would leave the flag set and turn every later stop() call into a no-op, with no way to retry the teardown. Reset the flag and re-raise when begin_stop() fails. Nothing has been torn down at that point, so a retry is safe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Use a stopping_complete event instead of begin_stop() Address review feedback questioning the extra infrastructure: instead of splitting the state transition out of stop() (begin_stop()) and guarding re-entry with an explicit flag, have stop() take an optional stopping_complete event which is set once the STOPPING state is entered, following the same pattern as backup/restore's validation_complete. Core.stop() keeps its original structure and re-entry semantics, and restart() waits for the event before returning. Should the stop task fail ahead of the state transition (only possible through a failing config write in _update_last_boot()), the event is never set and the restart request runs into the client timeout - an accepted trade-off to keep restart() simple. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6bbb82aec0 |
Return a proper API error when adding a duplicate store repository (#7087)
Adding a repository that is already in the store raised a plain StoreError. Since that is not an APIError, the api_process decorator logs it as an unexpected error and reports it to Sentry, where it is one of the most frequent issues (SUPERVISOR-1JYE, ~8000 affected installations in 90 days). The events are ordinary client actions: add-on setup flows and documentation links re-submitting a repository the user already has, plus third-party automation re-adding its repositories on every add-on start. Introduce StoreRepositoryAlreadyAddedError, which is both a StoreError and an APIConflict with an error key and message template, so the request fails with a structured 409 Conflict response and no Sentry report. Fixes SUPERVISOR-1JYE Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f4ec256b94 |
Report real OS version to Core versions consuming version_pending (#7086)
* Report real OS version to Core versions consuming version_pending The /os/info endpoint reports an installed update pending activation as the current version so that Core update entities unaware of version_pending don't offer the update again. Since home-assistant/core#177155 Core uses version_pending to determine the OS update state, so newer Core versions should get the real current version again. Limit the compat shim to Core versions predating that support. The Core PR merged 2026-07-24 04:21 UTC, after that day's 02:00 UTC nightly build, so the first nightly containing it is 2026-07-25's (2026.8.0.dev202607250xxx). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Use exact first nightly version for the version_pending gate The 2026-07-25 nightly (2026.8.0.dev202607250310) is the first build containing home-assistant/core#177155: the 2026-07-24 nightly was built from a commit predating the merge (verified via commit ancestry of the builder workflow runs). Replace the midnight floor with the actual published nightly version, matching the CORE_UNIX_SOCKET_MIN_VERSION precedent of using the exact nightly stamp. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4152855713 |
Rename backup and restore job stages from addon(s) to app(s) (#7059)
* Mark backup/restore app stage names as breaking Rename backup and restore job stages from addon/addons to app/apps, including delta and await restart stages, and update manager and tests accordingly. * Add legacy backup stage mapping and fix enum typo Map backup/restore job stage names to legacy websocket/REST v1 values while preserving v2 values, add API tests for both modes, and rename COPY_ADDITONAL_LOCATIONS to COPY_ADDITIONAL_LOCATIONS. |
||
|
|
96798b5041 | Migrate addon API error metadata to app naming (#7058) | ||
|
|
c119e0cff1 |
Rename Docker app container and builder names from addon_ to app_ (#7024)
* Rename Docker app container and builder names from addon_ to app_ * Update container state event tests to app_ names * Migrate legacy app container names on attach * Fix error handling * Fixes from feedback * Fix pytest --------- Co-authored-by: Stefan Agner <stefan@agner.ch> |
||
|
|
a101e2eb5b | Add websocket v2 jobs legacy-name toggle (#7051) | ||
|
|
b977824507 |
Fix Core API proxy stripping the multipart boundary from Content-Type (#7050)
The proxy forwarded aiohttp's parsed request.content_type property, which drops header parameters — including the multipart boundary. Core then cannot parse any multipart body proxied from an add-on and raises "boundary missed for Content-Type". Forward the raw Content-Type header instead, matching what the stream() proxy path already does. Fixes #7049 |
||
|
|
dfe727410c |
Fix length validation of str and password options with bounds (#7044)
* Fix length validation of str and password options with bounds App option schemas like str(1,32) validated the string value with vol.Range, which compares the value itself against the numeric bounds. Comparing a str with a float raises TypeError, which voluptuous reports as "invalid value or type (must have a partial ordering)". As a result, saving options always failed for any add-on using a bounded str or password schema; unbounded str/password was unaffected since vol.Range without bounds performs no comparison. Use vol.Length to check the string length against the bounds, which is the documented meaning of str(min,max). Also replace the str(value) literal (a self-equality check) with the str type so non-string values still fail validation, now with a clearer error message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Update API test for changed non-string option error message Validating a str option against the str type instead of the str(value) literal changed the voluptuous error for non-string values from "not a valid value" to "expected str". Update the expected message in the API options error test accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4a54a229b9 |
Migrate addon job names to app and scope legacy API compatibility (#7014)
* Migrate addon job names to app and trim legacy API aliases * Limit job name compatibility to app_manager_update only |
||
|
|
83d15aadcc |
Track installed OS update pending reboot activation (#7006)
* Track installed OS update pending reboot activation Since #6982 the OS update no longer reboots the host automatically, so the system keeps running the old version until the user reboots. In that window the update kept being offered: need_update compares the running OS version, which only changes on reboot. Requesting the same update again re-downloaded and reinstalled the full image. Worse, a Supervisor restart in that window dropped the in-memory REBOOT_REQUIRED issue and canceled the pending update altogether, because mark_healthy marks the booted slot active on startup, reverting the primary boot slot set by the rauc install. Track the installed version awaiting a reboot as version_pending and expose it in /os/info. A successful install sets it, updating again to that version is rejected with a hint to reboot, and need_update no longer reports true for an update that is already installed. On load, the pending state is recovered from rauc by comparing the primary boot slot (via a new GetPrimary D-Bus wrapper) with the booted slot, re-creating the REBOOT_REQUIRED issue as well. mark_healthy now skips marking the booted slot active while an update is pending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Split pending update detection condition Fix pylint R0916 (too-many-boolean-expressions) by splitting the guard in _detect_pending_update into a slot data validity check and the actual pending update check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Report pending OS update as current version in /os/info The Core update entity derives update availability by comparing version with version_latest from /os/info, so it keeps offering an update that is already installed until the system is rebooted. Report an installed update pending activation as the current version so existing Core releases reflect update availability correctly. Once Core consumes version_pending, this can be limited to Core versions predating that support. The hassos field of the root /info endpoint keeps reporting the running version, Core only uses it as a HAOS presence check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
35542d5bcc |
Replace addon with app in docker container names for apps (#6997)
* Support dual app/addon log identifiers during migration * Use single journald epoch query for dual app identifiers * Add API-keyed container epoch error handling * Template CONTAINER_LOG_EPOCH via extra fields |
||
|
|
a483d4a503 |
Make /os/update stop auto-rebooting after OS update (#6982)
* Make OS update require explicit reboot * Avoid protected OS field access in API update tests |
||
|
|
1857753e22 |
Redact app options in info for unprivileged apps (#6953)
The `/addons/{slug}/info` endpoint returned the target app's user options,
which can contain secrets such as passwords and API keys. The security
middleware grants every role (including the default role) access to any
`/.+/info` path, so an installed app with `hassio_api: true` and the default
role could read another app's options simply by requesting its info.
Redact the options field in info_data() unless the caller is entitled to see
it: Home Assistant Core (and other non-app internals), the app reading its
own info, or an app with the manager or admin role. Other apps reading a
different app's info now receive an empty options dict while all non-secret
metadata stays available for discovery. This mirrors the existing self-only
restriction on the dedicated /options/config endpoint.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
81e235376e |
Fix typos repo-wide and add codespell pre-commit hook (#6949)
* Fix typos in comments, docstrings and log messages Correct 39 spelling mistakes across comments, docstrings and log/error message strings throughout the package (e.g. "conection" -> "connection", "Incomming" -> "Incoming", "Rasie" -> "Raise"). All changes are confined to human-readable text; no identifiers, attributes or D-Bus contracts are touched, so there is no behavior change. Found with codespell. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix typos in tests and CI workflow Correct spelling mistakes in test comments, docstrings and data, plus one in the builder workflow, so the whole tree is clean for the codespell hook added next. The assertion in test_network_manager.py is updated to match the corrected "Unknown error while processing" log message in the source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add codespell pre-commit hook Wire up codespell so spelling mistakes in comments, docstrings and strings are caught automatically. The vendored frontend panel is excluded, and "hass" and "astroid" are added to the ignore list as known false positives (the Home Assistant abbreviation and the pylint dependency package). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Address review feedback Improve grammar in several of the touched comments and docstrings: use the plural "ignore conditions" for the list-returning property, add the missing auxiliary verb and fix agreement in the timezone-filter comment, fix "backups ... use" agreement, and reword "underlay" to "underlying" in the arch module docstring. Also drop the "*.json" skip from the codespell hook. It was carried over from another project but is unnecessary here (all tracked JSON is clean), and skipping it would needlessly leave translation and data JSON unchecked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Reword onboarding comment "overflight" was a literal calque of the German "überflogen"; use the idiomatic "skimmed through" instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dae48c62e4 |
Treat mount errors as API errors instead of unexpected failures (#6946)
Mount failures generally reflect user configuration or host conditions (unreachable server, wrong credentials, ...) rather than a Supervisor bug. As a plain HassioError, MountError reached the generic error branch of api_process, which logged a full traceback as an "Unexpected error during API call" and captured the exception to Sentry. The mount reload path already encoded the opposite intent by explicitly skipping Sentry for MountError. Make MountError an APIError so mount failures are surfaced as client-side errors with their explicit message, without a traceback or Sentry noise, matching the existing JobException handling. MountNotFound additionally inherits from APINotFound so it returns a 404 instead of a 400. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ecf890a41c |
Fix blacklist for v2 endpoints in security middleware, add v2 security tests (#6933)
The blacklist in the security middleware didn't take the v2 prefix into account, allowing to call routes that are supposed to be blacklisted for apps with hassio and homeassistant APIs enabled in app config. Add test checking that these routes are always blacklisted, and parametrize other tests using v2 endpoints in the test_security module. |
||
|
|
446a1aacd9 |
Return 404 for raspberrypi endpoints on boards without firmware (#6926)
* Return 404 for raspberrypi endpoints on boards without firmware Return "not found" when board doesn't have the firmware update available. Any other error may indicate it's worth retrying later (as discussed in [1]), so 404 is more appropriate here. [1] https://github.com/home-assistant/core/pull/172929#discussion_r3363706261 * Consistently return APINotFound with debug log |
||
|
|
1e5d7812cd |
Bump aiohttp from 3.14.0 to 3.14.1 (#6922)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Stefan Agner <stefan@agner.ch> Signed-off-by: dependabot[bot] <support@github.com> |
||
|
|
d910efa49e |
Surface Home Assistant update failures as translatable API errors (#6915)
Updating Core to a non-existent version (e.g. a mistyped beta tag) reported success on the CLI: the image pull failed with a 404, but update() swallowed the error via "with suppress(HomeAssistantError)" and then ran its post-update health check against the still-running old Core, which passed. Split the update routine into image install and start phases. A failed image install leaves the running Core untouched, so there is nothing to health-check or roll back; let it bubble out instead of masking it. Only failures after the image is in place (e.g. the new container starting unhealthy) fall through to the health check and rollback logic, where the error is now captured at debug level rather than silently suppressed. Model the update errors as client-facing APIError subclasses carrying an error_key so the frontend can translate them and they no longer reach Sentry as unexpected errors: - HomeAssistantUpdateError: generic update failure - HomeAssistantUpdateImageError: image download failed (includes the version) - HomeAssistantUpdateAlreadyInstalledError: requested version already installed |
||
|
|
8ec1c33aa4 |
Fix WebSocket transport None race condition in proxy (#6241)
Add a transport validity check before the WebSocket upgrade to handle clients that disconnect during the handshake. The connection can be lost between the Home Assistant API state check and the server.prepare() call, leaving request.transport as None. aiohttp's _pre_start() then raises ConnectionResetError (an AssertionError prior to aiohttp 3.14.0), which propagates out of the handler as an unhandled exception. The result is a 500 response and a Sentry report for what is really just a client disconnect. The fix detects the closed connection early and raises HTTPBadRequest with a clear reason, turning the race into a clean 4xx response with a warning log instead of error noise. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
2f331aafa9 |
Add Raspberry Pi firmware update API (#6886)
* Add Raspberry Pi firmware update API Expose `io.hass.os.Boards.RaspberryPi.Firmware` via a D-Bus proxy and `GET/POST /os/boards/raspberrypi/firmware[/update]` REST endpoints for Raspberry Pi 4 / 5 / CM4 (Yellow). Gated on OS Agent >= 1.9.0. The update job raises a `REBOOT_REQUIRED` resolution issue on success and rejects up front when the agent reports `update_blocked`. The `blocked_reason` field currently returns only `unsupported_boot_device` regardless of the underlying cause (CM4 without self-update, USB/NVMe boot, etc.), more reasons may be added later if we need to make distinction. Refs home-assistant/operating-system#4631 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix pylint issue in tests * Return blocked_reason=None instead of empty string when update is not blocked * Fix typo in update_raspberrypi_firmware docstring Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Reject API call for update early if blocked * Flip API availablility check in _check_rpi_firmware_available Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Remove extra newline in docstring --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
9f553c327c |
Only force a versioned Supervisor update in DEV mode (#6903)
Home Assistant Core now triggers a versionless Supervisor update during onboarding to ensure Supervisor is current before it continues setup. It treats a "no update available" response as the signal to proceed. In DEV mode the update endpoint bypassed the need_update check entirely and resolved a versionless request to the latest published version. So Core's onboarding call made a freshly-built DEV Supervisor install the latest published build and restart, which breaks the run_supervisor CI job (the API disappears mid-test with "connection refused"). Tie the bypass to an explicit version instead: specifying a version is still DEV-only, but a versionless request now always respects need_update. Since need_update is always False in DEV, Core's onboarding call becomes a no-op there, avoiding the update to the latest published Supervisor on the dev channel. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a973d22e35 |
Derive App state from container state (#6890)
* Derive App state from container state
The App.state setter mixed two responsibilities: it both mutated a
private `_state` field and dispatched side effects (WebSocket events,
issue dismissal, startup_event signaling). On top of that, an installed
but never-started app stayed in AppState.UNKNOWN forever, because the
attach() image-only fallback never fires a container state-change event
and the AppState therefore kept its constructor default. Conceptually,
ContainerState.UNKNOWN ("container does not exist") and AppState.UNKNOWN
("nothing observed yet") happened to share a name but meant different
things, which made the distinction easy to lose.
Make App.state a pure derived property. The source of truth is the last
observed ContainerState (cached on the App), plus a sticky operation-
error flag for start/stop failures that the docker event stream cannot
reflect. When no container has been observed yet, the derivation falls
back to install signals: an attached instance (image present) is
STOPPED, otherwise UNKNOWN. As a side effect, an installed-but-never-
started app now correctly reports STOPPED instead of UNKNOWN.
container_state_changed updates the cached container state and routes
all side effects through a single _emit_state_change helper that diffs
old vs new derived state. The two start/stop failure paths route
through _set_operation_error. Uninstall resets the cached signals so
the derivation naturally returns UNKNOWN.
Tests use a new tests/common.force_app_state helper that pokes the
underlying signals directly; the production class no longer carries
test-only setters.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix App state drive to AppState.UNKNOWN
* Unify state mutation through _update_state
Previously, state-driving signal changes were spread across two helpers
(_set_operation_error, _emit_state_change(old_state)) and required each
caller to capture self.state before mutating a private field — leaking
implementation details to call sites and raising the "why am I emitting
the old state?" question pointed out in code review.
Replace both helpers with a single _update_state(*, container_state=,
operation_error=) entry point. Callers describe what changed via
keyword arguments (None leaves a signal untouched); the helper captures
the previous state, applies the updates, recomputes the derived state
and emits side effects if anything changed.
Diff against a tracked _last_state instead of a freshly derived
"current" state, so that an out-of-band mutation between updates does
not silently shift the comparison baseline. The concrete case is
App.uninstall: instance.remove() clears the docker meta mid-flow, which
would otherwise reshape the derivation (RUNNING with no healthcheck
becomes STARTED instead of STARTUP) and suppress the STARTUP transition
that resolves the start-wait task. As a side effect, the initial
UNKNOWN -> STOPPED transition on attach is also now reliably emitted.
Switch the uninstall path to ContainerState.UNKNOWN ("we know there is
no container") rather than the constructor sentinel None.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Cache app state instead of deriving on every read
Building on the previous commit, make App.state a plain read of a
cached _state field rather than re-deriving on every property access.
The derivation moves to _derive_state(), and _update_state() is the
sole place that recomputes and assigns _state, so the value consumers
read always matches what was last emitted to listeners.
This removes the _last_state bookkeeping introduced previously: with a
single cached value there is no longer a separate "derived now" vs
"last emitted" distinction to reconcile, and out-of-band mutations
(e.g. instance.remove() clearing _meta during uninstall) can no longer
silently shift what state returns between updates.
Call _update_state() at the end of load() so the cached state settles
once attach() has run. Image-only attaches do not fire a docker event,
so without this an installed app would stay in the constructor-default
UNKNOWN until first start; this also makes the initial transition on
attach observable to listeners.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Pass operation error to _derive_state instead of storing it
The two state-driving signals were not symmetric. _container_state is
genuinely persisted state ("the last thing docker told us") that
re-derivation legitimately reads across calls. _operation_error, on the
other hand, is a momentary "force ERROR for this transition" signal; the
persistence of an error condition already lives in the cached _state.
Storing it as an instance attribute implied a sticky cross-call behavior
that no call path actually exercised: every caller either set it
explicitly right before deriving (start/stop failures, container events)
or ran argless only at load time, where no failure has occurred.
Drop the _operation_error field and pass operation_error as a parameter
to _derive_state(), defaulting to False in _update_state(). A container
observation now supersedes a prior error implicitly via the default,
which lets the container-event and uninstall call sites drop their
explicit operation_error=False.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Settle load state synchronously from current_state
The argless _update_state() settle at the end of load() raced attach()'s
container-state event. attach() fires DOCKER_CONTAINER_STATE_CHANGE via
the bus, which schedules the container_state_changed listener as a task
rather than running it inline. In the deprecated-arch early-return path
there is no await between attach() and the settle, so the listener had
not run yet: _container_state was still None and the settle derived
STOPPED (instance attached) — emitting a transient UNKNOWN->STOPPED even
for a running container before the listener corrected it. The main path
only avoided this incidentally, by having awaits (check_image,
save_persist) in between for the listener to run.
Derive the load-time state synchronously from instance.current_state()
instead of relying on the asynchronously delivered event. current_state()
returns the real container state, or UNKNOWN when only an image is
present (which derives to STOPPED), so both paths settle correctly
without racing the event.
Add a regression test that loading a running container settles to
STARTED, and mock current_state() in the state-listener test which
relies on a clean UNKNOWN baseline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
355396aeab |
Migrate addon/addons config paths and schema names to app/apps (#6865)
* Migrate config file and directory paths from addons to apps
- Rename addons.json -> apps.json (FILE_HASSIO_APPS constant)
- Rename addons/{core,data,local,git} -> apps/{core,data,local,git}
- Rename addon_configs -> app_configs
Backwards compatibility: on startup, Supervisor checks for legacy
paths and renames them if the new paths don't already exist.
- addons.json migration runs in AppManager.load_config (executor)
- Directory migrations run in bootstrap before initialize_system (executor)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Rename SCHEMA_ADDON(S)_* constants to SCHEMA_APP(S)_* in apps/validate.py
Update all references in supervisor and tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix remaining test references to legacy addons/* paths
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Opportunistic remove of addons dir since it should be empty post migration
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
|
|
0f881d69fe |
Model client-state Apps* errors as APIError (#6856)
* apps: model client-state Apps* errors as APIError Follow-up on #6739: with HassioError now logged and captured by Sentry in api_process, a handful of Apps* exceptions raised from AppManager.install/update/rebuild and AppModel._validate_availability surfaced as "unexpected" 400s with a noisy log entry and a Sentry event, even though they are all user/client-state errors (clicked install on an already-installed add-on, "no update available", local and store versions diverged, system architecture/machine/HA version incompatible, etc.). The dominant offender is SUPERVISOR-1JVV ("No update available for app core_mosquitto", ~19k events / ~12k users), but several siblings show the same shape. Map these through properly so the API returns clean, structured 400s: - Add modeled APIError subclasses in exceptions.py for the previously raw raises in apps/manager.py: AppAlreadyInstalledError, AppNotFoundError, AppNotInstalledError, AppNotInStoreError, AppNoUpdateAvailableError, AppRebuildVersionChangedError, AppRebuildImageBasedError. Each gets a stable error_key, a message_template, and an "addon" extra_field. - Add APIError to AppNotSupportedArchitectureError, AppNotSupportedMachineTypeError and AppNotSupportedHomeAssistantVersionError so they behave the same as the other Apps* APIErrors instead of being treated as unexpected. - Pass the app's display name (app.name from the add-on config) instead of the slug to extra_fields wherever an App or AppModel is available at the raise site, so users see "Mosquitto broker" rather than "core_mosquitto" in error messages. Slug is only used as a fallback when no app object exists (install of an unknown slug, update/rebuild of a slug that is not installed). - Update raise sites in apps/manager.py and apps/model.py to use the new typed exceptions and the addon= keyword. These are all runtime states users hit during normal interaction with the apps UI, not Supervisor bugs worth paging on. * apps: include slug alongside name in Apps* APIError extra_fields Address review feedback from #6856: clients still need the slug to look up additional add-on information (the name is for display only), and we should be consistent about it across the Apps* errors touched by this PR. Every Apps* APIError raised with an App/AppModel available now carries both `addon` (display name, used by the message_template) and `slug` in extra_fields. Raise sites in apps/manager.py and apps/model.py pass both. The two errors raised before an app object exists keep slug-only extra_fields and use {slug} in their message: - AppNotFoundError (install of an unknown slug) - AppNotInstalledError (update/rebuild of a slug not in self.local) Pre-existing Apps* APIErrors outside the scope of this PR (AppUnknownError, AppConfigurationInvalidError, AppBootConfigCannot ChangeError, AppNotRunningError, AppPortConflict, AppNotSupportedWrite StdinError, AppBuild*) will be migrated in a follow-up. * apps: introduce AppAPIError base for uniform addon/slug extra_fields Address review follow-up on #6856: the addon/slug convention was enforced only by hand-rolled __init__s, easy to drift on (forget slug, use a different key, etc.). Promote it to a base class that owns the shape of extra_fields for all App-related API errors. - Add AppAPIError(AppsError, APIError). Its __init__ takes `app: AppModel | App | AppStore | str` and uniformly populates extra_fields with `addon` (display name) and `slug`. Pass a string when no app object exists; only `slug` is set in that case. Extra per-error fields flow through **extra_fields and merge with the defaults. - Convert the new exceptions added in this PR (AppAlreadyInstalledError, AppNotFoundError, AppNotInstalledError, AppNotInStoreError, AppNoUpdateAvailableError, AppRebuildVersionChangedError) into thin subclasses that only declare error_key and message_template -- the __init__ is inherited. - Migrate the AppNotSupported* errors (architecture, machine type, HA version) and AppRebuildImageBasedError to use AppAPIError too; their bespoke per-error fields go through **extra_fields. They keep inheriting AppNotSupportedError so `except AppNotSupportedError` callers (e.g., AppModel._available) still work; MRO routes __init__ through AppAPIError. - Update raise sites in apps/manager.py and apps/model.py to pass `app=<obj-or-slug>` instead of repeating `addon=...` and `slug=...`. Pre-existing App* APIErrors outside this PR's scope (AppUnknownError, AppConfigurationInvalidError, AppBootConfigCannotChangeError, AppNotRunningError, AppPortConflict, AppNotSupportedWriteStdinError, AppBuild*) will be migrated to AppAPIError in a follow-up; the base class is in place for them. * apps: tighten AppAPIError model per review Address mdegat01's two follow-ups on #6856 (review approved as-is, this is the cleanup): - AppNotFoundError and AppNotInstalledError are raised before any App/AppModel object exists (unknown slug; not-installed slug). Pull them out of AppAPIError and inherit (AppsError, APIError) directly with a slug-only __init__ + {slug} message_template. Removes the conceptually-wrong str branch from AppAPIError.__init__: it now strictly requires an app-like object with .name and .slug. - Restore per-class typed __init__ on AppNotSupportedArchitectureError, AppNotSupportedMachineTypeError and AppNotSupportedHomeAssistantVersionError so callers get an explicit signature for the bespoke architectures/machine_types/version params instead of dumping them through **extra_fields. Each override just delegates to AppAPIError.__init__, which keeps ownership of the addon/slug shape. The list-joining for architectures/machine_types moves back into the override (raise sites pass the raw list again). AppRebuildImageBasedError takes no bespoke fields and stays a plain AppAPIError subclass. |
||
|
|
d3028d7bfc |
Enable flake8-pyi, flake8-return, flake8-raise ruff rules (#6861)
Enable the PYI, RET and RSE ruff rule sets and fix the resulting violations across the codebase. The pylint no-else-* checks (RET505-508) are now disabled since ruff covers them. The cleanups are mechanical: - RSE102: drop empty parentheses from `raise Exception()` when no arguments are passed. - RET505-508: drop `else` branches that follow a `return`, `raise`, `continue` or `break`, flattening control flow. - RET502/504: add explicit return values and remove redundant assign-then-return patterns. - PYI030/032/041: tidy up type annotations (collapse literal unions, use `object` for `__eq__`/`__ne__`, drop redundant numeric unions). |
||
|
|
ed91b18c4b |
tests: enable flake8-pytest-style (PT) ruff rules (#6857)
* tests: enable flake8-pytest-style (PT) ruff rules Enable the `PT` ruff rule set and fix the resulting violations across the test suite: - PT006: pass parametrize argument names as tuples instead of a single comma-separated string. - PT022: switch fixtures that have no teardown from `yield` to `return` so the lack of cleanup is obvious at a glance. - PT011: add `match=` to broad `pytest.raises(ValueError)` blocks so the expected error is anchored to a specific message. - PT012: hoist setup (patches, branching) out of `pytest.raises()` blocks so only the call that is expected to raise remains inside. - PT013: replace `from pytest import X` with `import pytest` and access attributes via the module. - PT015: replace `try/except` + `assert False` patterns with `pytest.raises(...)`. - PT017: replace `assert` on exceptions inside `except` blocks with `pytest.raises(...) as exc_info` and assert on `exc_info.value`. No behavioral changes to the tests; the full suite still passes. * tests: address review feedback on PT ruff rule enablement - Fix fixture return-type annotations after switching `yield` to `return` in tests/conftest.py: drop the `Generator[...]`/`AsyncGenerator[...]` wrapper for `dns_manager_service`, `supervisor_internet`, `websession`, and `mock_update_data` so the annotation matches what the fixture actually returns. - Correct the return-type annotation of `fixture_ip6config_service` from `IP4ConfigService` to `IP6ConfigService`. - Fix recurring "excepiton" typo in tests/utils/test_exception_helper.py. * tests: verify backup cleanup on permission error After `test_new_backup_permission_error` raises `BackupPermissionError`, assert that no tarfile was left behind and `tmp_path` is empty. The previous version only checked that the exception was raised, which missed any regression where a partial tarfile would survive the failed create. * tests: rename DNS_GOOD_V6 to DNS_V6_UNSUPPORTED The constant was named "good" but its tests assert that the URLs are rejected by the DNS validator. The IPv6 URLs are well-formed but currently rejected because IPv6 doesn't work with the Docker network (see `dns_url` in supervisor/validate.py). Rename the constant and the related test to make the intent obvious. |
||
|
|
267fc6cd71 |
mounts: make is_mounted honest about server reachability (#6838)
* mounts: use softerr for NFS instead of soft
Switch the NFS mount option from `soft` to `softerr`. For HAOS-style
supervisor mounts (media, share, backup — not the root filesystem) the
error semantics matter:
* `softerr` returns `ETIMEDOUT` on timeout instead of `EIO` (`soft`).
`EIO` is indistinguishable from "the disk is dying"; tools like
SQLite, restic, rsync, ffmpeg tend to treat it as a hard storage
failure (mark database corrupt, abort backup with a hard error,
etc.). `ETIMEDOUT` is unambiguously "the network/server is gone,
transient" and is more commonly handled as retry-later. Supervisor
can also surface a clear "server unreachable" notification rather
than a generic I/O error.
* `softerr` was added in kernel 5.10 precisely to give the fail-fast
behavior of `soft` with a distinct errno so well-behaved apps can
do the right thing.
* For writes-must-not-be-lost use cases (databases, paid storage,
evidence-grade logging) one would want `hard,intr` and a different
recovery story. HAOS NFS mounts are not that — they're add-on
storage where "the share went offline, try again later" is the
correct user-visible behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* mounts: make is_mounted honest about server reachability
systemd's "active/mounted" state is derived from /proc/self/mountinfo
and doesn't reflect whether the backing server actually answers. For
CIFS in particular, smb3_reconfigure never contacts the server, so a
reload of a dead share returns active/mounted with no recovery
attempted. PR #4882 added a Path.is_mount() cross-check to catch this,
but Path.is_mount() relies on os.stat() of the mountpoint root — and
both NFS (softreval) and CIFS (cached root inode attrs) can serve that
stat from local state without going to the wire, so it lies in exactly
the dead-share scenario it was meant to detect.
The visible failure: an API reload of a backup mount whose server had
gone away "succeeded", supervisor then kicked off sys_backups.reload()
against the dead share, and the executor fanned out hundreds of
"Task exception was never retrieved" OSError(112) tracebacks as each
backup tarball's stat() parked in the kernel and failed.
Replace the local-state checks with a statvfs() probe in
NetworkMount.is_mounted(). os.statvfs() returns per-filesystem data
(free blocks, total blocks) that has no client-side cache in either
kernel — neither cifs_statfs() nor nfs_statfs() has an early-return on
cache freshness; both build and send a real FSSTAT / QUERY_FS_INFO
request. So the kernel either reaches the server or gives up with
ETIMEDOUT / EHOSTDOWN / ECONNABORTED. is_mounted() now reflects actual
reachability, finally fulfilling PR #4882's stated intent.
With is_mounted() honest, the reload/restart machinery falls out
naturally: Mount.reload()'s fast path is just `if await self.is_mounted()`,
the post-reload check is the same call, and the "reload succeeded
systemd-wise but probe failed" branch collapses into the existing
"not mounted after reload, try restart" branch. mount(), _restart()
and update() already call is_mounted(); they now get the probe for
free.
To make the probe meaningful, the network mount option strings get
explicit kernel-side timeouts:
* NFS switches from `soft` to `softerr` so timeouts surface as
ETIMEDOUT rather than EIO. EIO is indistinguishable from
"disk dying" and gets misinterpreted by SQLite/restic/rsync as a
hard storage failure; ETIMEDOUT is unambiguously transient.
* CIFS gains `soft,echo_interval=10,retrans=0`, giving a ~30s
per-operation detection budget (3 x echo_interval since last server
response) that matches the NFS budget from `timeo=100,retrans=2`.
Both protocols now fail bounded operations in roughly the same time.
The probe is intentionally not wrapped in an asyncio timeout: the
kernel-side bound is authoritative, and an asyncio timeout would only
orphan the executor thread without unblocking the syscall. The probe
emits debug logs with timing so the ~30s syscall wait on a dead share
is visible in LOGLEVEL=debug traces instead of appearing as a hang.
Tests:
* mock_is_mount fixture extended to also patch os.statvfs so existing
tests that rely on a healthy mount don't need to know about the
probe.
* New manager test split into _healthy_skips_systemd (probe succeeds,
fast path, no systemd call) and _probe_failure_triggers_systemd_reload
(probe fails, escalation runs). API reload test covers both paths.
* Existing tests that simulated "mount is down" via
mock_is_mount.return_value=False updated to simulate probe failure
via OSError(EHOSTDOWN), since is_mount is no longer the signal.
* mounts: drive systemd job waits via JobRemoved instead of state polling
`_update_state_await` had a race that has been present since network
mounts were introduced (#4269) and survived every subsequent rewrite
(#4733,
|
||
|
|
f8880a72be |
Rename addon/addons to app/apps in filenames and imports (#6837)
* Rename addon/addons to app/apps in filenames and imports Continues the addon→app terminology migration (#6786). Renames all source files, test files, fixture files, and directories that contained 'addon'/'addons' in their names, and updates all imports accordingly. Resolution check files in supervisor/resolution/checks/ that were renamed override the slug property to preserve the existing API contract (slugs are exposed via the resolution info API and used to run checks by name). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Rename add-on.json fixture --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
67258dea4a |
Skip post-update health check when Core was not running on entry (#6821)
PR #6726 removed the early return after a HomeAssistantError from the post-update get_config() call so that a Core that stopped responding after an update would correctly trigger a rollback. That early return was, however, also load-bearing for the backup restore flow: Backup.restore_homeassistant() stops and removes Core before invoking core.update(target_version) and starts Core later in its own await_home_assistant_restart stage. With Core not running, _update() correctly skips the start step, but the unconditional post-update get_config() now always raises, sets error_state, and triggers a spurious rollback that re-pulls the previous image and leaves the system on the wrong version after the restore completes. Return early from update() when Core was not running on entry. The caller is responsible for starting Core and there is no live API to health-check at this point. Genuine update failures (Core was running, update broke it) are unaffected and still roll back. Also rename the local rollback to rollback_version for clarity. |
||
|
|
c772a9bbb0 |
Replace fixed-duration sleeps after bus events with gather (#6803)
* Replace fixed-duration sleeps after bus events with gather Several tests use ``await asyncio.sleep(...)`` to "wait for the listener to run" after firing a bus event. The fixed duration is real wall-clock time and the wait can be indeterministic — if the handler chain happens to need slightly more time on a busy CI runner, the assertion races the handler. ``Bus.fire_event`` returns the listener tasks since #6252; capture and ``await asyncio.gather(*tasks)`` instead of sleeping. Touches test_bus.py (the bus tests were poking scheduling instead of verifying their assertions), test_home_assistant_watchdog.py, test_plugin_base.py, addons/test_manager.py, docker/test_addon.py, and test_store_execute_reload.py. Other cleanups in the same spirit: - ``_fire_test_event`` in addons/test_addon.py becomes ``async def`` and gathers the listener tasks itself, so its 17 call sites collapse to a single ``await _fire_test_event(...)``. - The two test_store_execute_reload.py sites that used the private ``_update_connectivity()`` helper are reworked to set the cached connectivity flag directly and fire the event themselves so they can gather the listener tasks the same way. - The two ``sleep(1)`` post-pull drains in docker/test_interface.py collapse to ``sleep(0)`` (handler tasks are already gathered inside pull_image), saving ~2s. - The ``sleep(0.01)`` waits inside ``container_events()`` task bodies (api/test_addons.py, api/test_store.py, backups/test_manager.py) are just one-yield-to-the-parent and become ``sleep(0)``. Switching to ``gather`` exposes a few latent test mocks that were silently swallowing TypeErrors as background-task failures before: - ``CGroup.add_devices_allowed`` is ``async def`` but was patched as a plain MagicMock in docker/test_addon.py — now patched via ``new_callable=AsyncMock``. - The watchdog does ``await (await self.start())`` / ``await (await self.restart())`` because ``App.start`` / ``App.restart`` return ``asyncio.Task``. The mocks in addons/test_addon.py (test_app_watchdog, test_watchdog_on_stop, test_watchdog_during_attach) needed ``AsyncMock(return_value=<settled future>)`` to mirror that shape rather than a plain MagicMock. * Factor bus.fire_event + gather pattern into a helper Per review feedback, the ``await asyncio.gather(*coresys.bus.fire_event(...))`` incantation was scattered across many call sites. Add ``tests.common.fire_bus_event`` that takes the coresys, event and data, fires the event and awaits the spawned listener tasks. Convert all matching sites to use it, including the ``_fire_test_event`` wrapper in addons/test_addon.py which now just builds the ``DockerContainerStateEvent`` and delegates. |
||
|
|
ad1a9115d8 |
Improve and extend frontend probe after update with WebSocket check (#6811)
* Improve and extend frontend probe after update with WebSocket check The post-update health check introduced in #6311 added HomeAssistantAPI.check_frontend_available, which fetched the frontend through the existing Supervisor-internal API connection to Core. Since #6742 that connection optionally runs over a Unix socket with no authentication, so the request no longer exercises the same transport, auth and routing path that an external HTTP client uses. Move the frontend probe out of HomeAssistantAPI into a small frontend_check module that talks to Core's TCP endpoints via the plain websession with no authentication, mirroring what an external client would see. While doing this, extend the post-update verification to also probe the WebSocket endpoint: open /api/websocket and confirm the first frame is the auth_required text message. This catches the kind of WebSocket breakage seen in #6802, where api/config still listed websocket_api as loaded and GET / still returned HTML, but the WebSocket handshake completed with an immediate close frame and the frontend was unusable. The component check now also requires "http" to be loaded, in addition to "frontend" and "websocket_api", and iterates so every missing component is logged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address review feedback on WebSocket probe - Wrap ws_connect in asyncio.wait_for so the handshake has an explicit bounded timeout (the global websession's default timeout would otherwise apply). - Validate that the auth_required payload is a JSON object before calling .get("type"); a list/string would otherwise raise AttributeError at runtime. - Add a regression test covering a non-dict JSON payload. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
eb3c388618 |
Migrate persisted 'addon' field to 'app' in config files (#6786)
* Migrate persisted 'addon' field to 'app' in discovery and services config
Rename the 'addon' key to 'app' in persisted configuration files for
discovery messages (discovery.json), service modules (services.json),
and supervisor config (supervisor.json), as part of the broader
addon->app terminology migration.
Changes:
- Add ATTR_ADDON = "addon" to const.py for V1 API compat/migration
- Add ATTR_ADDONS_CUSTOM_LIST = "addons_custom_list" to const.py for migration
- Change ATTR_APPS_CUSTOM_LIST value from "addons_custom_list" to "apps_custom_list"
- Add _migrate_supervisor_config() schema pre-processor in validate.py to
transparently load old supervisor.json files using the old key
- Add ATTR_ADDON to services/const.py; change ATTR_APP value to "app"
- Add _migrate_addon_to_app() pre-processors to MQTT, MySQL, and discovery
schemas to load old config files that used the "addon" key
- Rename Message.addon -> Message.app in Discovery and update all references
- Keep hassio_push/discovery payload using "addon" key for HA compatibility
- GET /services/{service} and GET /discovery: V1 returns "addon" key,
V2 returns "app" key, via dedicated _v1 handler methods following the
backups/store pattern, registered with AppVersion guards in
_register_services() and _register_discovery()
- Broaden FileConfiguration schema type annotation to accept vol.All
validators in addition to vol.Schema
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add schema migration tests for addon->app config key rename
Test that backwards-compatible migration of old 'addon'/'addons_custom_list'
keys to 'app'/'apps_custom_list' works correctly in all affected schemas,
and that the new keys are accepted without modification.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add an __init__ to discovery tests
* Add app_api_client_with_prefix fixture and update V1/V2 tests
Move the app-level V1/V2 fixture to tests/api/conftest.py as
app_api_client_with_prefix for use across any endpoint that requires
app-level credentials (services_role, app.discovery, etc.).
- Add app_api_client_with_prefix fixture to conftest.py
- Update test_set_service_already_provided and test_del_service_not_provided
to use app_api_client_with_prefix (covers both v1 and v2)
- Add test_get_service_v1_v2_keys asserting addon/app key per version
- Update test_api_discovery_forbidden, test_api_send_del_discovery,
test_api_invalid_discovery to use app_api_client_with_prefix
- Split test_discovery_not_found into test_discovery_not_found_get
(uses api_client_with_prefix, GET requires homeassistant) and
test_discovery_not_found_delete (uses app_api_client_with_prefix)
- Add test_get_discovery_v1_v2_keys asserting addon/app key per version
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
|
|
f8dbafe0bb |
Drop redundant @pytest.mark.asyncio decorators (#6795)
The pytest config sets ``asyncio_mode = "auto"``, which already auto-marks every ``async def test_*`` as a coroutine test. The 38 ``@pytest.mark.asyncio`` decorators sprinkled across the suite were no-ops kept around from before that flag was set. Remove them along with the now-unused ``import pytest`` lines they were the only consumer of. Pure mechanical cleanup; no test behavior changes. |
||
|
|
61faa73be5 |
Return proper API errors when backup mount is down (#6785)
Follow-up on #6739: with HassioError now logged and captured by Sentry in api_process, BackupMountDownError surfaced as an "unexpected" 400 with a noisy log entry and a Sentry event (SUPERVISOR-1JXW), even though the user had simply asked to back up to a mount that was not currently available. Map this through properly so the API returns a clean, structured 400: - Make BackupMountDownError inherit from APIError, with error_key "backup_mount_down", message_template "Backup mount '{mount}' is down", and the mount name in extra_fields. Clients now get a normalized, translatable message and a stable key instead of the raw "<name> is down, cannot back-up to it" / "...cannot copy to it" strings. - Simplify both raise sites in BackupManager (_check_location and _copy_to_location) to just pass mount=. @api_process turns the result into a 400 without logging or Sentry capture, since this is now a modeled client-state error rather than an unexpected one. The mount being down is a runtime state issue users hit when their NAS/CIFS share is briefly unreachable, not a Supervisor bug worth paging on. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
33ab5b55f8 |
Treat JobException as a client-side API error (#6777)
* Treat JobException as a client-side API error Job condition guards (system not running, no free space, etc.) and concurrency rejections (another job in flight) raised by the @Job decorator are explicit precondition failures with descriptive messages, not unexpected errors. JobException inheriting HassioError directly meant api_process caught them in its HassioError branch — which since #6739 logs them as unexpected and captures them to Sentry. Inherit APIError instead so api_process surfaces these through its APIError branch with the original message and skips the unexpected-error path. Status stays at APIError's default 400, so the API contract is unchanged. Extended test_backup_immediate_errors to assert async_capture_exception is not called for the freeze and free-space condition guards. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Silence too-many-ancestors on plugin job error mixins The plugin-specific job error subclasses (CliJobError, ObserverJobError, MulticastJobError, CoreDNSJobError, AudioJobError) cross pylint's too-many-ancestors threshold once JobException inherits APIError. Add the same `# pylint: disable=too-many-ancestors` already used on the ResolutionNotFound subclasses with similar diamond inheritance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Disable too-many-ancestors globally instead of per class The pylint config already disables every other too-many-* rule "for the sake of readability", but kept too-many-ancestors and forced inline disables on diamond-inherited exception classes (the ResolutionNotFound subclasses, and now five plugin job error mixins after the JobException APIError change). Add too-many-ancestors to the global disable list and drop all eight inline annotations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9923b8580b |
Return proper API errors for invalid hostnames (#6776)
Follow-up on #6739: with HassioError now logged and captured by Sentry in api_process, hostname rejections from systemd-hostnamed surfaced as "unexpected" 400s with noisy log entries and a Sentry event, even though the user had simply submitted an invalid hostname. Map this through properly so the API returns a clean, structured 400: - Split ErrorType.INVALID_ARGS out of DBusInterfaceMethodError into its own DBusInvalidArgsError. The two cases collapsed there before are semantically different: UNKNOWN_METHOD / INVALID_SIGNATURE mean the call is broken (method missing or types wrong); INVALID_ARGS means the call is valid but the service rejected an argument's value. - Add HostInvalidHostnameError(HostError, APIError) with error_key and extra_fields so clients get a normalized message and a stable key rather than systemd's raw "Invalid static hostname '...'" text. - Translate DBusInvalidArgsError to HostInvalidHostnameError in SystemControl.set_hostname. @api_process turns the result into a 400 without logging or Sentry capture, since this is now a modeled client-input error rather than an unexpected one. Validation continues to live in hostnamed (hostname_is_valid() in systemd's src/basic/hostname-util.c); Supervisor only translates the rejection. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bc24fb5449 |
Refactor API registration to support v1/v2 via shared methods (#6769)
* Refactor API registration to support v1/v2 via shared methods - Add AppVersion StrEnum (V1, V2) to supervisor/api/const.py - Replace self.v2_app with self._v2_app and expose a versions property (dict[AppVersion, web.Application]) computed dynamically so that test fixtures reassigning self.webapp are automatically reflected in V1 - All _register_* methods now accept a required app: web.Application parameter; version-specific routes are gated with "if app is self.versions[AppVersion.V1/V2]:" - load() loops over enabled_versions (V1 always, V2 when feature-flagged) and calls each registration method once per version, no duplication - Static resources are registered before webapp.add_subapp() to avoid registering into a frozen router - add_subapp uses self.webapp directly for readability - Fold _register_v2_apps/_register_v2_backups/_register_v2_store into their respective unified methods; remove the now-defunct _register_v2_* helpers and the _api_apps/_api_backups/_api_store instance vars - _register_proxy and _register_ingress updated to accept app; legacy /homeassistant/* proxy routes gated behind V1 conditional Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add dual v1/v2 parametrization to API tests All 163 tests across 17 API modules that register identically on both v1 and v2 now run against both versions via api_client_with_prefix. - tests/api/conftest.py: advanced_logs_tester switched to api_client_with_prefix so log-endpoint tests are auto-parametrized; accepts optional v2_path_prefix kwarg for paths that differ by version - tests/api/test_{auth,discovery,dns,docker,hardware,host,ingress, jobs,mounts,network,os,resolution,security,services,supervisor}.py: api_client -> api_client_with_prefix with path prefix unpacking - supervisor/api/__init__.py: _register_panel() moved outside the version loop -- frontend static assets are V1-only - tests/api/test_panel.py: kept on plain api_client (V1-only) Tests intentionally kept V1-only: - auth/discovery: use indirect api_client parametrize for addon context - homeassistant: all tests call legacy /homeassistant/* paths (V1-only) - jobs (4 tests): inner @Job-decorated classes register names into a module-level set; re-running the same test raises RuntimeError Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Extend dual v1/v2 parametrization to homeassistant and jobs tests tests/api/conftest.py: - Add core_api_client_with_root fixture parametrized over three paths: v1-core: /core/... (canonical v1 path) v1-legacy: /homeassistant/... (legacy v1 alias, same handlers) v2-core: /v2/core/... (canonical v2 path) tests/api/test_homeassistant.py: - Switch all 17 api_client tests to core_api_client_with_root so each test runs against all three access paths (v1 canonical, v1 legacy alias, v2 canonical), exercising every registered route tests/api/test_jobs.py: - Promote four inner TestClass definitions to module-level helpers (_JobsTreeTestHelper, _JobManualCleanupTestHelper, _JobsSortedTestHelper, _JobWithErrorTestHelper) so that @Job name registration into the global _JOB_NAMES set only happens once at import time rather than on each parametrized test run - Replace closure references to outer-scope coresys with self.coresys - Use api_client_with_prefix for dual-version coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> |
||
|
|
61ca2524b2 |
Return proper API errors for mqtt/mysql service conflicts (#6767)
* Return proper API errors for mqtt/mysql service conflicts After #6739 added unexpected-error logging and Sentry capture to the api_process wrappers, SUPERVISOR-1JTQ and SUPERVISOR-1JWM surfaced as user-triggered service conflicts that were being treated as unexpected errors: - POST /services/{mqtt,mysql} when another app already provides the service. - DELETE /services/{mqtt,mysql} when no app currently provides it. Both paths raised a generic ServicesError, which the API layer turned into an opaque HTTP 400 without a translation key, and which #6739 now also logs and captures via Sentry. Introduce ServiceAlreadyProvidedError (409 Conflict) and ServiceNotProvidedError (404 Not Found) as new-style API exceptions with translation keys and extra_fields, plus a shared APIConflict base class for future 409 responses. The mqtt and mysql service modules now raise these instead, so the API returns structured, translatable responses and these expected user conflicts stop being captured as bugs. Fixes SUPERVISOR-1JTQ Fixes SUPERVISOR-1JWM Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Don't log handled errors verbose Missing/already present service information are well handled errors with clear API responses. The client is supposed to handle these errors. No need to log verbosly. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ff90e4b817 |
Fix UnboundLocalError when Core API fails post-update (#6761)
When get_config() raised HomeAssistantError after a Core update, the except block set error_state and fell through to the frontend check, which referenced an unbound `data` variable and raised UnboundLocalError. That aborted the update with a JobException and skipped the rollback path entirely. Move the frontend checks into an else branch of the try/except so they only run when get_config() succeeds. When it fails, error_state is set and control falls through to the rollback logic below, which is what PR #6726 intended. Fixes SUPERVISOR-1JVX Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7fb621234e |
Add Unix socket support for Core communication with feature flag (#6742)
* Use Unix socket for Supervisor to Core communication Reintroduce Unix socket support for Supervisor-to-Core communication (reverted in #6735) with the addition of a feature flag gate. The feature is now controlled by the `core_unix_socket` feature flag and disabled by default. When enabled and Core version supports it, Supervisor communicates with Core via a Unix socket at /run/os/core.sock instead of TCP. This eliminates the need for access token authentication on the socket path, as Core authenticates the peer by the socket connection itself. Key changes: - Add FeatureFlag.CORE_UNIX_SOCKET to gate the feature - HomeAssistantAPI: transport-aware session/url/websocket management - WSClient: separate connect() (Unix, no auth) and connect_with_auth() (TCP) class methods with proper error handling - APIProxy delegates websocket setup to api.connect_websocket() - Container state tracking for Unix session lifecycle - CI builder mounts /run/supervisor for integration tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Sort feature flags alphabetically * Drop per-call max_msg_size from WSClient Hardcode the WebSocket message size cap to 64 MB in WSClient and remove the parameter from WSClient.connect, connect_with_auth, _ws_connect, and HomeAssistantAPI.connect_websocket. This was only ever overridden by APIProxy, so threading it through four layers was unnecessary. max_msg_size is a cap, not a pre-allocation; aiohttp only grows buffers to the size of actual incoming messages. Supervisor's own control channel never approaches 64 MB, so unifying the limit has no runtime cost. Addresses review feedback on #6742. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
56abe94d74 |
Add versioned v2 API with apps terminology (#6741)
* Add versioned v2 API with apps terminology Introduce a v2 API sub-app mounted at /v2 that uses 'apps' terminology throughout, while keeping v1 fully backward-compatible. Key changes: - Add ATTR_ADDONS = 'addons' constant alongside ATTR_APPS = 'apps' so backup file data (which must remain 'addons' for backward compat) and v2 API responses can use distinct constants - Add FeatureFlag.SUPERVISOR_V2_API to gate v2 route registration - Mount aiohttp sub-app at /v2 in RestAPI.load() when flag is enabled - Add _AppSecurityPatterns frozen dataclass and _V1_PATTERNS/_V2_PATTERNS with strict per-version regex sets (no cross-version matching) - Add _register_v2_apps, _register_v2_backups, _register_v2_store route registration methods - Add v1 thin wrapper methods (*_v1) for all affected endpoints so business logic lives in the canonical v2 methods - Extract _info_data() helper in APIApps so v1 closure can bypass @api_process and still catch APIAppNotInstalled for store routing - Add _rename_apps_to_addons_in_backups(), _process_location_in_body(), _all_store_apps_info() shared helpers to eliminate duplication - Add api_client_v2, api_client_with_prefix, app_api_client_with_root, store_app_api_client_with_root parameterized test fixtures - Add test_v2_api_disabled_without_feature_flag - Parameterize backup, addons, and store tests to cover both v1 and v2 paths Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix pylint false positive for re.Pattern C extension methods re.Pattern methods (match, search, etc.) are C extension methods. Pylint cannot detect them via static analysis when re.Pattern is used as a type annotation in a dataclass field, producing false E1101 no-member errors. Add generated-members to inform pylint these members exist. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * pylint and feedback fixes * Copilot suggested fixes * Minor feedback fixes --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
38ddb3df54 |
Fix Core update rollback: delay image cleanup and fix missing rollback path (#6726)
* Delay old image cleanup until after health checks on Core update Move the old Docker image cleanup from inside _update() to after the post-update health checks (frontend loaded and accessible). This keeps the previous version's image available locally when a rollback is needed, avoiding a potentially slow re-download. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add test assertions for old image cleanup timing on Core update Verify that the old Docker image is cleaned up only after health checks pass, and not when a rollback is triggered. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix missing rollback when get_config fails after Core update The early return after setting error_state skipped the rollback block, leaving the system on a broken new version when the API stopped responding after update. The other health check failure paths correctly fall through to the rollback logic; this was the only one that didn't. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
a504d85745 |
Remove double newlines from build and check config output (#6743)
* Remove double newlines from build output The log lines from run command already have newline characters, so joining them with "\n" adds extra newlines. Joining them with an empty string preserves the original formatting of the logs. * Remove double newlines from check config output The log lines from run command already have newline characters, so joining them with "\n" adds extra newlines. Joining them with an empty string preserves the original formatting of the logs. * Fix pytest |
||
|
|
1218326af3 |
Add development feature toggle system (#6719)
* Add experimental feature toggle system Introduces an ExperimentalFeature enum and feature_flags config to allow toggling experimental features via the supervisor options API. The first feature flag is 'supervisor_v2_api' to gate the upcoming V2 API. Absent keys in options request = no change (partial update, consistent with existing options APIs). The info endpoint always returns all known feature flags and their current state for discoverability. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ExperimentalFeature -> FeatureFlag * Use explicit value of StrEnum to be typesafe Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Minor comment improvement Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Stefan Agner <stefan@agner.ch> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> |
||
|
|
ba8c49935b |
Refactor internal addon references to app/apps (#6717)
* Rename addon→app in docstrings and comments Updates all docstrings and inline comments across supervisor/ and tests/ to use the new app/apps terminology. No runtime behaviour is changed by this commit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Rename addon→app in code (variables, args, class names, functions) Renames all internal Python identifiers from addon/addons to app/apps: - Variable and argument names - Function and method names - Class names (Addon→App, AddonManager→AppManager, DockerAddon→DockerApp, all exception, check, and fixup classes, etc.) - String literals used as Python identifiers (pytest fixtures, parametrize param names, patch.object attribute strings, URL route match_info keys) External API contracts are preserved: JSON keys, error codes, discovery protocol fields, TypedDict/attr.s field names. Import module paths (supervisor/addons/) are also unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix partial backup/restore API to remap addons key to apps The external API accepts `addons` as the request body key (since ATTR_APPS = "addons"), but do_backup_partial and do_restore_partial now take an `apps` parameter after the rename. The **body expansion in both endpoints would pass `addons=...` causing a TypeError. Remap the key before expansion in both backup_partial and restore_partial: if ATTR_APPS in body: body["apps"] = body.pop(ATTR_APPS) Also adds test_restore_partial_with_addons_key to verify the restore path correctly receives apps= when addons is passed in the request body. This path had no existing test coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix merge error * Adjust AppLoggerAdapter to use app_name Co-authored-by: Stefan Agner <stefan@agner.ch> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Stefan Agner <stefan@agner.ch> |