Add system health section for the Supervisor (#43074)

pull/43114/head
Joakim Sørensen 2020-11-11 20:12:24 +01:00 committed by GitHub
parent 37bcfd1d2f
commit 403514ccb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 354 additions and 44 deletions

View File

@ -42,9 +42,11 @@ CONFIG_SCHEMA = vol.Schema(
)
DATA_INFO = "hassio_info"
DATA_HOST_INFO = "hassio_host_info"
DATA_CORE_INFO = "hassio_core_info"
DATA_HOST_INFO = "hassio_host_info"
DATA_INFO = "hassio_info"
DATA_OS_INFO = "hassio_os_info"
DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
HASSIO_UPDATE_INTERVAL = timedelta(minutes=55)
SERVICE_ADDON_START = "addon_start"
@ -218,6 +220,26 @@ def get_host_info(hass):
return hass.data.get(DATA_HOST_INFO)
@callback
@bind_hass
def get_supervisor_info(hass):
"""Return Supervisor information.
Async friendly.
"""
return hass.data.get(DATA_SUPERVISOR_INFO)
@callback
@bind_hass
def get_os_info(hass):
"""Return OS information.
Async friendly.
"""
return hass.data.get(DATA_OS_INFO)
@callback
@bind_hass
def get_core_info(hass):
@ -358,6 +380,8 @@ async def async_setup(hass, config):
hass.data[DATA_INFO] = await hassio.get_info()
hass.data[DATA_HOST_INFO] = await hassio.get_host_info()
hass.data[DATA_CORE_INFO] = await hassio.get_core_info()
hass.data[DATA_SUPERVISOR_INFO] = await hassio.get_supervisor_info()
hass.data[DATA_OS_INFO] = await hassio.get_os_info()
except HassioAPIError as err:
_LOGGER.warning("Can't read last version: %s", err)

View File

@ -82,6 +82,14 @@ class HassIO:
"""
return self.send_command("/host/info", method="get")
@api_data
def get_os_info(self):
"""Return data for the OS.
This method return a coroutine.
"""
return self.send_command("/os/info", method="get")
@api_data
def get_core_info(self):
"""Return data for Home Asssistant Core.
@ -90,6 +98,14 @@ class HassIO:
"""
return self.send_command("/core/info", method="get")
@api_data
def get_supervisor_info(self):
"""Return data for the Supervisor.
This method returns a coroutine.
"""
return self.send_command("/supervisor/info", method="get")
@api_data
def get_addon_info(self, addon):
"""Return data for a Add-on.

View File

@ -1,6 +1,6 @@
{
"domain": "hassio",
"name": "Hass.io",
"name": "Home Assistant Supervisor",
"documentation": "https://www.home-assistant.io/hassio",
"dependencies": ["http"],
"after_dependencies": ["panel_custom"],

View File

@ -0,0 +1,18 @@
{
"system_health": {
"info": {
"board": "Board",
"disk_total": "Disk Total",
"disk_used": "Disk Used",
"docker_version": "Docker Version",
"healthy": "Healthy",
"host_os": "Host Operating System",
"installed_addons": "Installed Add-ons",
"supervisor_api": "Supervisor API",
"supervisor_version": "Supervisor Version",
"supported": "Supported",
"update_channel": "Update Channel",
"version_api": "Version API"
}
}
}

View File

@ -0,0 +1,72 @@
"""Provide info to system health."""
import os
from homeassistant.components import system_health
from homeassistant.core import HomeAssistant, callback
SUPERVISOR_PING = f"http://{os.environ['HASSIO']}/supervisor/ping"
OBSERVER_URL = f"http://{os.environ['HASSIO']}:4357"
@callback
def async_register(
hass: HomeAssistant, register: system_health.SystemHealthRegistration
) -> None:
"""Register system health callbacks."""
register.async_register_info(system_health_info, "/hassio")
async def system_health_info(hass: HomeAssistant):
"""Get info for the info page."""
info = hass.components.hassio.get_info()
host_info = hass.components.hassio.get_host_info()
supervisor_info = hass.components.hassio.get_supervisor_info()
if supervisor_info.get("healthy"):
healthy = True
else:
healthy = {
"type": "failed",
"error": "Unhealthy",
"more_info": "/hassio/system",
}
if supervisor_info.get("supported"):
supported = True
else:
supported = {
"type": "failed",
"error": "Unsupported",
"more_info": "/hassio/system",
}
information = {
"host_os": host_info.get("operating_system"),
"update_channel": info.get("channel"),
"supervisor_version": info.get("supervisor"),
"docker_version": info.get("docker"),
"disk_total": f"{host_info.get('disk_total')} GB",
"disk_used": f"{host_info.get('disk_used')} GB",
"healthy": healthy,
"supported": supported,
}
if info.get("hassos") is not None:
os_info = hass.components.hassio.get_os_info()
information["board"] = os_info.get("board")
information["supervisor_api"] = system_health.async_check_can_reach_url(
hass, SUPERVISOR_PING, OBSERVER_URL
)
information["version_api"] = system_health.async_check_can_reach_url(
hass,
f"https://version.home-assistant.io/{info.get('channel')}.json",
"/hassio/system",
)
information["installed_addons"] = ", ".join(
f"{addon['name']} ({addon['version']})"
for addon in supervisor_info.get("addons", [])
)
return information

View File

@ -1,3 +1,18 @@
{
"title": "Hass.io"
}
"system_health": {
"info": {
"board": "Board",
"disk_total": "Disk Total",
"disk_used": "Disk Used",
"docker_version": "Docker Version",
"healthy": "Healthy",
"host_os": "Host Operating System",
"installed_addons": "Installed Add-ons",
"supervisor_api": "Supervisor API",
"supervisor_version": "Supervisor Version",
"supported": "Supported",
"update_channel": "Update Channel",
"version_api": "Version API"
}
}
}

View File

@ -1,20 +1,16 @@
{
"system_health": {
"info": {
"installation_type": "Installation Type",
"version": "Version",
"dev": "Development",
"virtualenv": "Virtual Environment",
"python_version": "Python Version",
"docker": "Docker",
"arch": "CPU Architecture",
"timezone": "Timezone",
"os_name": "Operating System Name",
"dev": "Development",
"docker": "Docker",
"installation_type": "Installation Type",
"os_name": "Operating System Family",
"os_version": "Operating System Version",
"supervisor": "Supervisor",
"host_os": "Home Assistant OS",
"chassis": "Chassis",
"docker_version": "Docker"
"python_version": "Python Version",
"timezone": "Timezone",
"version": "Version",
"virtualenv": "Virtual Environment"
}
}
}

View File

@ -15,5 +15,17 @@ def async_register(
async def system_health_info(hass):
"""Get info for the info page."""
info = await system_info.async_get_system_info(hass)
info.pop("hassio")
return info
return {
"version": info.get("version"),
"installation_type": info.get("installation_type"),
"dev": info.get("dev"),
"hassio": info.get("hassio"),
"docker": info.get("docker"),
"virtualenv": info.get("virtualenv"),
"python_version": info.get("python_version"),
"os_name": info.get("os_name"),
"os_version": info.get("os_version"),
"arch": info.get("arch"),
"timezone": info.get("timezone"),
}

View File

@ -1,20 +1,17 @@
{
"system_health": {
"info": {
"arch": "CPU Architecture",
"chassis": "Chassis",
"dev": "Development",
"docker": "Docker",
"docker_version": "Docker",
"host_os": "Home Assistant OS",
"installation_type": "Installation Type",
"os_name": "Operating System Name",
"os_version": "Operating System Version",
"python_version": "Python Version",
"supervisor": "Supervisor",
"timezone": "Timezone",
"version": "Version",
"virtualenv": "Virtual Environment"
}
"system_health": {
"info": {
"arch": "CPU Architecture",
"dev": "Development",
"docker": "Docker",
"installation_type": "Installation Type",
"os_name": "Operating System Family",
"os_version": "Operating System Version",
"python_version": "Python Version",
"timezone": "Timezone",
"version": "Version",
"virtualenv": "Virtual Environment"
}
}
},
"title": "Home Assistant"
}

View File

@ -80,6 +80,39 @@ async def test_api_host_info(hassio_handler, aioclient_mock):
assert data["operating_system"] == "Debian GNU/Linux 10 (buster)"
async def test_api_supervisor_info(hassio_handler, aioclient_mock):
"""Test setup with API Supervisor info."""
aioclient_mock.get(
"http://127.0.0.1/supervisor/info",
json={
"result": "ok",
"data": {"supported": True, "version": "2020.11.1", "channel": "stable"},
},
)
data = await hassio_handler.get_supervisor_info()
assert aioclient_mock.call_count == 1
assert data["supported"]
assert data["version"] == "2020.11.1"
assert data["channel"] == "stable"
async def test_api_os_info(hassio_handler, aioclient_mock):
"""Test setup with API OS info."""
aioclient_mock.get(
"http://127.0.0.1/os/info",
json={
"result": "ok",
"data": {"board": "odroid-n2", "version": "2020.11.1"},
},
)
data = await hassio_handler.get_os_info()
assert aioclient_mock.call_count == 1
assert data["board"] == "odroid-n2"
assert data["version"] == "2020.11.1"
async def test_api_host_info_error(hassio_handler, aioclient_mock):
"""Test setup with API Home Assistant info error."""
aioclient_mock.get(

View File

@ -44,6 +44,14 @@ def mock_all(aioclient_mock):
"http://127.0.0.1/core/info",
json={"result": "ok", "data": {"version_latest": "1.0.0"}},
)
aioclient_mock.get(
"http://127.0.0.1/os/info",
json={"result": "ok", "data": {"version_latest": "1.0.0"}},
)
aioclient_mock.get(
"http://127.0.0.1/supervisor/info",
json={"result": "ok", "data": {"version_latest": "1.0.0"}},
)
aioclient_mock.get(
"http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}}
)
@ -55,7 +63,7 @@ async def test_setup_api_ping(hass, aioclient_mock):
result = await async_setup_component(hass, "hassio", {})
assert result
assert aioclient_mock.call_count == 7
assert aioclient_mock.call_count == 9
assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0"
assert hass.components.hassio.is_hassio()
@ -94,7 +102,7 @@ async def test_setup_api_push_api_data(hass, aioclient_mock):
)
assert result
assert aioclient_mock.call_count == 7
assert aioclient_mock.call_count == 9
assert not aioclient_mock.mock_calls[1][2]["ssl"]
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
assert aioclient_mock.mock_calls[1][2]["watchdog"]
@ -110,7 +118,7 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock):
)
assert result
assert aioclient_mock.call_count == 7
assert aioclient_mock.call_count == 9
assert not aioclient_mock.mock_calls[1][2]["ssl"]
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
assert not aioclient_mock.mock_calls[1][2]["watchdog"]
@ -122,7 +130,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag
result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}})
assert result
assert aioclient_mock.call_count == 7
assert aioclient_mock.call_count == 9
assert not aioclient_mock.mock_calls[1][2]["ssl"]
assert aioclient_mock.mock_calls[1][2]["port"] == 8123
refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"]
@ -169,7 +177,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage
result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}})
assert result
assert aioclient_mock.call_count == 7
assert aioclient_mock.call_count == 9
assert not aioclient_mock.mock_calls[1][2]["ssl"]
assert aioclient_mock.mock_calls[1][2]["port"] == 8123
assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token
@ -183,7 +191,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock):
result = await async_setup_component(hass, "hassio", {"hassio": {}})
assert result
assert aioclient_mock.call_count == 7
assert aioclient_mock.call_count == 9
assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone"
with patch("homeassistant.util.dt.set_default_time_zone"):
@ -200,7 +208,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock):
result = await async_setup_component(hass, "hassio", {"hassio": {}})
assert result
assert aioclient_mock.call_count == 7
assert aioclient_mock.call_count == 9
assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456"

