diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 413391d01..4ad374b5d 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -48,6 +48,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
jq \
dbus \
network-manager \
+ apparmor-utils \
libpulse0 \
&& bash <(curl https://getvcn.codenotary.com -L) \
&& rm -rf /var/lib/apt/lists/*
diff --git a/scripts/supervisor.sh b/scripts/supervisor.sh
index 82b5c8674..21b0b4baa 100755
--- a/scripts/supervisor.sh
+++ b/scripts/supervisor.sh
@@ -88,6 +88,21 @@ function init_udev() {
udevadm trigger && udevadm settle
}
+function init_os-agent() {
+ if pgrep os-agent; then
+ echo "os-agent is running"
+ return 0
+ fi
+
+ if [ ! -f /usr/sbin/os-agent ]; then
+ curl -Lo /usr/sbin/os-agent https://github.com/home-assistant/os-agent/releases/latest/download/os-agent-debian-amd64.bin
+ curl -Lo /etc/dbus-1/system.d/io.hass.conf https://raw.githubusercontent.com/home-assistant/os-agent/main/contrib/io.hass.conf
+ chmod a+x /usr/sbin/os-agent
+ fi
+
+ /usr/sbin/os-agent &
+}
+
echo "Run Supervisor"
start_docker
@@ -99,6 +114,7 @@ if [ "$( docker container inspect -f '{{.State.Status}}' hassio_supervisor )" ==
docker rm -f hassio_supervisor
init_dbus
init_udev
+ init_os-agent
cleanup_lastboot
run_supervisor
stop_docker
@@ -111,6 +127,7 @@ else
cleanup_docker
init_dbus
init_udev
+ init_os-agent
run_supervisor
stop_docker
-fi
\ No newline at end of file
+fi
diff --git a/supervisor/bootstrap.py b/supervisor/bootstrap.py
index 204905bf4..582c5556a 100644
--- a/supervisor/bootstrap.py
+++ b/supervisor/bootstrap.py
@@ -33,7 +33,7 @@ from .const import (
)
from .core import Core
from .coresys import CoreSys
-from .dbus import DBusManager
+from .dbus.manager import DBusManager
from .discovery import Discovery
from .hardware.module import HardwareManager
from .hassos import HassOS
diff --git a/supervisor/coresys.py b/supervisor/coresys.py
index 898909935..fabc49e43 100644
--- a/supervisor/coresys.py
+++ b/supervisor/coresys.py
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
from .arch import CpuArch
from .auth import Auth
from .core import Core
- from .dbus import DBusManager
+ from .dbus.manager import DBusManager
from .discovery import Discovery
from .hardware.module import HardwareManager
from .hassos import HassOS
diff --git a/supervisor/dbus/__init__.py b/supervisor/dbus/__init__.py
index 81780ba48..ac5e8768f 100644
--- a/supervisor/dbus/__init__.py
+++ b/supervisor/dbus/__init__.py
@@ -1,84 +1 @@
-"""D-Bus interface objects."""
-import logging
-from typing import List
-
-from ..const import SOCKET_DBUS
-from ..coresys import CoreSys, CoreSysAttributes
-from .hostname import Hostname
-from .interface import DBusInterface
-from .logind import Logind
-from .network import NetworkManager
-from .rauc import Rauc
-from .systemd import Systemd
-from .timedate import TimeDate
-
-_LOGGER: logging.Logger = logging.getLogger(__name__)
-
-
-class DBusManager(CoreSysAttributes):
- """A DBus Interface handler."""
-
- def __init__(self, coresys: CoreSys) -> None:
- """Initialize D-Bus interface."""
- self.coresys: CoreSys = coresys
-
- self._systemd: Systemd = Systemd()
- self._logind: Logind = Logind()
- self._hostname: Hostname = Hostname()
- self._rauc: Rauc = Rauc()
- self._network: NetworkManager = NetworkManager()
- self._timedate: TimeDate = TimeDate()
-
- @property
- def systemd(self) -> Systemd:
- """Return the systemd interface."""
- return self._systemd
-
- @property
- def logind(self) -> Logind:
- """Return the logind interface."""
- return self._logind
-
- @property
- def timedate(self) -> TimeDate:
- """Return the timedate interface."""
- return self._timedate
-
- @property
- def hostname(self) -> Hostname:
- """Return the hostname interface."""
- return self._hostname
-
- @property
- def rauc(self) -> Rauc:
- """Return the rauc interface."""
- return self._rauc
-
- @property
- def network(self) -> NetworkManager:
- """Return NetworkManager interface."""
- return self._network
-
- async def load(self) -> None:
- """Connect interfaces to D-Bus."""
- if not SOCKET_DBUS.exists():
- _LOGGER.error(
- "No D-Bus support on Host. Disabled any kind of host control!"
- )
- return
-
- dbus_loads: List[DBusInterface] = [
- self.systemd,
- self.logind,
- self.hostname,
- self.timedate,
- self.network,
- self.rauc,
- ]
- for dbus in dbus_loads:
- try:
- await dbus.connect()
- except Exception as err: # pylint: disable=broad-except
- _LOGGER.warning("Can't load dbus interface %s: %s", dbus.name, err)
-
- self.sys_host.supported_features.cache_clear()
+"""D-Bus interfaces."""
diff --git a/supervisor/dbus/agent/__init__.py b/supervisor/dbus/agent/__init__.py
new file mode 100644
index 000000000..de567f2b8
--- /dev/null
+++ b/supervisor/dbus/agent/__init__.py
@@ -0,0 +1,99 @@
+"""OS-Agent implementation for DBUS."""
+import asyncio
+import logging
+from typing import Any, Dict
+
+from awesomeversion import AwesomeVersion
+
+from ...exceptions import DBusError, DBusInterfaceError
+from ...utils.gdbus import DBus
+from ..const import (
+ DBUS_ATTR_DIAGNOSTICS,
+ DBUS_ATTR_VERSION,
+ DBUS_NAME_HAOS,
+ DBUS_OBJECT_HAOS,
+)
+from ..interface import DBusInterface, dbus_property
+from ..utils import dbus_connected
+from .apparmor import AppArmor
+from .cgroup import CGroup
+from .datadisk import DataDisk
+from .system import System
+
+_LOGGER: logging.Logger = logging.getLogger(__name__)
+
+
+class OSAgent(DBusInterface):
+ """Handle D-Bus interface for OS-Agent."""
+
+ name = DBUS_NAME_HAOS
+
+ def __init__(self) -> None:
+ """Initialize Properties."""
+ self.properties: Dict[str, Any] = {}
+
+ self._cgroup: CGroup = CGroup()
+ self._apparmor: AppArmor = AppArmor()
+ self._system: System = System()
+ self._datadisk: DataDisk = DataDisk()
+
+ @property
+ def cgroup(self) -> CGroup:
+ """Return CGroup DBUS object."""
+ return self._cgroup
+
+ @property
+ def apparmor(self) -> AppArmor:
+ """Return AppArmor DBUS object."""
+ return self._apparmor
+
+ @property
+ def system(self) -> System:
+ """Return System DBUS object."""
+ return self._system
+
+ @property
+ def datadisk(self) -> DataDisk:
+ """Return DataDisk DBUS object."""
+ return self._datadisk
+
+ @property
+ @dbus_property
+ def version(self) -> AwesomeVersion:
+ """Return version of OS-Agent."""
+ return AwesomeVersion(self.properties[DBUS_ATTR_VERSION])
+
+ @property
+ @dbus_property
+ def diagnostics(self) -> bool:
+ """Return if diagnostics is enabled on OS-Agent."""
+ return self.properties[DBUS_ATTR_DIAGNOSTICS]
+
+ @diagnostics.setter
+ def diagnostics(self, value: bool) -> None:
+ """Enable or disable OS-Agent diagnostics."""
+ asyncio.create_task(
+ self.dbus.set_property(DBUS_NAME_HAOS, DBUS_ATTR_DIAGNOSTICS, value)
+ )
+
+ async def connect(self) -> None:
+ """Connect to system's D-Bus."""
+ try:
+ self.dbus = await DBus.connect(DBUS_NAME_HAOS, DBUS_OBJECT_HAOS)
+ await self.cgroup.connect()
+ await self.apparmor.connect()
+ await self.system.connect()
+ await self.datadisk.connect()
+ except DBusError:
+ _LOGGER.warning("Can't connect to OS-Agent")
+ except DBusInterfaceError:
+ _LOGGER.warning(
+ "No OS-Agent support on the host. Some Host functions have been disabled."
+ )
+
+ @dbus_connected
+ async def update(self):
+ """Update Properties."""
+ self.properties = await self.dbus.get_properties(DBUS_NAME_HAOS)
+ await self.apparmor.update()
+ await self.datadisk.update()
diff --git a/supervisor/dbus/agent/apparmor.py b/supervisor/dbus/agent/apparmor.py
new file mode 100644
index 000000000..17433d29c
--- /dev/null
+++ b/supervisor/dbus/agent/apparmor.py
@@ -0,0 +1,48 @@
+"""AppArmor object for OS-Agent."""
+from pathlib import Path
+from typing import Any, Dict
+
+from awesomeversion import AwesomeVersion
+
+from ...utils.gdbus import DBus
+from ..const import (
+ DBUS_ATTR_PARSER_VERSION,
+ DBUS_NAME_HAOS,
+ DBUS_NAME_HAOS_APPARMOR,
+ DBUS_OBJECT_HAOS_APPARMOR,
+)
+from ..interface import DBusInterface, dbus_property
+from ..utils import dbus_connected
+
+
+class AppArmor(DBusInterface):
+ """AppArmor object for OS Agent."""
+
+ def __init__(self) -> None:
+ """Initialize Properties."""
+ self.properties: Dict[str, Any] = {}
+
+ @property
+ @dbus_property
+ def version(self) -> AwesomeVersion:
+ """Return version of host AppArmor parser."""
+ return AwesomeVersion(self.properties[DBUS_ATTR_PARSER_VERSION])
+
+ async def connect(self) -> None:
+ """Get connection information."""
+ self.dbus = await DBus.connect(DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_APPARMOR)
+
+ @dbus_connected
+ async def update(self):
+ """Update Properties."""
+ self.properties = await self.dbus.get_properties(DBUS_NAME_HAOS_APPARMOR)
+
+ @dbus_connected
+ async def load_profile(self, profile: Path, cache: Path) -> None:
+ """Load/Update AppArmor profile."""
+ await self.dbus.AppArmor.LoadProfile(profile.as_posix(), cache.as_posix())
+
+ @dbus_connected
+ async def unload_profile(self, profile: Path, cache: Path) -> None:
+ """Remove AppArmor profile."""
+ await self.dbus.AppArmor.UnloadProfile(profile.as_posix(), cache.as_posix())
diff --git a/supervisor/dbus/agent/cgroup.py b/supervisor/dbus/agent/cgroup.py
new file mode 100644
index 000000000..e51f225ad
--- /dev/null
+++ b/supervisor/dbus/agent/cgroup.py
@@ -0,0 +1,19 @@
+"""CGroup object for OS-Agent."""
+
+from ...utils.gdbus import DBus
+from ..const import DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_CGROUP
+from ..interface import DBusInterface
+from ..utils import dbus_connected
+
+
+class CGroup(DBusInterface):
+ """CGroup object for OS Agent."""
+
+ async def connect(self) -> None:
+ """Get connection information."""
+ self.dbus = await DBus.connect(DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_CGROUP)
+
+ @dbus_connected
+ async def add_devices_allowed(self, container_id: str, permission: str) -> None:
+ """Update cgroup devices and add new devices."""
+ await self.dbus.CGroup.AddDevicesAllowed(container_id, permission)
diff --git a/supervisor/dbus/agent/datadisk.py b/supervisor/dbus/agent/datadisk.py
new file mode 100644
index 000000000..479a2fb89
--- /dev/null
+++ b/supervisor/dbus/agent/datadisk.py
@@ -0,0 +1,41 @@
+"""DataDisk object for OS-Agent."""
+from pathlib import Path
+from typing import Any, Dict
+
+from ...utils.gdbus import DBus
+from ..const import (
+ DBUS_ATTR_CURRENT_DEVICE,
+ DBUS_NAME_HAOS,
+ DBUS_NAME_HAOS_DATADISK,
+ DBUS_OBJECT_HAOS_DATADISK,
+)
+from ..interface import DBusInterface, dbus_property
+from ..utils import dbus_connected
+
+
+class DataDisk(DBusInterface):
+ """DataDisk object for OS Agent."""
+
+ def __init__(self) -> None:
+ """Initialize Properties."""
+ self.properties: Dict[str, Any] = {}
+
+ @property
+ @dbus_property
+ def current_device(self) -> Path:
+ """Return current device used for data."""
+ return Path(self.properties[DBUS_ATTR_CURRENT_DEVICE])
+
+ async def connect(self) -> None:
+ """Get connection information."""
+ self.dbus = await DBus.connect(DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_DATADISK)
+
+ @dbus_connected
+ async def update(self):
+ """Update Properties."""
+ self.properties = await self.dbus.get_properties(DBUS_NAME_HAOS_DATADISK)
+
+ @dbus_connected
+ async def change_device(self, device: Path) -> None:
+ """Load/Update AppArmor profile."""
+ await self.dbus.DataDisk.ChangeDevice(device.as_posix())
diff --git a/supervisor/dbus/agent/system.py b/supervisor/dbus/agent/system.py
new file mode 100644
index 000000000..beddd5e00
--- /dev/null
+++ b/supervisor/dbus/agent/system.py
@@ -0,0 +1,19 @@
+"""System object for OS-Agent."""
+
+from ...utils.gdbus import DBus
+from ..const import DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_SYSTEM
+from ..interface import DBusInterface
+from ..utils import dbus_connected
+
+
+class System(DBusInterface):
+ """System object for OS Agent."""
+
+ async def connect(self) -> None:
+ """Get connection information."""
+ self.dbus = await DBus.connect(DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_SYSTEM)
+
+ @dbus_connected
+ async def schedule_wipe_device(self) -> None:
+ """Schedule a factory reset on next system boot."""
+ await self.dbus.System.ScheduleWipeDevice()
diff --git a/supervisor/dbus/const.py b/supervisor/dbus/const.py
index 1c9e12821..1351363b9 100644
--- a/supervisor/dbus/const.py
+++ b/supervisor/dbus/const.py
@@ -20,6 +20,12 @@ DBUS_NAME_NM_CONNECTION_ACTIVE_CHANGED = (
DBUS_NAME_SYSTEMD = "org.freedesktop.systemd1"
DBUS_NAME_LOGIND = "org.freedesktop.login1"
DBUS_NAME_TIMEDATE = "org.freedesktop.timedate1"
+DBUS_NAME_HAOS = "io.hass.os"
+DBUS_NAME_HAOS_CGROUP = "io.hass.os.CGroup"
+DBUS_NAME_HAOS_APPARMOR = "io.hass.os.AppArmor"
+DBUS_NAME_HAOS_SYSTEM = "io.hass.os.System"
+DBUS_NAME_HAOS_DATADISK = "io.hass.os.DataDisk"
+
DBUS_OBJECT_BASE = "/"
DBUS_OBJECT_DNS = "/org/freedesktop/NetworkManager/DnsManager"
@@ -29,6 +35,11 @@ DBUS_OBJECT_NM = "/org/freedesktop/NetworkManager"
DBUS_OBJECT_SYSTEMD = "/org/freedesktop/systemd1"
DBUS_OBJECT_LOGIND = "/org/freedesktop/login1"
DBUS_OBJECT_TIMEDATE = "/org/freedesktop/timedate1"
+DBUS_OBJECT_HAOS = "/io/hass/os"
+DBUS_OBJECT_HAOS_CGROUP = "/io/hass/os/CGroup"
+DBUS_OBJECT_HAOS_APPARMOR = "/io/hass/os/AppArmor"
+DBUS_OBJECT_HAOS_SYSTEM = "/io/hass/os/System"
+DBUS_OBJECT_HAOS_DATADISK = "/io/hass/os/DataDisk"
DBUS_ATTR_ACTIVE_CONNECTIONS = "ActiveConnections"
DBUS_ATTR_ACTIVE_CONNECTION = "ActiveConnection"
@@ -77,6 +88,9 @@ DBUS_ATTR_LOCALRTC = "LocalRTC"
DBUS_ATTR_NTP = "NTP"
DBUS_ATTR_NTPSYNCHRONIZED = "NTPSynchronized"
DBUS_ATTR_TIMEUSEC = "TimeUSec"
+DBUS_ATTR_DIAGNOSTICS = "Diagnostics"
+DBUS_ATTR_CURRENT_DEVICE = "CurrentDevice"
+DBUS_ATTR_PARSER_VERSION = "ParserVersion"
class RaucState(str, Enum):
diff --git a/supervisor/dbus/manager.py b/supervisor/dbus/manager.py
new file mode 100644
index 000000000..8ea61dc02
--- /dev/null
+++ b/supervisor/dbus/manager.py
@@ -0,0 +1,93 @@
+"""D-Bus interface objects."""
+import logging
+from typing import List
+
+from ..const import SOCKET_DBUS
+from ..coresys import CoreSys, CoreSysAttributes
+from .agent import OSAgent
+from .hostname import Hostname
+from .interface import DBusInterface
+from .logind import Logind
+from .network import NetworkManager
+from .rauc import Rauc
+from .systemd import Systemd
+from .timedate import TimeDate
+
+_LOGGER: logging.Logger = logging.getLogger(__name__)
+
+
+class DBusManager(CoreSysAttributes):
+ """A DBus Interface handler."""
+
+ def __init__(self, coresys: CoreSys) -> None:
+ """Initialize D-Bus interface."""
+ self.coresys: CoreSys = coresys
+
+ self._systemd: Systemd = Systemd()
+ self._logind: Logind = Logind()
+ self._hostname: Hostname = Hostname()
+ self._rauc: Rauc = Rauc()
+ self._network: NetworkManager = NetworkManager()
+ self._agent: OSAgent = OSAgent()
+ self._timedate: TimeDate = TimeDate()
+
+ @property
+ def systemd(self) -> Systemd:
+ """Return the systemd interface."""
+ return self._systemd
+
+ @property
+ def logind(self) -> Logind:
+ """Return the logind interface."""
+ return self._logind
+
+ @property
+ def hostname(self) -> Hostname:
+ """Return the hostname interface."""
+ return self._hostname
+
+ @property
+ def rauc(self) -> Rauc:
+ """Return the rauc interface."""
+ return self._rauc
+
+ @property
+ def network(self) -> NetworkManager:
+ """Return NetworkManager interface."""
+ return self._network
+
+ @property
+ def agent(self) -> OSAgent:
+ """Return OS-Agent interface."""
+ return self._agent
+
+ @property
+ def timedate(self) -> TimeDate:
+ """Return the timedate interface."""
+ return self._timedate
+
+ async def load(self) -> None:
+ """Connect interfaces to D-Bus."""
+ if not SOCKET_DBUS.exists():
+ _LOGGER.error(
+ "No D-Bus support on Host. Disabled any kind of host control!"
+ )
+ return
+
+ dbus_loads: List[DBusInterface] = [
+ self.agent,
+ self.systemd,
+ self.logind,
+ self.hostname,
+ self.timedate,
+ self.network,
+ self.rauc,
+ ]
+ for dbus in dbus_loads:
+ _LOGGER.info("Load dbus interface %s", dbus.name)
+ try:
+ await dbus.connect()
+ except Exception as err: # pylint: disable=broad-except
+ _LOGGER.warning("Can't load dbus interface %s: %s", dbus.name, err)
+
+ self.sys_host.supported_features.cache_clear()
diff --git a/supervisor/host/__init__.py b/supervisor/host/__init__.py
index 5b384fa5e..329b203df 100644
--- a/supervisor/host/__init__.py
+++ b/supervisor/host/__init__.py
@@ -4,10 +4,10 @@ from functools import lru_cache
import logging
from typing import List
-from ..const import HostFeature
from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import HassioError, PulseAudioError
from .apparmor import AppArmorControl
+from .const import HostFeature
from .control import SystemControl
from .info import InfoCenter
from .network import NetworkManager
@@ -85,8 +85,11 @@ class HostManager(CoreSysAttributes):
if self.sys_dbus.timedate.is_connected:
features.append(HostFeature.TIMEDATE)
+ if self.sys_dbus.agent.is_connected:
+ features.append(HostFeature.AGENT)
+
if self.sys_hassos.available:
- features.append(HostFeature.HASSOS)
+ features.append(HostFeature.HAOS)
return features
@@ -100,6 +103,9 @@ class HostManager(CoreSysAttributes):
if self.sys_dbus.network.is_connected:
await self.network.update()
+ if self.sys_dbus.agent.is_connected:
+ await self.sys_dbus.agent.update()
+
with suppress(PulseAudioError):
await self.sound.update()
diff --git a/supervisor/host/const.py b/supervisor/host/const.py
index d8762c380..1d41dd8dd 100644
--- a/supervisor/host/const.py
+++ b/supervisor/host/const.py
@@ -33,3 +33,16 @@ class WifiMode(str, Enum):
MESH = "mesh"
ADHOC = "adhoc"
AP = "ap"
+
+
+class HostFeature(str, Enum):
+ """Host feature."""
+
+ HAOS = "haos"
+ HOSTNAME = "hostname"
+ NETWORK = "network"
+ REBOOT = "reboot"
+ SERVICES = "services"
+ SHUTDOWN = "shutdown"
+ AGENT = "agent"
+ TIMEDATE = "timedate"
diff --git a/supervisor/misc/tasks.py b/supervisor/misc/tasks.py
index f405b2c8b..7a515f448 100644
--- a/supervisor/misc/tasks.py
+++ b/supervisor/misc/tasks.py
@@ -1,7 +1,7 @@
"""A collection of tasks."""
import logging
-from ..const import AddonState, HostFeature
+from ..const import AddonState
from ..coresys import CoreSysAttributes
from ..exceptions import (
AddonsError,
@@ -12,6 +12,7 @@ from ..exceptions import (
MulticastError,
ObserverError,
)
+from ..host.const import HostFeature
from ..jobs.decorator import Job, JobCondition
_LOGGER: logging.Logger = logging.getLogger(__name__)
diff --git a/supervisor/resolution/evaluations/network_manager.py b/supervisor/resolution/evaluations/network_manager.py
index 24984a205..8c92ec7a5 100644
--- a/supervisor/resolution/evaluations/network_manager.py
+++ b/supervisor/resolution/evaluations/network_manager.py
@@ -1,8 +1,9 @@
"""Evaluation class for network manager."""
from typing import List
-from ...const import CoreState, HostFeature
+from ...const import CoreState
from ...coresys import CoreSys
+from ...host.const import HostFeature
from ..const import UnsupportedReason
from .base import EvaluateBase
diff --git a/supervisor/resolution/evaluations/systemd.py b/supervisor/resolution/evaluations/systemd.py
index 716958e03..2501c9232 100644
--- a/supervisor/resolution/evaluations/systemd.py
+++ b/supervisor/resolution/evaluations/systemd.py
@@ -1,8 +1,9 @@
"""Evaluation class for systemd."""
from typing import List
-from ...const import CoreState, HostFeature
+from ...const import CoreState
from ...coresys import CoreSys
+from ...host.const import HostFeature
from ..const import UnsupportedReason
from .base import EvaluateBase
diff --git a/supervisor/utils/gdbus.py b/supervisor/utils/gdbus.py
index d900a21b1..6766d8a96 100644
--- a/supervisor/utils/gdbus.py
+++ b/supervisor/utils/gdbus.py
@@ -67,6 +67,7 @@ MONITOR: str = "gdbus monitor --system --dest {bus}"
WAIT: str = "gdbus wait --system --activate {bus} --timeout 5 {bus}"
DBUS_METHOD_GETALL: str = "org.freedesktop.DBus.Properties.GetAll"
+DBUS_METHOD_SET: str = "org.freedesktop.DBus.Properties.Set"
def _convert_bytes(value: str) -> str:
@@ -225,6 +226,16 @@ class DBus:
_LOGGER.error("No attributes returned for %s", interface)
raise DBusFatalError() from err
+ async def set_property(
+ self, interface: str, name: str, value: Any
+ ) -> Dict[str, Any]:
+ """Set a property from interface."""
+ try:
+ return (await self.call_dbus(DBUS_METHOD_SET, interface, name, value))[0]
+ except IndexError as err:
+ _LOGGER.error("No Set attribute %s for %s", name, interface)
+ raise DBusFatalError() from err
+
async def _send(self, command: List[str], silent=False) -> str:
"""Send command over dbus."""
# Run command
diff --git a/tests/dbus/agent/__init__.py b/tests/dbus/agent/__init__.py
new file mode 100644
index 000000000..c17dfc96c
--- /dev/null
+++ b/tests/dbus/agent/__init__.py
@@ -0,0 +1 @@
+"""Tests for OS-Agent."""
diff --git a/tests/dbus/agent/test_agent.py b/tests/dbus/agent/test_agent.py
new file mode 100644
index 000000000..87befe755
--- /dev/null
+++ b/tests/dbus/agent/test_agent.py
@@ -0,0 +1,15 @@
+"""Test OSAgent dbus interface."""
+
+from supervisor.coresys import CoreSys
+
+
+async def test_dbus_osagent(coresys: CoreSys):
+ """Test coresys dbus connection."""
+ assert coresys.dbus.agent.version is None
+ assert coresys.dbus.agent.diagnostics is None
+
+ await coresys.dbus.agent.connect()
+ await coresys.dbus.agent.update()
+
+ assert coresys.dbus.agent.version == "1.1.0"
+ assert coresys.dbus.agent.diagnostics
diff --git a/tests/dbus/agent/test_apparmor.py b/tests/dbus/agent/test_apparmor.py
new file mode 100644
index 000000000..a825a56b0
--- /dev/null
+++ b/tests/dbus/agent/test_apparmor.py
@@ -0,0 +1,53 @@
+"""Test AppArmor/Agent dbus interface."""
+from pathlib import Path
+
+import pytest
+
+from supervisor.coresys import CoreSys
+from supervisor.exceptions import DBusNotConnectedError
+
+
+async def test_dbus_osagent_apparmor(coresys: CoreSys):
+ """Test coresys dbus connection."""
+ assert coresys.dbus.agent.apparmor.version is None
+
+ await coresys.dbus.agent.connect()
+ await coresys.dbus.agent.update()
+
+ assert coresys.dbus.agent.apparmor.version == "2.13.2"
+
+
+async def test_dbus_osagent_apparmor_load(coresys: CoreSys):
+ """Load AppArmor Profile on host."""
+
+ with pytest.raises(DBusNotConnectedError):
+ await coresys.dbus.agent.apparmor.load_profile(
+ Path("/data/apparmor/profile"), Path("/data/apparmor/cache")
+ )
+
+ await coresys.dbus.agent.connect()
+
+ assert (
+ await coresys.dbus.agent.apparmor.load_profile(
+ Path("/data/apparmor/profile"), Path("/data/apparmor/cache")
+ )
+ is None
+ )
+
+
+async def test_dbus_osagent_apparmor_unload(coresys: CoreSys):
+ """Unload AppArmor Profile on host."""
+
+ with pytest.raises(DBusNotConnectedError):
+ await coresys.dbus.agent.apparmor.unload_profile(
+ Path("/data/apparmor/profile"), Path("/data/apparmor/cache")
+ )
+
+ await coresys.dbus.agent.connect()
+
+ assert (
+ await coresys.dbus.agent.apparmor.unload_profile(
+ Path("/data/apparmor/profile"), Path("/data/apparmor/cache")
+ )
+ is None
+ )
diff --git a/tests/dbus/agent/test_cgroup.py b/tests/dbus/agent/test_cgroup.py
new file mode 100644
index 000000000..583dfedf0
--- /dev/null
+++ b/tests/dbus/agent/test_cgroup.py
@@ -0,0 +1,20 @@
+"""Test CGroup/Agent dbus interface."""
+
+import pytest
+
+from supervisor.coresys import CoreSys
+from supervisor.exceptions import DBusNotConnectedError
+
+
+async def test_dbus_osagent_cgroup_add_devices(coresys: CoreSys):
+ """Test wipe data partition on host."""
+
+ with pytest.raises(DBusNotConnectedError):
+ await coresys.dbus.agent.cgroup.add_devices_allowed("9324kl23j4kl", "*:* rwm")
+
+ await coresys.dbus.agent.connect()
+
+ assert (
+ await coresys.dbus.agent.cgroup.add_devices_allowed("9324kl23j4kl", "*:* rwm")
+ is None
+ )
diff --git a/tests/dbus/agent/test_datadisk.py b/tests/dbus/agent/test_datadisk.py
new file mode 100644
index 000000000..a0d72b5c4
--- /dev/null
+++ b/tests/dbus/agent/test_datadisk.py
@@ -0,0 +1,28 @@
+"""Test Datadisk/Agent dbus interface."""
+from pathlib import Path
+
+import pytest
+
+from supervisor.coresys import CoreSys
+from supervisor.exceptions import DBusNotConnectedError
+
+
+async def test_dbus_osagent_datadisk(coresys: CoreSys):
+ """Test coresys dbus connection."""
+ assert coresys.dbus.agent.datadisk.current_device is None
+
+ await coresys.dbus.agent.connect()
+ await coresys.dbus.agent.update()
+
+ assert coresys.dbus.agent.datadisk.current_device.as_posix() == "/dev/sda"
+
+
+async def test_dbus_osagent_datadisk_change_device(coresys: CoreSys):
+ """Change datadisk on device."""
+
+ with pytest.raises(DBusNotConnectedError):
+ await coresys.dbus.agent.datadisk.change_device(Path("/dev/sdb"))
+
+ await coresys.dbus.agent.connect()
+
+ assert await coresys.dbus.agent.datadisk.change_device(Path("/dev/sdb")) is None
diff --git a/tests/dbus/agent/test_system.py b/tests/dbus/agent/test_system.py
new file mode 100644
index 000000000..63fc67b29
--- /dev/null
+++ b/tests/dbus/agent/test_system.py
@@ -0,0 +1,17 @@
+"""Test System/Agent dbus interface."""
+
+import pytest
+
+from supervisor.coresys import CoreSys
+from supervisor.exceptions import DBusNotConnectedError
+
+
+async def test_dbus_osagent_system_wipe(coresys: CoreSys):
+ """Test wipe data partition on host."""
+
+ with pytest.raises(DBusNotConnectedError):
+ await coresys.dbus.agent.system.schedule_wipe_device()
+
+ await coresys.dbus.agent.connect()
+
+ assert await coresys.dbus.agent.system.schedule_wipe_device() is None
diff --git a/tests/dbus/network/__init__.py b/tests/dbus/network/__init__.py
new file mode 100644
index 000000000..e46cba1a9
--- /dev/null
+++ b/tests/dbus/network/__init__.py
@@ -0,0 +1 @@
+"""Tests for DBUS NetworkManager."""
diff --git a/tests/fixtures/io_hass_os.json b/tests/fixtures/io_hass_os.json
new file mode 100644
index 000000000..af7eab57e
--- /dev/null
+++ b/tests/fixtures/io_hass_os.json
@@ -0,0 +1 @@
+{ "Version": "1.1.0", "Diagnostics": true }
diff --git a/tests/fixtures/io_hass_os.xml b/tests/fixtures/io_hass_os.xml
new file mode 100644
index 000000000..06cff9ea7
--- /dev/null
+++ b/tests/fixtures/io_hass_os.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/io_hass_os_AppArmor-LoadProfile.fixture b/tests/fixtures/io_hass_os_AppArmor-LoadProfile.fixture
new file mode 100644
index 000000000..3c2c10f15
--- /dev/null
+++ b/tests/fixtures/io_hass_os_AppArmor-LoadProfile.fixture
@@ -0,0 +1 @@
+(,)
\ No newline at end of file
diff --git a/tests/fixtures/io_hass_os_AppArmor-UnloadProfile.fixture b/tests/fixtures/io_hass_os_AppArmor-UnloadProfile.fixture
new file mode 100644
index 000000000..3c2c10f15
--- /dev/null
+++ b/tests/fixtures/io_hass_os_AppArmor-UnloadProfile.fixture
@@ -0,0 +1 @@
+(,)
\ No newline at end of file
diff --git a/tests/fixtures/io_hass_os_AppArmor.json b/tests/fixtures/io_hass_os_AppArmor.json
new file mode 100644
index 000000000..57632f0f6
--- /dev/null
+++ b/tests/fixtures/io_hass_os_AppArmor.json
@@ -0,0 +1 @@
+{ "ParserVersion": "2.13.2" }
diff --git a/tests/fixtures/io_hass_os_AppArmor.xml b/tests/fixtures/io_hass_os_AppArmor.xml
new file mode 100644
index 000000000..0cd2178a2
--- /dev/null
+++ b/tests/fixtures/io_hass_os_AppArmor.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/io_hass_os_CGroup-AddDevicesAllowed.fixture b/tests/fixtures/io_hass_os_CGroup-AddDevicesAllowed.fixture
new file mode 100644
index 000000000..3c2c10f15
--- /dev/null
+++ b/tests/fixtures/io_hass_os_CGroup-AddDevicesAllowed.fixture
@@ -0,0 +1 @@
+(,)
\ No newline at end of file
diff --git a/tests/fixtures/io_hass_os_CGroup.xml b/tests/fixtures/io_hass_os_CGroup.xml
new file mode 100644
index 000000000..9ce67937d
--- /dev/null
+++ b/tests/fixtures/io_hass_os_CGroup.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/io_hass_os_DataDisk-ChangeDevice.fixture b/tests/fixtures/io_hass_os_DataDisk-ChangeDevice.fixture
new file mode 100644
index 000000000..3c2c10f15
--- /dev/null
+++ b/tests/fixtures/io_hass_os_DataDisk-ChangeDevice.fixture
@@ -0,0 +1 @@
+(,)
\ No newline at end of file
diff --git a/tests/fixtures/io_hass_os_DataDisk.json b/tests/fixtures/io_hass_os_DataDisk.json
new file mode 100644
index 000000000..1599cc0fc
--- /dev/null
+++ b/tests/fixtures/io_hass_os_DataDisk.json
@@ -0,0 +1 @@
+{ "CurrentDevice": "/dev/sda" }
diff --git a/tests/fixtures/io_hass_os_DataDisk.xml b/tests/fixtures/io_hass_os_DataDisk.xml
new file mode 100644
index 000000000..9c64e9a2d
--- /dev/null
+++ b/tests/fixtures/io_hass_os_DataDisk.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/io_hass_os_System-ScheduleWipeDevice.fixture b/tests/fixtures/io_hass_os_System-ScheduleWipeDevice.fixture
new file mode 100644
index 000000000..3c2c10f15
--- /dev/null
+++ b/tests/fixtures/io_hass_os_System-ScheduleWipeDevice.fixture
@@ -0,0 +1 @@
+(,)
\ No newline at end of file
diff --git a/tests/fixtures/io_hass_os_System.xml b/tests/fixtures/io_hass_os_System.xml
new file mode 100644
index 000000000..43030956f
--- /dev/null
+++ b/tests/fixtures/io_hass_os_System.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/resolution/evaluation/test_evaluate_systemd.py b/tests/resolution/evaluation/test_evaluate_systemd.py
index 654b42245..5a79c4ca5 100644
--- a/tests/resolution/evaluation/test_evaluate_systemd.py
+++ b/tests/resolution/evaluation/test_evaluate_systemd.py
@@ -2,8 +2,9 @@
# pylint: disable=import-error,protected-access
from unittest.mock import MagicMock, patch
-from supervisor.const import CoreState, HostFeature
+from supervisor.const import CoreState
from supervisor.coresys import CoreSys
+from supervisor.host.const import HostFeature
from supervisor.resolution.evaluations.systemd import EvaluateSystemd