diff --git a/supervisor/addons/manager.py b/supervisor/addons/manager.py index 93dd9968a..8616fda50 100644 --- a/supervisor/addons/manager.py +++ b/supervisor/addons/manager.py @@ -67,6 +67,10 @@ class AddonManager(CoreSysAttributes): return self.store.get(addon_slug) return None + def get_local_only(self, addon_slug: str) -> Addon | None: + """Return an installed add-on from slug.""" + return self.local.get(addon_slug) + def from_token(self, token: str) -> Addon | None: """Return an add-on from Supervisor token.""" for addon in self.installed: diff --git a/supervisor/api/discovery.py b/supervisor/api/discovery.py index 22040edc0..82e37c0e7 100644 --- a/supervisor/api/discovery.py +++ b/supervisor/api/discovery.py @@ -1,7 +1,7 @@ """Init file for Supervisor network RESTful API.""" import logging -from typing import Any, cast +from typing import Any from aiohttp import web import voluptuous as vol @@ -56,8 +56,8 @@ class APIDiscovery(CoreSysAttributes): } for message in self.sys_discovery.list_messages if ( - discovered := cast( - Addon, self.sys_addons.get(message.addon, local_only=True) + discovered := self.sys_addons.get_local_only( + message.addon, ) ) and discovered.state == AddonState.STARTED diff --git a/supervisor/api/store.py b/supervisor/api/store.py index 5887a5618..3bcb41891 100644 --- a/supervisor/api/store.py +++ b/supervisor/api/store.py @@ -126,9 +126,7 @@ class APIStore(CoreSysAttributes): """Generate addon information.""" installed = ( - cast(Addon, self.sys_addons.get(addon.slug, local_only=True)) - if addon.is_installed - else None + self.sys_addons.get_local_only(addon.slug) if addon.is_installed else None ) data = { diff --git a/supervisor/ingress.py b/supervisor/ingress.py index 834a518cb..3a30fb39d 100644 --- a/supervisor/ingress.py +++ b/supervisor/ingress.py @@ -35,7 +35,7 @@ class Ingress(FileConfiguration, CoreSysAttributes): """Return addon they have this ingress token.""" if token not in self.tokens: return None - return self.sys_addons.get(self.tokens[token], local_only=True) + 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.""" diff --git a/supervisor/plugins/base.py b/supervisor/plugins/base.py index b1252817f..070db433b 100644 --- a/supervisor/plugins/base.py +++ b/supervisor/plugins/base.py @@ -63,7 +63,11 @@ class PluginBase(ABC, FileConfiguration, CoreSysAttributes): def need_update(self) -> bool: """Return True if an update is available.""" try: - return self.version < self.latest_version + return ( + self.version is not None + and self.latest_version is not None + and self.version < self.latest_version + ) except (AwesomeVersionException, TypeError): return False @@ -153,6 +157,10 @@ class PluginBase(ABC, FileConfiguration, CoreSysAttributes): async def start(self) -> None: """Start system plugin.""" + @abstractmethod + async def stop(self) -> None: + """Stop system plugin.""" + async def load(self) -> None: """Load system plugin.""" self.start_watchdog() diff --git a/supervisor/plugins/cli.py b/supervisor/plugins/cli.py index d6758e46f..782113115 100644 --- a/supervisor/plugins/cli.py +++ b/supervisor/plugins/cli.py @@ -6,6 +6,7 @@ Code: https://github.com/home-assistant/plugin-cli from collections.abc import Awaitable import logging import secrets +from typing import cast from awesomeversion import AwesomeVersion @@ -55,7 +56,7 @@ class PluginCli(PluginBase): @property def supervisor_token(self) -> str: """Return an access token for the Supervisor API.""" - return self._data.get(ATTR_ACCESS_TOKEN) + return cast(str, self._data[ATTR_ACCESS_TOKEN]) @Job( name="plugin_cli_update", diff --git a/supervisor/plugins/dns.py b/supervisor/plugins/dns.py index 5da4254a6..c10327e7d 100644 --- a/supervisor/plugins/dns.py +++ b/supervisor/plugins/dns.py @@ -71,8 +71,8 @@ class PluginDns(PluginBase): self.slug = "dns" self.coresys: CoreSys = coresys self.instance: DockerDNS = DockerDNS(coresys) - self.resolv_template: jinja2.Template | None = None - self.hosts_template: jinja2.Template | None = None + self._resolv_template: jinja2.Template | None = None + self._hosts_template: jinja2.Template | None = None self._hosts: list[HostEntry] = [] self._loop: bool = False @@ -147,11 +147,25 @@ class PluginDns(PluginBase): """Set fallback DNS enabled.""" self._data[ATTR_FALLBACK] = value + @property + def hosts_template(self) -> jinja2.Template: + """Get hosts jinja template.""" + if not self._hosts_template: + raise RuntimeError("Hosts template not set!") + return self._hosts_template + + @property + def resolv_template(self) -> jinja2.Template: + """Get resolv jinja template.""" + if not self._resolv_template: + raise RuntimeError("Resolv template not set!") + return self._resolv_template + async def load(self) -> None: """Load DNS setup.""" # Initialize CoreDNS Template try: - self.resolv_template = jinja2.Template( + self._resolv_template = jinja2.Template( await self.sys_run_in_executor(RESOLV_TMPL.read_text, encoding="utf-8") ) except OSError as err: @@ -162,7 +176,7 @@ class PluginDns(PluginBase): _LOGGER.error("Can't read resolve.tmpl: %s", err) try: - self.hosts_template = jinja2.Template( + self._hosts_template = jinja2.Template( await self.sys_run_in_executor(HOSTS_TMPL.read_text, encoding="utf-8") ) except OSError as err: @@ -176,7 +190,9 @@ class PluginDns(PluginBase): await super().load() # Update supervisor - await self._write_resolv(HOST_RESOLV) + # Resolv template should always be set but just in case don't fail load + if self._resolv_template: + await self._write_resolv(HOST_RESOLV) # Reinitializing aiohttp.ClientSession after DNS setup makes sure that # aiodns is using the right DNS servers (see #5857). @@ -428,12 +444,6 @@ class PluginDns(PluginBase): async def _write_resolv(self, resolv_conf: Path) -> None: """Update/Write resolv.conf file.""" - if not self.resolv_template: - _LOGGER.warning( - "Resolv template is missing, cannot write/update %s", resolv_conf - ) - return - nameservers = [str(self.sys_docker.network.dns), "127.0.0.11"] # Read resolv config diff --git a/supervisor/plugins/observer.py b/supervisor/plugins/observer.py index 1d8b7fe76..2a60c6a0f 100644 --- a/supervisor/plugins/observer.py +++ b/supervisor/plugins/observer.py @@ -5,6 +5,7 @@ Code: https://github.com/home-assistant/plugin-observer import logging import secrets +from typing import cast import aiohttp from awesomeversion import AwesomeVersion @@ -60,7 +61,7 @@ class PluginObserver(PluginBase): @property def supervisor_token(self) -> str: """Return an access token for the Observer API.""" - return self._data.get(ATTR_ACCESS_TOKEN) + return cast(str, self._data[ATTR_ACCESS_TOKEN]) @Job( name="plugin_observer_update", @@ -90,6 +91,10 @@ class PluginObserver(PluginBase): _LOGGER.error("Can't start observer plugin") raise ObserverError() from err + async def stop(self) -> None: + """Raise. Supervisor should not stop observer.""" + raise RuntimeError("Stopping observer without a restart is not supported!") + async def stats(self) -> DockerStats: """Return stats of observer.""" try: diff --git a/supervisor/resolution/checks/addon_pwned.py b/supervisor/resolution/checks/addon_pwned.py index f5f76578e..d7f1427fc 100644 --- a/supervisor/resolution/checks/addon_pwned.py +++ b/supervisor/resolution/checks/addon_pwned.py @@ -67,10 +67,11 @@ class CheckAddonPwned(CheckBase): @Job(name="check_addon_pwned_approve", conditions=[JobCondition.INTERNET_SYSTEM]) async def approve_check(self, reference: str | None = None) -> bool: """Approve check if it is affected by issue.""" - addon = self.sys_addons.get(reference) + if not reference: + return False # Uninstalled - if not addon or not addon.is_installed: + if not (addon := self.sys_addons.get_local_only(reference)): return False # Not in use anymore diff --git a/supervisor/resolution/checks/detached_addon_missing.py b/supervisor/resolution/checks/detached_addon_missing.py index 73833cf5f..1d09fa329 100644 --- a/supervisor/resolution/checks/detached_addon_missing.py +++ b/supervisor/resolution/checks/detached_addon_missing.py @@ -29,9 +29,11 @@ class CheckDetachedAddonMissing(CheckBase): async def approve_check(self, reference: str | None = None) -> bool: """Approve check if it is affected by issue.""" - return ( - addon := self.sys_addons.get(reference, local_only=True) - ) and addon.is_detached + if not reference: + return False + + addon = self.sys_addons.get_local_only(reference) + return addon is not None and addon.is_detached @property def issue(self) -> IssueType: diff --git a/supervisor/resolution/checks/detached_addon_removed.py b/supervisor/resolution/checks/detached_addon_removed.py index 9a43e1fa2..9510fe376 100644 --- a/supervisor/resolution/checks/detached_addon_removed.py +++ b/supervisor/resolution/checks/detached_addon_removed.py @@ -27,9 +27,11 @@ class CheckDetachedAddonRemoved(CheckBase): async def approve_check(self, reference: str | None = None) -> bool: """Approve check if it is affected by issue.""" - return ( - addon := self.sys_addons.get(reference, local_only=True) - ) and addon.is_detached + if not reference: + return False + + addon = self.sys_addons.get_local_only(reference) + return addon is not None and addon.is_detached @property def issue(self) -> IssueType: diff --git a/supervisor/resolution/checks/disabled_data_disk.py b/supervisor/resolution/checks/disabled_data_disk.py index e798118c8..1edcc4eb9 100644 --- a/supervisor/resolution/checks/disabled_data_disk.py +++ b/supervisor/resolution/checks/disabled_data_disk.py @@ -35,6 +35,9 @@ class CheckDisabledDataDisk(CheckBase): async def approve_check(self, reference: str | None = None) -> bool: """Approve check if it is affected by issue.""" + if not reference: + return False + resolved = await self.sys_dbus.udisks2.resolve_device( DeviceSpecification(path=Path(reference)) ) @@ -43,7 +46,7 @@ class CheckDisabledDataDisk(CheckBase): def _is_disabled_data_disk(self, block_device: UDisks2Block) -> bool: """Return true if filesystem block device has name indicating it was disabled by OS.""" return ( - block_device.filesystem + block_device.filesystem is not None and block_device.id_label == FILESYSTEM_LABEL_DISABLED_DATA_DISK ) diff --git a/supervisor/resolution/checks/dns_server.py b/supervisor/resolution/checks/dns_server.py index 602c00898..40e2d52c9 100644 --- a/supervisor/resolution/checks/dns_server.py +++ b/supervisor/resolution/checks/dns_server.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta +from typing import Literal from aiodns import DNSResolver from aiodns.error import DNSError @@ -16,7 +17,7 @@ from .base import CheckBase async def check_server( - loop: asyncio.AbstractEventLoop, server: str, qtype: str + loop: asyncio.AbstractEventLoop, server: str, qtype: Literal["A"] | Literal["AAAA"] ) -> None: """Check a DNS server and report issues.""" ip_addr = server[6:] if server.startswith("dns://") else server @@ -54,13 +55,15 @@ class CheckDNSServer(CheckBase): *[check_server(self.sys_loop, server, "A") for server in dns_servers], return_exceptions=True, ) - for i in (r for r in range(len(results)) if isinstance(results[r], DNSError)): - self.sys_resolution.create_issue( - IssueType.DNS_SERVER_FAILED, - ContextType.DNS_SERVER, - reference=dns_servers[i], - ) - await async_capture_exception(results[i]) + # pylint: disable-next=consider-using-enumerate + for i in range(len(results)): + if isinstance(result := results[i], DNSError): + self.sys_resolution.create_issue( + IssueType.DNS_SERVER_FAILED, + ContextType.DNS_SERVER, + reference=dns_servers[i], + ) + await async_capture_exception(result) @Job(name="check_dns_server_approve", conditions=[JobCondition.INTERNET_SYSTEM]) async def approve_check(self, reference: str | None = None) -> bool: diff --git a/supervisor/resolution/checks/dns_server_ipv6.py b/supervisor/resolution/checks/dns_server_ipv6.py index 58e97ada5..8b6f9f8a9 100644 --- a/supervisor/resolution/checks/dns_server_ipv6.py +++ b/supervisor/resolution/checks/dns_server_ipv6.py @@ -5,8 +5,6 @@ from datetime import timedelta from aiodns.error import DNSError -from supervisor.resolution.checks.dns_server import check_server - from ...const import CoreState from ...coresys import CoreSys from ...jobs.const import JobCondition, JobExecutionLimit @@ -14,6 +12,7 @@ from ...jobs.decorator import Job from ...utils.sentry import async_capture_exception from ..const import DNS_ERROR_NO_DATA, ContextType, IssueType from .base import CheckBase +from .dns_server import check_server def setup(coresys: CoreSys) -> CheckBase: @@ -37,18 +36,18 @@ class CheckDNSServerIPv6(CheckBase): *[check_server(self.sys_loop, server, "AAAA") for server in dns_servers], return_exceptions=True, ) - for i in ( - r - for r in range(len(results)) - if isinstance(results[r], DNSError) - and results[r].args[0] != DNS_ERROR_NO_DATA - ): - self.sys_resolution.create_issue( - IssueType.DNS_SERVER_IPV6_ERROR, - ContextType.DNS_SERVER, - reference=dns_servers[i], - ) - await async_capture_exception(results[i]) + # pylint: disable-next=consider-using-enumerate + for i in range(len(results)): + if ( + isinstance(result := results[i], DNSError) + and result.args[0] != DNS_ERROR_NO_DATA + ): + self.sys_resolution.create_issue( + IssueType.DNS_SERVER_IPV6_ERROR, + ContextType.DNS_SERVER, + reference=dns_servers[i], + ) + await async_capture_exception(result) @Job( name="check_dns_server_ipv6_approve", conditions=[JobCondition.INTERNET_SYSTEM] diff --git a/supervisor/resolution/checks/multiple_data_disks.py b/supervisor/resolution/checks/multiple_data_disks.py index 6c28c7aeb..1dc75037c 100644 --- a/supervisor/resolution/checks/multiple_data_disks.py +++ b/supervisor/resolution/checks/multiple_data_disks.py @@ -35,6 +35,9 @@ class CheckMultipleDataDisks(CheckBase): async def approve_check(self, reference: str | None = None) -> bool: """Approve check if it is affected by issue.""" + if not reference: + return False + resolved = await self.sys_dbus.udisks2.resolve_device( DeviceSpecification(path=Path(reference)) ) @@ -43,7 +46,7 @@ class CheckMultipleDataDisks(CheckBase): def _block_device_has_name_issue(self, block_device: UDisks2Block) -> bool: """Return true if filesystem block device incorrectly has data disk name.""" return ( - block_device.filesystem + block_device.filesystem is not None and block_device.id_label == FILESYSTEM_LABEL_DATA_DISK and block_device.device != self.sys_dbus.agent.datadisk.current_device ) diff --git a/supervisor/resolution/data.py b/supervisor/resolution/data.py index fc92bb7e8..4f2bb8407 100644 --- a/supervisor/resolution/data.py +++ b/supervisor/resolution/data.py @@ -1,6 +1,6 @@ """Data objects.""" -from uuid import UUID, uuid4 +from uuid import uuid4 import attr @@ -20,7 +20,7 @@ class Issue: type: IssueType = attr.ib() context: ContextType = attr.ib() reference: str | None = attr.ib(default=None) - uuid: UUID = attr.ib(factory=lambda: uuid4().hex, eq=False, init=False) + uuid: str = attr.ib(factory=lambda: uuid4().hex, eq=False, init=False) @attr.s(frozen=True, slots=True) @@ -30,7 +30,7 @@ class Suggestion: type: SuggestionType = attr.ib() context: ContextType = attr.ib() reference: str | None = attr.ib(default=None) - uuid: UUID = attr.ib(factory=lambda: uuid4().hex, eq=False, init=False) + uuid: str = attr.ib(factory=lambda: uuid4().hex, eq=False, init=False) @attr.s(frozen=True, slots=True) diff --git a/supervisor/resolution/evaluations/apparmor.py b/supervisor/resolution/evaluations/apparmor.py index 940da7362..a7f6f9259 100644 --- a/supervisor/resolution/evaluations/apparmor.py +++ b/supervisor/resolution/evaluations/apparmor.py @@ -33,7 +33,7 @@ class EvaluateAppArmor(EvaluateBase): """Return a list of valid states when this evaluation can run.""" return [CoreState.INITIALIZE] - async def evaluate(self) -> None: + async def evaluate(self) -> bool: """Run evaluation.""" try: apparmor = await self.sys_run_in_executor( diff --git a/supervisor/resolution/evaluations/container.py b/supervisor/resolution/evaluations/container.py index f18f1fa22..f1497b449 100644 --- a/supervisor/resolution/evaluations/container.py +++ b/supervisor/resolution/evaluations/container.py @@ -38,7 +38,7 @@ class EvaluateContainer(EvaluateBase): """Initialize the evaluation class.""" super().__init__(coresys) self.coresys = coresys - self._images = set() + self._images: set[str] = set() @property def reason(self) -> UnsupportedReason: @@ -61,8 +61,8 @@ class EvaluateContainer(EvaluateBase): return { self.sys_homeassistant.image, self.sys_supervisor.image, - *(plugin.image for plugin in self.sys_plugins.all_plugins), - *(addon.image for addon in self.sys_addons.installed), + *(plugin.image for plugin in self.sys_plugins.all_plugins if plugin.image), + *(addon.image for addon in self.sys_addons.installed if addon.image), } async def evaluate(self) -> bool: diff --git a/supervisor/resolution/evaluations/content_trust.py b/supervisor/resolution/evaluations/content_trust.py index 7415b7a3b..c5648fd0a 100644 --- a/supervisor/resolution/evaluations/content_trust.py +++ b/supervisor/resolution/evaluations/content_trust.py @@ -29,6 +29,6 @@ class EvaluateContentTrust(EvaluateBase): """Return a list of valid states when this evaluation can run.""" return [CoreState.INITIALIZE, CoreState.SETUP, CoreState.RUNNING] - async def evaluate(self) -> None: + async def evaluate(self) -> bool: """Run evaluation.""" return not self.sys_security.content_trust diff --git a/supervisor/resolution/evaluations/dbus.py b/supervisor/resolution/evaluations/dbus.py index 5d8d3000e..81ed9f36a 100644 --- a/supervisor/resolution/evaluations/dbus.py +++ b/supervisor/resolution/evaluations/dbus.py @@ -29,6 +29,6 @@ class EvaluateDbus(EvaluateBase): """Return a list of valid states when this evaluation can run.""" return [CoreState.INITIALIZE] - async def evaluate(self) -> None: + async def evaluate(self) -> bool: """Run evaluation.""" - return not SOCKET_DBUS.exists() + return not await self.sys_run_in_executor(SOCKET_DBUS.exists) diff --git a/supervisor/resolution/evaluations/dns_server.py b/supervisor/resolution/evaluations/dns_server.py index 02942de52..ea712248e 100644 --- a/supervisor/resolution/evaluations/dns_server.py +++ b/supervisor/resolution/evaluations/dns_server.py @@ -29,7 +29,7 @@ class EvaluateDNSServer(EvaluateBase): """Return a list of valid states when this evaluation can run.""" return [CoreState.RUNNING] - async def evaluate(self) -> None: + async def evaluate(self) -> bool: """Run evaluation.""" return ( not self.sys_plugins.dns.fallback diff --git a/supervisor/resolution/evaluations/docker_configuration.py b/supervisor/resolution/evaluations/docker_configuration.py index 473d5b76f..e074dee7d 100644 --- a/supervisor/resolution/evaluations/docker_configuration.py +++ b/supervisor/resolution/evaluations/docker_configuration.py @@ -36,7 +36,7 @@ class EvaluateDockerConfiguration(EvaluateBase): """Return a list of valid states when this evaluation can run.""" return [CoreState.INITIALIZE] - async def evaluate(self): + async def evaluate(self) -> bool: """Run evaluation.""" storage_driver = self.sys_docker.info.storage logging_driver = self.sys_docker.info.logging diff --git a/supervisor/resolution/evaluations/docker_version.py b/supervisor/resolution/evaluations/docker_version.py index f779d2bff..219dbe034 100644 --- a/supervisor/resolution/evaluations/docker_version.py +++ b/supervisor/resolution/evaluations/docker_version.py @@ -29,6 +29,6 @@ class EvaluateDockerVersion(EvaluateBase): """Return a list of valid states when this evaluation can run.""" return [CoreState.INITIALIZE] - async def evaluate(self): + async def evaluate(self) -> bool: """Run evaluation.""" return not self.sys_docker.info.supported_version diff --git a/supervisor/resolution/evaluations/job_conditions.py b/supervisor/resolution/evaluations/job_conditions.py index 4f7cd2a0b..2a59adefc 100644 --- a/supervisor/resolution/evaluations/job_conditions.py +++ b/supervisor/resolution/evaluations/job_conditions.py @@ -29,6 +29,6 @@ class EvaluateJobConditions(EvaluateBase): """Return a list of valid states when this evaluation can run.""" return [CoreState.INITIALIZE, CoreState.SETUP, CoreState.RUNNING] - async def evaluate(self) -> None: + async def evaluate(self) -> bool: """Run evaluation.""" return len(self.sys_jobs.ignore_conditions) > 0 diff --git a/supervisor/resolution/evaluations/lxc.py b/supervisor/resolution/evaluations/lxc.py index bece3ed25..1a63ade03 100644 --- a/supervisor/resolution/evaluations/lxc.py +++ b/supervisor/resolution/evaluations/lxc.py @@ -32,10 +32,10 @@ class EvaluateLxc(EvaluateBase): """Return a list of valid states when this evaluation can run.""" return [CoreState.INITIALIZE] - async def evaluate(self): + async def evaluate(self) -> bool: """Run evaluation.""" - def check_lxc(): + def check_lxc() -> bool: with suppress(OSError): if "container=lxc" in Path("/proc/1/environ").read_text( encoding="utf-8" diff --git a/supervisor/resolution/evaluations/network_manager.py b/supervisor/resolution/evaluations/network_manager.py index a81529657..022df2a1e 100644 --- a/supervisor/resolution/evaluations/network_manager.py +++ b/supervisor/resolution/evaluations/network_manager.py @@ -30,6 +30,6 @@ class EvaluateNetworkManager(EvaluateBase): """Return a list of valid states when this evaluation can run.""" return [CoreState.SETUP, CoreState.RUNNING] - async def evaluate(self): + async def evaluate(self) -> bool: """Run evaluation.""" return HostFeature.NETWORK not in self.sys_host.features diff --git a/supervisor/resolution/evaluations/operating_system.py b/supervisor/resolution/evaluations/operating_system.py index 4093507fb..9516d51f3 100644 --- a/supervisor/resolution/evaluations/operating_system.py +++ b/supervisor/resolution/evaluations/operating_system.py @@ -31,7 +31,7 @@ class EvaluateOperatingSystem(EvaluateBase): """Return a list of valid states when this evaluation can run.""" return [CoreState.SETUP] - async def evaluate(self): + async def evaluate(self) -> bool: """Run evaluation.""" if self.sys_os.available: return False diff --git a/supervisor/resolution/evaluations/os_agent.py b/supervisor/resolution/evaluations/os_agent.py index 056363d6f..d397abc40 100644 --- a/supervisor/resolution/evaluations/os_agent.py +++ b/supervisor/resolution/evaluations/os_agent.py @@ -30,6 +30,6 @@ class EvaluateOSAgent(EvaluateBase): """Return a list of valid states when this evaluation can run.""" return [CoreState.SETUP] - async def evaluate(self): + async def evaluate(self) -> bool: """Run evaluation.""" return HostFeature.OS_AGENT not in self.sys_host.features diff --git a/supervisor/resolution/evaluations/privileged.py b/supervisor/resolution/evaluations/privileged.py index a3b9733b2..397ebbb52 100644 --- a/supervisor/resolution/evaluations/privileged.py +++ b/supervisor/resolution/evaluations/privileged.py @@ -29,6 +29,6 @@ class EvaluatePrivileged(EvaluateBase): """Return a list of valid states when this evaluation can run.""" return [CoreState.INITIALIZE] - async def evaluate(self): + async def evaluate(self) -> bool: """Run evaluation.""" return not self.sys_supervisor.instance.privileged diff --git a/supervisor/resolution/evaluations/restart_policy.py b/supervisor/resolution/evaluations/restart_policy.py index cf138adf4..e0572eb27 100644 --- a/supervisor/resolution/evaluations/restart_policy.py +++ b/supervisor/resolution/evaluations/restart_policy.py @@ -21,7 +21,7 @@ class EvaluateRestartPolicy(EvaluateBase): """Initialize the evaluation class.""" super().__init__(coresys) self.coresys = coresys - self._containers: list[str] = [] + self._containers: set[str] = set() @property def reason(self) -> UnsupportedReason: diff --git a/supervisor/resolution/evaluations/supervisor_version.py b/supervisor/resolution/evaluations/supervisor_version.py index 30480666f..738924220 100644 --- a/supervisor/resolution/evaluations/supervisor_version.py +++ b/supervisor/resolution/evaluations/supervisor_version.py @@ -29,6 +29,6 @@ class EvaluateSupervisorVersion(EvaluateBase): """Return a list of valid states when this evaluation can run.""" return [CoreState.RUNNING, CoreState.STARTUP] - async def evaluate(self) -> None: + async def evaluate(self) -> bool: """Run evaluation.""" return not self.sys_updater.auto_update and self.sys_supervisor.need_update diff --git a/supervisor/resolution/evaluations/systemd.py b/supervisor/resolution/evaluations/systemd.py index d5ea0e604..b3af34a7d 100644 --- a/supervisor/resolution/evaluations/systemd.py +++ b/supervisor/resolution/evaluations/systemd.py @@ -6,6 +6,14 @@ from ...host.const import HostFeature from ..const import UnsupportedReason from .base import EvaluateBase +FEATURES_REQUIRED: tuple[HostFeature, ...] = ( + HostFeature.HOSTNAME, + HostFeature.SERVICES, + HostFeature.SHUTDOWN, + HostFeature.REBOOT, + HostFeature.TIMEDATE, +) + def setup(coresys: CoreSys) -> EvaluateBase: """Initialize evaluation-setup function.""" @@ -30,15 +38,8 @@ class EvaluateSystemd(EvaluateBase): """Return a list of valid states when this evaluation can run.""" return [CoreState.SETUP] - async def evaluate(self): + async def evaluate(self) -> bool: """Run evaluation.""" return any( - feature not in self.sys_host.features - for feature in ( - HostFeature.HOSTNAME, - HostFeature.SERVICES, - HostFeature.SHUTDOWN, - HostFeature.REBOOT, - HostFeature.TIMEDATE, - ) + feature not in self.sys_host.features for feature in FEATURES_REQUIRED ) diff --git a/supervisor/resolution/evaluations/virtualization_image.py b/supervisor/resolution/evaluations/virtualization_image.py index 62b76de7b..183805696 100644 --- a/supervisor/resolution/evaluations/virtualization_image.py +++ b/supervisor/resolution/evaluations/virtualization_image.py @@ -29,11 +29,11 @@ class EvaluateVirtualizationImage(EvaluateBase): """Return a list of valid states when this evaluation can run.""" return [CoreState.SETUP] - async def evaluate(self): + async def evaluate(self) -> bool: """Run evaluation.""" if not self.sys_os.available: return False - return self.sys_host.info.virtualization and self.sys_os.board not in { + return bool(self.sys_host.info.virtualization) and self.sys_os.board not in { "ova", "generic-aarch64", } diff --git a/supervisor/resolution/fixups/addon_disable_boot.py b/supervisor/resolution/fixups/addon_disable_boot.py index b3d8052b9..86575e69f 100644 --- a/supervisor/resolution/fixups/addon_disable_boot.py +++ b/supervisor/resolution/fixups/addon_disable_boot.py @@ -20,7 +20,10 @@ class FixupAddonDisableBoot(FixupBase): async def process_fixup(self, reference: str | None = None) -> None: """Initialize the fixup class.""" - if not (addon := self.sys_addons.get(reference, local_only=True)): + if not reference: + return + + if not (addon := self.sys_addons.get_local_only(reference)): _LOGGER.info("Cannot change addon %s as it does not exist", reference) return diff --git a/supervisor/resolution/fixups/addon_execute_rebuild.py b/supervisor/resolution/fixups/addon_execute_rebuild.py index 90ee9131d..4dd453118 100644 --- a/supervisor/resolution/fixups/addon_execute_rebuild.py +++ b/supervisor/resolution/fixups/addon_execute_rebuild.py @@ -20,7 +20,10 @@ class FixupAddonExecuteRebuild(FixupBase): async def process_fixup(self, reference: str | None = None) -> None: """Rebuild the addon's container.""" - addon = self.sys_addons.get(reference, local_only=True) + if not reference: + return + + addon = self.sys_addons.get_local_only(reference) if not addon: _LOGGER.info( "Cannot rebuild addon %s as it is not installed, dismissing suggestion", diff --git a/supervisor/resolution/fixups/addon_execute_remove.py b/supervisor/resolution/fixups/addon_execute_remove.py index ce9418e5b..b3398fe93 100644 --- a/supervisor/resolution/fixups/addon_execute_remove.py +++ b/supervisor/resolution/fixups/addon_execute_remove.py @@ -20,7 +20,10 @@ class FixupAddonExecuteRemove(FixupBase): async def process_fixup(self, reference: str | None = None) -> None: """Initialize the fixup class.""" - if not (addon := self.sys_addons.get(reference, local_only=True)): + if not reference: + return + + if not (addon := self.sys_addons.get_local_only(reference)): _LOGGER.info("Addon %s already removed", reference) return diff --git a/supervisor/resolution/fixups/addon_execute_repair.py b/supervisor/resolution/fixups/addon_execute_repair.py index 0439ea18c..5302a3838 100644 --- a/supervisor/resolution/fixups/addon_execute_repair.py +++ b/supervisor/resolution/fixups/addon_execute_repair.py @@ -25,7 +25,10 @@ class FixupAddonExecuteRepair(FixupBase): async def process_fixup(self, reference: str | None = None) -> None: """Pull the addons image.""" - addon = self.sys_addons.get(reference, local_only=True) + if not reference: + return + + addon = self.sys_addons.get_local_only(reference) if not addon: _LOGGER.info( "Cannot repair addon %s as it is not installed, dismissing suggestion", diff --git a/supervisor/resolution/fixups/addon_execute_restart.py b/supervisor/resolution/fixups/addon_execute_restart.py index c09610e0f..0b00427bd 100644 --- a/supervisor/resolution/fixups/addon_execute_restart.py +++ b/supervisor/resolution/fixups/addon_execute_restart.py @@ -20,7 +20,10 @@ class FixupAddonExecuteRestart(FixupBase): async def process_fixup(self, reference: str | None = None) -> None: """Initialize the fixup class.""" - if not (addon := self.sys_addons.get(reference, local_only=True)): + if not reference: + return + + if not (addon := self.sys_addons.get_local_only(reference)): _LOGGER.info("Cannot restart addon %s as it does not exist", reference) return diff --git a/supervisor/resolution/fixups/addon_execute_start.py b/supervisor/resolution/fixups/addon_execute_start.py index 4eb526128..359bdf3c2 100644 --- a/supervisor/resolution/fixups/addon_execute_start.py +++ b/supervisor/resolution/fixups/addon_execute_start.py @@ -21,7 +21,10 @@ class FixupAddonExecuteStart(FixupBase): async def process_fixup(self, reference: str | None = None) -> None: """Initialize the fixup class.""" - if not (addon := self.sys_addons.get(reference, local_only=True)): + if not reference: + return + + if not (addon := self.sys_addons.get_local_only(reference)): _LOGGER.info("Cannot start addon %s as it does not exist", reference) return diff --git a/supervisor/resolution/fixups/base.py b/supervisor/resolution/fixups/base.py index e0ef19ee4..906e78ed2 100644 --- a/supervisor/resolution/fixups/base.py +++ b/supervisor/resolution/fixups/base.py @@ -22,9 +22,7 @@ class FixupBase(ABC, CoreSysAttributes): """Execute the evaluation.""" if not fixing_suggestion: # Get suggestion to fix - fixing_suggestion: Suggestion | None = next( - iter(self.all_suggestions), None - ) + fixing_suggestion = next(iter(self.all_suggestions), None) # No suggestion if fixing_suggestion is None: diff --git a/supervisor/resolution/fixups/store_execute_reload.py b/supervisor/resolution/fixups/store_execute_reload.py index 5e603eb7d..cb512587b 100644 --- a/supervisor/resolution/fixups/store_execute_reload.py +++ b/supervisor/resolution/fixups/store_execute_reload.py @@ -32,6 +32,9 @@ class FixupStoreExecuteReload(FixupBase): ) async def process_fixup(self, reference: str | None = None) -> None: """Initialize the fixup class.""" + if not reference: + return + _LOGGER.info("Reload Store: %s", reference) try: repository = self.sys_store.get(reference) diff --git a/supervisor/resolution/fixups/store_execute_remove.py b/supervisor/resolution/fixups/store_execute_remove.py index 40af37fb7..6eb720102 100644 --- a/supervisor/resolution/fixups/store_execute_remove.py +++ b/supervisor/resolution/fixups/store_execute_remove.py @@ -20,6 +20,9 @@ class FixupStoreExecuteRemove(FixupBase): async def process_fixup(self, reference: str | None = None) -> None: """Initialize the fixup class.""" + if not reference: + return + _LOGGER.info("Remove invalid Store: %s", reference) try: repository = self.sys_store.get(reference) diff --git a/supervisor/resolution/fixups/store_execute_reset.py b/supervisor/resolution/fixups/store_execute_reset.py index d5736b40b..12597cc31 100644 --- a/supervisor/resolution/fixups/store_execute_reset.py +++ b/supervisor/resolution/fixups/store_execute_reset.py @@ -34,6 +34,9 @@ class FixupStoreExecuteReset(FixupBase): ) async def process_fixup(self, reference: str | None = None) -> None: """Initialize the fixup class.""" + if not reference: + return + _LOGGER.info("Reset corrupt Store: %s", reference) try: repository = self.sys_store.get(reference) @@ -41,9 +44,11 @@ class FixupStoreExecuteReset(FixupBase): _LOGGER.warning("Can't find store %s for fixup", reference) return - await self.sys_run_in_executor( - partial(remove_folder, folder=repository.git.path, content_only=True) - ) + # Local add-ons are not a git repo, can't remove and re-pull + if repository.git: + await self.sys_run_in_executor( + partial(remove_folder, folder=repository.git.path, content_only=True) + ) # Load data again try: diff --git a/supervisor/resolution/fixups/system_adopt_data_disk.py b/supervisor/resolution/fixups/system_adopt_data_disk.py index 4bf675dbf..577cf9381 100644 --- a/supervisor/resolution/fixups/system_adopt_data_disk.py +++ b/supervisor/resolution/fixups/system_adopt_data_disk.py @@ -23,6 +23,9 @@ class FixupSystemAdoptDataDisk(FixupBase): async def process_fixup(self, reference: str | None = None) -> None: """Initialize the fixup class.""" + if not reference: + return + if not ( new_resolved := await self.sys_dbus.udisks2.resolve_device( DeviceSpecification(path=Path(reference)) diff --git a/supervisor/resolution/fixups/system_rename_data_disk.py b/supervisor/resolution/fixups/system_rename_data_disk.py index e60a80d13..09d7d8bff 100644 --- a/supervisor/resolution/fixups/system_rename_data_disk.py +++ b/supervisor/resolution/fixups/system_rename_data_disk.py @@ -23,6 +23,9 @@ class FixupSystemRenameDataDisk(FixupBase): async def process_fixup(self, reference: str | None = None) -> None: """Initialize the fixup class.""" + if not reference: + return + resolved = await self.sys_dbus.udisks2.resolve_device( DeviceSpecification(path=Path(reference)) ) diff --git a/supervisor/resolution/module.py b/supervisor/resolution/module.py index 4ef7b3acd..8e40b273a 100644 --- a/supervisor/resolution/module.py +++ b/supervisor/resolution/module.py @@ -257,7 +257,7 @@ class ResolutionManager(FileConfiguration, CoreSysAttributes): if not self.issues_for_suggestion(suggestion): self.dismiss_suggestion(suggestion) - def dismiss_unsupported(self, reason: Issue) -> None: + def dismiss_unsupported(self, reason: UnsupportedReason) -> None: """Dismiss a reason for unsupported.""" if reason not in self._unsupported: raise ResolutionError(f"The reason {reason} is not active", _LOGGER.warning) diff --git a/supervisor/store/addon.py b/supervisor/store/addon.py index ea9d070e7..7275cbba5 100644 --- a/supervisor/store/addon.py +++ b/supervisor/store/addon.py @@ -30,7 +30,7 @@ class AddonStore(AddonModel): @property def is_installed(self) -> bool: """Return True if an add-on is installed.""" - return self.sys_addons.get(self.slug, local_only=True) is not None + return self.sys_addons.get_local_only(self.slug) is not None @property def is_detached(self) -> bool: diff --git a/tests/api/middleware/test_security.py b/tests/api/middleware/test_security.py index 78d667107..78016f0c6 100644 --- a/tests/api/middleware/test_security.py +++ b/tests/api/middleware/test_security.py @@ -55,6 +55,15 @@ async def api_token_validation(aiohttp_client, coresys: CoreSys) -> TestClient: yield await aiohttp_client(api.webapp) +@pytest.fixture(name="plugin_tokens") +async def fixture_plugin_tokens(coresys: CoreSys) -> None: + """Mock plugin tokens used in middleware.""" + # pylint: disable=protected-access + coresys.plugins.cli._data["access_token"] = "c_123456" + coresys.plugins.observer._data["access_token"] = "o_123456" + # pylint: enable=protected-access + + @pytest.mark.asyncio async def test_api_security_system_initialize(api_system: TestClient, coresys: CoreSys): """Test security.""" @@ -185,6 +194,7 @@ async def test_bad_requests( ("post", "/addons/abc123/sys_options", set()), ], ) +@pytest.mark.usefixtures("plugin_tokens") async def test_token_validation( api_token_validation: TestClient, install_addon_example: Addon, @@ -210,6 +220,7 @@ async def test_token_validation( assert resp.status == 403 +@pytest.mark.usefixtures("plugin_tokens") async def test_home_assistant_paths(api_token_validation: TestClient, coresys: CoreSys): """Test Home Assistant only paths.""" coresys.homeassistant.supervisor_token = "abc123" diff --git a/tests/plugins/test_dns.py b/tests/plugins/test_dns.py index 4c71da1b8..665e58626 100644 --- a/tests/plugins/test_dns.py +++ b/tests/plugins/test_dns.py @@ -209,7 +209,6 @@ async def test_load_error( assert "Can't read resolve.tmpl" in caplog.text assert "Can't read hosts.tmpl" in caplog.text - assert "Resolv template is missing" in caplog.text assert coresys.core.healthy is True caplog.clear() @@ -218,7 +217,6 @@ async def test_load_error( assert "Can't read resolve.tmpl" in caplog.text assert "Can't read hosts.tmpl" in caplog.text - assert "Resolv template is missing" in caplog.text assert coresys.core.healthy is False diff --git a/tests/resolution/check/test_check_addon_pwned.py b/tests/resolution/check/test_check_addon_pwned.py index 0eef61649..81bca5b8f 100644 --- a/tests/resolution/check/test_check_addon_pwned.py +++ b/tests/resolution/check/test_check_addon_pwned.py @@ -73,10 +73,6 @@ async def test_approve(coresys: CoreSys, supervisor_internet): coresys.security.verify_secret = AsyncMock(return_value=None) assert not await addon_pwned.approve_check(reference=addon.slug) - addon.is_installed = False - coresys.security.verify_secret = AsyncMock(side_effect=PwnedSecret) - assert not await addon_pwned.approve_check(reference=addon.slug) - async def test_with_global_disable(coresys: CoreSys, caplog): """Test when pwned is globally disabled."""