Allow return of all addon info in one api
This commit is contained in:
parent
d3031e2eae
commit
60e5e76fbd
|
@ -106,7 +106,7 @@ from ..exceptions import (
|
|||
PwnedSecret,
|
||||
)
|
||||
from ..validate import docker_ports
|
||||
from .const import ATTR_SIGNED, CONTENT_TYPE_BINARY
|
||||
from .const import ATTR_SIGNED, CONTENT_TYPE_BINARY, AddonView
|
||||
from .utils import api_process, api_process_raw, api_validate, json_loads
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
@ -130,6 +130,109 @@ SCHEMA_OPTIONS = vol.Schema(
|
|||
SCHEMA_SECURITY = vol.Schema({vol.Optional(ATTR_PROTECTED): vol.Boolean()})
|
||||
|
||||
|
||||
async def _generate_addon_dict(
|
||||
addon: Addon, *, view: AddonView = AddonView.SIMPLE, stats: bool = False
|
||||
) -> dict[str, Any]:
|
||||
"""Generate dictionary for addon."""
|
||||
data = (
|
||||
{}
|
||||
if view == AddonView.NONE
|
||||
else {
|
||||
ATTR_NAME: addon.name,
|
||||
ATTR_SLUG: addon.slug,
|
||||
ATTR_DESCRIPTON: addon.description,
|
||||
ATTR_ADVANCED: addon.advanced,
|
||||
ATTR_STAGE: addon.stage,
|
||||
ATTR_VERSION: addon.version,
|
||||
ATTR_VERSION_LATEST: addon.latest_version,
|
||||
ATTR_UPDATE_AVAILABLE: addon.need_update,
|
||||
ATTR_AVAILABLE: addon.available,
|
||||
ATTR_DETACHED: addon.is_detached,
|
||||
ATTR_HOMEASSISTANT: addon.homeassistant_version,
|
||||
ATTR_STATE: addon.state,
|
||||
ATTR_REPOSITORY: addon.repository,
|
||||
ATTR_BUILD: addon.need_build,
|
||||
ATTR_URL: addon.url,
|
||||
ATTR_ICON: addon.with_icon,
|
||||
ATTR_LOGO: addon.with_logo,
|
||||
}
|
||||
)
|
||||
|
||||
if view == AddonView.FULL:
|
||||
data |= {
|
||||
ATTR_HOSTNAME: addon.hostname,
|
||||
ATTR_DNS: addon.dns,
|
||||
ATTR_LONG_DESCRIPTION: addon.long_description,
|
||||
ATTR_PROTECTED: addon.protected,
|
||||
ATTR_RATING: rating_security(addon),
|
||||
ATTR_BOOT: addon.boot,
|
||||
ATTR_OPTIONS: addon.options,
|
||||
ATTR_SCHEMA: addon.schema_ui,
|
||||
ATTR_ARCH: addon.supported_arch,
|
||||
ATTR_MACHINE: addon.supported_machine,
|
||||
ATTR_NETWORK: addon.ports,
|
||||
ATTR_NETWORK_DESCRIPTION: addon.ports_description,
|
||||
ATTR_HOST_NETWORK: addon.host_network,
|
||||
ATTR_HOST_PID: addon.host_pid,
|
||||
ATTR_HOST_IPC: addon.host_ipc,
|
||||
ATTR_HOST_UTS: addon.host_uts,
|
||||
ATTR_HOST_DBUS: addon.host_dbus,
|
||||
ATTR_PRIVILEGED: addon.privileged,
|
||||
ATTR_FULL_ACCESS: addon.with_full_access,
|
||||
ATTR_APPARMOR: addon.apparmor,
|
||||
ATTR_CHANGELOG: addon.with_changelog,
|
||||
ATTR_DOCUMENTATION: addon.with_documentation,
|
||||
ATTR_STDIN: addon.with_stdin,
|
||||
ATTR_HASSIO_API: addon.access_hassio_api,
|
||||
ATTR_HASSIO_ROLE: addon.hassio_role,
|
||||
ATTR_AUTH_API: addon.access_auth_api,
|
||||
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
|
||||
ATTR_GPIO: addon.with_gpio,
|
||||
ATTR_USB: addon.with_usb,
|
||||
ATTR_UART: addon.with_uart,
|
||||
ATTR_KERNEL_MODULES: addon.with_kernel_modules,
|
||||
ATTR_DEVICETREE: addon.with_devicetree,
|
||||
ATTR_UDEV: addon.with_udev,
|
||||
ATTR_DOCKER_API: addon.access_docker_api,
|
||||
ATTR_VIDEO: addon.with_video,
|
||||
ATTR_AUDIO: addon.with_audio,
|
||||
ATTR_STARTUP: addon.startup,
|
||||
ATTR_SERVICES: _pretty_services(addon),
|
||||
ATTR_DISCOVERY: addon.discovery,
|
||||
ATTR_TRANSLATIONS: addon.translations,
|
||||
ATTR_INGRESS: addon.with_ingress,
|
||||
ATTR_SIGNED: addon.signed,
|
||||
ATTR_WEBUI: addon.webui,
|
||||
ATTR_INGRESS_ENTRY: addon.ingress_entry,
|
||||
ATTR_INGRESS_URL: addon.ingress_url,
|
||||
ATTR_INGRESS_PORT: addon.ingress_port,
|
||||
ATTR_INGRESS_PANEL: addon.ingress_panel,
|
||||
ATTR_AUDIO_INPUT: addon.audio_input,
|
||||
ATTR_AUDIO_OUTPUT: addon.audio_output,
|
||||
ATTR_AUTO_UPDATE: addon.auto_update,
|
||||
ATTR_IP_ADDRESS: str(addon.ip_address),
|
||||
ATTR_WATCHDOG: addon.watchdog,
|
||||
ATTR_DEVICES: addon.static_devices
|
||||
+ [device.path for device in addon.devices],
|
||||
}
|
||||
|
||||
if stats:
|
||||
stats: DockerStats = await addon.stats()
|
||||
|
||||
data |= {
|
||||
ATTR_CPU_PERCENT: stats.cpu_percent,
|
||||
ATTR_MEMORY_USAGE: stats.memory_usage,
|
||||
ATTR_MEMORY_LIMIT: stats.memory_limit,
|
||||
ATTR_MEMORY_PERCENT: stats.memory_percent,
|
||||
ATTR_NETWORK_RX: stats.network_rx,
|
||||
ATTR_NETWORK_TX: stats.network_tx,
|
||||
ATTR_BLK_READ: stats.blk_read,
|
||||
ATTR_BLK_WRITE: stats.blk_write,
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class APIAddons(CoreSysAttributes):
|
||||
"""Handle RESTful API for add-on functions."""
|
||||
|
||||
|
@ -155,28 +258,14 @@ class APIAddons(CoreSysAttributes):
|
|||
@api_process
|
||||
async def list(self, request: web.Request) -> dict[str, Any]:
|
||||
"""Return all add-ons or repositories."""
|
||||
data_addons = [
|
||||
{
|
||||
ATTR_NAME: addon.name,
|
||||
ATTR_SLUG: addon.slug,
|
||||
ATTR_DESCRIPTON: addon.description,
|
||||
ATTR_ADVANCED: addon.advanced,
|
||||
ATTR_STAGE: addon.stage,
|
||||
ATTR_VERSION: addon.version,
|
||||
ATTR_VERSION_LATEST: addon.latest_version,
|
||||
ATTR_UPDATE_AVAILABLE: addon.need_update,
|
||||
ATTR_AVAILABLE: addon.available,
|
||||
ATTR_DETACHED: addon.is_detached,
|
||||
ATTR_HOMEASSISTANT: addon.homeassistant_version,
|
||||
ATTR_STATE: addon.state,
|
||||
ATTR_REPOSITORY: addon.repository,
|
||||
ATTR_BUILD: addon.need_build,
|
||||
ATTR_URL: addon.url,
|
||||
ATTR_ICON: addon.with_icon,
|
||||
ATTR_LOGO: addon.with_logo,
|
||||
}
|
||||
for addon in self.sys_addons.installed
|
||||
]
|
||||
view = vol.Coerce(AddonView)(request.query.get("view", AddonView.SIMPLE))
|
||||
stats = vol.Boolean()(request.query.get("stats", False))
|
||||
data_addons = await asyncio.gather(
|
||||
*[
|
||||
_generate_addon_dict(addon, view=view, stats=stats)
|
||||
for addon in self.sys_addons.installed
|
||||
]
|
||||
)
|
||||
|
||||
return {ATTR_ADDONS: data_addons}
|
||||
|
||||
|
@ -188,82 +277,10 @@ class APIAddons(CoreSysAttributes):
|
|||
async def info(self, request: web.Request) -> dict[str, Any]:
|
||||
"""Return add-on information."""
|
||||
addon: AnyAddon = self._extract_addon(request)
|
||||
view = vol.Coerce(AddonView)(request.query.get("view", AddonView.FULL))
|
||||
stats = vol.Boolean()(request.query.get("stats", False))
|
||||
|
||||
data = {
|
||||
ATTR_NAME: addon.name,
|
||||
ATTR_SLUG: addon.slug,
|
||||
ATTR_HOSTNAME: addon.hostname,
|
||||
ATTR_DNS: addon.dns,
|
||||
ATTR_DESCRIPTON: addon.description,
|
||||
ATTR_LONG_DESCRIPTION: addon.long_description,
|
||||
ATTR_ADVANCED: addon.advanced,
|
||||
ATTR_STAGE: addon.stage,
|
||||
ATTR_REPOSITORY: addon.repository,
|
||||
ATTR_VERSION_LATEST: addon.latest_version,
|
||||
ATTR_PROTECTED: addon.protected,
|
||||
ATTR_RATING: rating_security(addon),
|
||||
ATTR_BOOT: addon.boot,
|
||||
ATTR_OPTIONS: addon.options,
|
||||
ATTR_SCHEMA: addon.schema_ui,
|
||||
ATTR_ARCH: addon.supported_arch,
|
||||
ATTR_MACHINE: addon.supported_machine,
|
||||
ATTR_HOMEASSISTANT: addon.homeassistant_version,
|
||||
ATTR_URL: addon.url,
|
||||
ATTR_DETACHED: addon.is_detached,
|
||||
ATTR_AVAILABLE: addon.available,
|
||||
ATTR_BUILD: addon.need_build,
|
||||
ATTR_NETWORK: addon.ports,
|
||||
ATTR_NETWORK_DESCRIPTION: addon.ports_description,
|
||||
ATTR_HOST_NETWORK: addon.host_network,
|
||||
ATTR_HOST_PID: addon.host_pid,
|
||||
ATTR_HOST_IPC: addon.host_ipc,
|
||||
ATTR_HOST_UTS: addon.host_uts,
|
||||
ATTR_HOST_DBUS: addon.host_dbus,
|
||||
ATTR_PRIVILEGED: addon.privileged,
|
||||
ATTR_FULL_ACCESS: addon.with_full_access,
|
||||
ATTR_APPARMOR: addon.apparmor,
|
||||
ATTR_ICON: addon.with_icon,
|
||||
ATTR_LOGO: addon.with_logo,
|
||||
ATTR_CHANGELOG: addon.with_changelog,
|
||||
ATTR_DOCUMENTATION: addon.with_documentation,
|
||||
ATTR_STDIN: addon.with_stdin,
|
||||
ATTR_HASSIO_API: addon.access_hassio_api,
|
||||
ATTR_HASSIO_ROLE: addon.hassio_role,
|
||||
ATTR_AUTH_API: addon.access_auth_api,
|
||||
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
|
||||
ATTR_GPIO: addon.with_gpio,
|
||||
ATTR_USB: addon.with_usb,
|
||||
ATTR_UART: addon.with_uart,
|
||||
ATTR_KERNEL_MODULES: addon.with_kernel_modules,
|
||||
ATTR_DEVICETREE: addon.with_devicetree,
|
||||
ATTR_UDEV: addon.with_udev,
|
||||
ATTR_DOCKER_API: addon.access_docker_api,
|
||||
ATTR_VIDEO: addon.with_video,
|
||||
ATTR_AUDIO: addon.with_audio,
|
||||
ATTR_STARTUP: addon.startup,
|
||||
ATTR_SERVICES: _pretty_services(addon),
|
||||
ATTR_DISCOVERY: addon.discovery,
|
||||
ATTR_TRANSLATIONS: addon.translations,
|
||||
ATTR_INGRESS: addon.with_ingress,
|
||||
ATTR_SIGNED: addon.signed,
|
||||
ATTR_STATE: addon.state,
|
||||
ATTR_WEBUI: addon.webui,
|
||||
ATTR_INGRESS_ENTRY: addon.ingress_entry,
|
||||
ATTR_INGRESS_URL: addon.ingress_url,
|
||||
ATTR_INGRESS_PORT: addon.ingress_port,
|
||||
ATTR_INGRESS_PANEL: addon.ingress_panel,
|
||||
ATTR_AUDIO_INPUT: addon.audio_input,
|
||||
ATTR_AUDIO_OUTPUT: addon.audio_output,
|
||||
ATTR_AUTO_UPDATE: addon.auto_update,
|
||||
ATTR_IP_ADDRESS: str(addon.ip_address),
|
||||
ATTR_VERSION: addon.version,
|
||||
ATTR_UPDATE_AVAILABLE: addon.need_update,
|
||||
ATTR_WATCHDOG: addon.watchdog,
|
||||
ATTR_DEVICES: addon.static_devices
|
||||
+ [device.path for device in addon.devices],
|
||||
}
|
||||
|
||||
return data
|
||||
return await _generate_addon_dict(addon, view=view, stats=stats)
|
||||
|
||||
@api_process
|
||||
async def options(self, request: web.Request) -> None:
|
||||
|
@ -371,18 +388,7 @@ class APIAddons(CoreSysAttributes):
|
|||
"""Return resource information."""
|
||||
addon = self._extract_addon(request)
|
||||
|
||||
stats: DockerStats = await addon.stats()
|
||||
|
||||
return {
|
||||
ATTR_CPU_PERCENT: stats.cpu_percent,
|
||||
ATTR_MEMORY_USAGE: stats.memory_usage,
|
||||
ATTR_MEMORY_LIMIT: stats.memory_limit,
|
||||
ATTR_MEMORY_PERCENT: stats.memory_percent,
|
||||
ATTR_NETWORK_RX: stats.network_rx,
|
||||
ATTR_NETWORK_TX: stats.network_tx,
|
||||
ATTR_BLK_READ: stats.blk_read,
|
||||
ATTR_BLK_WRITE: stats.blk_write,
|
||||
}
|
||||
return _generate_addon_dict(addon, view=AddonView.NONE, stats=True)
|
||||
|
||||
@api_process
|
||||
def uninstall(self, request: web.Request) -> Awaitable[None]:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
"""Const for API."""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
CONTENT_TYPE_BINARY = "application/octet-stream"
|
||||
CONTENT_TYPE_JSON = "application/json"
|
||||
CONTENT_TYPE_PNG = "image/png"
|
||||
|
@ -53,3 +55,11 @@ ATTR_UPDATE_TYPE = "update_type"
|
|||
ATTR_USE_NTP = "use_ntp"
|
||||
ATTR_USAGE = "usage"
|
||||
ATTR_VENDOR = "vendor"
|
||||
|
||||
|
||||
class AddonView(StrEnum):
|
||||
"""Addon view."""
|
||||
|
||||
FULL = "full"
|
||||
SIMPLE = "simple"
|
||||
NONE = "none"
|
||||
|
|
|
@ -4,6 +4,7 @@ import asyncio
|
|||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
from aiohttp.test_utils import TestClient
|
||||
import pytest
|
||||
|
||||
from supervisor.addons.addon import Addon
|
||||
from supervisor.addons.build import AddonBuild
|
||||
|
@ -15,7 +16,8 @@ from supervisor.docker.const import ContainerState
|
|||
from supervisor.docker.monitor import DockerContainerStateEvent
|
||||
from supervisor.store.repository import Repository
|
||||
|
||||
from ..const import TEST_ADDON_SLUG
|
||||
from tests.common import load_json_fixture
|
||||
from tests.const import TEST_ADDON_SLUG
|
||||
|
||||
|
||||
def _create_test_event(name: str, state: ContainerState) -> DockerContainerStateEvent:
|
||||
|
@ -28,6 +30,15 @@ def _create_test_event(name: str, state: ContainerState) -> DockerContainerState
|
|||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="container_stats")
|
||||
async def fixture_container_stats(container: MagicMock) -> MagicMock:
|
||||
"""Mock container stats."""
|
||||
container.status = "running"
|
||||
container.stats.return_value = load_json_fixture("container_stats.json")
|
||||
|
||||
yield container
|
||||
|
||||
|
||||
async def test_addons_info(
|
||||
api_client: TestClient, coresys: CoreSys, install_addon_ssh: Addon
|
||||
):
|
||||
|
@ -212,3 +223,46 @@ async def test_api_addon_rebuild_healthcheck(
|
|||
assert state_changes == [AddonState.STOPPED, AddonState.STARTUP]
|
||||
assert install_addon_ssh.state == AddonState.STARTED
|
||||
assert resp.status == 200
|
||||
|
||||
|
||||
async def test_api_addon_list(
|
||||
api_client: TestClient, coresys: CoreSys, install_addon_ssh: Addon, container_stats
|
||||
):
|
||||
"""Test listing addons."""
|
||||
resp = await api_client.get("/addons")
|
||||
result = await resp.json()
|
||||
|
||||
assert len(result["data"]["addons"]) == 1
|
||||
addon_data = result["data"]["addons"][0]
|
||||
assert addon_data["name"] == "Terminal & SSH"
|
||||
assert addon_data["version"] == "9.2.1"
|
||||
assert "hostname" not in addon_data
|
||||
assert "rating" not in addon_data
|
||||
assert "cpu_percent" not in addon_data
|
||||
assert "memory_percent" not in addon_data
|
||||
|
||||
# Now full view
|
||||
resp = await api_client.get("/addons?view=full")
|
||||
result = await resp.json()
|
||||
|
||||
assert len(result["data"]["addons"]) == 1
|
||||
addon_data = result["data"]["addons"][0]
|
||||
assert addon_data["name"] == "Terminal & SSH"
|
||||
assert addon_data["version"] == "9.2.1"
|
||||
assert addon_data["hostname"] == "local-ssh"
|
||||
assert addon_data["rating"] == 6
|
||||
assert "cpu_percent" not in addon_data
|
||||
assert "memory_percent" not in addon_data
|
||||
|
||||
# Now full with stats
|
||||
resp = await api_client.get("/addons?view=full&stats=true")
|
||||
result = await resp.json()
|
||||
|
||||
assert len(result["data"]["addons"]) == 1
|
||||
addon_data = result["data"]["addons"][0]
|
||||
assert addon_data["name"] == "Terminal & SSH"
|
||||
assert addon_data["version"] == "9.2.1"
|
||||
assert addon_data["hostname"] == "local-ssh"
|
||||
assert addon_data["rating"] == 6
|
||||
assert addon_data["cpu_percent"] == 90.0
|
||||
assert addon_data["memory_percent"] == 1.49
|
||||
|
|
Loading…
Reference in New Issue