diff --git a/supervisor/api/__init__.py b/supervisor/api/__init__.py index 20aa270ec..71213935b 100644 --- a/supervisor/api/__init__.py +++ b/supervisor/api/__init__.py @@ -17,7 +17,6 @@ from .docker import APIDocker from .hardware import APIHardware from .homeassistant import APIHomeAssistant from .host import APIHost -from .info import APIInfo from .ingress import APIIngress from .jobs import APIJobs from .middleware.security import SecurityMiddleware @@ -27,6 +26,7 @@ from .observer import APIObserver from .os import APIOS from .proxy import APIProxy from .resolution import APIResoulution +from .root import APIRoot from .security import APISecurity from .services import APIServices from .store import APIStore @@ -70,7 +70,7 @@ class RestAPI(CoreSysAttributes): self._register_hardware() self._register_homeassistant() self._register_host() - self._register_info() + self._register_root() self._register_ingress() self._register_multicast() self._register_network() @@ -228,12 +228,21 @@ class RestAPI(CoreSysAttributes): ] ) - def _register_info(self) -> None: - """Register info functions.""" - api_info = APIInfo() - api_info.coresys = self.coresys + def _register_root(self) -> None: + """Register root functions.""" + api_root = APIRoot() + api_root.coresys = self.coresys - self.webapp.add_routes([web.get("/info", api_info.info)]) + self.webapp.add_routes([web.get("/info", api_root.info)]) + self.webapp.add_routes([web.post("/refresh_updates", api_root.refresh_updates)]) + self.webapp.add_routes( + [web.get("/available_updates", api_root.available_updates)] + ) + + # Remove 2023 + self.webapp.add_routes( + [web.get("/supervisor/available_updates", api_root.available_updates)] + ) def _register_resolution(self) -> None: """Register info functions.""" @@ -284,9 +293,6 @@ class RestAPI(CoreSysAttributes): self.webapp.add_routes( [ - web.get( - "/supervisor/available_updates", api_supervisor.available_updates - ), web.get("/supervisor/ping", api_supervisor.ping), web.get("/supervisor/info", api_supervisor.info), web.get("/supervisor/stats", api_supervisor.stats), diff --git a/supervisor/api/info.py b/supervisor/api/info.py deleted file mode 100644 index efee6e29b..000000000 --- a/supervisor/api/info.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Init file for Supervisor info RESTful API.""" -import logging -from typing import Any - -from aiohttp import web - -from ..const import ( - ATTR_ARCH, - ATTR_CHANNEL, - ATTR_DOCKER, - ATTR_FEATURES, - ATTR_HASSOS, - ATTR_HOMEASSISTANT, - ATTR_HOSTNAME, - ATTR_LOGGING, - ATTR_MACHINE, - ATTR_OPERATING_SYSTEM, - ATTR_STATE, - ATTR_SUPERVISOR, - ATTR_SUPPORTED, - ATTR_SUPPORTED_ARCH, - ATTR_TIMEZONE, -) -from ..coresys import CoreSysAttributes -from .utils import api_process - -_LOGGER: logging.Logger = logging.getLogger(__name__) - - -class APIInfo(CoreSysAttributes): - """Handle RESTful API for info functions.""" - - @api_process - async def info(self, request: web.Request) -> dict[str, Any]: - """Show system info.""" - return { - ATTR_SUPERVISOR: self.sys_supervisor.version, - ATTR_HOMEASSISTANT: self.sys_homeassistant.version, - ATTR_HASSOS: self.sys_os.version, - ATTR_DOCKER: self.sys_docker.info.version, - ATTR_HOSTNAME: self.sys_host.info.hostname, - ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system, - ATTR_FEATURES: self.sys_host.features, - ATTR_MACHINE: self.sys_machine, - ATTR_ARCH: self.sys_arch.default, - ATTR_STATE: self.sys_core.state, - ATTR_SUPPORTED_ARCH: self.sys_arch.supported, - ATTR_SUPPORTED: self.sys_core.supported, - ATTR_CHANNEL: self.sys_updater.channel, - ATTR_LOGGING: self.sys_config.logging, - ATTR_TIMEZONE: self.sys_timezone, - } diff --git a/supervisor/api/root.py b/supervisor/api/root.py new file mode 100644 index 000000000..35594a0dd --- /dev/null +++ b/supervisor/api/root.py @@ -0,0 +1,114 @@ +"""Init file for Supervisor Root RESTful API.""" +import asyncio +import logging +from typing import Any + +from aiohttp import web + +from ..const import ( + ATTR_ARCH, + ATTR_CHANNEL, + ATTR_DOCKER, + ATTR_FEATURES, + ATTR_HASSOS, + ATTR_HOMEASSISTANT, + ATTR_HOSTNAME, + ATTR_ICON, + ATTR_LOGGING, + ATTR_MACHINE, + ATTR_NAME, + ATTR_OPERATING_SYSTEM, + ATTR_STATE, + ATTR_SUPERVISOR, + ATTR_SUPPORTED, + ATTR_SUPPORTED_ARCH, + ATTR_TIMEZONE, + ATTR_VERSION_LATEST, +) +from ..coresys import CoreSysAttributes +from .const import ATTR_AVAILABLE_UPDATES, ATTR_PANEL_PATH, ATTR_UPDATE_TYPE +from .utils import api_process + +_LOGGER: logging.Logger = logging.getLogger(__name__) + + +class APIRoot(CoreSysAttributes): + """Handle RESTful API for root functions.""" + + @api_process + async def info(self, request: web.Request) -> dict[str, Any]: + """Show system info.""" + return { + ATTR_SUPERVISOR: self.sys_supervisor.version, + ATTR_HOMEASSISTANT: self.sys_homeassistant.version, + ATTR_HASSOS: self.sys_os.version, + ATTR_DOCKER: self.sys_docker.info.version, + ATTR_HOSTNAME: self.sys_host.info.hostname, + ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system, + ATTR_FEATURES: self.sys_host.features, + ATTR_MACHINE: self.sys_machine, + ATTR_ARCH: self.sys_arch.default, + ATTR_STATE: self.sys_core.state, + ATTR_SUPPORTED_ARCH: self.sys_arch.supported, + ATTR_SUPPORTED: self.sys_core.supported, + ATTR_CHANNEL: self.sys_updater.channel, + ATTR_LOGGING: self.sys_config.logging, + ATTR_TIMEZONE: self.sys_timezone, + } + + @api_process + async def available_updates(self, request: web.Request) -> dict[str, Any]: + """Return a list of items with available updates.""" + available_updates = [] + + # Core + if self.sys_homeassistant.need_update: + available_updates.append( + { + ATTR_UPDATE_TYPE: "core", + ATTR_PANEL_PATH: "/update-available/core", + ATTR_VERSION_LATEST: self.sys_homeassistant.latest_version, + } + ) + + # Supervisor + if self.sys_supervisor.need_update: + available_updates.append( + { + ATTR_UPDATE_TYPE: "supervisor", + ATTR_PANEL_PATH: "/update-available/supervisor", + ATTR_VERSION_LATEST: self.sys_supervisor.latest_version, + } + ) + + # OS + if self.sys_os.need_update: + available_updates.append( + { + ATTR_UPDATE_TYPE: "os", + ATTR_PANEL_PATH: "/update-available/os", + ATTR_VERSION_LATEST: self.sys_os.latest_version, + } + ) + + # Add-ons + available_updates.extend( + { + ATTR_UPDATE_TYPE: "addon", + ATTR_NAME: addon.name, + ATTR_ICON: f"/addons/{addon.slug}/icon" if addon.with_icon else None, + ATTR_PANEL_PATH: f"/update-available/{addon.slug}", + ATTR_VERSION_LATEST: addon.latest_version, + } + for addon in self.sys_addons.installed + if addon.need_update + ) + + return {ATTR_AVAILABLE_UPDATES: available_updates} + + @api_process + async def refresh_updates(self, request: web.Request) -> None: + """Refresh All system updates information.""" + await asyncio.shield( + asyncio.gather(self.sys_updater.reload(), self.sys_store.reload()) + ) diff --git a/supervisor/api/supervisor.py b/supervisor/api/supervisor.py index d094631df..e39c76dbc 100644 --- a/supervisor/api/supervisor.py +++ b/supervisor/api/supervisor.py @@ -50,7 +50,6 @@ from ..coresys import CoreSysAttributes from ..exceptions import APIError from ..utils.validate import validate_timezone from ..validate import repositories, version_tag, wait_boot -from .const import ATTR_AVAILABLE_UPDATES, ATTR_PANEL_PATH, ATTR_UPDATE_TYPE from .utils import api_process, api_process_raw, api_validate _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -240,53 +239,3 @@ class APISupervisor(CoreSysAttributes): def logs(self, request: web.Request) -> Awaitable[bytes]: """Return supervisor Docker logs.""" return self.sys_supervisor.logs() - - @api_process - async def available_updates(self, request: web.Request) -> dict[str, Any]: - """Return a list of items with available updates.""" - available_updates = [] - - # Core - if self.sys_homeassistant.need_update: - available_updates.append( - { - ATTR_UPDATE_TYPE: "core", - ATTR_PANEL_PATH: "/update-available/core", - ATTR_VERSION_LATEST: self.sys_homeassistant.latest_version, - } - ) - - # Supervisor - if self.sys_supervisor.need_update: - available_updates.append( - { - ATTR_UPDATE_TYPE: "supervisor", - ATTR_PANEL_PATH: "/update-available/supervisor", - ATTR_VERSION_LATEST: self.sys_supervisor.latest_version, - } - ) - - # OS - if self.sys_os.need_update: - available_updates.append( - { - ATTR_UPDATE_TYPE: "os", - ATTR_PANEL_PATH: "/update-available/os", - ATTR_VERSION_LATEST: self.sys_os.latest_version, - } - ) - - # Add-ons - available_updates.extend( - { - ATTR_UPDATE_TYPE: "addon", - ATTR_NAME: addon.name, - ATTR_ICON: f"/addons/{addon.slug}/icon" if addon.with_icon else None, - ATTR_PANEL_PATH: f"/update-available/{addon.slug}", - ATTR_VERSION_LATEST: addon.latest_version, - } - for addon in self.sys_addons.installed - if addon.need_update - ) - - return {ATTR_AVAILABLE_UPDATES: available_updates} diff --git a/tests/api/test_root.py b/tests/api/test_root.py new file mode 100644 index 000000000..d66bb8009 --- /dev/null +++ b/tests/api/test_root.py @@ -0,0 +1,84 @@ +"""Test Supervisor API.""" +# pylint: disable=protected-access +from unittest.mock import AsyncMock + +import pytest + +from supervisor.api.const import ATTR_AVAILABLE_UPDATES +from supervisor.coresys import CoreSys + +from tests.const import TEST_ADDON_SLUG + + +@pytest.mark.asyncio +async def test_api_info(api_client): + """Test docker info api.""" + resp = await api_client.get("/info") + result = await resp.json() + + assert result["data"]["supervisor"] == "DEV" + assert result["data"]["docker"] == "1.0.0" + assert result["data"]["supported"] is True + assert result["data"]["channel"] == "stable" + assert result["data"]["logging"] == "info" + assert result["data"]["timezone"] == "UTC" + + +@pytest.mark.asyncio +async def test_api_available_updates( + install_addon_ssh, + api_client, + coresys: CoreSys, +): + """Test available_updates.""" + installed_addon = coresys.addons.get(TEST_ADDON_SLUG) + installed_addon.persist["version"] = "1.2.3" + + async def available_updates(): + return (await (await api_client.get("/available_updates")).json())["data"][ + ATTR_AVAILABLE_UPDATES + ] + + updates = await available_updates() + assert len(updates) == 1 + assert updates[-1] == { + "icon": None, + "name": "Terminal & SSH", + "panel_path": "/update-available/local_ssh", + "update_type": "addon", + "version_latest": "9.2.1", + } + + coresys.updater._data["hassos"] = "321" + coresys.os._version = "123" + updates = await available_updates() + assert len(updates) == 2 + assert updates[0] == { + "panel_path": "/update-available/os", + "update_type": "os", + "version_latest": "321", + } + + coresys.updater._data["homeassistant"] = "321" + coresys.homeassistant.version = "123" + updates = await available_updates() + assert len(updates) == 3 + assert updates[0] == { + "panel_path": "/update-available/core", + "update_type": "core", + "version_latest": "321", + } + + +@pytest.mark.asyncio +async def test_api_refresh_updates(api_client, coresys: CoreSys): + """Test docker info api.""" + + coresys.updater.reload = AsyncMock() + coresys.store.reload = AsyncMock() + + resp = await api_client.post("/refresh_updates") + assert resp.status == 200 + + assert coresys.updater.reload.called + assert coresys.store.reload.called diff --git a/tests/api/test_supervisor.py b/tests/api/test_supervisor.py index 9a05476ed..8fec74784 100644 --- a/tests/api/test_supervisor.py +++ b/tests/api/test_supervisor.py @@ -2,11 +2,8 @@ # pylint: disable=protected-access import pytest -from supervisor.api.const import ATTR_AVAILABLE_UPDATES from supervisor.coresys import CoreSys -from tests.const import TEST_ADDON_SLUG - @pytest.mark.asyncio async def test_api_supervisor_options_debug(api_client, coresys: CoreSys): @@ -16,49 +13,3 @@ async def test_api_supervisor_options_debug(api_client, coresys: CoreSys): await api_client.post("/supervisor/options", json={"debug": True}) assert coresys.config.debug - - -@pytest.mark.asyncio -async def test_api_supervisor_available_updates( - install_addon_ssh, - api_client, - coresys: CoreSys, -): - """Test available_updates.""" - installed_addon = coresys.addons.get(TEST_ADDON_SLUG) - installed_addon.persist["version"] = "1.2.3" - - async def available_updates(): - return (await (await api_client.get("/supervisor/available_updates")).json())[ - "data" - ][ATTR_AVAILABLE_UPDATES] - - updates = await available_updates() - assert len(updates) == 1 - assert updates[-1] == { - "icon": None, - "name": "Terminal & SSH", - "panel_path": "/update-available/local_ssh", - "update_type": "addon", - "version_latest": "9.2.1", - } - - coresys.updater._data["hassos"] = "321" - coresys.os._version = "123" - updates = await available_updates() - assert len(updates) == 2 - assert updates[0] == { - "panel_path": "/update-available/os", - "update_type": "os", - "version_latest": "321", - } - - coresys.updater._data["homeassistant"] = "321" - coresys.homeassistant.version = "123" - updates = await available_updates() - assert len(updates) == 3 - assert updates[0] == { - "panel_path": "/update-available/core", - "update_type": "core", - "version_latest": "321", - }