mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-12-11 04:12:26 -06:00
* Simplify ensure_access_token Make the caller of ensure_access_token responsible for connection error handling. This is especially useful for API connection checks, as it avoids an extra call to the API (if we fail to connect when refreshing the token there is no point in calling the API to check if it is up). Document the change in the docstring. Also avoid the overhead of creating a Job object. We can simply use an asyncio.Lock() to ensure only one coroutine is refreshing the token at a time. This also avoids Job interference in Exception handling. * Remove check_port from API checks Remove check_port usage from Home Assistant API connection checks. Simply rely on errors raised from actual connection attempts. During a Supervisor startup when Home Assistant Core is running (e.g. after a Supervisor update) we make about 10 successful API checks. The old code path did a port check and then a connection check, causing two socket creation. The new code without the separate port check safes 10 socket creations per startup (the aiohttp connections are reused, hence do not cause only one socket creation). * Log API exceptions on call site Since make_request is no longer logging API exceptions on its own, we need to log them where we call make_request. This approach gives the user more context about what Supervisor was trying to do when the error happened. * Avoid unnecessary nesting * Improve error when ingress panel update fails * Add comment about fast path
209 lines
6.7 KiB
Python
209 lines
6.7 KiB
Python
"""Fetch last versions from webserver."""
|
|
|
|
from datetime import timedelta
|
|
import logging
|
|
import random
|
|
import secrets
|
|
|
|
from .addons.addon import Addon
|
|
from .const import (
|
|
ATTR_PORTS,
|
|
ATTR_SESSION,
|
|
ATTR_SESSION_DATA,
|
|
FILE_HASSIO_INGRESS,
|
|
IngressSessionData,
|
|
IngressSessionDataDict,
|
|
)
|
|
from .coresys import CoreSys, CoreSysAttributes
|
|
from .exceptions import HomeAssistantAPIError
|
|
from .utils import check_port
|
|
from .utils.common import FileConfiguration
|
|
from .utils.dt import utc_from_timestamp, utcnow
|
|
from .validate import SCHEMA_INGRESS_CONFIG
|
|
|
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Ingress(FileConfiguration, CoreSysAttributes):
|
|
"""Fetch last versions from version.json."""
|
|
|
|
def __init__(self, coresys: CoreSys):
|
|
"""Initialize updater."""
|
|
super().__init__(FILE_HASSIO_INGRESS, SCHEMA_INGRESS_CONFIG)
|
|
self.coresys: CoreSys = coresys
|
|
self.tokens: dict[str, str] = {}
|
|
|
|
def get(self, token: str) -> Addon | None:
|
|
"""Return addon they have this ingress token."""
|
|
if token not in self.tokens:
|
|
return None
|
|
return self.sys_addons.get_local_only(self.tokens[token])
|
|
|
|
def get_session_data(self, session_id: str) -> IngressSessionData | None:
|
|
"""Return complementary data of current session or None."""
|
|
if data := self.sessions_data.get(session_id):
|
|
return IngressSessionData.from_dict(data)
|
|
return None
|
|
|
|
@property
|
|
def sessions(self) -> dict[str, float]:
|
|
"""Return sessions."""
|
|
return self._data[ATTR_SESSION]
|
|
|
|
@property
|
|
def sessions_data(self) -> dict[str, IngressSessionDataDict]:
|
|
"""Return sessions_data."""
|
|
return self._data[ATTR_SESSION_DATA]
|
|
|
|
@property
|
|
def ports(self) -> dict[str, int]:
|
|
"""Return list of dynamic ports."""
|
|
return self._data[ATTR_PORTS]
|
|
|
|
@property
|
|
def addons(self) -> list[Addon]:
|
|
"""Return list of ingress Add-ons."""
|
|
addons = []
|
|
for addon in self.sys_addons.installed:
|
|
if not addon.with_ingress:
|
|
continue
|
|
addons.append(addon)
|
|
return addons
|
|
|
|
async def load(self) -> None:
|
|
"""Update internal data."""
|
|
self._update_token_list()
|
|
self._cleanup_sessions()
|
|
|
|
_LOGGER.info("Loaded %d ingress sessions", len(self.sessions))
|
|
|
|
async def reload(self) -> None:
|
|
"""Reload/Validate sessions."""
|
|
self._cleanup_sessions()
|
|
self._update_token_list()
|
|
|
|
async def unload(self) -> None:
|
|
"""Shutdown sessions."""
|
|
await self.save_data()
|
|
|
|
def _cleanup_sessions(self) -> None:
|
|
"""Remove not used sessions."""
|
|
now = utcnow()
|
|
|
|
sessions = {}
|
|
sessions_data: dict[str, IngressSessionDataDict] = {}
|
|
for session, valid in self.sessions.items():
|
|
# check if timestamp valid, to avoid crash on malformed timestamp
|
|
try:
|
|
valid_dt = utc_from_timestamp(valid)
|
|
except OverflowError:
|
|
_LOGGER.warning("Session timestamp %f is invalid!", valid)
|
|
continue
|
|
|
|
if valid_dt < now:
|
|
continue
|
|
|
|
# Is valid
|
|
sessions[session] = valid
|
|
if session_data := self.sessions_data.get(session):
|
|
sessions_data[session] = session_data
|
|
|
|
# Write back
|
|
self.sessions.clear()
|
|
self.sessions.update(sessions)
|
|
self.sessions_data.clear()
|
|
self.sessions_data.update(sessions_data)
|
|
|
|
def _update_token_list(self) -> None:
|
|
"""Regenerate token <-> Add-on map."""
|
|
self.tokens.clear()
|
|
|
|
# Read all ingress token and build a map
|
|
for addon in self.addons:
|
|
if addon.ingress_token:
|
|
self.tokens[addon.ingress_token] = addon.slug
|
|
|
|
def create_session(self, data: IngressSessionData | None = None) -> str:
|
|
"""Create new session."""
|
|
session = secrets.token_hex(64)
|
|
valid = utcnow() + timedelta(minutes=15)
|
|
|
|
self.sessions[session] = valid.timestamp()
|
|
if data is not None:
|
|
self.sessions_data[session] = data.to_dict()
|
|
|
|
return session
|
|
|
|
def validate_session(self, session: str) -> bool:
|
|
"""Return True if session valid and make it longer valid."""
|
|
if session not in self.sessions:
|
|
_LOGGER.debug("Session %s is not known", session)
|
|
return False
|
|
|
|
# check if timestamp valid, to avoid crash on malformed timestamp
|
|
try:
|
|
valid_until = utc_from_timestamp(self.sessions[session])
|
|
except OverflowError:
|
|
self.sessions[session] = (utcnow() + timedelta(minutes=15)).timestamp()
|
|
return True
|
|
|
|
# Is still valid?
|
|
if valid_until < utcnow():
|
|
_LOGGER.debug("Session is no longer valid (%f/%f)", valid_until, utcnow())
|
|
return False
|
|
|
|
# Update time
|
|
valid_until = valid_until + timedelta(minutes=15)
|
|
self.sessions[session] = valid_until.timestamp()
|
|
|
|
return True
|
|
|
|
async def get_dynamic_port(self, addon_slug: str) -> int:
|
|
"""Get/Create a dynamic port from range."""
|
|
if addon_slug in self.ports:
|
|
return self.ports[addon_slug]
|
|
|
|
port = None
|
|
while (
|
|
port is None
|
|
or port in self.ports.values()
|
|
or await check_port(self.sys_docker.network.gateway, port)
|
|
):
|
|
port = random.randint(62000, 65500)
|
|
|
|
# Save port for next time
|
|
self.ports[addon_slug] = port
|
|
await self.save_data()
|
|
return port
|
|
|
|
async def del_dynamic_port(self, addon_slug: str) -> None:
|
|
"""Remove a previously assigned dynamic port."""
|
|
if addon_slug not in self.ports:
|
|
return
|
|
|
|
del self.ports[addon_slug]
|
|
await self.save_data()
|
|
|
|
async def update_hass_panel(self, addon: Addon):
|
|
"""Return True if Home Assistant up and running."""
|
|
if not await self.sys_homeassistant.core.is_running():
|
|
_LOGGER.debug("Ignoring panel update on Core")
|
|
return
|
|
|
|
# Update UI
|
|
method = "post" if addon.ingress_panel else "delete"
|
|
try:
|
|
async with self.sys_homeassistant.api.make_request(
|
|
method, f"api/hassio_push/panel/{addon.slug}"
|
|
) as resp:
|
|
if resp.status in (200, 201):
|
|
_LOGGER.info("Update Ingress as panel for %s", addon.slug)
|
|
else:
|
|
_LOGGER.warning(
|
|
"Failed to update the Ingress panel for %s with %i",
|
|
addon.slug,
|
|
resp.status,
|
|
)
|
|
except HomeAssistantAPIError as err:
|
|
_LOGGER.error("Panel update request failed for %s: %s", addon.slug, err)
|