mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-27 11:06:32 +00:00
Add JobManager and Job decorator (#2225)
* Adds condition decorator to block execution that require internet * Fix exsisting tests * Add internet state to network info * Add healthy condition * Add tests * It's all changed * rename
This commit is contained in:
parent
cd34a40dd8
commit
a18b706f99
1
setup.py
1
setup.py
@ -41,6 +41,7 @@ setup(
|
|||||||
"supervisor.docker",
|
"supervisor.docker",
|
||||||
"supervisor.homeassistant",
|
"supervisor.homeassistant",
|
||||||
"supervisor.host",
|
"supervisor.host",
|
||||||
|
"supervisor.job",
|
||||||
"supervisor.misc",
|
"supervisor.misc",
|
||||||
"supervisor.plugins",
|
"supervisor.plugins",
|
||||||
"supervisor.resolution.evaluations",
|
"supervisor.resolution.evaluations",
|
||||||
|
@ -17,6 +17,7 @@ from ..const import (
|
|||||||
ATTR_ENABLED,
|
ATTR_ENABLED,
|
||||||
ATTR_FREQUENCY,
|
ATTR_FREQUENCY,
|
||||||
ATTR_GATEWAY,
|
ATTR_GATEWAY,
|
||||||
|
ATTR_HOST_CONNECTIVITY,
|
||||||
ATTR_INTERFACE,
|
ATTR_INTERFACE,
|
||||||
ATTR_INTERFACES,
|
ATTR_INTERFACES,
|
||||||
ATTR_IPV4,
|
ATTR_IPV4,
|
||||||
@ -29,6 +30,7 @@ from ..const import (
|
|||||||
ATTR_PSK,
|
ATTR_PSK,
|
||||||
ATTR_SIGNAL,
|
ATTR_SIGNAL,
|
||||||
ATTR_SSID,
|
ATTR_SSID,
|
||||||
|
ATTR_SUPERVISOR_INTERNET,
|
||||||
ATTR_TYPE,
|
ATTR_TYPE,
|
||||||
ATTR_VLAN,
|
ATTR_VLAN,
|
||||||
ATTR_WIFI,
|
ATTR_WIFI,
|
||||||
@ -159,6 +161,8 @@ class APINetwork(CoreSysAttributes):
|
|||||||
ATTR_GATEWAY: str(self.sys_docker.network.gateway),
|
ATTR_GATEWAY: str(self.sys_docker.network.gateway),
|
||||||
ATTR_DNS: str(self.sys_docker.network.dns),
|
ATTR_DNS: str(self.sys_docker.network.dns),
|
||||||
},
|
},
|
||||||
|
ATTR_HOST_CONNECTIVITY: self.sys_host.network.connectivity,
|
||||||
|
ATTR_SUPERVISOR_INTERNET: self.sys_supervisor.connectivity,
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
|
@ -10,6 +10,8 @@ import sentry_sdk
|
|||||||
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
|
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
|
||||||
from sentry_sdk.integrations.logging import LoggingIntegration
|
from sentry_sdk.integrations.logging import LoggingIntegration
|
||||||
|
|
||||||
|
from supervisor.job import JobManager
|
||||||
|
|
||||||
from .addons import AddonManager
|
from .addons import AddonManager
|
||||||
from .api import RestAPI
|
from .api import RestAPI
|
||||||
from .arch import CpuArch
|
from .arch import CpuArch
|
||||||
@ -55,6 +57,7 @@ async def initialize_coresys() -> CoreSys:
|
|||||||
|
|
||||||
# Initialize core objects
|
# Initialize core objects
|
||||||
coresys.resolution = ResolutionManager(coresys)
|
coresys.resolution = ResolutionManager(coresys)
|
||||||
|
coresys.jobs = JobManager(coresys)
|
||||||
coresys.core = Core(coresys)
|
coresys.core = Core(coresys)
|
||||||
coresys.plugins = PluginManager(coresys)
|
coresys.plugins = PluginManager(coresys)
|
||||||
coresys.arch = CpuArch(coresys)
|
coresys.arch = CpuArch(coresys)
|
||||||
|
@ -151,6 +151,7 @@ ATTR_HEALTHY = "healthy"
|
|||||||
ATTR_HOMEASSISTANT = "homeassistant"
|
ATTR_HOMEASSISTANT = "homeassistant"
|
||||||
ATTR_HOMEASSISTANT_API = "homeassistant_api"
|
ATTR_HOMEASSISTANT_API = "homeassistant_api"
|
||||||
ATTR_HOST = "host"
|
ATTR_HOST = "host"
|
||||||
|
ATTR_HOST_CONNECTIVITY = "host_connectivity"
|
||||||
ATTR_HOST_DBUS = "host_dbus"
|
ATTR_HOST_DBUS = "host_dbus"
|
||||||
ATTR_HOST_IPC = "host_ipc"
|
ATTR_HOST_IPC = "host_ipc"
|
||||||
ATTR_HOST_NETWORK = "host_network"
|
ATTR_HOST_NETWORK = "host_network"
|
||||||
@ -247,6 +248,7 @@ ATTR_STDIN = "stdin"
|
|||||||
ATTR_STORAGE = "storage"
|
ATTR_STORAGE = "storage"
|
||||||
ATTR_SUGGESTIONS = "suggestions"
|
ATTR_SUGGESTIONS = "suggestions"
|
||||||
ATTR_SUPERVISOR = "supervisor"
|
ATTR_SUPERVISOR = "supervisor"
|
||||||
|
ATTR_SUPERVISOR_INTERNET = "supervisor_internet"
|
||||||
ATTR_SUPPORTED = "supported"
|
ATTR_SUPPORTED = "supported"
|
||||||
ATTR_SUPPORTED_ARCH = "supported_arch"
|
ATTR_SUPPORTED_ARCH = "supported_arch"
|
||||||
ATTR_SYSTEM = "system"
|
ATTR_SYSTEM = "system"
|
||||||
|
@ -6,6 +6,8 @@ from typing import Awaitable, List, Optional
|
|||||||
|
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
|
||||||
|
from supervisor.host.const import ConnectivityState
|
||||||
|
|
||||||
from .const import RUN_SUPERVISOR_STATE, AddonStartup, CoreState
|
from .const import RUN_SUPERVISOR_STATE, AddonStartup, CoreState
|
||||||
from .coresys import CoreSys, CoreSysAttributes
|
from .coresys import CoreSys, CoreSysAttributes
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
@ -57,6 +59,13 @@ class Core(CoreSysAttributes):
|
|||||||
# Load information from container
|
# Load information from container
|
||||||
await self.sys_supervisor.load()
|
await self.sys_supervisor.load()
|
||||||
|
|
||||||
|
# Check internet on startup
|
||||||
|
if not self.sys_host.network.connectivity == ConnectivityState.FULL:
|
||||||
|
await self.sys_host.network.check_connectivity()
|
||||||
|
|
||||||
|
if not self.sys_supervisor.connectivity:
|
||||||
|
await self.sys_supervisor.check_connectivity()
|
||||||
|
|
||||||
# Evaluate the system
|
# Evaluate the system
|
||||||
await self.sys_resolution.evaluate.evaluate_system()
|
await self.sys_resolution.evaluate.evaluate_system()
|
||||||
|
|
||||||
|
@ -22,19 +22,20 @@ if TYPE_CHECKING:
|
|||||||
from .dbus import DBusManager
|
from .dbus import DBusManager
|
||||||
from .discovery import Discovery
|
from .discovery import Discovery
|
||||||
from .hassos import HassOS
|
from .hassos import HassOS
|
||||||
from .misc.scheduler import Scheduler
|
|
||||||
from .misc.hwmon import HwMonitor
|
|
||||||
from .misc.tasks import Tasks
|
|
||||||
from .homeassistant import HomeAssistant
|
from .homeassistant import HomeAssistant
|
||||||
from .host import HostManager
|
from .host import HostManager
|
||||||
from .ingress import Ingress
|
from .ingress import Ingress
|
||||||
from .services import ServiceManager
|
from .job import JobManager
|
||||||
from .snapshots import SnapshotManager
|
from .misc.hwmon import HwMonitor
|
||||||
from .supervisor import Supervisor
|
from .misc.scheduler import Scheduler
|
||||||
from .store import StoreManager
|
from .misc.tasks import Tasks
|
||||||
from .updater import Updater
|
|
||||||
from .plugins import PluginManager
|
from .plugins import PluginManager
|
||||||
from .resolution import ResolutionManager
|
from .resolution import ResolutionManager
|
||||||
|
from .services import ServiceManager
|
||||||
|
from .snapshots import SnapshotManager
|
||||||
|
from .store import StoreManager
|
||||||
|
from .supervisor import Supervisor
|
||||||
|
from .updater import Updater
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
@ -83,6 +84,7 @@ class CoreSys:
|
|||||||
self._hwmonitor: Optional[HwMonitor] = None
|
self._hwmonitor: Optional[HwMonitor] = None
|
||||||
self._plugins: Optional[PluginManager] = None
|
self._plugins: Optional[PluginManager] = None
|
||||||
self._resolution: Optional[ResolutionManager] = None
|
self._resolution: Optional[ResolutionManager] = None
|
||||||
|
self._jobs: Optional[JobManager] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dev(self) -> bool:
|
def dev(self) -> bool:
|
||||||
@ -413,6 +415,20 @@ class CoreSys:
|
|||||||
raise RuntimeError("resolution manager already set!")
|
raise RuntimeError("resolution manager already set!")
|
||||||
self._resolution = value
|
self._resolution = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def jobs(self) -> JobManager:
|
||||||
|
"""Return resolution manager object."""
|
||||||
|
if self._jobs is None:
|
||||||
|
raise RuntimeError("job manager not set!")
|
||||||
|
return self._jobs
|
||||||
|
|
||||||
|
@jobs.setter
|
||||||
|
def jobs(self, value: JobManager) -> None:
|
||||||
|
"""Set a resolution manager object."""
|
||||||
|
if self._jobs:
|
||||||
|
raise RuntimeError("job manager already set!")
|
||||||
|
self._jobs = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def machine(self) -> Optional[str]:
|
def machine(self) -> Optional[str]:
|
||||||
"""Return machine type string."""
|
"""Return machine type string."""
|
||||||
@ -588,6 +604,11 @@ class CoreSysAttributes:
|
|||||||
"""Return Resolution manager object."""
|
"""Return Resolution manager object."""
|
||||||
return self.coresys.resolution
|
return self.coresys.resolution
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_jobs(self) -> JobManager:
|
||||||
|
"""Return Job manager object."""
|
||||||
|
return self.coresys.jobs
|
||||||
|
|
||||||
def sys_run_in_executor(
|
def sys_run_in_executor(
|
||||||
self, funct: Callable[..., T], *args: Any
|
self, funct: Callable[..., T], *args: Any
|
||||||
) -> Coroutine[Any, Any, T]:
|
) -> Coroutine[Any, Any, T]:
|
||||||
|
@ -65,6 +65,11 @@ class NetworkManager(DBusInterface):
|
|||||||
settings, device_object, DBUS_OBJECT_BASE
|
settings, device_object, DBUS_OBJECT_BASE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@dbus_connected
|
||||||
|
async def check_connectivity(self) -> Awaitable[Any]:
|
||||||
|
"""Check the connectivity of the host."""
|
||||||
|
return await self.dbus.CheckConnectivity()
|
||||||
|
|
||||||
async def connect(self) -> None:
|
async def connect(self) -> None:
|
||||||
"""Connect to system's D-Bus."""
|
"""Connect to system's D-Bus."""
|
||||||
try:
|
try:
|
||||||
|
@ -293,3 +293,10 @@ class ResolutionError(HassioError):
|
|||||||
|
|
||||||
class ResolutionNotFound(ResolutionError):
|
class ResolutionNotFound(ResolutionError):
|
||||||
"""Raise if suggestion/issue was not found."""
|
"""Raise if suggestion/issue was not found."""
|
||||||
|
|
||||||
|
|
||||||
|
# Job
|
||||||
|
|
||||||
|
|
||||||
|
class JobException(HassioError):
|
||||||
|
"""Base job exception."""
|
||||||
|
@ -33,3 +33,13 @@ class WifiMode(str, Enum):
|
|||||||
MESH = "mesh"
|
MESH = "mesh"
|
||||||
ADHOC = "adhoc"
|
ADHOC = "adhoc"
|
||||||
AP = "ap"
|
AP = "ap"
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectivityState(int, Enum):
|
||||||
|
"""Connectivity State."""
|
||||||
|
|
||||||
|
UNKNOWN = 0
|
||||||
|
NONE = 1
|
||||||
|
PORTAL = 2
|
||||||
|
LIMITED = 3
|
||||||
|
FULL = 4
|
||||||
|
@ -27,7 +27,13 @@ from ..exceptions import (
|
|||||||
HostNetworkNotFound,
|
HostNetworkNotFound,
|
||||||
HostNotSupportedError,
|
HostNotSupportedError,
|
||||||
)
|
)
|
||||||
from .const import AuthMethod, InterfaceMethod, InterfaceType, WifiMode
|
from .const import (
|
||||||
|
AuthMethod,
|
||||||
|
ConnectivityState,
|
||||||
|
InterfaceMethod,
|
||||||
|
InterfaceType,
|
||||||
|
WifiMode,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -38,6 +44,12 @@ class NetworkManager(CoreSysAttributes):
|
|||||||
def __init__(self, coresys: CoreSys):
|
def __init__(self, coresys: CoreSys):
|
||||||
"""Initialize system center handling."""
|
"""Initialize system center handling."""
|
||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
|
self._connectivity = ConnectivityState.FULL
|
||||||
|
|
||||||
|
@property
|
||||||
|
def connectivity(self) -> ConnectivityState:
|
||||||
|
"""Return true current connectivity state."""
|
||||||
|
return self._connectivity
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def interfaces(self) -> List[Interface]:
|
def interfaces(self) -> List[Interface]:
|
||||||
@ -60,6 +72,14 @@ class NetworkManager(CoreSysAttributes):
|
|||||||
|
|
||||||
return list(dict.fromkeys(servers))
|
return list(dict.fromkeys(servers))
|
||||||
|
|
||||||
|
async def check_connectivity(self):
|
||||||
|
"""Check the internet connection."""
|
||||||
|
try:
|
||||||
|
state = await self.sys_dbus.network.check_connectivity()
|
||||||
|
self._connectivity = ConnectivityState(state[0])
|
||||||
|
except (DBusError, IndexError):
|
||||||
|
self._connectivity = ConnectivityState.UNKNOWN
|
||||||
|
|
||||||
def get(self, inet_name: str) -> Interface:
|
def get(self, inet_name: str) -> Interface:
|
||||||
"""Return interface from interface name."""
|
"""Return interface from interface name."""
|
||||||
if inet_name not in self.sys_dbus.network.interfaces:
|
if inet_name not in self.sys_dbus.network.interfaces:
|
||||||
|
76
supervisor/job/__init__.py
Normal file
76
supervisor/job/__init__.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
"""Supervisor job manager."""
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||||
|
|
||||||
|
|
||||||
|
class SupervisorJob(CoreSysAttributes):
|
||||||
|
"""Supervisor running job class."""
|
||||||
|
|
||||||
|
def __init__(self, coresys: CoreSys, name: str):
|
||||||
|
"""Initialize the JobManager class."""
|
||||||
|
self.coresys: CoreSys = coresys
|
||||||
|
self.name: str = name
|
||||||
|
self._progress: int = 0
|
||||||
|
self._stage: Optional[str] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def progress(self) -> int:
|
||||||
|
"""Return the current progress."""
|
||||||
|
return self._progress
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stage(self) -> Optional[str]:
|
||||||
|
"""Return the current stage."""
|
||||||
|
return self._stage
|
||||||
|
|
||||||
|
async def update(
|
||||||
|
self, progress: Optional[int] = None, stage: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
"""Update the job object."""
|
||||||
|
if progress is not None:
|
||||||
|
if progress >= round(100):
|
||||||
|
self.sys_jobs.remove_job(self)
|
||||||
|
return
|
||||||
|
self._progress = round(progress)
|
||||||
|
if stage is not None:
|
||||||
|
self._stage = stage
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Job updated; name: %s, progress: %s, stage: %s",
|
||||||
|
self.name,
|
||||||
|
self.progress,
|
||||||
|
self.stage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class JobManager(CoreSysAttributes):
|
||||||
|
"""Job class."""
|
||||||
|
|
||||||
|
def __init__(self, coresys: CoreSys):
|
||||||
|
"""Initialize the JobManager class."""
|
||||||
|
self.coresys: CoreSys = coresys
|
||||||
|
self._jobs: Dict[str, SupervisorJob] = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def jobs(self) -> List[SupervisorJob]:
|
||||||
|
"""Return a list of current jobs."""
|
||||||
|
return self._jobs
|
||||||
|
|
||||||
|
def get_job(self, name: str) -> SupervisorJob:
|
||||||
|
"""Return a job, create one if it does not exsist."""
|
||||||
|
if name not in self._jobs:
|
||||||
|
self._jobs[name] = SupervisorJob(self.coresys, name)
|
||||||
|
|
||||||
|
return self._jobs[name]
|
||||||
|
|
||||||
|
def remove_job(self, job: SupervisorJob) -> None:
|
||||||
|
"""Remove a job."""
|
||||||
|
if job.name in self._jobs:
|
||||||
|
del self._jobs[job.name]
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Clear all jobs."""
|
||||||
|
self._jobs.clear()
|
110
supervisor/job/decorator.py
Normal file
110
supervisor/job/decorator.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
"""Job decorator."""
|
||||||
|
from enum import Enum
|
||||||
|
import logging
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from ..const import CoreState
|
||||||
|
from ..coresys import CoreSys
|
||||||
|
from ..exceptions import HassioError, JobException
|
||||||
|
from ..host.const import ConnectivityState
|
||||||
|
from ..resolution.const import MINIMUM_FREE_SPACE_THRESHOLD, ContextType, IssueType
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
||||||
|
|
||||||
|
|
||||||
|
class JobCondition(str, Enum):
|
||||||
|
"""Job condition enum."""
|
||||||
|
|
||||||
|
FREE_SPACE = "free_space"
|
||||||
|
HEALTHY = "healthy"
|
||||||
|
INTERNET = "internet"
|
||||||
|
|
||||||
|
|
||||||
|
class Job:
|
||||||
|
"""Supervisor job decorator."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
conditions: Optional[List[JobCondition]] = None,
|
||||||
|
cleanup: bool = True,
|
||||||
|
):
|
||||||
|
"""Initialize the Job class."""
|
||||||
|
self.name = name
|
||||||
|
self.conditions = conditions
|
||||||
|
self.cleanup = cleanup
|
||||||
|
self._coresys: Optional[CoreSys] = None
|
||||||
|
self._method = None
|
||||||
|
|
||||||
|
def __call__(self, method):
|
||||||
|
"""Call the wrapper logic."""
|
||||||
|
self._method = method
|
||||||
|
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
"""Wrap the method."""
|
||||||
|
if self.name is None:
|
||||||
|
self.name = str(self._method.__qualname__).lower().replace(".", "_")
|
||||||
|
try:
|
||||||
|
self._coresys = args[0].coresys
|
||||||
|
except AttributeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self._coresys:
|
||||||
|
raise JobException(f"coresys is missing on {self.name}")
|
||||||
|
|
||||||
|
job = self._coresys.jobs.get_job(self.name)
|
||||||
|
|
||||||
|
if self.conditions and not await self._check_conditions():
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await self._method(*args, **kwargs)
|
||||||
|
except HassioError as err:
|
||||||
|
_LOGGER.error(err)
|
||||||
|
raise JobException() from err
|
||||||
|
finally:
|
||||||
|
if self.cleanup:
|
||||||
|
self._coresys.jobs.remove_job(job)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
async def _check_conditions(self):
|
||||||
|
"""Check conditions."""
|
||||||
|
if JobCondition.HEALTHY in self.conditions:
|
||||||
|
if not self._coresys.core.healthy:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"'%s' blocked from execution, system is not healthy",
|
||||||
|
self._method.__qualname__,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if JobCondition.FREE_SPACE in self.conditions:
|
||||||
|
free_space = self._coresys.host.info.free_space
|
||||||
|
if free_space < MINIMUM_FREE_SPACE_THRESHOLD:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"'%s' blocked from execution, not enough free space (%sGB) left on the device",
|
||||||
|
self._method.__qualname__,
|
||||||
|
free_space,
|
||||||
|
)
|
||||||
|
self._coresys.resolution.create_issue(
|
||||||
|
IssueType.FREE_SPACE, ContextType.SYSTEM
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if JobCondition.INTERNET in self.conditions:
|
||||||
|
if self._coresys.core.state == CoreState.RUNNING:
|
||||||
|
await self._coresys.host.network.check_connectivity()
|
||||||
|
await self._coresys.supervisor.check_connectivity()
|
||||||
|
if (
|
||||||
|
not self._coresys.supervisor.connectivity
|
||||||
|
or self._coresys.host.network.connectivity != ConnectivityState.FULL
|
||||||
|
):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"'%s' blocked from execution, no internet connection",
|
||||||
|
self._method.__qualname__,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
@ -12,7 +12,7 @@ from ..exceptions import (
|
|||||||
MulticastError,
|
MulticastError,
|
||||||
ObserverError,
|
ObserverError,
|
||||||
)
|
)
|
||||||
from ..resolution.const import MINIMUM_FREE_SPACE_THRESHOLD, ContextType, IssueType
|
from ..job.decorator import Job, JobCondition
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -113,6 +113,7 @@ class Tasks(CoreSysAttributes):
|
|||||||
|
|
||||||
_LOGGER.info("All core tasks are scheduled")
|
_LOGGER.info("All core tasks are scheduled")
|
||||||
|
|
||||||
|
@Job(conditions=[JobCondition.HEALTHY, JobCondition.FREE_SPACE])
|
||||||
async def _update_addons(self):
|
async def _update_addons(self):
|
||||||
"""Check if an update is available for an Add-on and update it."""
|
"""Check if an update is available for an Add-on and update it."""
|
||||||
for addon in self.sys_addons.all:
|
for addon in self.sys_addons.all:
|
||||||
@ -128,17 +129,6 @@ class Tasks(CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check free space
|
|
||||||
if self.sys_host.info.free_space < MINIMUM_FREE_SPACE_THRESHOLD:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Not enough free space, pausing add-on updates - available space %f",
|
|
||||||
self.sys_host.info.free_space,
|
|
||||||
)
|
|
||||||
self.sys_resolution.create_issue(
|
|
||||||
IssueType.FREE_SPACE, ContextType.SYSTEM
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Run Add-on update sequential
|
# Run Add-on update sequential
|
||||||
# avoid issue on slow IO
|
# avoid issue on slow IO
|
||||||
_LOGGER.info("Add-on auto update process %s", addon.slug)
|
_LOGGER.info("Add-on auto update process %s", addon.slug)
|
||||||
@ -147,20 +137,12 @@ class Tasks(CoreSysAttributes):
|
|||||||
except AddonsError:
|
except AddonsError:
|
||||||
_LOGGER.error("Can't auto update Add-on %s", addon.slug)
|
_LOGGER.error("Can't auto update Add-on %s", addon.slug)
|
||||||
|
|
||||||
|
@Job(conditions=[JobCondition.HEALTHY, JobCondition.FREE_SPACE])
|
||||||
async def _update_supervisor(self):
|
async def _update_supervisor(self):
|
||||||
"""Check and run update of Supervisor Supervisor."""
|
"""Check and run update of Supervisor Supervisor."""
|
||||||
if not self.sys_supervisor.need_update:
|
if not self.sys_supervisor.need_update:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check free space
|
|
||||||
if self.sys_host.info.free_space < MINIMUM_FREE_SPACE_THRESHOLD:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Not enough free space, pausing supervisor update - available space %s",
|
|
||||||
self.sys_host.info.free_space,
|
|
||||||
)
|
|
||||||
self.sys_resolution.create_issue(IssueType.FREE_SPACE, ContextType.SYSTEM)
|
|
||||||
return
|
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Found new Supervisor version %s, updating",
|
"Found new Supervisor version %s, updating",
|
||||||
self.sys_supervisor.latest_version,
|
self.sys_supervisor.latest_version,
|
||||||
|
@ -12,6 +12,7 @@ from supervisor.utils.json import read_json_file
|
|||||||
from ..const import REPOSITORY_CORE, REPOSITORY_LOCAL
|
from ..const import REPOSITORY_CORE, REPOSITORY_LOCAL
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..exceptions import JsonFileError
|
from ..exceptions import JsonFileError
|
||||||
|
from ..job.decorator import Job, JobCondition
|
||||||
from .addon import AddonStore
|
from .addon import AddonStore
|
||||||
from .data import StoreData
|
from .data import StoreData
|
||||||
from .repository import Repository
|
from .repository import Repository
|
||||||
@ -52,17 +53,22 @@ class StoreManager(CoreSysAttributes):
|
|||||||
await asyncio.wait(tasks)
|
await asyncio.wait(tasks)
|
||||||
|
|
||||||
# read data from repositories
|
# read data from repositories
|
||||||
self.data.update()
|
await self.load()
|
||||||
self._read_addons()
|
self._read_addons()
|
||||||
|
|
||||||
|
@Job(conditions=[JobCondition.INTERNET, JobCondition.HEALTHY])
|
||||||
async def update_repositories(self, list_repositories):
|
async def update_repositories(self, list_repositories):
|
||||||
"""Add a new custom repository."""
|
"""Add a new custom repository."""
|
||||||
|
job = self.sys_jobs.get_job("storemanager_update_repositories")
|
||||||
new_rep = set(list_repositories)
|
new_rep = set(list_repositories)
|
||||||
old_rep = set(self.repositories)
|
old_rep = set(self.repositories)
|
||||||
|
|
||||||
# add new repository
|
# add new repository
|
||||||
async def _add_repository(url):
|
async def _add_repository(url: str, step: int):
|
||||||
"""Add a repository."""
|
"""Add a repository."""
|
||||||
|
await job.update(
|
||||||
|
progress=job.progress + step, stage=f"Checking {url} started"
|
||||||
|
)
|
||||||
repository = Repository(self.coresys, url)
|
repository = Repository(self.coresys, url)
|
||||||
if not await repository.load():
|
if not await repository.load():
|
||||||
_LOGGER.error("Can't load data from repository %s", url)
|
_LOGGER.error("Can't load data from repository %s", url)
|
||||||
@ -85,7 +91,9 @@ class StoreManager(CoreSysAttributes):
|
|||||||
|
|
||||||
self.repositories[url] = repository
|
self.repositories[url] = repository
|
||||||
|
|
||||||
tasks = [_add_repository(url) for url in new_rep - old_rep]
|
await job.update(progress=10, stage="Check repositories")
|
||||||
|
repos = new_rep - old_rep
|
||||||
|
tasks = [_add_repository(url, 80 / len(repos)) for url in repos]
|
||||||
if tasks:
|
if tasks:
|
||||||
await asyncio.wait(tasks)
|
await asyncio.wait(tasks)
|
||||||
|
|
||||||
@ -95,9 +103,14 @@ class StoreManager(CoreSysAttributes):
|
|||||||
self.sys_config.drop_addon_repository(url)
|
self.sys_config.drop_addon_repository(url)
|
||||||
|
|
||||||
# update data
|
# update data
|
||||||
|
await job.update(progress=90, stage="Update addons")
|
||||||
self.data.update()
|
self.data.update()
|
||||||
|
|
||||||
|
await job.update(progress=95, stage="Read addons")
|
||||||
self._read_addons()
|
self._read_addons()
|
||||||
|
|
||||||
|
await job.update(progress=100)
|
||||||
|
|
||||||
def _read_addons(self) -> None:
|
def _read_addons(self) -> None:
|
||||||
"""Reload add-ons inside store."""
|
"""Reload add-ons inside store."""
|
||||||
all_addons = set(self.data.addons)
|
all_addons = set(self.data.addons)
|
||||||
|
@ -8,6 +8,7 @@ from tempfile import TemporaryDirectory
|
|||||||
from typing import Awaitable, Optional
|
from typing import Awaitable, Optional
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
from aiohttp.client_exceptions import ClientError
|
||||||
from packaging.version import parse as pkg_parse
|
from packaging.version import parse as pkg_parse
|
||||||
|
|
||||||
from .const import SUPERVISOR_VERSION, URL_HASSIO_APPARMOR
|
from .const import SUPERVISOR_VERSION, URL_HASSIO_APPARMOR
|
||||||
@ -32,6 +33,7 @@ class Supervisor(CoreSysAttributes):
|
|||||||
"""Initialize hass object."""
|
"""Initialize hass object."""
|
||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self.instance: DockerSupervisor = DockerSupervisor(coresys)
|
self.instance: DockerSupervisor = DockerSupervisor(coresys)
|
||||||
|
self._connectivity: bool = False
|
||||||
|
|
||||||
async def load(self) -> None:
|
async def load(self) -> None:
|
||||||
"""Prepare Home Assistant object."""
|
"""Prepare Home Assistant object."""
|
||||||
@ -43,6 +45,11 @@ class Supervisor(CoreSysAttributes):
|
|||||||
with suppress(DockerError):
|
with suppress(DockerError):
|
||||||
await self.instance.cleanup()
|
await self.instance.cleanup()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def connectivity(self) -> bool:
|
||||||
|
"""Return true if we are connected to the internet."""
|
||||||
|
return self._connectivity
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ip_address(self) -> IPv4Address:
|
def ip_address(self) -> IPv4Address:
|
||||||
"""Return IP of Supervisor instance."""
|
"""Return IP of Supervisor instance."""
|
||||||
@ -166,3 +173,14 @@ class Supervisor(CoreSysAttributes):
|
|||||||
await self.instance.retag()
|
await self.instance.retag()
|
||||||
except DockerError:
|
except DockerError:
|
||||||
_LOGGER.error("Repair of Supervisor failed")
|
_LOGGER.error("Repair of Supervisor failed")
|
||||||
|
|
||||||
|
async def check_connectivity(self):
|
||||||
|
"""Check the connection."""
|
||||||
|
try:
|
||||||
|
await self.sys_websession.head(
|
||||||
|
"https://version.home-assistant.io/online.txt", timeout=10
|
||||||
|
)
|
||||||
|
except (ClientError, asyncio.TimeoutError):
|
||||||
|
self._connectivity = False
|
||||||
|
else:
|
||||||
|
self._connectivity = True
|
||||||
|
@ -25,6 +25,7 @@ from .const import (
|
|||||||
)
|
)
|
||||||
from .coresys import CoreSysAttributes
|
from .coresys import CoreSysAttributes
|
||||||
from .exceptions import HassioUpdaterError
|
from .exceptions import HassioUpdaterError
|
||||||
|
from .job.decorator import Job, JobCondition
|
||||||
from .utils import AsyncThrottle
|
from .utils import AsyncThrottle
|
||||||
from .utils.json import JsonConfig
|
from .utils.json import JsonConfig
|
||||||
from .validate import SCHEMA_UPDATER_CONFIG
|
from .validate import SCHEMA_UPDATER_CONFIG
|
||||||
@ -158,6 +159,7 @@ class Updater(JsonConfig, CoreSysAttributes):
|
|||||||
self._data[ATTR_CHANNEL] = value
|
self._data[ATTR_CHANNEL] = value
|
||||||
|
|
||||||
@AsyncThrottle(timedelta(seconds=30))
|
@AsyncThrottle(timedelta(seconds=30))
|
||||||
|
@Job(conditions=[JobCondition.HEALTHY])
|
||||||
async def fetch_data(self):
|
async def fetch_data(self):
|
||||||
"""Fetch current versions from Github.
|
"""Fetch current versions from Github.
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ from supervisor.bootstrap import initialize_coresys
|
|||||||
from supervisor.coresys import CoreSys
|
from supervisor.coresys import CoreSys
|
||||||
from supervisor.dbus.network import NetworkManager
|
from supervisor.dbus.network import NetworkManager
|
||||||
from supervisor.docker import DockerAPI
|
from supervisor.docker import DockerAPI
|
||||||
|
from supervisor.host.const import ConnectivityState
|
||||||
from supervisor.utils.gdbus import DBus
|
from supervisor.utils.gdbus import DBus
|
||||||
|
|
||||||
from tests.common import exists_fixture, load_fixture, load_json_fixture
|
from tests.common import exists_fixture, load_fixture, load_json_fixture
|
||||||
@ -136,6 +137,10 @@ async def coresys(loop, docker, network_manager, aiohttp_client) -> CoreSys:
|
|||||||
# Mock docker
|
# Mock docker
|
||||||
coresys_obj._docker = docker
|
coresys_obj._docker = docker
|
||||||
|
|
||||||
|
# Set internet state
|
||||||
|
coresys_obj.supervisor._connectivity = True
|
||||||
|
coresys_obj.host.network._connectivity = ConnectivityState.FULL
|
||||||
|
|
||||||
yield coresys_obj
|
yield coresys_obj
|
||||||
|
|
||||||
|
|
||||||
|
41
tests/host/test_connectivity.py
Normal file
41
tests/host/test_connectivity.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"""Test supported features."""
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from supervisor.coresys import CoreSys
|
||||||
|
from supervisor.host.const import ConnectivityState
|
||||||
|
|
||||||
|
|
||||||
|
async def test_connectivity_unknown(coresys: CoreSys):
|
||||||
|
"""Test host unknown connectivity."""
|
||||||
|
with patch("supervisor.utils.gdbus.DBus._send", return_value="[0]"):
|
||||||
|
await coresys.host.network.check_connectivity()
|
||||||
|
assert coresys.host.network.connectivity == ConnectivityState.UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
|
async def test_connectivity_none(coresys: CoreSys):
|
||||||
|
"""Test host none connectivity."""
|
||||||
|
with patch("supervisor.utils.gdbus.DBus._send", return_value="[1]"):
|
||||||
|
await coresys.host.network.check_connectivity()
|
||||||
|
assert coresys.host.network.connectivity == ConnectivityState.NONE
|
||||||
|
|
||||||
|
|
||||||
|
async def test_connectivity_portal(coresys: CoreSys):
|
||||||
|
"""Test host portal connectivity."""
|
||||||
|
with patch("supervisor.utils.gdbus.DBus._send", return_value="[2]"):
|
||||||
|
await coresys.host.network.check_connectivity()
|
||||||
|
assert coresys.host.network.connectivity == ConnectivityState.PORTAL
|
||||||
|
|
||||||
|
|
||||||
|
async def test_connectivity_limited(coresys: CoreSys):
|
||||||
|
"""Test host limited connectivity."""
|
||||||
|
with patch("supervisor.utils.gdbus.DBus._send", return_value="[3]"):
|
||||||
|
await coresys.host.network.check_connectivity()
|
||||||
|
assert coresys.host.network.connectivity == ConnectivityState.LIMITED
|
||||||
|
|
||||||
|
|
||||||
|
async def test_connectivity_full(coresys: CoreSys):
|
||||||
|
"""Test host full connectivity."""
|
||||||
|
with patch("supervisor.utils.gdbus.DBus._send", return_value="[4]"):
|
||||||
|
await coresys.host.network.check_connectivity()
|
||||||
|
assert coresys.host.network.connectivity == ConnectivityState.FULL
|
73
tests/job/test_job_decorator.py
Normal file
73
tests/job/test_job_decorator.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
"""Test the condition decorators."""
|
||||||
|
# pylint: disable=protected-access,import-error
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from supervisor.coresys import CoreSys
|
||||||
|
from supervisor.job.decorator import Job, JobCondition
|
||||||
|
|
||||||
|
|
||||||
|
async def test_healthy(coresys: CoreSys):
|
||||||
|
"""Test the healty decorator."""
|
||||||
|
|
||||||
|
class TestClass:
|
||||||
|
"""Test class."""
|
||||||
|
|
||||||
|
def __init__(self, coresys: CoreSys):
|
||||||
|
"""Initialize the test class."""
|
||||||
|
self.coresys = coresys
|
||||||
|
|
||||||
|
@Job(conditions=[JobCondition.HEALTHY])
|
||||||
|
async def execute(self):
|
||||||
|
"""Execute the class method."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
test = TestClass(coresys)
|
||||||
|
assert await test.execute()
|
||||||
|
|
||||||
|
coresys.core.healthy = False
|
||||||
|
assert not await test.execute()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_internet(coresys: CoreSys):
|
||||||
|
"""Test the internet decorator."""
|
||||||
|
|
||||||
|
class TestClass:
|
||||||
|
"""Test class."""
|
||||||
|
|
||||||
|
def __init__(self, coresys: CoreSys):
|
||||||
|
"""Initialize the test class."""
|
||||||
|
self.coresys = coresys
|
||||||
|
|
||||||
|
@Job(conditions=[JobCondition.INTERNET])
|
||||||
|
async def execute(self):
|
||||||
|
"""Execute the class method."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
test = TestClass(coresys)
|
||||||
|
assert await test.execute()
|
||||||
|
|
||||||
|
coresys.supervisor._connectivity = False
|
||||||
|
assert not await test.execute()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_free_space(coresys: CoreSys):
|
||||||
|
"""Test the free_space decorator."""
|
||||||
|
|
||||||
|
class TestClass:
|
||||||
|
"""Test class."""
|
||||||
|
|
||||||
|
def __init__(self, coresys: CoreSys):
|
||||||
|
"""Initialize the test class."""
|
||||||
|
self.coresys = coresys
|
||||||
|
|
||||||
|
@Job(conditions=[JobCondition.FREE_SPACE])
|
||||||
|
async def execute(self):
|
||||||
|
"""Execute the class method."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
test = TestClass(coresys)
|
||||||
|
with patch("shutil.disk_usage", return_value=(42, 42, (1024.0 ** 3))):
|
||||||
|
assert await test.execute()
|
||||||
|
|
||||||
|
with patch("shutil.disk_usage", return_value=(42, 42, (512.0 ** 3))):
|
||||||
|
assert not await test.execute()
|
39
tests/job/test_job_manager.py
Normal file
39
tests/job/test_job_manager.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"""Test the condition decorators."""
|
||||||
|
# pylint: disable=protected-access,import-error
|
||||||
|
from supervisor.coresys import CoreSys
|
||||||
|
|
||||||
|
TEST_JOB = "test"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_add_job(coresys: CoreSys):
|
||||||
|
"""Test adding jobs."""
|
||||||
|
job = coresys.jobs.get_job(TEST_JOB)
|
||||||
|
|
||||||
|
assert job.name in coresys.jobs.jobs
|
||||||
|
|
||||||
|
|
||||||
|
async def test_remove_job_directly(coresys: CoreSys):
|
||||||
|
"""Test removing jobs from manager."""
|
||||||
|
job = coresys.jobs.get_job(TEST_JOB)
|
||||||
|
|
||||||
|
assert job.name in coresys.jobs.jobs
|
||||||
|
coresys.jobs.remove_job(job)
|
||||||
|
assert job.name not in coresys.jobs.jobs
|
||||||
|
|
||||||
|
|
||||||
|
async def test_remove_job_with_progress(coresys: CoreSys):
|
||||||
|
"""Test removing jobs by setting progress to 100."""
|
||||||
|
job = coresys.jobs.get_job(TEST_JOB)
|
||||||
|
|
||||||
|
assert job.name in coresys.jobs.jobs
|
||||||
|
await job.update(progress=100)
|
||||||
|
assert job.name not in coresys.jobs.jobs
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_job(coresys: CoreSys):
|
||||||
|
"""Test updating jobs."""
|
||||||
|
job = coresys.jobs.get_job(TEST_JOB)
|
||||||
|
|
||||||
|
await job.update(progress=50, stage="stage")
|
||||||
|
assert job.progress == 50
|
||||||
|
assert job.stage == "stage"
|
Loading…
x
Reference in New Issue
Block a user