1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2025-12-21 02:49:41 +00:00
Files
supervisor/supervisor/core.py
Stefan Agner 8e4a87c751 Load Home Assistant OS component earlier (#6068)
Load the Home Assistant OS component earlier in the Supervisor
lifecycle to ensure that updater has board information available
when checking for updates. This makes sure that we have the latest
OS update information right on Supervisor start.
2025-08-06 10:53:30 +02:00

442 lines
16 KiB
Python

"""Main file for Supervisor."""
import asyncio
from collections.abc import Awaitable
from contextlib import suppress
from datetime import timedelta
import logging
from typing import Self
from .const import (
ATTR_STARTUP,
RUN_SUPERVISOR_STATE,
STARTING_STATES,
AddonStartup,
BusEvent,
CoreState,
)
from .coresys import CoreSys, CoreSysAttributes
from .exceptions import (
HassioError,
HomeAssistantCrashError,
HomeAssistantError,
SupervisorUpdateError,
WhoamiError,
WhoamiSSLError,
)
from .homeassistant.core import LANDINGPAGE
from .resolution.const import ContextType, IssueType, SuggestionType, UnhealthyReason
from .utils.dt import utcnow
from .utils.sentry import async_capture_exception
from .utils.whoami import retrieve_whoami
_LOGGER: logging.Logger = logging.getLogger(__name__)
class Core(CoreSysAttributes):
"""Main object of Supervisor."""
def __init__(self, coresys: CoreSys) -> None:
"""Initialize Supervisor object."""
self.coresys: CoreSys = coresys
self._state: CoreState = CoreState.INITIALIZE
self.exit_code: int = 0
@property
def state(self) -> CoreState:
"""Return state of the core."""
return self._state
@property
def supported(self) -> bool:
"""Return true if the installation is supported."""
return len(self.sys_resolution.unsupported) == 0
@property
def healthy(self) -> bool:
"""Return true if the installation is healthy."""
return len(self.sys_resolution.unhealthy) == 0
async def _write_run_state(self):
"""Write run state for s6 service supervisor."""
try:
await self.sys_run_in_executor(
RUN_SUPERVISOR_STATE.write_text, str(self._state), encoding="utf-8"
)
except OSError as err:
_LOGGER.warning(
"Can't update the Supervisor state to %s: %s", self._state, err
)
async def post_init(self) -> Self:
"""Post init actions that must be done in event loop."""
await self._write_run_state()
return self
async def set_state(self, new_state: CoreState) -> None:
"""Set core into new state."""
if self._state == new_state:
return
self._state = new_state
await self._write_run_state()
# Don't attempt to notify anyone on CLOSE as we're about to stop the event loop
if self._state != CoreState.CLOSE:
self.sys_bus.fire_event(BusEvent.SUPERVISOR_STATE_CHANGE, self._state)
# These will be received by HA after startup has completed which won't make sense
if self._state not in STARTING_STATES:
self.sys_homeassistant.websocket.supervisor_update_event(
"info", {"state": self._state}
)
async def connect(self) -> None:
"""Connect Supervisor container."""
# Load information from container
await self.sys_supervisor.load()
# Evaluate the system
await self.sys_resolution.evaluate.evaluate_system()
# Check supervisor version/update
if self.sys_config.version == self.sys_supervisor.version:
return
# Somethings going wrong
_LOGGER.error(
"Update '%s' of Supervisor '%s' failed!",
self.sys_config.version,
self.sys_supervisor.version,
)
if self.sys_supervisor.need_update:
self.sys_resolution.create_issue(
IssueType.UPDATE_ROLLBACK, ContextType.SUPERVISOR
)
self.sys_resolution.add_unhealthy_reason(UnhealthyReason.SUPERVISOR)
# Fix wrong version in config / avoid boot loop on OS
self.sys_config.version = self.sys_supervisor.version
await self.sys_config.save_data()
async def setup(self) -> None:
"""Start setting up supervisor orchestration."""
await self.set_state(CoreState.SETUP)
# Initialize websession early. At this point we'll use the Docker DNS proxy
# at 127.0.0.11, which does not have the fallback feature and hence might
# fail in certain environments. But a websession is required to get the
# initial version information after a device wipe or otherwise empty state
# (e.g. CI environment, Supervised).
#
# An OS installation has the plug-in container images pre-installed, so we
# setup can continue even if this early websession fails to connect to the
# internet. We'll reinitialize the websession when the DNS plug-in is up to
# make sure the DNS plug-in along with its fallback capabilities is used
# (see #5857).
await self.coresys.init_websession()
# Check internet on startup
await self.sys_supervisor.check_connectivity()
# Order can be important!
setup_loads: list[Awaitable[None]] = [
# rest api views
self.sys_api.load(),
# Load Host Hardware
self.sys_hardware.load(),
# Load DBus
self.sys_dbus.load(),
# Load Host
self.sys_host.load(),
# Load HassOS
self.sys_os.load(),
# Adjust timezone / time settings
self._adjust_system_datetime(),
# Load mounts
self.sys_mounts.load(),
# Load Docker manager
self.sys_docker.load(),
# load last available data
self.sys_updater.load(),
# Load Plugins container
self.sys_plugins.load(),
# Load Home Assistant
self.sys_homeassistant.load(),
# Load CPU/Arch
self.sys_arch.load(),
# Load Stores
self.sys_store.load(),
# Load Add-ons
self.sys_addons.load(),
# load last available data
self.sys_backups.load(),
# load services
self.sys_services.load(),
# Load discovery
self.sys_discovery.load(),
# Load ingress
self.sys_ingress.load(),
# Load Resoulution
self.sys_resolution.load(),
]
# Execute each load task in secure context
for setup_task in setup_loads:
try:
await setup_task
except Exception as err: # pylint: disable=broad-except
_LOGGER.critical(
"Fatal error happening on load Task %s: %s",
setup_task,
err,
exc_info=True,
)
self.sys_resolution.add_unhealthy_reason(UnhealthyReason.SETUP)
await async_capture_exception(err)
# Set OS Agent diagnostics if needed
if (
self.sys_config.diagnostics is not None
and self.sys_dbus.agent.diagnostics != self.sys_config.diagnostics
and not self.sys_dev
and self.supported
):
try:
await self.sys_dbus.agent.set_diagnostics(self.sys_config.diagnostics)
except Exception as err: # pylint: disable=broad-except
_LOGGER.warning(
"Could not set diagnostics to %s due to %s",
self.sys_config.diagnostics,
err,
)
await async_capture_exception(err)
# Evaluate the system
await self.sys_resolution.evaluate.evaluate_system()
async def start(self) -> None:
"""Start Supervisor orchestration."""
await self.set_state(CoreState.STARTUP)
# Check if system is healthy
if not self.supported:
_LOGGER.warning("System running in a unsupported environment!")
if not self.healthy:
_LOGGER.critical(
"System is running in an unhealthy state and needs manual intervention!"
)
# Mark booted partition as healthy
await self.sys_os.mark_healthy()
# On release channel, try update itself if auto update enabled
if self.sys_supervisor.need_update and self.sys_updater.auto_update:
if not self.healthy:
_LOGGER.warning("Ignoring Supervisor updates!")
else:
with suppress(SupervisorUpdateError):
await self.sys_supervisor.update()
return
try:
# Start addon mark as initialize
await self.sys_addons.boot(AddonStartup.INITIALIZE)
# HomeAssistant is already running, only Supervisor restarted
if await self.sys_hardware.helper.last_boot() == self.sys_config.last_boot:
_LOGGER.info("Detected Supervisor restart")
return
# reset register services / discovery
await self.sys_services.reset()
# start addon mark as system
await self.sys_addons.boot(AddonStartup.SYSTEM)
# start addon mark as services
await self.sys_addons.boot(AddonStartup.SERVICES)
# run HomeAssistant
if (
self.sys_homeassistant.boot
and not await self.sys_homeassistant.core.is_running()
):
_LOGGER.info("Start Home Assistant Core")
try:
await self.sys_homeassistant.core.start()
except HomeAssistantCrashError as err:
_LOGGER.error("Can't start Home Assistant Core - rebuiling")
await async_capture_exception(err)
with suppress(HomeAssistantError):
await self.sys_homeassistant.core.rebuild()
except HomeAssistantError as err:
await async_capture_exception(err)
else:
_LOGGER.info("Skipping start of Home Assistant")
# Core is not running
if self.sys_homeassistant.core.error_state:
self.sys_resolution.create_issue(
IssueType.FATAL_ERROR,
ContextType.CORE,
suggestions=[SuggestionType.EXECUTE_REPAIR],
)
# start addon mark as application
await self.sys_addons.boot(AddonStartup.APPLICATION)
# store new last boot
await self._update_last_boot()
finally:
# Add core tasks into scheduler
await self.sys_tasks.load()
# If landingpage / run upgrade in background
if self.sys_homeassistant.version == LANDINGPAGE:
self.sys_create_task(self.sys_homeassistant.core.install())
# Upate Host/Deivce information
self.sys_create_task(self.sys_host.reload())
self.sys_create_task(self.sys_updater.reload())
self.sys_create_task(self.sys_resolution.healthcheck())
await self.set_state(CoreState.RUNNING)
self.sys_homeassistant.websocket.supervisor_update_event(
"supervisor", {ATTR_STARTUP: "complete"}
)
_LOGGER.info("Supervisor is up and running")
async def stop(self) -> None:
"""Stop a running orchestration."""
# store new last boot / prevent time adjustments
if self.state in (CoreState.RUNNING, CoreState.SHUTDOWN):
await self._update_last_boot()
if self.state in (CoreState.STOPPING, CoreState.CLOSE):
return
# don't process scheduler anymore
await self.set_state(CoreState.STOPPING)
# Stage 1
try:
async with asyncio.timeout(10):
await asyncio.wait(
[
self.sys_create_task(coro)
for coro in (
self.sys_api.stop(),
self.sys_scheduler.shutdown(),
self.sys_docker.unload(),
)
]
)
except TimeoutError:
_LOGGER.warning("Stage 1: Force Shutdown!")
# Stage 2
try:
async with asyncio.timeout(10):
await asyncio.wait(
[
self.sys_create_task(coro)
for coro in (
self.sys_websession.close(),
self.sys_ingress.unload(),
self.sys_hardware.unload(),
self.sys_dbus.unload(),
)
]
)
except TimeoutError:
_LOGGER.warning("Stage 2: Force Shutdown!")
await self.set_state(CoreState.CLOSE)
_LOGGER.info("Supervisor is down - %d", self.exit_code)
self.sys_loop.stop()
async def shutdown(self, *, remove_homeassistant_container: bool = False) -> None:
"""Shutdown all running containers in correct order."""
# don't process scheduler anymore
if self.state == CoreState.RUNNING:
await self.set_state(CoreState.SHUTDOWN)
# Shutdown Application Add-ons, using Home Assistant API
await self.sys_addons.shutdown(AddonStartup.APPLICATION)
# Close Home Assistant
with suppress(HassioError):
await self.sys_homeassistant.core.stop(
remove_container=remove_homeassistant_container
)
# Shutdown System Add-ons
await self.sys_addons.shutdown(AddonStartup.SERVICES)
await self.sys_addons.shutdown(AddonStartup.SYSTEM)
await self.sys_addons.shutdown(AddonStartup.INITIALIZE)
# Shutdown all Plugins
if self.state in (CoreState.STOPPING, CoreState.SHUTDOWN):
await self.sys_plugins.shutdown()
async def _update_last_boot(self) -> None:
"""Update last boot time."""
if not (last_boot := await self.sys_hardware.helper.last_boot()):
_LOGGER.error("Could not update last boot information!")
return
self.sys_config.last_boot = last_boot
await self.sys_config.save_data()
async def _adjust_system_datetime(self) -> None:
"""Adjust system time/date on startup."""
# If no timezone is detect or set
# If we are not connected or time sync
if (
self.sys_config.timezone
or self.sys_host.info.timezone not in ("Etc/UTC", None)
) and self.sys_host.info.dt_synchronized:
return
# Get Timezone data
try:
try:
data = await retrieve_whoami(self.sys_websession, True)
except WhoamiSSLError:
# SSL Date Issue & possible time drift
_LOGGER.info("Whoami service SSL error")
data = await retrieve_whoami(self.sys_websession, False)
except WhoamiError as err:
_LOGGER.warning("Can't adjust Time/Date settings: %s", err)
return
await self.sys_config.set_timezone(self.sys_config.timezone or data.timezone)
# Calculate if system time is out of sync
delta = data.dt_utc - utcnow()
if delta <= timedelta(days=3) or self.sys_host.info.dt_synchronized:
return
_LOGGER.warning("System time/date shift over more than 3 days found!")
await self.sys_host.control.set_datetime(data.dt_utc)
await self.sys_supervisor.check_connectivity()
async def repair(self) -> None:
"""Repair system integrity."""
_LOGGER.info("Starting repair of Supervisor Environment")
await self.sys_run_in_executor(self.sys_docker.repair)
# Fix plugins
await self.sys_plugins.repair()
# Restore core functionality
await self.sys_addons.repair()
await self.sys_homeassistant.core.repair()
# Tag version for latest
await self.sys_supervisor.repair()
_LOGGER.info("Finished repair of Supervisor Environment")