View File

@ -0,0 +1,113 @@
"""Test hassio system health."""
import asyncio
import os
from aiohttp import ClientError
from homeassistant.setup import async_setup_component
from .test_init import MOCK_ENVIRON
from tests.async_mock import patch
from tests.common import get_system_health_info
async def test_hassio_system_health(hass, aioclient_mock):
"""Test hassio system health."""
aioclient_mock.get("http://127.0.0.1/info", json={"result": "ok", "data": {}})
aioclient_mock.get("http://127.0.0.1/host/info", json={"result": "ok", "data": {}})
aioclient_mock.get("http://127.0.0.1/os/info", json={"result": "ok", "data": {}})
aioclient_mock.get("http://127.0.0.1/supervisor/ping", text="")
aioclient_mock.get("https://version.home-assistant.io/stable.json", text="")
aioclient_mock.get(
"http://127.0.0.1/supervisor/info", json={"result": "ok", "data": {}}
)
hass.config.components.add("hassio")
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(hass, "system_health", {})
hass.data["hassio_info"] = {
"channel": "stable",
"supervisor": "2020.11.1",
"docker": "19.0.3",
"hassos": True,
}
hass.data["hassio_host_info"] = {
"operating_system": "Home Assistant OS 5.9",
"disk_total": "32.0",
"disk_used": "30.0",
}
hass.data["hassio_os_info"] = {"board": "odroid-n2"}
hass.data["hassio_supervisor_info"] = {
"healthy": True,
"supported": True,
"addons": [{"name": "Awesome Addon", "version": "1.0.0"}],
}
info = await get_system_health_info(hass, "hassio")
for key, val in info.items():
if asyncio.iscoroutine(val):
info[key] = await val
assert info == {
"board": "odroid-n2",
"disk_total": "32.0 GB",
"disk_used": "30.0 GB",
"docker_version": "19.0.3",
"healthy": True,
"host_os": "Home Assistant OS 5.9",
"installed_addons": "Awesome Addon (1.0.0)",
"supervisor_api": "ok",
"supervisor_version": "2020.11.1",
"supported": True,
"update_channel": "stable",
"version_api": "ok",
}
async def test_hassio_system_health_with_issues(hass, aioclient_mock):
"""Test hassio system health."""
aioclient_mock.get("http://127.0.0.1/info", json={"result": "ok", "data": {}})
aioclient_mock.get("http://127.0.0.1/host/info", json={"result": "ok", "data": {}})
aioclient_mock.get("http://127.0.0.1/os/info", json={"result": "ok", "data": {}})
aioclient_mock.get("http://127.0.0.1/supervisor/ping", text="")
aioclient_mock.get("https://version.home-assistant.io/stable.json", exc=ClientError)
aioclient_mock.get(
"http://127.0.0.1/supervisor/info", json={"result": "ok", "data": {}}
)
hass.config.components.add("hassio")
with patch.dict(os.environ, MOCK_ENVIRON):
assert await async_setup_component(hass, "system_health", {})
hass.data["hassio_info"] = {"channel": "stable"}
hass.data["hassio_host_info"] = {}
hass.data["hassio_os_info"] = {}
hass.data["hassio_supervisor_info"] = {
"healthy": False,
"supported": False,
}
info = await get_system_health_info(hass, "hassio")
for key, val in info.items():
if asyncio.iscoroutine(val):
info[key] = await val
assert info["healthy"] == {
"error": "Unhealthy",
"more_info": "/hassio/system",
"type": "failed",
}
assert info["supported"] == {
"error": "Unsupported",
"more_info": "/hassio/system",
"type": "failed",
}
assert info["version_api"] == {
"error": "unreachable",
"more_info": "/hassio/system",
"type": "failed",
}

View File

@ -72,6 +72,12 @@ async def mock_supervisor_fixture(hass, aioclient_mock):
), patch(
"homeassistant.components.hassio.HassIO.get_host_info",
return_value={},
), patch(
"homeassistant.components.hassio.HassIO.get_supervisor_info",
return_value={},
), patch(
"homeassistant.components.hassio.HassIO.get_os_info",
return_value={},
), patch(
"homeassistant.components.hassio.HassIO.get_ingress_panels",
return_value={"panels": {}},