Add Core Update API (#3413)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Pascal Vizeli 2022-01-24 10:32:23 +01:00 committed by GitHub
parent 4ae61814d4
commit c3019bce7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 214 additions and 162 deletions

View File

@ -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),

View File

@ -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,
}

114
supervisor/api/root.py Normal file
View File

@ -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())
)

View File

@ -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}

84
tests/api/test_root.py Normal file
View File

@ -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

View File

@ -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",
}