diff --git a/requirements.txt b/requirements.txt
index 961fc6cfc..aeb4724d4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -23,3 +23,4 @@ securetar==2022.2.0
sentry-sdk==1.15.0
voluptuous==0.13.1
dbus-fast==1.84.1
+typing_extensions==4.3.0
diff --git a/requirements_tests.txt b/requirements_tests.txt
index a4a70bc4e..cf33b86bd 100644
--- a/requirements_tests.txt
+++ b/requirements_tests.txt
@@ -13,3 +13,4 @@ pytest-timeout==2.1.0
pytest==7.2.1
pyupgrade==3.3.1
time-machine==2.9.0
+typing_extensions==4.3.0
diff --git a/supervisor/api/const.py b/supervisor/api/const.py
index 6a444261c..d96361e09 100644
--- a/supervisor/api/const.py
+++ b/supervisor/api/const.py
@@ -9,33 +9,44 @@ CONTENT_TYPE_URL = "application/x-www-form-urlencoded"
COOKIE_INGRESS = "ingress_session"
-ATTR_APPARMOR_VERSION = "apparmor_version"
ATTR_AGENT_VERSION = "agent_version"
+ATTR_APPARMOR_VERSION = "apparmor_version"
+ATTR_ATTRIBUTES = "attributes"
ATTR_AVAILABLE_UPDATES = "available_updates"
ATTR_BOOT_TIMESTAMP = "boot_timestamp"
ATTR_BOOTS = "boots"
ATTR_BROADCAST_LLMNR = "broadcast_llmnr"
ATTR_BROADCAST_MDNS = "broadcast_mdns"
+ATTR_BY_ID = "by_id"
+ATTR_CHILDREN = "children"
+ATTR_CONNECTION_BUS = "connection_bus"
ATTR_DATA_DISK = "data_disk"
ATTR_DEVICE = "device"
+ATTR_DEV_PATH = "dev_path"
ATTR_DISK_LED = "disk_led"
+ATTR_DRIVES = "drives"
ATTR_DT_SYNCHRONIZED = "dt_synchronized"
ATTR_DT_UTC = "dt_utc"
+ATTR_EJECTABLE = "ejectable"
ATTR_FALLBACK = "fallback"
+ATTR_FILESYSTEMS = "filesystems"
ATTR_HEARTBEAT_LED = "heartbeat_led"
ATTR_IDENTIFIERS = "identifiers"
ATTR_LLMNR = "llmnr"
ATTR_LLMNR_HOSTNAME = "llmnr_hostname"
ATTR_MDNS = "mdns"
+ATTR_MODEL = "model"
+ATTR_MOUNT_POINTS = "mount_points"
ATTR_PANEL_PATH = "panel_path"
ATTR_POWER_LED = "power_led"
+ATTR_REMOVABLE = "removable"
+ATTR_REVISION = "revision"
+ATTR_SEAT = "seat"
ATTR_SIGNED = "signed"
ATTR_STARTUP_TIME = "startup_time"
-ATTR_UPDATE_TYPE = "update_type"
-ATTR_USE_NTP = "use_ntp"
-ATTR_BY_ID = "by_id"
ATTR_SUBSYSTEM = "subsystem"
ATTR_SYSFS = "sysfs"
-ATTR_DEV_PATH = "dev_path"
-ATTR_ATTRIBUTES = "attributes"
-ATTR_CHILDREN = "children"
+ATTR_TIME_DETECTED = "time_detected"
+ATTR_UPDATE_TYPE = "update_type"
+ATTR_USE_NTP = "use_ntp"
+ATTR_VENDOR = "vendor"
diff --git a/supervisor/api/hardware.py b/supervisor/api/hardware.py
index 89b50b0d0..e289bd02a 100644
--- a/supervisor/api/hardware.py
+++ b/supervisor/api/hardware.py
@@ -4,16 +4,41 @@ from typing import Any
from aiohttp import web
-from ..const import ATTR_AUDIO, ATTR_DEVICES, ATTR_INPUT, ATTR_NAME, ATTR_OUTPUT
+from ..const import (
+ ATTR_AUDIO,
+ ATTR_DEVICES,
+ ATTR_ID,
+ ATTR_INPUT,
+ ATTR_NAME,
+ ATTR_OUTPUT,
+ ATTR_SERIAL,
+ ATTR_SIZE,
+ ATTR_SYSTEM,
+)
from ..coresys import CoreSysAttributes
+from ..dbus.udisks2 import UDisks2
+from ..dbus.udisks2.block import UDisks2Block
+from ..dbus.udisks2.drive import UDisks2Drive
from ..hardware.data import Device
from .const import (
ATTR_ATTRIBUTES,
ATTR_BY_ID,
ATTR_CHILDREN,
+ ATTR_CONNECTION_BUS,
ATTR_DEV_PATH,
+ ATTR_DEVICE,
+ ATTR_DRIVES,
+ ATTR_EJECTABLE,
+ ATTR_FILESYSTEMS,
+ ATTR_MODEL,
+ ATTR_MOUNT_POINTS,
+ ATTR_REMOVABLE,
+ ATTR_REVISION,
+ ATTR_SEAT,
ATTR_SUBSYSTEM,
ATTR_SYSFS,
+ ATTR_TIME_DETECTED,
+ ATTR_VENDOR,
)
from .utils import api_process
@@ -21,7 +46,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
def device_struct(device: Device) -> dict[str, Any]:
- """Return a dict with information of a interface to be used in th API."""
+ """Return a dict with information of a interface to be used in the API."""
return {
ATTR_NAME: device.name,
ATTR_SYSFS: device.sysfs,
@@ -33,6 +58,42 @@ def device_struct(device: Device) -> dict[str, Any]:
}
+def filesystem_struct(fs_block: UDisks2Block) -> dict[str, Any]:
+ """Return a dict with information of a filesystem block device to be used in the API."""
+ return {
+ ATTR_DEVICE: str(fs_block.device),
+ ATTR_ID: fs_block.id,
+ ATTR_SIZE: fs_block.size,
+ ATTR_NAME: fs_block.id_label,
+ ATTR_SYSTEM: fs_block.hint_system,
+ ATTR_MOUNT_POINTS: [
+ str(mount_point) for mount_point in fs_block.filesystem.mount_points
+ ],
+ }
+
+
+def drive_struct(udisks2: UDisks2, drive: UDisks2Drive) -> dict[str, Any]:
+ """Return a dict with information of a disk to be used in the API."""
+ return {
+ ATTR_VENDOR: drive.vendor,
+ ATTR_MODEL: drive.model,
+ ATTR_REVISION: drive.revision,
+ ATTR_SERIAL: drive.serial,
+ ATTR_ID: drive.id,
+ ATTR_SIZE: drive.size,
+ ATTR_TIME_DETECTED: drive.time_detected.isoformat(),
+ ATTR_CONNECTION_BUS: drive.connection_bus,
+ ATTR_SEAT: drive.seat,
+ ATTR_REMOVABLE: drive.removable,
+ ATTR_EJECTABLE: drive.ejectable,
+ ATTR_FILESYSTEMS: [
+ filesystem_struct(block)
+ for block in udisks2.block_devices
+ if block.filesystem and block.drive == drive.object_path
+ ],
+ }
+
+
class APIHardware(CoreSysAttributes):
"""Handle RESTful API for hardware functions."""
@@ -42,7 +103,11 @@ class APIHardware(CoreSysAttributes):
return {
ATTR_DEVICES: [
device_struct(device) for device in self.sys_hardware.devices
- ]
+ ],
+ ATTR_DRIVES: [
+ drive_struct(self.sys_dbus.udisks2, drive)
+ for drive in self.sys_dbus.udisks2.drives
+ ],
}
@api_process
diff --git a/supervisor/dbus/agent/__init__.py b/supervisor/dbus/agent/__init__.py
index 2fd4dbd85..532d0ed5f 100644
--- a/supervisor/dbus/agent/__init__.py
+++ b/supervisor/dbus/agent/__init__.py
@@ -35,7 +35,7 @@ class OSAgent(DBusInterfaceProxy):
def __init__(self) -> None:
"""Initialize Properties."""
- self.properties: dict[str, Any] = {}
+ super().__init__()
self._apparmor: AppArmor = AppArmor()
self._board: BoardManager = BoardManager()
diff --git a/supervisor/dbus/agent/apparmor.py b/supervisor/dbus/agent/apparmor.py
index 26810630d..66a66c6f0 100644
--- a/supervisor/dbus/agent/apparmor.py
+++ b/supervisor/dbus/agent/apparmor.py
@@ -1,6 +1,5 @@
"""AppArmor object for OS-Agent."""
from pathlib import Path
-from typing import Any
from awesomeversion import AwesomeVersion
@@ -21,10 +20,6 @@ class AppArmor(DBusInterfaceProxy):
object_path: str = DBUS_OBJECT_HAOS_APPARMOR
properties_interface: str = DBUS_IFACE_HAOS_APPARMOR
- def __init__(self) -> None:
- """Initialize Properties."""
- self.properties: dict[str, Any] = {}
-
@property
@dbus_property
def version(self) -> AwesomeVersion:
diff --git a/supervisor/dbus/agent/boards/__init__.py b/supervisor/dbus/agent/boards/__init__.py
index 5b6a6cb77..ebba126d2 100644
--- a/supervisor/dbus/agent/boards/__init__.py
+++ b/supervisor/dbus/agent/boards/__init__.py
@@ -1,6 +1,5 @@
"""Board management for OS Agent."""
import logging
-from typing import Any
from dbus_fast.aio.message_bus import MessageBus
@@ -30,8 +29,9 @@ class BoardManager(DBusInterfaceProxy):
def __init__(self) -> None:
"""Initialize properties."""
+ super().__init__()
+
self._board_proxy: BoardProxy | None = None
- self.properties: dict[str, Any] = {}
@property
@dbus_property
diff --git a/supervisor/dbus/agent/boards/interface.py b/supervisor/dbus/agent/boards/interface.py
index 46edc4b00..f167622ac 100644
--- a/supervisor/dbus/agent/boards/interface.py
+++ b/supervisor/dbus/agent/boards/interface.py
@@ -1,7 +1,5 @@
"""Board dbus proxy interface."""
-from typing import Any
-
from ...const import DBUS_IFACE_HAOS_BOARDS, DBUS_NAME_HAOS, DBUS_OBJECT_HAOS_BOARDS
from ...interface import DBusInterfaceProxy
@@ -13,10 +11,11 @@ class BoardProxy(DBusInterfaceProxy):
def __init__(self, name: str) -> None:
"""Initialize properties."""
+ super().__init__()
+
self._name: str = name
self.object_path: str = f"{DBUS_OBJECT_HAOS_BOARDS}/{name}"
self.properties_interface: str = f"{DBUS_IFACE_HAOS_BOARDS}.{name}"
- self.properties: dict[str, Any] = {}
@property
def name(self) -> str:
diff --git a/supervisor/dbus/agent/datadisk.py b/supervisor/dbus/agent/datadisk.py
index cfae729e3..a16c69319 100644
--- a/supervisor/dbus/agent/datadisk.py
+++ b/supervisor/dbus/agent/datadisk.py
@@ -1,6 +1,5 @@
"""DataDisk object for OS-Agent."""
from pathlib import Path
-from typing import Any
from ..const import (
DBUS_ATTR_CURRENT_DEVICE,
@@ -19,10 +18,6 @@ class DataDisk(DBusInterfaceProxy):
object_path: str = DBUS_OBJECT_HAOS_DATADISK
properties_interface: str = DBUS_IFACE_HAOS_DATADISK
- def __init__(self) -> None:
- """Initialize Properties."""
- self.properties: dict[str, Any] = {}
-
@property
@dbus_property
def current_device(self) -> Path:
diff --git a/supervisor/dbus/const.py b/supervisor/dbus/const.py
index b80ca4e7c..a75aef2d9 100644
--- a/supervisor/dbus/const.py
+++ b/supervisor/dbus/const.py
@@ -10,12 +10,16 @@ DBUS_NAME_RAUC = "de.pengutronix.rauc"
DBUS_NAME_RESOLVED = "org.freedesktop.resolve1"
DBUS_NAME_SYSTEMD = "org.freedesktop.systemd1"
DBUS_NAME_TIMEDATE = "org.freedesktop.timedate1"
+DBUS_NAME_UDISKS2 = "org.freedesktop.UDisks2"
DBUS_IFACE_ACCESSPOINT = "org.freedesktop.NetworkManager.AccessPoint"
+DBUS_IFACE_BLOCK = "org.freedesktop.UDisks2.Block"
DBUS_IFACE_CONNECTION_ACTIVE = "org.freedesktop.NetworkManager.Connection.Active"
DBUS_IFACE_DEVICE = "org.freedesktop.NetworkManager.Device"
DBUS_IFACE_DEVICE_WIRELESS = "org.freedesktop.NetworkManager.Device.Wireless"
DBUS_IFACE_DNS = "org.freedesktop.NetworkManager.DnsManager"
+DBUS_IFACE_DRIVE = "org.freedesktop.UDisks2.Drive"
+DBUS_IFACE_FILESYSTEM = "org.freedesktop.UDisks2.Filesystem"
DBUS_IFACE_HAOS = "io.hass.os"
DBUS_IFACE_HAOS_APPARMOR = "io.hass.os.AppArmor"
DBUS_IFACE_HAOS_BOARDS = "io.hass.os.Boards"
@@ -26,11 +30,13 @@ DBUS_IFACE_HOSTNAME = "org.freedesktop.hostname1"
DBUS_IFACE_IP4CONFIG = "org.freedesktop.NetworkManager.IP4Config"
DBUS_IFACE_IP6CONFIG = "org.freedesktop.NetworkManager.IP6Config"
DBUS_IFACE_NM = "org.freedesktop.NetworkManager"
+DBUS_IFACE_PARTITION_TABLE = "org.freedesktop.UDisks2.PartitionTable"
DBUS_IFACE_RAUC_INSTALLER = "de.pengutronix.rauc.Installer"
DBUS_IFACE_RESOLVED_MANAGER = "org.freedesktop.resolve1.Manager"
DBUS_IFACE_SETTINGS_CONNECTION = "org.freedesktop.NetworkManager.Settings.Connection"
DBUS_IFACE_SYSTEMD_MANAGER = "org.freedesktop.systemd1.Manager"
DBUS_IFACE_TIMEDATE = "org.freedesktop.timedate1"
+DBUS_IFACE_UDISKS2_MANAGER = "org.freedesktop.UDisks2.Manager"
DBUS_SIGNAL_NM_CONNECTION_ACTIVE_CHANGED = (
"org.freedesktop.NetworkManager.Connection.Active.StateChanged"
@@ -52,6 +58,7 @@ DBUS_OBJECT_RESOLVED = "/org/freedesktop/resolve1"
DBUS_OBJECT_SETTINGS = "/org/freedesktop/NetworkManager/Settings"
DBUS_OBJECT_SYSTEMD = "/org/freedesktop/systemd1"
DBUS_OBJECT_TIMEDATE = "/org/freedesktop/timedate1"
+DBUS_OBJECT_UDISKS2 = "/org/freedesktop/UDisks2/Manager"
DBUS_ATTR_ACTIVE_ACCESSPOINT = "ActiveAccessPoint"
DBUS_ATTR_ACTIVE_CONNECTION = "ActiveConnection"
@@ -64,6 +71,7 @@ DBUS_ATTR_CHASSIS = "Chassis"
DBUS_ATTR_COMPATIBLE = "Compatible"
DBUS_ATTR_CONFIGURATION = "Configuration"
DBUS_ATTR_CONNECTION = "Connection"
+DBUS_ATTR_CONNECTION_BUS = "ConnectionBus"
DBUS_ATTR_CONNECTION_ENABLED = "ConnectivityCheckEnabled"
DBUS_ATTR_CONNECTIVITY = "Connectivity"
DBUS_ATTR_CURRENT_DEVICE = "CurrentDevice"
@@ -71,7 +79,9 @@ DBUS_ATTR_CURRENT_DNS_SERVER = "CurrentDNSServer"
DBUS_ATTR_CURRENT_DNS_SERVER_EX = "CurrentDNSServerEx"
DBUS_ATTR_DEFAULT = "Default"
DBUS_ATTR_DEPLOYMENT = "Deployment"
+DBUS_ATTR_DEVICE = "Device"
DBUS_ATTR_DEVICE_INTERFACE = "Interface"
+DBUS_ATTR_DEVICE_NUMBER = "DeviceNumber"
DBUS_ATTR_DEVICE_TYPE = "DeviceType"
DBUS_ATTR_DEVICES = "Devices"
DBUS_ATTR_DIAGNOSTICS = "Diagnostics"
@@ -85,7 +95,9 @@ DBUS_ATTR_DNSSEC_NEGATIVE_TRUST_ANCHORS = "DNSSECNegativeTrustAnchors"
DBUS_ATTR_DNSSEC_STATISTICS = "DNSSECStatistics"
DBUS_ATTR_DNSSEC_SUPPORTED = "DNSSECSupported"
DBUS_ATTR_DOMAINS = "Domains"
+DBUS_ATTR_DRIVE = "Drive"
DBUS_ATTR_DRIVER = "Driver"
+DBUS_ATTR_EJECTABLE = "Ejectable"
DBUS_ATTR_FALLBACK_DNS = "FallbackDNS"
DBUS_ATTR_FALLBACK_DNS_EX = "FallbackDNSEx"
DBUS_ATTR_FINISH_TIMESTAMP = "FinishTimestamp"
@@ -93,8 +105,17 @@ DBUS_ATTR_FIRMWARE_TIMESTAMP_MONOTONIC = "FirmwareTimestampMonotonic"
DBUS_ATTR_FREQUENCY = "Frequency"
DBUS_ATTR_GATEWAY = "Gateway"
DBUS_ATTR_HEARTBEAT_LED = "HeartbeatLED"
+DBUS_ATTR_HINT_AUTO = "HintAuto"
+DBUS_ATTR_HINT_IGNORE = "HintIgnore"
+DBUS_ATTR_HINT_NAME = "HintName"
+DBUS_ATTR_HINT_SYSTEM = "HintSystem"
DBUS_ATTR_HWADDRESS = "HwAddress"
DBUS_ATTR_ID = "Id"
+DBUS_ATTR_ID_LABEL = "IdLabel"
+DBUS_ATTR_ID_ID_TYPE = "IdType"
+DBUS_ATTR_ID_USAGE = "IdUsage"
+DBUS_ATTR_ID_UUID = "IdUUID"
+DBUS_ATTR_ID_VERSION = "IdVersion"
DBUS_ATTR_IP4CONFIG = "Ip4Config"
DBUS_ATTR_IP6CONFIG = "Ip6Config"
DBUS_ATTR_KERNEL_RELEASE = "KernelRelease"
@@ -105,6 +126,8 @@ DBUS_ATTR_LLMNR_HOSTNAME = "LLMNRHostname"
DBUS_ATTR_LOADER_TIMESTAMP_MONOTONIC = "LoaderTimestampMonotonic"
DBUS_ATTR_MANAGED = "Managed"
DBUS_ATTR_MODE = "Mode"
+DBUS_ATTR_MODEL = "Model"
+DBUS_ATTR_MOUNT_POINTS = "MountPoints"
DBUS_ATTR_MULTICAST_DNS = "MulticastDNS"
DBUS_ATTR_NAMESERVER_DATA = "NameserverData"
DBUS_ATTR_NAMESERVERS = "Nameservers"
@@ -113,16 +136,26 @@ DBUS_ATTR_NTPSYNCHRONIZED = "NTPSynchronized"
DBUS_ATTR_OPERATING_SYSTEM_PRETTY_NAME = "OperatingSystemPrettyName"
DBUS_ATTR_OPERATION = "Operation"
DBUS_ATTR_PARSER_VERSION = "ParserVersion"
+DBUS_ATTR_PARTITIONS = "Partitions"
DBUS_ATTR_POWER_LED = "PowerLED"
DBUS_ATTR_PRIMARY_CONNECTION = "PrimaryConnection"
+DBUS_ATTR_READ_ONLY = "ReadOnly"
+DBUS_ATTR_REMOVABLE = "Removable"
DBUS_ATTR_RESOLV_CONF_MODE = "ResolvConfMode"
+DBUS_ATTR_REVISION = "Revision"
DBUS_ATTR_RCMANAGER = "RcManager"
+DBUS_ATTR_SEAT = "Seat"
+DBUS_ATTR_SERIAL = "Serial"
+DBUS_ATTR_SIZE = "Size"
DBUS_ATTR_SSID = "Ssid"
DBUS_ATTR_STATE = "State"
DBUS_ATTR_STATE_FLAGS = "StateFlags"
DBUS_ATTR_STATIC_HOSTNAME = "StaticHostname"
DBUS_ATTR_STATIC_OPERATING_SYSTEM_CPE_NAME = "OperatingSystemCPEName"
DBUS_ATTR_STRENGTH = "Strength"
+DBUS_ATTR_SUPPORTED_FILESYSTEMS = "SupportedFilesystems"
+DBUS_ATTR_SYMLINKS = "Symlinks"
+DBUS_ATTR_TIME_DETECTED = "TimeDetected"
DBUS_ATTR_TIMEUSEC = "TimeUSec"
DBUS_ATTR_TIMEZONE = "Timezone"
DBUS_ATTR_TRANSACTION_STATISTICS = "TransactionStatistics"
@@ -130,7 +163,9 @@ DBUS_ATTR_TYPE = "Type"
DBUS_ATTR_USERSPACE_TIMESTAMP_MONOTONIC = "UserspaceTimestampMonotonic"
DBUS_ATTR_UUID = "Uuid"
DBUS_ATTR_VARIANT = "Variant"
+DBUS_ATTR_VENDOR = "Vendor"
DBUS_ATTR_VERSION = "Version"
+DBUS_ATTR_WWN = "WWN"
class RaucState(str, Enum):
diff --git a/supervisor/dbus/hostname.py b/supervisor/dbus/hostname.py
index 91009b616..b0889efec 100644
--- a/supervisor/dbus/hostname.py
+++ b/supervisor/dbus/hostname.py
@@ -1,6 +1,5 @@
"""D-Bus interface for hostname."""
import logging
-from typing import Any
from dbus_fast.aio.message_bus import MessageBus
@@ -33,10 +32,6 @@ class Hostname(DBusInterfaceProxy):
object_path: str = DBUS_OBJECT_HOSTNAME
properties_interface: str = DBUS_IFACE_HOSTNAME
- def __init__(self):
- """Initialize Properties."""
- self.properties: dict[str, Any] = {}
-
async def connect(self, bus: MessageBus):
"""Connect to system's D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
diff --git a/supervisor/dbus/interface.py b/supervisor/dbus/interface.py
index 244483ce4..26ec58942 100644
--- a/supervisor/dbus/interface.py
+++ b/supervisor/dbus/interface.py
@@ -45,7 +45,22 @@ class DBusInterface(ABC):
async def connect(self, bus: MessageBus) -> None:
"""Connect to D-Bus."""
- self.dbus = await DBus.connect(bus, self.bus_name, self.object_path)
+ await self.initialize(await DBus.connect(bus, self.bus_name, self.object_path))
+
+ async def initialize(self, connected_dbus: DBus) -> None:
+ """Initialize object with already connected dbus object."""
+ if not connected_dbus.connected:
+ raise ValueError("must be a connected DBus object")
+
+ if (
+ connected_dbus.bus_name != self.bus_name
+ or connected_dbus.object_path != self.object_path
+ ):
+ raise ValueError(
+ f"must be a DBus object connected to bus {self.bus_name} and object {self.object_path}"
+ )
+
+ self.dbus = connected_dbus
def disconnect(self) -> None:
"""Disconnect from D-Bus."""
@@ -69,10 +84,18 @@ class DBusInterfaceProxy(DBusInterface):
properties: dict[str, Any] | None = None
sync_properties: bool = True
+ def __init__(self):
+ """Initialize properties."""
+ self.properties = {}
+
async def connect(self, bus: MessageBus) -> None:
"""Connect to D-Bus."""
await super().connect(bus)
+ async def initialize(self, connected_dbus: DBus) -> None:
+ """Initialize object with already connected dbus object."""
+ await super().initialize(connected_dbus)
+
if not self.dbus.properties:
self.disconnect()
raise DBusInterfaceError(
diff --git a/supervisor/dbus/manager.py b/supervisor/dbus/manager.py
index 0ef8fcf63..939e6cccd 100644
--- a/supervisor/dbus/manager.py
+++ b/supervisor/dbus/manager.py
@@ -17,6 +17,7 @@ from .rauc import Rauc
from .resolved import Resolved
from .systemd import Systemd
from .timedate import TimeDate
+from .udisks2 import UDisks2
_LOGGER: logging.Logger = logging.getLogger(__name__)
@@ -36,6 +37,7 @@ class DBusManager(CoreSysAttributes):
self._agent: OSAgent = OSAgent()
self._timedate: TimeDate = TimeDate()
self._resolved: Resolved = Resolved()
+ self._udisks2: UDisks2 = UDisks2()
self._bus: MessageBus | None = None
@property
@@ -78,6 +80,11 @@ class DBusManager(CoreSysAttributes):
"""Return the resolved interface."""
return self._resolved
+ @property
+ def udisks2(self) -> UDisks2:
+ """Return the udisks2 interface."""
+ return self._udisks2
+
@property
def bus(self) -> MessageBus | None:
"""Return the message bus."""
@@ -95,6 +102,7 @@ class DBusManager(CoreSysAttributes):
self.resolved,
self.systemd,
self.timedate,
+ self.udisks2,
]
async def load(self) -> None:
diff --git a/supervisor/dbus/network/__init__.py b/supervisor/dbus/network/__init__.py
index f1d598602..610ad9ae6 100644
--- a/supervisor/dbus/network/__init__.py
+++ b/supervisor/dbus/network/__init__.py
@@ -50,12 +50,12 @@ class NetworkManager(DBusInterfaceProxy):
def __init__(self) -> None:
"""Initialize Properties."""
+ super().__init__()
+
self._dns: NetworkManagerDNS = NetworkManagerDNS()
self._settings: NetworkManagerSettings = NetworkManagerSettings()
self._interfaces: dict[str, NetworkInterface] = {}
- self.properties: dict[str, Any] = {}
-
@property
def dns(self) -> NetworkManagerDNS:
"""Return NetworkManager DNS interface."""
diff --git a/supervisor/dbus/network/accesspoint.py b/supervisor/dbus/network/accesspoint.py
index a06251122..5ad4c30af 100644
--- a/supervisor/dbus/network/accesspoint.py
+++ b/supervisor/dbus/network/accesspoint.py
@@ -1,7 +1,5 @@
"""Connection object for Network Manager."""
-from typing import Any
-
from ..const import (
DBUS_ATTR_FREQUENCY,
DBUS_ATTR_HWADDRESS,
@@ -27,8 +25,9 @@ class NetworkWirelessAP(DBusInterfaceProxy):
def __init__(self, object_path: str) -> None:
"""Initialize NetworkWireless AP object."""
+ super().__init__()
+
self.object_path: str = object_path
- self.properties: dict[str, Any] = {}
@property
@dbus_property
diff --git a/supervisor/dbus/network/connection.py b/supervisor/dbus/network/connection.py
index b6e8baa00..fc77042bf 100644
--- a/supervisor/dbus/network/connection.py
+++ b/supervisor/dbus/network/connection.py
@@ -35,8 +35,9 @@ class NetworkConnection(DBusInterfaceProxy):
def __init__(self, object_path: str) -> None:
"""Initialize NetworkConnection object."""
+ super().__init__()
+
self.object_path: str = object_path
- self.properties: dict[str, Any] = {}
self._ipv4: IpConfiguration | None = None
self._ipv6: IpConfiguration | None = None
diff --git a/supervisor/dbus/network/dns.py b/supervisor/dbus/network/dns.py
index e93c0ecda..b414b4813 100644
--- a/supervisor/dbus/network/dns.py
+++ b/supervisor/dbus/network/dns.py
@@ -40,10 +40,11 @@ class NetworkManagerDNS(DBusInterfaceProxy):
def __init__(self) -> None:
"""Initialize Properties."""
+ super().__init__()
+
self._mode: str | None = None
self._rc_manager: str | None = None
self._configuration: list[DNSConfiguration] = []
- self.properties: dict[str, Any] = {}
@property
def mode(self) -> str | None:
diff --git a/supervisor/dbus/network/interface.py b/supervisor/dbus/network/interface.py
index eb8de4407..373cd16a2 100644
--- a/supervisor/dbus/network/interface.py
+++ b/supervisor/dbus/network/interface.py
@@ -35,8 +35,9 @@ class NetworkInterface(DBusInterfaceProxy):
def __init__(self, nm_dbus: DBus, object_path: str) -> None:
"""Initialize NetworkConnection object."""
+ super().__init__()
+
self.object_path: str = object_path
- self.properties: dict[str, Any] = {}
self.primary: bool = False
diff --git a/supervisor/dbus/network/ip_configuration.py b/supervisor/dbus/network/ip_configuration.py
index f9b5c5674..6a17ab198 100644
--- a/supervisor/dbus/network/ip_configuration.py
+++ b/supervisor/dbus/network/ip_configuration.py
@@ -8,7 +8,6 @@ from ipaddress import (
ip_address,
ip_interface,
)
-from typing import Any
from ...const import ATTR_ADDRESS, ATTR_PREFIX
from ..const import (
@@ -30,12 +29,13 @@ class IpConfiguration(DBusInterfaceProxy):
def __init__(self, object_path: str, ip4: bool = True) -> None:
"""Initialize properties."""
+ super().__init__()
+
self._ip4: bool = ip4
self.object_path: str = object_path
self.properties_interface: str = (
DBUS_IFACE_IP4CONFIG if ip4 else DBUS_IFACE_IP6CONFIG
)
- self.properties: dict[str, Any] = {}
@property
@dbus_property
diff --git a/supervisor/dbus/network/wireless.py b/supervisor/dbus/network/wireless.py
index ac27123a5..99f19aba8 100644
--- a/supervisor/dbus/network/wireless.py
+++ b/supervisor/dbus/network/wireless.py
@@ -27,8 +27,9 @@ class NetworkWireless(DBusInterfaceProxy):
def __init__(self, object_path: str) -> None:
"""Initialize NetworkConnection object."""
+ super().__init__()
+
self.object_path: str = object_path
- self.properties: dict[str, Any] = {}
self._active: NetworkWirelessAP | None = None
diff --git a/supervisor/dbus/rauc.py b/supervisor/dbus/rauc.py
index ae098137d..44794f1d8 100644
--- a/supervisor/dbus/rauc.py
+++ b/supervisor/dbus/rauc.py
@@ -34,14 +34,14 @@ class Rauc(DBusInterfaceProxy):
def __init__(self):
"""Initialize Properties."""
+ super().__init__()
+
self._operation: str | None = None
self._last_error: str | None = None
self._compatible: str | None = None
self._variant: str | None = None
self._boot_slot: str | None = None
- self.properties: dict[str, Any] = {}
-
async def connect(self, bus: MessageBus):
"""Connect to D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
diff --git a/supervisor/dbus/resolved.py b/supervisor/dbus/resolved.py
index 34deb6cf3..89c387df6 100644
--- a/supervisor/dbus/resolved.py
+++ b/supervisor/dbus/resolved.py
@@ -2,7 +2,6 @@
from __future__ import annotations
import logging
-from typing import Any
from dbus_fast.aio.message_bus import MessageBus
@@ -53,10 +52,6 @@ class Resolved(DBusInterfaceProxy):
object_path: str = DBUS_OBJECT_RESOLVED
properties_interface: str = DBUS_IFACE_RESOLVED_MANAGER
- def __init__(self):
- """Initialize Properties."""
- self.properties: dict[str, Any] = {}
-
async def connect(self, bus: MessageBus):
"""Connect to D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
diff --git a/supervisor/dbus/systemd.py b/supervisor/dbus/systemd.py
index b5cba172a..dc8c76103 100644
--- a/supervisor/dbus/systemd.py
+++ b/supervisor/dbus/systemd.py
@@ -1,6 +1,5 @@
"""Interface to Systemd over D-Bus."""
import logging
-from typing import Any
from dbus_fast.aio.message_bus import MessageBus
@@ -34,10 +33,6 @@ class Systemd(DBusInterfaceProxy):
sync_properties: bool = False
properties_interface: str = DBUS_IFACE_SYSTEMD_MANAGER
- def __init__(self) -> None:
- """Initialize Properties."""
- self.properties: dict[str, Any] = {}
-
async def connect(self, bus: MessageBus):
"""Connect to D-Bus."""
_LOGGER.info("Load dbus interface %s", self.name)
diff --git a/supervisor/dbus/timedate.py b/supervisor/dbus/timedate.py
index 3249d7434..f940aedc4 100644
--- a/supervisor/dbus/timedate.py
+++ b/supervisor/dbus/timedate.py
@@ -1,7 +1,6 @@
"""Interface to systemd-timedate over D-Bus."""
from datetime import datetime
import logging
-from typing import Any
from dbus_fast.aio.message_bus import MessageBus
@@ -33,10 +32,6 @@ class TimeDate(DBusInterfaceProxy):
object_path: str = DBUS_OBJECT_TIMEDATE
properties_interface: str = DBUS_IFACE_TIMEDATE
- def __init__(self) -> None:
- """Initialize Properties."""
- self.properties: dict[str, Any] = {}
-
@property
@dbus_property
def timezone(self) -> str:
diff --git a/supervisor/dbus/udisks2/__init__.py b/supervisor/dbus/udisks2/__init__.py
new file mode 100644
index 000000000..47eaf4fde
--- /dev/null
+++ b/supervisor/dbus/udisks2/__init__.py
@@ -0,0 +1,146 @@
+"""Interface to UDisks2 over D-Bus."""
+import logging
+from typing import Any
+
+from awesomeversion import AwesomeVersion
+from dbus_fast.aio import MessageBus
+
+from ...exceptions import DBusError, DBusInterfaceError, DBusObjectError
+from ..const import (
+ DBUS_ATTR_SUPPORTED_FILESYSTEMS,
+ DBUS_ATTR_VERSION,
+ DBUS_IFACE_UDISKS2_MANAGER,
+ DBUS_NAME_UDISKS2,
+ DBUS_OBJECT_BASE,
+ DBUS_OBJECT_UDISKS2,
+)
+from ..interface import DBusInterfaceProxy, dbus_property
+from ..utils import dbus_connected
+from .block import UDisks2Block
+from .const import UDISKS2_DEFAULT_OPTIONS
+from .data import DeviceSpecification
+from .drive import UDisks2Drive
+
+_LOGGER: logging.Logger = logging.getLogger(__name__)
+
+
+class UDisks2(DBusInterfaceProxy):
+ """Handle D-Bus interface for UDisks2.
+
+ http://storaged.org/doc/udisks2-api/latest/
+ """
+
+ name: str = DBUS_NAME_UDISKS2
+ bus_name: str = DBUS_NAME_UDISKS2
+ object_path: str = DBUS_OBJECT_UDISKS2
+ properties_interface: str = DBUS_IFACE_UDISKS2_MANAGER
+
+ _block_devices: dict[str, UDisks2Block] = {}
+ _drives: dict[str, UDisks2Drive] = {}
+
+ async def connect(self, bus: MessageBus):
+ """Connect to D-Bus."""
+ try:
+ await super().connect(bus)
+ except DBusError:
+ _LOGGER.warning("Can't connect to udisks2")
+ except DBusInterfaceError:
+ _LOGGER.warning(
+ "No udisks2 support on the host. Host control has been disabled."
+ )
+
+ @dbus_connected
+ async def update(self, changed: dict[str, Any] | None = None) -> None:
+ """Update properties via D-Bus.
+
+ Also rebuilds cache of available block devices and drives.
+ """
+ await super().update(changed)
+
+ if not changed:
+ # Cache block devices
+ block_devices = await self.dbus.Manager.call_get_block_devices(
+ UDISKS2_DEFAULT_OPTIONS
+ )
+
+ for removed in self._block_devices.keys() - set(block_devices):
+ self._block_devices[removed].shutdown()
+
+ await self._resolve_block_device_paths(block_devices)
+
+ # Cache drives
+ drives = {
+ device.drive
+ for device in self.block_devices
+ if device.drive != DBUS_OBJECT_BASE
+ }
+
+ for removed in self._drives.keys() - drives:
+ self._drives[removed].shutdown()
+
+ self._drives = {
+ drive: self._drives[drive]
+ if drive in self._drives
+ else await UDisks2Drive.new(drive, self.dbus.bus)
+ for drive in drives
+ }
+
+ @property
+ @dbus_property
+ def version(self) -> AwesomeVersion:
+ """UDisks2 version."""
+ return AwesomeVersion(self.properties[DBUS_ATTR_VERSION])
+
+ @property
+ @dbus_property
+ def supported_filesystems(self) -> list[str]:
+ """Return list of supported filesystems."""
+ return self.properties[DBUS_ATTR_SUPPORTED_FILESYSTEMS]
+
+ @property
+ def block_devices(self) -> list[UDisks2Block]:
+ """List of available block devices."""
+ return list(self._block_devices.values())
+
+ @property
+ def drives(self) -> list[UDisks2Drive]:
+ """List of available drives."""
+ return list(self._drives.values())
+
+ @dbus_connected
+ def get_drive(self, drive_path: str) -> UDisks2Drive:
+ """Get additional info on drive from object path."""
+ if drive_path not in self._drives:
+ raise DBusObjectError(f"Drive {drive_path} not found")
+
+ return self._drives[drive_path]
+
+ @dbus_connected
+ def get_block_device(self, device_path: str) -> UDisks2Block:
+ """Get additional info on block device from object path."""
+ if device_path not in self._block_devices:
+ raise DBusObjectError(f"Block device {device_path} not found")
+
+ return self._block_devices[device_path]
+
+ @dbus_connected
+ async def resolve_device(self, devspec: DeviceSpecification) -> list[UDisks2Block]:
+ """Return list of device object paths for specification."""
+ return await self._resolve_block_device_paths(
+ await self.dbus.Manager.call_resolve_device(
+ devspec.to_dict(), UDISKS2_DEFAULT_OPTIONS
+ )
+ )
+
+ async def _resolve_block_device_paths(
+ self, block_devices: list[str]
+ ) -> list[UDisks2Block]:
+ """Resolve block device object paths to objects. Cache new ones if necessary."""
+ resolved = {
+ device: self._block_devices[device]
+ if device in self._block_devices
+ else await UDisks2Block.new(device, self.dbus.bus)
+ for device in block_devices
+ }
+ self._block_devices.update(resolved)
+ return list(resolved.values())
diff --git a/supervisor/dbus/udisks2/block.py b/supervisor/dbus/udisks2/block.py
new file mode 100644
index 000000000..e552ae966
--- /dev/null
+++ b/supervisor/dbus/udisks2/block.py
@@ -0,0 +1,206 @@
+"""Interface to UDisks2 Block Device over D-Bus."""
+from collections.abc import Callable
+from pathlib import Path
+
+from awesomeversion import AwesomeVersion
+from dbus_fast.aio import MessageBus
+
+from ..const import (
+ DBUS_ATTR_DEVICE,
+ DBUS_ATTR_DEVICE_NUMBER,
+ DBUS_ATTR_DRIVE,
+ DBUS_ATTR_HINT_AUTO,
+ DBUS_ATTR_HINT_IGNORE,
+ DBUS_ATTR_HINT_NAME,
+ DBUS_ATTR_HINT_SYSTEM,
+ DBUS_ATTR_ID,
+ DBUS_ATTR_ID_ID_TYPE,
+ DBUS_ATTR_ID_LABEL,
+ DBUS_ATTR_ID_USAGE,
+ DBUS_ATTR_ID_UUID,
+ DBUS_ATTR_ID_VERSION,
+ DBUS_ATTR_READ_ONLY,
+ DBUS_ATTR_SIZE,
+ DBUS_ATTR_SYMLINKS,
+ DBUS_IFACE_BLOCK,
+ DBUS_IFACE_FILESYSTEM,
+ DBUS_IFACE_PARTITION_TABLE,
+ DBUS_NAME_UDISKS2,
+)
+from ..interface import DBusInterfaceProxy, dbus_property
+from ..utils import dbus_connected
+from .const import UDISKS2_DEFAULT_OPTIONS, FormatType
+from .data import FormatOptions
+from .filesystem import UDisks2Filesystem
+from .partition_table import UDisks2PartitionTable
+
+ADDITIONAL_INTERFACES: dict[str, Callable[[str], DBusInterfaceProxy]] = {
+ DBUS_IFACE_FILESYSTEM: UDisks2Filesystem
+}
+
+
+class UDisks2Block(DBusInterfaceProxy):
+ """Handle D-Bus interface for UDisks2 block device object.
+
+ http://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.Block.html
+ """
+
+ name: str = DBUS_IFACE_BLOCK
+ bus_name: str = DBUS_NAME_UDISKS2
+ properties_interface: str = DBUS_IFACE_BLOCK
+
+ _filesystem: UDisks2Filesystem | None = None
+ _partition_table: UDisks2PartitionTable | None = None
+
+ def __init__(self, object_path: str, *, sync_properties: bool = True) -> None:
+ """Initialize object."""
+ self.object_path = object_path
+ self.sync_properties = sync_properties
+ super().__init__()
+
+ async def connect(self, bus: MessageBus) -> None:
+ """Connect to bus."""
+ await super().connect(bus)
+
+ if DBUS_IFACE_FILESYSTEM in self.dbus.proxies:
+ self._filesystem = UDisks2Filesystem(
+ self.object_path, sync_properties=self.sync_properties
+ )
+ await self._filesystem.initialize(self.dbus)
+
+ if DBUS_IFACE_PARTITION_TABLE in self.dbus.proxies:
+ self._partition_table = UDisks2PartitionTable(
+ self.object_path, sync_properties=self.sync_properties
+ )
+ await self._partition_table.initialize(self.dbus)
+
+ @staticmethod
+ async def new(
+ object_path: str, bus: MessageBus, *, sync_properties: bool = True
+ ) -> "UDisks2Block":
+ """Create and connect object."""
+ obj = UDisks2Block(object_path, sync_properties=sync_properties)
+ await obj.connect(bus)
+ return obj
+
+ @property
+ def filesystem(self) -> UDisks2Filesystem | None:
+ """Filesystem interface if block device is one."""
+ return self._filesystem
+
+ @property
+ def partition_table(self) -> UDisks2PartitionTable | None:
+ """Partition table interface if block device is one."""
+ return self._partition_table
+
+ @property
+ @dbus_property
+ def device(self) -> Path:
+ """Return device file."""
+ return Path(bytes(self.properties[DBUS_ATTR_DEVICE]).decode())
+
+ @property
+ @dbus_property
+ def id(self) -> str:
+ """Return unique identifer."""
+ return self.properties[DBUS_ATTR_ID]
+
+ @property
+ @dbus_property
+ def size(self) -> int:
+ """Return size."""
+ return self.properties[DBUS_ATTR_SIZE]
+
+ @property
+ @dbus_property
+ def read_only(self) -> bool:
+ """Return whether device is read only."""
+ return self.properties[DBUS_ATTR_READ_ONLY]
+
+ @property
+ @dbus_property
+ def symlinks(self) -> list[Path]:
+ """Return list of symlinks."""
+ return [
+ Path(bytes(symlink).decode(encoding="utf-8"))
+ for symlink in self.properties[DBUS_ATTR_SYMLINKS]
+ ]
+
+ @property
+ @dbus_property
+ def device_number(self) -> int:
+ """Return device number."""
+ return self.properties[DBUS_ATTR_DEVICE_NUMBER]
+
+ @property
+ @dbus_property
+ def id_usage(self) -> str:
+ """Return expected usage of structured data by probing signatures (if known)."""
+ return self.properties[DBUS_ATTR_ID_USAGE]
+
+ @property
+ @dbus_property
+ def id_type(self) -> str:
+ """Return more specific usage information on structured data (if known)."""
+ return self.properties[DBUS_ATTR_ID_ID_TYPE]
+
+ @property
+ @dbus_property
+ def id_version(self) -> AwesomeVersion:
+ """Return version of filesystem or other structured data."""
+ return AwesomeVersion(self.properties[DBUS_ATTR_ID_VERSION])
+
+ @property
+ @dbus_property
+ def id_label(self) -> str:
+ """Return label of filesystem or other structured data."""
+ return self.properties[DBUS_ATTR_ID_LABEL]
+
+ @property
+ @dbus_property
+ def id_uuid(self) -> str:
+ """Return uuid of filesystem or other structured data."""
+ return self.properties[DBUS_ATTR_ID_UUID]
+
+ @property
+ @dbus_property
+ def hint_auto(self) -> bool:
+ """Return true if device should be automatically started (mounted, unlocked, etc)."""
+ return self.properties[DBUS_ATTR_HINT_AUTO]
+
+ @property
+ @dbus_property
+ def hint_ignore(self) -> bool:
+ """Return true if device should be hidden from users."""
+ return self.properties[DBUS_ATTR_HINT_IGNORE]
+
+ @property
+ @dbus_property
+ def hint_name(self) -> str:
+ """Return name that should be presented to users."""
+ return self.properties[DBUS_ATTR_HINT_NAME]
+
+ @property
+ @dbus_property
+ def hint_system(self) -> bool:
+ """Return true if device is considered a system device.."""
+ return self.properties[DBUS_ATTR_HINT_SYSTEM]
+
+ @property
+ @dbus_property
+ def drive(self) -> str:
+ """Return object path for drive.
+
+ Provide to UDisks2.get_drive for UDisks2Drive object.
+ """
+ return self.properties[DBUS_ATTR_DRIVE]
+
+ @dbus_connected
+ async def format(
+ self, type_: FormatType = FormatType.GPT, options: FormatOptions | None = None
+ ) -> None:
+ """Format block device."""
+ options = options.to_dict() if options else {}
+ await self.dbus.Block.call_format(
+ type_.value, options | UDISKS2_DEFAULT_OPTIONS
+ )
diff --git a/supervisor/dbus/udisks2/const.py b/supervisor/dbus/udisks2/const.py
new file mode 100644
index 000000000..d842b12c7
--- /dev/null
+++ b/supervisor/dbus/udisks2/const.py
@@ -0,0 +1,37 @@
+"""Constants for UDisks2."""
+
+from enum import Enum
+
+UDISKS2_DEFAULT_OPTIONS = {"auth.no_user_interaction": True}
+
+
+class EncryptType(str, Enum):
+ """Encryption type."""
+
+ LUKS1 = "luks1"
+ LUKS2 = "luks2"
+
+
+class EraseMode(str, Enum):
+ """Erase mode."""
+
+ ZERO = "zero"
+ ATA_SECURE_ERASE = "ata-secure-erase"
+ ATA_SECURE_ERASE_ENHANCED = "ata-secure-erase-enhanced"
+
+
+class FormatType(str, Enum):
+ """Format type."""
+
+ EMPTY = "empty"
+ SWAP = "swap"
+ DOS = "dos"
+ GPT = "gpt"
+
+
+class PartitionTableType(str, Enum):
+ """Partition Table type."""
+
+ DOS = "dos"
+ GPT = "gpt"
+ UNKNOWN = ""
diff --git a/supervisor/dbus/udisks2/data.py b/supervisor/dbus/udisks2/data.py
new file mode 100644
index 000000000..a3e4f9544
--- /dev/null
+++ b/supervisor/dbus/udisks2/data.py
@@ -0,0 +1,294 @@
+"""Data for UDisks2."""
+
+from dataclasses import dataclass
+from inspect import get_annotations
+from pathlib import Path
+from typing import Any, TypedDict
+
+from dbus_fast import Variant
+from typing_extensions import NotRequired
+
+from .const import EncryptType, EraseMode
+
+
+def _optional_variant(signature: str, value: Any | None) -> Variant | None:
+ """Output variant if value is not none."""
+ return Variant(signature, value) if value is not None else None
+
+
+UDisks2StandardOptionsDataType = TypedDict(
+ "UDisks2StandardOptionsDataType",
+ {"auth.no_user_interaction": NotRequired[bool]},
+)
+
+
+@dataclass(slots=True)
+class UDisks2StandardOptions:
+ """UDisks2 standard options.
+
+ http://storaged.org/doc/udisks2-api/latest/udisks-std-options.html
+ """
+
+ auth_no_user_interaction: bool | None = None
+
+ @staticmethod
+ def from_dict(data: UDisks2StandardOptionsDataType) -> "UDisks2StandardOptions":
+ """Create UDisks2StandardOptions from dict."""
+ return UDisks2StandardOptions(
+ auth_no_user_interaction=data.get("auth.no_user_interaction"),
+ )
+
+ def to_dict(self) -> dict[str, Variant]:
+ """Return dict representation."""
+ data = {
+ "auth.no_user_interaction": _optional_variant(
+ "b", self.auth_no_user_interaction
+ ),
+ }
+ return {k: v for k, v in data.items() if v}
+
+
+_udisks2_standard_options_annotations = get_annotations(UDisks2StandardOptionsDataType)
+
+
+class DeviceSpecificationDataType(TypedDict, total=False):
+ """Device specification data type."""
+
+ path: str
+ label: str
+ uuid: str
+
+
+@dataclass(slots=True)
+class DeviceSpecification:
+ """Device specification.
+
+ http://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.Manager.html#gdbus-method-org-freedesktop-UDisks2-Manager.ResolveDevice
+ """
+
+ path: Path | None = None
+ label: str | None = None
+ uuid: str | None = None
+
+ @staticmethod
+ def from_dict(data: DeviceSpecificationDataType) -> "DeviceSpecification":
+ """Create DeviceSpecification from dict."""
+ return DeviceSpecification(
+ path=Path(data.get("path")),
+ label=data.get("label"),
+ uuid=data.get("uuid"),
+ )
+
+ def to_dict(self) -> dict[str, Variant]:
+ """Return dict representation."""
+ data = {
+ "path": Variant("s", str(self.path)) if self.path else None,
+ "label": _optional_variant("s", self.label),
+ "uuid": _optional_variant("s", self.uuid),
+ }
+ return {k: v for k, v in data.items() if v}
+
+
+FormatOptionsDataType = TypedDict(
+ "FormatOptionsDataType",
+ {
+ "label": NotRequired[str],
+ "take-ownership": NotRequired[bool],
+ "encrypt.passphrase": NotRequired[bytearray],
+ "encrypt.type": NotRequired[str],
+ "erase": NotRequired[str],
+ "update-partition-type": NotRequired[bool],
+ "no-block": NotRequired[bool],
+ "dry-run-first": NotRequired[bool],
+ "no-discard": NotRequired[bool],
+ "tear-down": NotRequired[bool],
+ }
+ | _udisks2_standard_options_annotations,
+)
+
+
+@dataclass(slots=True)
+class FormatOptions(UDisks2StandardOptions):
+ """Options for formatting a block device.
+
+ http://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.Block.html#gdbus-method-org-freedesktop-UDisks2-Block.Format
+ """
+
+ label: str | None = None
+ take_ownership: bool | None = None
+ encrypt_passpharase: str | None = None
+ encrypt_type: EncryptType | None = None
+ erase: EraseMode | None = None
+ update_partition_type: bool | None = None
+ no_block: bool | None = None
+ dry_run_first: bool | None = None
+ no_discard: bool | None = None
+ tear_down: bool | None = None
+
+ @staticmethod
+ def from_dict(data: FormatOptionsDataType) -> "FormatOptions":
+ """Create FormatOptions from dict."""
+ return FormatOptions(
+ label=data.get("label"),
+ take_ownership=data.get("take-ownership"),
+ encrypt_passpharase=bytes(data["encrypt.passphrase"]).decode(
+ encoding="utf-8"
+ )
+ if "encrypt.passphrase" in data
+ else None,
+ encrypt_type=EncryptType(data["encrypt.type"])
+ if "encrypt.type" in data
+ else None,
+ erase=EncryptType(data["erase"]) if "erase" in data else None,
+ update_partition_type=data.get("update-partition-type"),
+ no_block=data.get("no-block"),
+ dry_run_first=data.get("dry-run-first"),
+ no_discard=data.get("no-discard"),
+ tear_down=data.get("tear-down"),
+ # UDisks2 standard options
+ auth_no_user_interaction=data.get("auth.no_user_interaction"),
+ )
+
+ def to_dict(self) -> dict[str, Variant]:
+ """Return dict representation."""
+ data = {
+ "label": _optional_variant("s", self.label),
+ "take-ownership": _optional_variant("b", self.take_ownership),
+ "encrypt.passphrase": Variant(
+ "ay", bytearray(self.encrypt_passpharase, encoding="utf-8")
+ )
+ if self.encrypt_passpharase
+ else None,
+ "encrypt.type": Variant("s", self.encrypt_type.value)
+ if self.encrypt_type
+ else None,
+ "erase": Variant("s", self.erase.value) if self.erase else None,
+ "update-partition-type": _optional_variant("b", self.update_partition_type),
+ "no-block": _optional_variant("b", self.no_block),
+ "dry-run-first": _optional_variant("b", self.dry_run_first),
+ "no-discard": _optional_variant("b", self.no_discard),
+ "tear-down": _optional_variant("b", self.tear_down),
+ # UDisks2 standard options
+ "auth.no_user_interaction": _optional_variant(
+ "b", self.auth_no_user_interaction
+ ),
+ }
+ return {k: v for k, v in data.items() if v}
+
+
+MountOptionsDataType = TypedDict(
+ "MountOptionsDataType",
+ {
+ "fstype": NotRequired[str],
+ "options": NotRequired[str],
+ }
+ | _udisks2_standard_options_annotations,
+)
+
+
+@dataclass
+class MountOptions(UDisks2StandardOptions):
+ """Filesystem mount options.
+
+ http://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.Filesystem.html#gdbus-method-org-freedesktop-UDisks2-Filesystem.Mount
+ """
+
+ fstype: str | None = None
+ options: list[str] | None = None
+
+ @staticmethod
+ def from_dict(data: MountOptionsDataType) -> "MountOptions":
+ """Create MountOptions from dict."""
+ return MountOptions(
+ fstype=data.get("fstype"),
+ options=data["options"].split(",") if "options" in data else None,
+ # UDisks2 standard options
+ auth_no_user_interaction=data.get("auth.no_user_interaction"),
+ )
+
+ def to_dict(self) -> dict[str, Variant]:
+ """Return dict representation."""
+ data = {
+ "fstype": _optional_variant("s", self.fstype),
+ "options": Variant("s", ",".join(self.options)) if self.options else None,
+ # UDisks2 standard options
+ "auth.no_user_interaction": _optional_variant(
+ "b", self.auth_no_user_interaction
+ ),
+ }
+ return {k: v for k, v in data.items() if v is not None}
+
+
+UnmountOptionsDataType = TypedDict(
+ "UnountOptionsDataType",
+ {
+ "force": NotRequired[bool],
+ }
+ | _udisks2_standard_options_annotations,
+)
+
+
+@dataclass
+class UnmountOptions(UDisks2StandardOptions):
+ """Filesystem unmount options.
+
+ http://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.Filesystem.html#gdbus-method-org-freedesktop-UDisks2-Filesystem.Unmount
+ """
+
+ force: bool | None = None
+
+ @staticmethod
+ def from_dict(data: UnmountOptionsDataType) -> "UnmountOptions":
+ """Create MountOptions from dict."""
+ return UnmountOptions(
+ force=data.get("force"),
+ # UDisks2 standard options
+ auth_no_user_interaction=data.get("auth.no_user_interaction"),
+ )
+
+ def to_dict(self) -> dict[str, Variant]:
+ """Return dict representation."""
+ data = {
+ "force": _optional_variant("b", self.force),
+ # UDisks2 standard options
+ "auth.no_user_interaction": _optional_variant(
+ "b", self.auth_no_user_interaction
+ ),
+ }
+ return {k: v for k, v in data.items() if v}
+
+
+CreatePartitionOptionsDataType = TypedDict(
+ "CreatePartitionOptionsDataType",
+ {"partition-type": NotRequired[str]} | _udisks2_standard_options_annotations,
+)
+
+
+@dataclass
+class CreatePartitionOptions(UDisks2StandardOptions):
+ """Create partition options.
+
+ http://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.PartitionTable.html#gdbus-method-org-freedesktop-UDisks2-PartitionTable.CreatePartition
+ """
+
+ partition_type: str | None = None
+
+ @staticmethod
+ def from_dict(data: CreatePartitionOptionsDataType) -> "CreatePartitionOptions":
+ """Create CreatePartitionOptions from dict."""
+ return CreatePartitionOptions(
+ partition_type=data.get("partition-type"),
+ # UDisks2 standard options
+ auth_no_user_interaction=data.get("auth.no_user_interaction"),
+ )
+
+ def to_dict(self) -> dict[str, Variant]:
+ """Return dict representation."""
+ data = {
+ "partition-type": _optional_variant("s", self.partition_type),
+ # UDisks2 standard options
+ "auth.no_user_interaction": _optional_variant(
+ "b", self.auth_no_user_interaction
+ ),
+ }
+ return {k: v for k, v in data.items() if v}
diff --git a/supervisor/dbus/udisks2/drive.py b/supervisor/dbus/udisks2/drive.py
new file mode 100644
index 000000000..d5f219c23
--- /dev/null
+++ b/supervisor/dbus/udisks2/drive.py
@@ -0,0 +1,127 @@
+"""Interface to UDisks2 Drive over D-Bus."""
+
+from datetime import datetime, timezone
+
+from dbus_fast.aio import MessageBus
+
+from ..const import (
+ DBUS_ATTR_CONNECTION_BUS,
+ DBUS_ATTR_EJECTABLE,
+ DBUS_ATTR_ID,
+ DBUS_ATTR_MODEL,
+ DBUS_ATTR_REMOVABLE,
+ DBUS_ATTR_REVISION,
+ DBUS_ATTR_SEAT,
+ DBUS_ATTR_SERIAL,
+ DBUS_ATTR_SIZE,
+ DBUS_ATTR_TIME_DETECTED,
+ DBUS_ATTR_VENDOR,
+ DBUS_ATTR_WWN,
+ DBUS_IFACE_DRIVE,
+ DBUS_NAME_UDISKS2,
+)
+from ..interface import DBusInterfaceProxy, dbus_property
+from ..utils import dbus_connected
+from .const import UDISKS2_DEFAULT_OPTIONS
+
+
+class UDisks2Drive(DBusInterfaceProxy):
+ """Handle D-Bus interface for UDisks2 drive object.
+
+ http://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.Drive.html
+ """
+
+ name: str = DBUS_IFACE_DRIVE
+ bus_name: str = DBUS_NAME_UDISKS2
+ properties_interface: str = DBUS_IFACE_DRIVE
+
+ def __init__(self, object_path: str) -> None:
+ """Initialize object."""
+ self.object_path = object_path
+ super().__init__()
+
+ @staticmethod
+ async def new(object_path: str, bus: MessageBus) -> "UDisks2Drive":
+ """Create and connect object."""
+ obj = UDisks2Drive(object_path)
+ await obj.connect(bus)
+ return obj
+
+ @property
+ @dbus_property
+ def vendor(self) -> str:
+ """Return vendor name if known."""
+ return self.properties[DBUS_ATTR_VENDOR]
+
+ @property
+ @dbus_property
+ def model(self) -> str:
+ """Return model name if known."""
+ return self.properties[DBUS_ATTR_MODEL]
+
+ @property
+ @dbus_property
+ def revision(self) -> str:
+ """Return firmware revision."""
+ return self.properties[DBUS_ATTR_REVISION]
+
+ @property
+ @dbus_property
+ def serial(self) -> str:
+ """Return serial number."""
+ return self.properties[DBUS_ATTR_SERIAL]
+
+ @property
+ @dbus_property
+ def wwn(self) -> str:
+ """Return WWN (http://en.wikipedia.org/wiki/World_Wide_Name) if known."""
+ return self.properties[DBUS_ATTR_WWN]
+
+ @property
+ @dbus_property
+ def id(self) -> str:
+ """Return unique and persistent id."""
+ return self.properties[DBUS_ATTR_ID]
+
+ @property
+ @dbus_property
+ def size(self) -> int:
+ """Return drive size."""
+ return self.properties[DBUS_ATTR_SIZE]
+
+ @property
+ @dbus_property
+ def time_detected(self) -> datetime:
+ """Return time drive first detected."""
+ return datetime.fromtimestamp(
+ self.properties[DBUS_ATTR_TIME_DETECTED] * 10**-6
+ ).astimezone(timezone.utc)
+
+ @property
+ @dbus_property
+ def connection_bus(self) -> str:
+ """Return physical connection bus used (usb, sdio, etc)."""
+ return self.properties[DBUS_ATTR_CONNECTION_BUS]
+
+ @property
+ @dbus_property
+ def seat(self) -> str:
+ """Return seat drive is plugged into if any."""
+ return self.properties[DBUS_ATTR_SEAT]
+
+ @property
+ @dbus_property
+ def removable(self) -> bool:
+ """Return true if drive is considered removable by user."""
+ return self.properties[DBUS_ATTR_REMOVABLE]
+
+ @property
+ @dbus_property
+ def ejectable(self) -> bool:
+ """Return true if drive accepts an eject command."""
+ return self.properties[DBUS_ATTR_EJECTABLE]
+
+ @dbus_connected
+ async def eject(self) -> None:
+ """Eject media from drive."""
+ await self.dbus.Drive.call_eject(UDISKS2_DEFAULT_OPTIONS)
diff --git a/supervisor/dbus/udisks2/filesystem.py b/supervisor/dbus/udisks2/filesystem.py
new file mode 100644
index 000000000..3befe9c81
--- /dev/null
+++ b/supervisor/dbus/udisks2/filesystem.py
@@ -0,0 +1,78 @@
+"""Interface to UDisks2 Filesystem over D-Bus."""
+
+from pathlib import Path
+
+from ..const import (
+ DBUS_ATTR_MOUNT_POINTS,
+ DBUS_ATTR_SIZE,
+ DBUS_IFACE_FILESYSTEM,
+ DBUS_NAME_UDISKS2,
+)
+from ..interface import DBusInterfaceProxy, dbus_property
+from ..utils import dbus_connected
+from .const import UDISKS2_DEFAULT_OPTIONS
+from .data import MountOptions, UnmountOptions
+
+
+class UDisks2Filesystem(DBusInterfaceProxy):
+ """Handle D-Bus interface for UDisks2 filesystem device object.
+
+ http://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.Filesystem.html
+ """
+
+ name: str = DBUS_IFACE_FILESYSTEM
+ bus_name: str = DBUS_NAME_UDISKS2
+ properties_interface: str = DBUS_IFACE_FILESYSTEM
+
+ def __init__(self, object_path: str, *, sync_properties: bool = True) -> None:
+ """Initialize object."""
+ self.object_path = object_path
+ self.sync_properties = sync_properties
+ super().__init__()
+
+ @property
+ @dbus_property
+ def mount_points(self) -> list[Path]:
+ """Return mount points."""
+ return [
+ Path(bytes(mount_point).decode())
+ for mount_point in self.properties[DBUS_ATTR_MOUNT_POINTS]
+ ]
+
+ @property
+ @dbus_property
+ def size(self) -> int:
+ """Return size."""
+ return self.properties[DBUS_ATTR_SIZE]
+
+ @dbus_connected
+ async def mount(self, options: MountOptions | None = None) -> str:
+ """Mount filesystem.
+
+ Caller cannot provide mountpoint. UDisks selects a folder within /run/media/$USER
+ if not overridden in /etc/fstab. Therefore unclear if this can be useful to supervisor.
+ http://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.Filesystem.html#gdbus-method-org-freedesktop-UDisks2-Filesystem.Mount
+ """
+ options = options.to_dict() if options else {}
+ return await self.dbus.Filesystem.call_mount(options | UDISKS2_DEFAULT_OPTIONS)
+
+ @dbus_connected
+ async def unmount(self, options: UnmountOptions | None = None) -> None:
+ """Unmount filesystem."""
+ options = options.to_dict() if options else {}
+ await self.dbus.Filesystem.call_unmount(options | UDISKS2_DEFAULT_OPTIONS)
+
+ @dbus_connected
+ async def set_label(self) -> None:
+ """Set filesystem label."""
+ await self.dbus.Filesystem.call_set_label(UDISKS2_DEFAULT_OPTIONS)
+
+ @dbus_connected
+ async def check(self) -> bool:
+ """Check filesystem for consistency. Returns true if it passed."""
+ return await self.dbus.Filesystem.call_check(UDISKS2_DEFAULT_OPTIONS)
+
+ @dbus_connected
+ async def repair(self) -> bool:
+ """Attempt to repair filesystem. Returns true if repair was successful."""
+ return await self.dbus.Filesystem.call_repair(UDISKS2_DEFAULT_OPTIONS)
diff --git a/supervisor/dbus/udisks2/partition_table.py b/supervisor/dbus/udisks2/partition_table.py
new file mode 100644
index 000000000..3725fc262
--- /dev/null
+++ b/supervisor/dbus/udisks2/partition_table.py
@@ -0,0 +1,63 @@
+"""Interface to UDisks2 Partition Table over D-Bus."""
+
+from ..const import (
+ DBUS_ATTR_PARTITIONS,
+ DBUS_ATTR_TYPE,
+ DBUS_IFACE_PARTITION_TABLE,
+ DBUS_NAME_UDISKS2,
+)
+from ..interface import DBusInterfaceProxy, dbus_property
+from ..utils import dbus_connected
+from .const import UDISKS2_DEFAULT_OPTIONS, PartitionTableType
+from .data import CreatePartitionOptions
+
+
+class UDisks2PartitionTable(DBusInterfaceProxy):
+ """Handle D-Bus interface for UDisks2 partition table device object.
+
+ http://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.PartitionTable.html
+ """
+
+ name: str = DBUS_IFACE_PARTITION_TABLE
+ bus_name: str = DBUS_NAME_UDISKS2
+ properties_interface: str = DBUS_IFACE_PARTITION_TABLE
+
+ def __init__(self, object_path: str, *, sync_properties: bool = True) -> None:
+ """Initialize object."""
+ self.object_path = object_path
+ self.sync_properties = sync_properties
+ super().__init__()
+
+ @property
+ @dbus_property
+ def partitions(self) -> list[str]:
+ """List of object paths of partitions that belong to this table.
+
+ Provide to UDisks2.get_block_device for UDisks2Block object.
+ """
+ return self.properties[DBUS_ATTR_PARTITIONS]
+
+ @property
+ @dbus_property
+ def type(self) -> PartitionTableType:
+ """Type of partition table."""
+ return PartitionTableType(self.properties[DBUS_ATTR_TYPE])
+
+ @dbus_connected
+ async def create_partition(
+ self,
+ offset: int = 0,
+ size: int = 0,
+ type_: PartitionTableType = PartitionTableType.UNKNOWN,
+ name: str = "",
+ options: CreatePartitionOptions | None = None,
+ ) -> str:
+ """Create a new partition and return object path of new block device.
+
+ 'UNKNOWN' for type here means UDisks2 selects default based on partition table and OS.
+ Use with UDisks2Block.new to get block object. Or UDisks2.get_block_device after UDisks2.update.
+ """
+ options = options.to_dict() if options else {}
+ return await self.dbus.PartitionTable.call_create_partition(
+ offset, size, type_, name, options | UDISKS2_DEFAULT_OPTIONS
+ )
diff --git a/supervisor/host/const.py b/supervisor/host/const.py
index 52b9a0cd1..45393ced5 100644
--- a/supervisor/host/const.py
+++ b/supervisor/host/const.py
@@ -42,16 +42,17 @@ class WifiMode(str, Enum):
class HostFeature(str, Enum):
"""Host feature."""
+ DISK = "disk"
HAOS = "haos"
HOSTNAME = "hostname"
+ JOURNAL = "journal"
NETWORK = "network"
+ OS_AGENT = "os_agent"
REBOOT = "reboot"
RESOLVED = "resolved"
SERVICES = "services"
SHUTDOWN = "shutdown"
- OS_AGENT = "os_agent"
TIMEDATE = "timedate"
- JOURNAL = "journal"
class LogFormat(str, Enum):
diff --git a/supervisor/host/manager.py b/supervisor/host/manager.py
index 5e217d747..299504796 100644
--- a/supervisor/host/manager.py
+++ b/supervisor/host/manager.py
@@ -107,6 +107,9 @@ class HostManager(CoreSysAttributes):
if self.logs.available:
features.append(HostFeature.JOURNAL)
+ if self.sys_dbus.udisks2.is_connected:
+ features.append(HostFeature.DISK)
+
return features
async def reload(self):
@@ -122,6 +125,9 @@ class HostManager(CoreSysAttributes):
if self.sys_dbus.agent.is_connected:
await self.sys_dbus.agent.update()
+ if self.sys_dbus.udisks2.is_connected:
+ await self.sys_dbus.udisks2.update()
+
with suppress(PulseAudioError):
await self.sound.update()
diff --git a/supervisor/utils/dbus.py b/supervisor/utils/dbus.py
index 653104231..b3f4980c8 100644
--- a/supervisor/utils/dbus.py
+++ b/supervisor/utils/dbus.py
@@ -147,11 +147,21 @@ class DBus:
)
self._add_interfaces()
+ @property
+ def proxies(self) -> dict[str, ProxyInterface]:
+ """Return all proxies."""
+ return self._proxies
+
@property
def bus(self) -> MessageBus:
"""Get message bus."""
return self._bus
+ @property
+ def connected(self) -> bool:
+ """Is connected."""
+ return self._proxy_obj is not None
+
@property
def properties(self) -> DBusCallWrapper | None:
"""Get properties proxy interface."""
diff --git a/tests/api/test_hardware.py b/tests/api/test_hardware.py
index 116ca376f..492a43216 100644
--- a/tests/api/test_hardware.py
+++ b/tests/api/test_hardware.py
@@ -1,13 +1,15 @@
"""Test Docker API."""
from pathlib import Path
+from aiohttp.test_utils import TestClient
import pytest
+from supervisor.coresys import CoreSys
from supervisor.hardware.data import Device
@pytest.mark.asyncio
-async def test_api_hardware_info(api_client):
+async def test_api_hardware_info(api_client: TestClient):
"""Test docker info api."""
resp = await api_client.get("/hardware/info")
result = await resp.json()
@@ -16,7 +18,7 @@ async def test_api_hardware_info(api_client):
@pytest.mark.asyncio
-async def test_api_hardware_info_device(api_client, coresys):
+async def test_api_hardware_info_device(api_client: TestClient, coresys: CoreSys):
"""Test docker info api."""
coresys.hardware.update_device(
Device(
@@ -37,3 +39,26 @@ async def test_api_hardware_info_device(api_client, coresys):
assert result["result"] == "ok"
assert result["data"]["devices"][-1]["name"] == "sda"
assert result["data"]["devices"][-1]["by_id"] == "/dev/serial/by-id/test"
+
+
+async def test_api_hardware_info_drives(api_client: TestClient, coresys: CoreSys):
+ """Test drive info."""
+ await coresys.dbus.udisks2.connect(coresys.dbus.bus)
+
+ resp = await api_client.get("/hardware/info")
+ result = await resp.json()
+
+ assert result["result"] == "ok"
+ assert {
+ drive["id"]: {fs["id"] for fs in drive["filesystems"]}
+ for drive in result["data"]["drives"]
+ } == {
+ "BJTD4R-0x97cde291": {
+ "by-id-mmc-BJTD4R_0x97cde291-part1",
+ "by-id-mmc-BJTD4R_0x97cde291-part3",
+ },
+ "SSK-SSK-Storage-DF56419883D56": {
+ "by-id-usb-SSK_SSK_Storage_DF56419883D56-0:0-part1"
+ },
+ "Generic-Flash-Disk-61BCDDB6": {"by-uuid-2802-1EDE"},
+ }
diff --git a/tests/api/test_host.py b/tests/api/test_host.py
index 581fc3874..0282f3fdc 100644
--- a/tests/api/test_host.py
+++ b/tests/api/test_host.py
@@ -48,6 +48,7 @@ async def test_api_host_features(
coresys.host.sys_dbus.timedate.is_connected = False
coresys.host.sys_dbus.agent.is_connected = False
coresys.host.sys_dbus.resolved.is_connected = False
+ coresys.host.sys_dbus.udisks2.is_connected = False
resp = await api_client.get("/host/info")
result = await resp.json()
@@ -59,6 +60,7 @@ async def test_api_host_features(
assert "timedate" not in result["data"]["features"]
assert "os_agent" not in result["data"]["features"]
assert "resolved" not in result["data"]["features"]
+ assert "disk" not in result["data"]["features"]
coresys.host.sys_dbus.systemd.is_connected = True
coresys.host.supported_features.cache_clear()
@@ -98,6 +100,12 @@ async def test_api_host_features(
result = await resp.json()
assert "resolved" in result["data"]["features"]
+ coresys.host.sys_dbus.udisks2.is_connected = True
+ coresys.host.supported_features.cache_clear()
+ resp = await api_client.get("/host/info")
+ result = await resp.json()
+ assert "disk" in result["data"]["features"]
+
async def test_api_llmnr_mdns_info(
api_client: TestClient, coresys_disk_info: CoreSys, dbus_is_connected
diff --git a/tests/conftest.py b/tests/conftest.py
index f690ee203..6359f0ebe 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -3,6 +3,7 @@ from functools import partial
from inspect import unwrap
from pathlib import Path
import re
+from typing import Any
from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch
from uuid import uuid4
@@ -49,6 +50,7 @@ from supervisor.dbus.network import NetworkManager
from supervisor.dbus.resolved import Resolved
from supervisor.dbus.systemd import Systemd
from supervisor.dbus.timedate import TimeDate
+from supervisor.dbus.udisks2 import UDisks2
from supervisor.docker.manager import DockerAPI
from supervisor.docker.monitor import DockerMonitor
from supervisor.host.logs import LogsControl
@@ -118,15 +120,40 @@ async def dbus_bus() -> MessageBus:
yield bus
+def _process_pseudo_variant(data: dict[str, Any]) -> Any:
+ """Process pseudo variant into value."""
+ if data["_type"] == "ay":
+ return bytearray(data["_value"], encoding="utf-8")
+ if data["_type"] == "aay":
+ return [bytearray(i, encoding="utf-8") for i in data["_value"]]
+
+ # Unknown type, return as is
+ return data
+
+
+def process_dbus_json(data: Any) -> Any:
+ """Replace pseudo-variants with values of unsupported json types as necessary."""
+ if not isinstance(data, dict):
+ return data
+
+ if len(data.keys()) == 2 and "_type" in data and "_value" in data:
+ return _process_pseudo_variant(data)
+
+ return {k: process_dbus_json(v) for k, v in data.items()}
+
+
def mock_get_properties(object_path: str, interface: str) -> str:
"""Mock get dbus properties."""
- latest = object_path.split("/")[-1]
+ base, _, latest = object_path.rpartition("/")
fixture = interface.replace(".", "_")
- if latest.isnumeric():
+ if latest.isnumeric() or base in [
+ "/org/freedesktop/UDisks2/block_devices",
+ "/org/freedesktop/UDisks2/drives",
+ ]:
fixture = f"{fixture}_{latest}"
- return load_json_fixture(f"{fixture}.json")
+ return process_dbus_json(load_json_fixture(f"{fixture}.json"))
async def mock_init_proxy(self):
@@ -226,7 +253,7 @@ def dbus(dbus_bus: MessageBus) -> list[str]:
)
if exists_fixture(f"{fixture}.json"):
- return load_json_fixture(f"{fixture}.json")
+ return process_dbus_json(load_json_fixture(f"{fixture}.json"))
with patch("supervisor.utils.dbus.DBus.call_dbus", new=mock_call_dbus), patch(
"supervisor.utils.dbus.DBus._init_proxy", new=mock_init_proxy
@@ -313,6 +340,12 @@ async def resolved(dbus: DBus, dbus_bus: MessageBus) -> Resolved:
yield await mock_dbus_interface(dbus, dbus_bus, Resolved())
+@pytest.fixture
+async def udisks2(dbus: DBus, dbus_bus: MessageBus) -> UDisks2:
+ """Mock UDisks2."""
+ yield await mock_dbus_interface(dbus, dbus_bus, UDisks2())
+
+
@pytest.fixture
async def coresys(
event_loop, docker, network_manager, dbus_bus, aiohttp_client, run_dir
diff --git a/tests/dbus/test_interface.py b/tests/dbus/test_interface.py
index 232234829..b13338d58 100644
--- a/tests/dbus/test_interface.py
+++ b/tests/dbus/test_interface.py
@@ -8,8 +8,9 @@ from dbus_fast.aio.message_bus import MessageBus
import pytest
from supervisor.dbus.const import DBUS_OBJECT_BASE
-from supervisor.dbus.interface import DBusInterfaceProxy
+from supervisor.dbus.interface import DBusInterface, DBusInterfaceProxy
from supervisor.exceptions import DBusInterfaceError
+from supervisor.utils.dbus import DBus
from tests.common import fire_property_change_signal, fire_watched_signal
@@ -120,3 +121,40 @@ async def test_proxy_missing_properties_interface(dbus_bus: MessageBus):
with pytest.raises(DBusInterfaceError):
await proxy.connect(dbus_bus)
assert proxy.is_connected is False
+
+
+async def test_initialize(dbus_bus: MessageBus):
+ """Test initialize for reusing connected dbus object."""
+ proxy = DBusInterface()
+ proxy.bus_name = "org.freedesktop.UDisks2"
+ proxy.object_path = "/org/freedesktop/UDisks2/block_devices/sda"
+
+ assert proxy.is_connected is False
+
+ with pytest.raises(ValueError, match="must be a connected DBus object"):
+ await proxy.initialize(
+ DBus(
+ dbus_bus,
+ "org.freedesktop.UDisks2",
+ "/org/freedesktop/UDisks2/block_devices/sda",
+ )
+ )
+
+ with pytest.raises(
+ ValueError,
+ match="must be a DBus object connected to bus org.freedesktop.UDisks2 and object /org/freedesktop/UDisks2/block_devices/sda",
+ ):
+ await proxy.initialize(
+ await DBus.connect(
+ dbus_bus, "org.freedesktop.hostname1", "/org/freedesktop/hostname1"
+ )
+ )
+
+ await proxy.initialize(
+ await DBus.connect(
+ dbus_bus,
+ "org.freedesktop.UDisks2",
+ "/org/freedesktop/UDisks2/block_devices/sda",
+ )
+ )
+ assert proxy.is_connected is True
diff --git a/tests/dbus/udisks2/__init__.py b/tests/dbus/udisks2/__init__.py
new file mode 100644
index 000000000..56028888e
--- /dev/null
+++ b/tests/dbus/udisks2/__init__.py
@@ -0,0 +1 @@
+"""Tests for UDisks2."""
diff --git a/tests/dbus/udisks2/test_block.py b/tests/dbus/udisks2/test_block.py
new file mode 100644
index 000000000..d9429e744
--- /dev/null
+++ b/tests/dbus/udisks2/test_block.py
@@ -0,0 +1,70 @@
+"""Test UDisks2 Block Device interface."""
+
+import asyncio
+from pathlib import Path
+
+from dbus_fast.aio.message_bus import MessageBus
+import pytest
+
+from supervisor.dbus.udisks2.block import UDisks2Block
+from supervisor.dbus.udisks2.const import FormatType, PartitionTableType
+from supervisor.dbus.udisks2.data import FormatOptions
+
+from tests.common import fire_property_change_signal
+
+
+async def test_block_device_info(dbus: list[str], dbus_bus: MessageBus):
+ """Test block device info."""
+ sda = UDisks2Block("/org/freedesktop/UDisks2/block_devices/sda")
+ sda1 = UDisks2Block(
+ "/org/freedesktop/UDisks2/block_devices/sda1", sync_properties=False
+ )
+
+ assert sda.drive is None
+ assert sda.device is None
+ assert sda.id_label is None
+ assert sda.partition_table is None
+ assert sda1.id_label is None
+ assert sda1.symlinks is None
+ assert sda1.filesystem is None
+
+ await sda.connect(dbus_bus)
+ await sda1.connect(dbus_bus)
+
+ assert sda.drive == "/org/freedesktop/UDisks2/drives/SSK_SSK_Storage_DF56419883D56"
+ assert sda.device == Path("/dev/sda")
+ assert sda.id_label == ""
+ assert sda.partition_table.type == PartitionTableType.GPT
+ assert sda.filesystem is None
+
+ assert sda1.id_label == "hassos-data"
+ assert sda1.symlinks == [
+ Path("/dev/disk/by-id/usb-SSK_SSK_Storage_DF56419883D56-0:0-part1"),
+ Path("/dev/disk/by-label/hassos-data"),
+ Path("/dev/disk/by-partlabel/hassos-data-external"),
+ Path("/dev/disk/by-partuuid/6f3f99f4-4d34-476b-b051-77886da57fa9"),
+ Path(
+ "/dev/disk/by-path/platform-xhci-hcd.1.auto-usb-0:1.4:1.0-scsi-0:0:0:0-part1"
+ ),
+ Path("/dev/disk/by-uuid/b82b23cb-0c47-4bbb-acf5-2a2afa8894a2"),
+ ]
+ assert sda1.partition_table is None
+ assert sda1.filesystem.mount_points == []
+
+ fire_property_change_signal(sda, {"IdLabel": "test"})
+ await asyncio.sleep(0)
+ assert sda.id_label == "test"
+
+ with pytest.raises(AssertionError):
+ fire_property_change_signal(sda1)
+
+
+async def test_format(dbus: list[str], dbus_bus: MessageBus):
+ """Test formatting block device."""
+ sda = UDisks2Block("/org/freedesktop/UDisks2/block_devices/sda")
+ await sda.connect(dbus_bus)
+
+ await sda.format(FormatType.GPT, FormatOptions(label="test"))
+ assert dbus == [
+ "/org/freedesktop/UDisks2/block_devices/sda-org.freedesktop.UDisks2.Block.Format"
+ ]
diff --git a/tests/dbus/udisks2/test_drive.py b/tests/dbus/udisks2/test_drive.py
new file mode 100644
index 000000000..f4373ae5d
--- /dev/null
+++ b/tests/dbus/udisks2/test_drive.py
@@ -0,0 +1,44 @@
+"""Test UDisks2 Drive."""
+
+import asyncio
+from datetime import datetime, timezone
+
+from dbus_fast.aio.message_bus import MessageBus
+
+from supervisor.dbus.udisks2.drive import UDisks2Drive
+
+from tests.common import fire_property_change_signal
+
+
+async def test_drive_info(dbus: list[str], dbus_bus: MessageBus):
+ """Test drive info."""
+ ssk = UDisks2Drive("/org/freedesktop/UDisks2/drives/SSK_SSK_Storage_DF56419883D56")
+
+ assert ssk.vendor is None
+ assert ssk.model is None
+ assert ssk.size is None
+ assert ssk.time_detected is None
+ assert ssk.ejectable is None
+
+ await ssk.connect(dbus_bus)
+
+ assert ssk.vendor == "SSK"
+ assert ssk.model == "SSK Storage"
+ assert ssk.size == 250059350016
+ assert ssk.time_detected == datetime(2023, 2, 8, 23, 1, 44, 240492, timezone.utc)
+ assert ssk.ejectable is False
+
+ fire_property_change_signal(ssk, {"Ejectable": True})
+ await asyncio.sleep(0)
+ assert ssk.ejectable is True
+
+
+async def test_eject(dbus: list[str], dbus_bus: MessageBus):
+ """Test eject."""
+ flash = UDisks2Drive("/org/freedesktop/UDisks2/drives/Generic_Flash_Disk_61BCDDB6")
+ await flash.connect(dbus_bus)
+
+ await flash.eject()
+ assert dbus == [
+ "/org/freedesktop/UDisks2/drives/Generic_Flash_Disk_61BCDDB6-org.freedesktop.UDisks2.Drive.Eject"
+ ]
diff --git a/tests/dbus/udisks2/test_filesystem.py b/tests/dbus/udisks2/test_filesystem.py
new file mode 100644
index 000000000..db4c186b1
--- /dev/null
+++ b/tests/dbus/udisks2/test_filesystem.py
@@ -0,0 +1,84 @@
+"""Test UDisks2 Filesystem."""
+
+import asyncio
+from pathlib import Path
+
+from dbus_fast.aio.message_bus import MessageBus
+import pytest
+
+from supervisor.dbus.udisks2.data import MountOptions, UnmountOptions
+from supervisor.dbus.udisks2.filesystem import UDisks2Filesystem
+
+from tests.common import fire_property_change_signal
+
+
+@pytest.fixture(name="sda1")
+async def fixture_sda1(dbus: list[str], dbus_bus: MessageBus) -> UDisks2Filesystem:
+ """Return connected sda1 filesystem object."""
+ filesystem = UDisks2Filesystem("/org/freedesktop/UDisks2/block_devices/sda1")
+ await filesystem.connect(dbus_bus)
+ return filesystem
+
+
+async def test_filesystem_info(dbus: list[str], dbus_bus: MessageBus):
+ """Test filesystem info."""
+ sda1 = UDisks2Filesystem("/org/freedesktop/UDisks2/block_devices/sda1")
+ sdb1 = UDisks2Filesystem(
+ "/org/freedesktop/UDisks2/block_devices/sdb1", sync_properties=False
+ )
+
+ assert sda1.size is None
+ assert sda1.mount_points is None
+ assert sdb1.size is None
+ assert sdb1.mount_points is None
+
+ await sda1.connect(dbus_bus)
+ await sdb1.connect(dbus_bus)
+
+ assert sda1.size == 250058113024
+ assert sda1.mount_points == []
+ assert sdb1.size == 67108864
+ assert sdb1.mount_points == [Path("/mnt/data/supervisor/media/ext")]
+
+ fire_property_change_signal(
+ sda1, {"MountPoints": [bytearray("/mnt/media", encoding="utf-8")]}
+ )
+ await asyncio.sleep(0)
+ assert sda1.mount_points == [Path("/mnt/media")]
+
+ with pytest.raises(AssertionError):
+ fire_property_change_signal(
+ sdb1, {"MountPoints": [bytearray("/mnt/media", encoding="utf-8")]}
+ )
+
+
+async def test_mount(dbus: list[str], sda1: UDisks2Filesystem):
+ """Test mount."""
+ assert await sda1.mount(MountOptions(fstype="gpt")) == "/run/media/dev/hassos_data"
+ assert dbus == [
+ "/org/freedesktop/UDisks2/block_devices/sda1-org.freedesktop.UDisks2.Filesystem.Mount"
+ ]
+
+
+async def test_unmount(dbus: list[str], sda1: UDisks2Filesystem):
+ """Test unmount."""
+ await sda1.unmount(UnmountOptions(force=True))
+ assert dbus == [
+ "/org/freedesktop/UDisks2/block_devices/sda1-org.freedesktop.UDisks2.Filesystem.Unmount"
+ ]
+
+
+async def test_check(dbus: list[str], sda1: UDisks2Filesystem):
+ """Test check."""
+ assert await sda1.check() is True
+ assert dbus == [
+ "/org/freedesktop/UDisks2/block_devices/sda1-org.freedesktop.UDisks2.Filesystem.Check"
+ ]
+
+
+async def test_repair(dbus: list[str], sda1: UDisks2Filesystem):
+ """Test repair."""
+ assert await sda1.repair() is True
+ assert dbus == [
+ "/org/freedesktop/UDisks2/block_devices/sda1-org.freedesktop.UDisks2.Filesystem.Repair"
+ ]
diff --git a/tests/dbus/udisks2/test_manager.py b/tests/dbus/udisks2/test_manager.py
new file mode 100644
index 000000000..71fc546be
--- /dev/null
+++ b/tests/dbus/udisks2/test_manager.py
@@ -0,0 +1,111 @@
+"""Test UDisks2 Manager interface."""
+
+import asyncio
+
+from awesomeversion import AwesomeVersion
+import pytest
+
+from supervisor.coresys import CoreSys
+from supervisor.dbus.udisks2.data import DeviceSpecification
+from supervisor.exceptions import DBusNotConnectedError, DBusObjectError
+
+from tests.common import fire_property_change_signal
+
+
+async def test_udisks2_manager_info(coresys: CoreSys, dbus: list[str]):
+ """Test udisks2 manager dbus connection."""
+ dbus.clear()
+ assert coresys.dbus.udisks2.supported_filesystems is None
+
+ await coresys.dbus.udisks2.connect(coresys.dbus.bus)
+
+ assert coresys.dbus.udisks2.supported_filesystems == [
+ "ext4",
+ "vfat",
+ "ntfs",
+ "exfat",
+ "swap",
+ ]
+ assert coresys.dbus.udisks2.version == AwesomeVersion("2.9.2")
+ assert {block.object_path for block in coresys.dbus.udisks2.block_devices} == {
+ "/org/freedesktop/UDisks2/block_devices/loop0",
+ "/org/freedesktop/UDisks2/block_devices/mmcblk1",
+ "/org/freedesktop/UDisks2/block_devices/mmcblk1p1",
+ "/org/freedesktop/UDisks2/block_devices/mmcblk1p2",
+ "/org/freedesktop/UDisks2/block_devices/mmcblk1p3",
+ "/org/freedesktop/UDisks2/block_devices/sda",
+ "/org/freedesktop/UDisks2/block_devices/sda1",
+ "/org/freedesktop/UDisks2/block_devices/sdb",
+ "/org/freedesktop/UDisks2/block_devices/sdb1",
+ "/org/freedesktop/UDisks2/block_devices/zram1",
+ }
+ assert {drive.object_path for drive in coresys.dbus.udisks2.drives} == {
+ "/org/freedesktop/UDisks2/drives/BJTD4R_0x97cde291",
+ "/org/freedesktop/UDisks2/drives/SSK_SSK_Storage_DF56419883D56",
+ "/org/freedesktop/UDisks2/drives/Generic_Flash_Disk_61BCDDB6",
+ }
+ assert dbus == [
+ "/org/freedesktop/UDisks2/Manager-org.freedesktop.UDisks2.Manager.GetBlockDevices"
+ ]
+
+ dbus.clear()
+ fire_property_change_signal(
+ coresys.dbus.udisks2, {"SupportedFilesystems": ["ext4"]}
+ )
+ await asyncio.sleep(0)
+ assert coresys.dbus.udisks2.supported_filesystems == ["ext4"]
+ assert dbus == []
+
+
+async def test_get_block_device(coresys: CoreSys):
+ """Test get block device by object path."""
+ with pytest.raises(DBusNotConnectedError):
+ coresys.dbus.udisks2.get_block_device(
+ "/org/freedesktop/UDisks2/block_devices/sda1"
+ )
+
+ await coresys.dbus.udisks2.connect(coresys.dbus.bus)
+
+ block_device = coresys.dbus.udisks2.get_block_device(
+ "/org/freedesktop/UDisks2/block_devices/sda1"
+ )
+ assert block_device.id_label == "hassos-data"
+
+ with pytest.raises(DBusObjectError):
+ coresys.dbus.udisks2.get_block_device("non_existent")
+
+
+async def test_get_drive(coresys: CoreSys):
+ """Test get drive by object path."""
+ with pytest.raises(DBusNotConnectedError):
+ coresys.dbus.udisks2.get_drive(
+ "/org/freedesktop/UDisks2/drives/BJTD4R_0x97cde291"
+ )
+
+ await coresys.dbus.udisks2.connect(coresys.dbus.bus)
+
+ drive = coresys.dbus.udisks2.get_drive(
+ "/org/freedesktop/UDisks2/drives/BJTD4R_0x97cde291"
+ )
+ assert drive.id == "BJTD4R-0x97cde291"
+
+ with pytest.raises(DBusObjectError):
+ coresys.dbus.udisks2.get_drive("non_existent")
+
+
+async def test_resolve_device(coresys: CoreSys, dbus: list[str]):
+ """Test resolve device."""
+ with pytest.raises(DBusNotConnectedError):
+ await coresys.dbus.udisks2.resolve_device(DeviceSpecification(path="/dev/sda1"))
+
+ await coresys.dbus.udisks2.connect(coresys.dbus.bus)
+
+ dbus.clear()
+ devices = await coresys.dbus.udisks2.resolve_device(
+ DeviceSpecification(path="/dev/sda1")
+ )
+ assert len(devices) == 1
+ assert devices[0].id_label == "hassos-data"
+ assert dbus == [
+ "/org/freedesktop/UDisks2/Manager-org.freedesktop.UDisks2.Manager.ResolveDevice"
+ ]
diff --git a/tests/dbus/udisks2/test_partition_table.py b/tests/dbus/udisks2/test_partition_table.py
new file mode 100644
index 000000000..a6ea81001
--- /dev/null
+++ b/tests/dbus/udisks2/test_partition_table.py
@@ -0,0 +1,79 @@
+"""Test UDisks2 Partition Table."""
+
+import asyncio
+
+from dbus_fast.aio.message_bus import MessageBus
+import pytest
+
+from supervisor.dbus.udisks2.const import PartitionTableType
+from supervisor.dbus.udisks2.data import CreatePartitionOptions
+from supervisor.dbus.udisks2.partition_table import UDisks2PartitionTable
+
+from tests.common import fire_property_change_signal
+
+
+async def test_partition_table_info(dbus: list[str], dbus_bus: MessageBus):
+ """Test partition table info."""
+ sda = UDisks2PartitionTable("/org/freedesktop/UDisks2/block_devices/sda")
+ sdb = UDisks2PartitionTable(
+ "/org/freedesktop/UDisks2/block_devices/sdb", sync_properties=False
+ )
+
+ assert sda.type is None
+ assert sda.partitions is None
+ assert sdb.type is None
+ assert sdb.partitions is None
+
+ await sda.connect(dbus_bus)
+ await sdb.connect(dbus_bus)
+
+ assert sda.type == PartitionTableType.GPT
+ assert sda.partitions == ["/org/freedesktop/UDisks2/block_devices/sda1"]
+ assert sdb.type == PartitionTableType.GPT
+ assert sdb.partitions == ["/org/freedesktop/UDisks2/block_devices/sdb1"]
+
+ fire_property_change_signal(
+ sda,
+ {
+ "Partitions": [
+ "/org/freedesktop/UDisks2/block_devices/sda1",
+ "/org/freedesktop/UDisks2/block_devices/sda2",
+ ]
+ },
+ )
+ await asyncio.sleep(0)
+ assert sda.partitions == [
+ "/org/freedesktop/UDisks2/block_devices/sda1",
+ "/org/freedesktop/UDisks2/block_devices/sda2",
+ ]
+
+ with pytest.raises(AssertionError):
+ fire_property_change_signal(
+ sdb,
+ {
+ "MountPoints": [
+ "/org/freedesktop/UDisks2/block_devices/sdb",
+ "/org/freedesktop/UDisks2/block_devices/sdb",
+ ]
+ },
+ )
+
+
+async def test_create_partition(dbus: list[str], dbus_bus: MessageBus):
+ """Test create partition."""
+ sda = UDisks2PartitionTable("/org/freedesktop/UDisks2/block_devices/sda")
+ await sda.connect(dbus_bus)
+
+ assert (
+ await sda.create_partition(
+ offset=0,
+ size=1000000,
+ type_=PartitionTableType.DOS,
+ name="hassos-data",
+ options=CreatePartitionOptions(partition_type="primary"),
+ )
+ == "/org/freedesktop/UDisks2/block_devices/sda2"
+ )
+ assert dbus == [
+ "/org/freedesktop/UDisks2/block_devices/sda-org.freedesktop.UDisks2.PartitionTable.CreatePartition"
+ ]
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Block_loop0.json b/tests/fixtures/org_freedesktop_UDisks2_Block_loop0.json
new file mode 100644
index 000000000..da07be59f
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Block_loop0.json
@@ -0,0 +1,27 @@
+{
+ "Device": { "_type": "ay", "_value": "/dev/loop0" },
+ "PreferredDevice": { "_type": "ay", "_value": "/dev/loop0" },
+ "Symlinks": [],
+ "DeviceNumber": 1792,
+ "Id": "",
+ "Size": 0,
+ "ReadOnly": false,
+ "Drive": "/",
+ "MDRaid": "/",
+ "MDRaidMember": "/",
+ "IdUsage": "",
+ "IdType": "",
+ "IdVersion": "",
+ "IdLabel": "",
+ "IdUUID": "",
+ "Configuration": [],
+ "CryptoBackingDevice": "/",
+ "HintPartitionable": true,
+ "HintSystem": true,
+ "HintIgnore": false,
+ "HintAuto": false,
+ "HintName": "",
+ "HintIconName": "",
+ "HintSymbolicIconName": "",
+ "UserspaceMountOptions": []
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Block_mmcblk1.json b/tests/fixtures/org_freedesktop_UDisks2_Block_mmcblk1.json
new file mode 100644
index 000000000..b40f7a90b
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Block_mmcblk1.json
@@ -0,0 +1,33 @@
+{
+ "Device": { "_type": "ay", "_value": "/dev/mmcblk1" },
+ "PreferredDevice": { "_type": "ay", "_value": "/dev/mmcblk1" },
+ "Symlinks": {
+ "_type": "aay",
+ "_value": [
+ "/dev/disk/by-id/mmc-BJTD4R_0x97cde291",
+ "/dev/disk/by-path/platform-ffe07000.mmc"
+ ]
+ },
+ "DeviceNumber": 45824,
+ "Id": "by-id-mmc-BJTD4R_0x97cde291",
+ "Size": 31268536320,
+ "ReadOnly": false,
+ "Drive": "/org/freedesktop/UDisks2/drives/BJTD4R_0x97cde291",
+ "MDRaid": "/",
+ "MDRaidMember": "/",
+ "IdUsage": "",
+ "IdType": "",
+ "IdVersion": "",
+ "IdLabel": "",
+ "IdUUID": "",
+ "Configuration": [],
+ "CryptoBackingDevice": "/",
+ "HintPartitionable": true,
+ "HintSystem": true,
+ "HintIgnore": false,
+ "HintAuto": false,
+ "HintName": "",
+ "HintIconName": "",
+ "HintSymbolicIconName": "",
+ "UserspaceMountOptions": []
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Block_mmcblk1p1.json b/tests/fixtures/org_freedesktop_UDisks2_Block_mmcblk1p1.json
new file mode 100644
index 000000000..03e31cd74
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Block_mmcblk1p1.json
@@ -0,0 +1,37 @@
+{
+ "Device": { "_type": "ay", "_value": "/dev/mmcblk1p1" },
+ "PreferredDevice": { "_type": "ay", "_value": "/dev/mmcblk1p1" },
+ "Symlinks": {
+ "_type": "aay",
+ "_value": [
+ "/dev/disk/by-id/mmc-BJTD4R_0x97cde291-part1",
+ "/dev/disk/by-label/hassos-boot",
+ "/dev/disk/by-partlabel/hassos-boot",
+ "/dev/disk/by-partuuid/48617373-01",
+ "/dev/disk/by-path/platform-ffe07000.mmc-part1",
+ "/dev/disk/by-uuid/16DD-EED4"
+ ]
+ },
+ "DeviceNumber": 45825,
+ "Id": "by-id-mmc-BJTD4R_0x97cde291-part1",
+ "Size": 25165824,
+ "ReadOnly": false,
+ "Drive": "/org/freedesktop/UDisks2/drives/BJTD4R_0x97cde291",
+ "MDRaid": "/",
+ "MDRaidMember": "/",
+ "IdUsage": "filesystem",
+ "IdType": "vfat",
+ "IdVersion": "FAT16",
+ "IdLabel": "hassos-boot",
+ "IdUUID": "16DD-EED4",
+ "Configuration": [],
+ "CryptoBackingDevice": "/",
+ "HintPartitionable": true,
+ "HintSystem": true,
+ "HintIgnore": false,
+ "HintAuto": false,
+ "HintName": "",
+ "HintIconName": "",
+ "HintSymbolicIconName": "",
+ "UserspaceMountOptions": []
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Block_mmcblk1p2.json b/tests/fixtures/org_freedesktop_UDisks2_Block_mmcblk1p2.json
new file mode 100644
index 000000000..3bfb69a1f
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Block_mmcblk1p2.json
@@ -0,0 +1,34 @@
+{
+ "Device": { "_type": "ay", "_value": "/dev/mmcblk1p2" },
+ "PreferredDevice": { "_type": "ay", "_value": "/dev/mmcblk1p2" },
+ "Symlinks": {
+ "_type": "aay",
+ "_value": [
+ "/dev/disk/by-id/mmc-BJTD4R_0x97cde291-part2",
+ "/dev/disk/by-partuuid/48617373-02",
+ "/dev/disk/by-path/platform-ffe07000.mmc-part2"
+ ]
+ },
+ "DeviceNumber": 45826,
+ "Id": "by-id-mmc-BJTD4R_0x97cde291-part2",
+ "Size": 1024,
+ "ReadOnly": false,
+ "Drive": "/org/freedesktop/UDisks2/drives/BJTD4R_0x97cde291",
+ "MDRaid": "/",
+ "MDRaidMember": "/",
+ "IdUsage": "",
+ "IdType": "",
+ "IdVersion": "",
+ "IdLabel": "",
+ "IdUUID": "",
+ "Configuration": [],
+ "CryptoBackingDevice": "/",
+ "HintPartitionable": true,
+ "HintSystem": true,
+ "HintIgnore": false,
+ "HintAuto": false,
+ "HintName": "",
+ "HintIconName": "",
+ "HintSymbolicIconName": "",
+ "UserspaceMountOptions": []
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Block_mmcblk1p3.json b/tests/fixtures/org_freedesktop_UDisks2_Block_mmcblk1p3.json
new file mode 100644
index 000000000..64c56eb84
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Block_mmcblk1p3.json
@@ -0,0 +1,36 @@
+{
+ "Device": { "_type": "ay", "_value": "/dev/mmcblk1p3" },
+ "PreferredDevice": { "_type": "ay", "_value": "/dev/mmcblk1p3" },
+ "Symlinks": {
+ "_type": "aay",
+ "_value": [
+ "/dev/disk/by-id/mmc-BJTD4R_0x97cde291-part3",
+ "/dev/disk/by-label/hassos-overlay",
+ "/dev/disk/by-partuuid/48617373-03",
+ "/dev/disk/by-path/platform-ffe07000.mmc-part3",
+ "/dev/disk/by-uuid/0cd0d026-8c69-484e-bbf1-8197019fa7df"
+ ]
+ },
+ "DeviceNumber": 45827,
+ "Id": "by-id-mmc-BJTD4R_0x97cde291-part3",
+ "Size": 100663296,
+ "ReadOnly": false,
+ "Drive": "/org/freedesktop/UDisks2/drives/BJTD4R_0x97cde291",
+ "MDRaid": "/",
+ "MDRaidMember": "/",
+ "IdUsage": "filesystem",
+ "IdType": "ext4",
+ "IdVersion": "1.0",
+ "IdLabel": "hassos-overlay",
+ "IdUUID": "0cd0d026-8c69-484e-bbf1-8197019fa7df",
+ "Configuration": [],
+ "CryptoBackingDevice": "/",
+ "HintPartitionable": true,
+ "HintSystem": true,
+ "HintIgnore": false,
+ "HintAuto": false,
+ "HintName": "",
+ "HintIconName": "",
+ "HintSymbolicIconName": "",
+ "UserspaceMountOptions": []
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Block_sda.json b/tests/fixtures/org_freedesktop_UDisks2_Block_sda.json
new file mode 100644
index 000000000..324a361fc
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Block_sda.json
@@ -0,0 +1,39 @@
+{
+ "Device": {
+ "_type": "ay",
+ "_value": "/dev/sda"
+ },
+ "PreferredDevice": {
+ "_type": "ay",
+ "_value": "/dev/sda"
+ },
+ "Symlinks": {
+ "_type": "aay",
+ "_value": [
+ "/dev/disk/by-id/usb-SSK_SSK_Storage_DF56419883D56-0:0",
+ "/dev/disk/by-path/platform-xhci-hcd.1.auto-usb-0:1.4:1.0-scsi-0:0:0:0"
+ ]
+ },
+ "DeviceNumber": 2048,
+ "Id": "by-id-usb-SSK_SSK_Storage_DF56419883D56-0:0",
+ "Size": 250059350016,
+ "ReadOnly": false,
+ "Drive": "/org/freedesktop/UDisks2/drives/SSK_SSK_Storage_DF56419883D56",
+ "MDRaid": "/",
+ "MDRaidMember": "/",
+ "IdUsage": "",
+ "IdType": "",
+ "IdVersion": "",
+ "IdLabel": "",
+ "IdUUID": "",
+ "Configuration": [],
+ "CryptoBackingDevice": "/",
+ "HintPartitionable": true,
+ "HintSystem": false,
+ "HintIgnore": false,
+ "HintAuto": true,
+ "HintName": "",
+ "HintIconName": "",
+ "HintSymbolicIconName": "",
+ "UserspaceMountOptions": []
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Block_sda1.json b/tests/fixtures/org_freedesktop_UDisks2_Block_sda1.json
new file mode 100644
index 000000000..b55383156
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Block_sda1.json
@@ -0,0 +1,37 @@
+{
+ "Device": { "_type": "ay", "_value": "/dev/sda1" },
+ "PreferredDevice": { "_type": "ay", "_value": "/dev/sda1" },
+ "Symlinks": {
+ "_type": "aay",
+ "_value": [
+ "/dev/disk/by-id/usb-SSK_SSK_Storage_DF56419883D56-0:0-part1",
+ "/dev/disk/by-label/hassos-data",
+ "/dev/disk/by-partlabel/hassos-data-external",
+ "/dev/disk/by-partuuid/6f3f99f4-4d34-476b-b051-77886da57fa9",
+ "/dev/disk/by-path/platform-xhci-hcd.1.auto-usb-0:1.4:1.0-scsi-0:0:0:0-part1",
+ "/dev/disk/by-uuid/b82b23cb-0c47-4bbb-acf5-2a2afa8894a2"
+ ]
+ },
+ "DeviceNumber": 2049,
+ "Id": "by-id-usb-SSK_SSK_Storage_DF56419883D56-0:0-part1",
+ "Size": 250058113024,
+ "ReadOnly": false,
+ "Drive": "/org/freedesktop/UDisks2/drives/SSK_SSK_Storage_DF56419883D56",
+ "MDRaid": "/",
+ "MDRaidMember": "/",
+ "IdUsage": "filesystem",
+ "IdType": "ext4",
+ "IdVersion": "1.0",
+ "IdLabel": "hassos-data",
+ "IdUUID": "b82b23cb-0c47-4bbb-acf5-2a2afa8894a2",
+ "Configuration": [],
+ "CryptoBackingDevice": "/",
+ "HintPartitionable": true,
+ "HintSystem": false,
+ "HintIgnore": false,
+ "HintAuto": true,
+ "HintName": "",
+ "HintIconName": "",
+ "HintSymbolicIconName": "",
+ "UserspaceMountOptions": []
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Block_sdb.json b/tests/fixtures/org_freedesktop_UDisks2_Block_sdb.json
new file mode 100644
index 000000000..7e672d0aa
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Block_sdb.json
@@ -0,0 +1,33 @@
+{
+ "Device": { "_type": "ay", "_value": "/dev/sdb" },
+ "PreferredDevice": { "_type": "ay", "_value": "/dev/sdb" },
+ "Symlinks": {
+ "_type": "aay",
+ "_value": [
+ "/dev/disk/by-id/usb-Generic_Flash_Disk_61BCDDB6-0:0",
+ "/dev/disk/by-path/platform-xhci-hcd.1.auto-usb-0:1.2:1.0-scsi-0:0:0:0"
+ ]
+ },
+ "DeviceNumber": 2064,
+ "Id": "",
+ "Size": 8054112256,
+ "ReadOnly": false,
+ "Drive": "/org/freedesktop/UDisks2/drives/Generic_Flash_Disk_61BCDDB6",
+ "MDRaid": "/",
+ "MDRaidMember": "/",
+ "IdUsage": "",
+ "IdType": "",
+ "IdVersion": "",
+ "IdLabel": "",
+ "IdUUID": "",
+ "Configuration": [],
+ "CryptoBackingDevice": "/",
+ "HintPartitionable": true,
+ "HintSystem": false,
+ "HintIgnore": false,
+ "HintAuto": true,
+ "HintName": "",
+ "HintIconName": "",
+ "HintSymbolicIconName": "",
+ "UserspaceMountOptions": []
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Block_sdb1.json b/tests/fixtures/org_freedesktop_UDisks2_Block_sdb1.json
new file mode 100644
index 000000000..b23f44522
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Block_sdb1.json
@@ -0,0 +1,34 @@
+{
+ "Device": { "_type": "ay", "_value": "/dev/sdb1" },
+ "PreferredDevice": { "_type": "ay", "_value": "/dev/sdb1" },
+ "Symlinks": {
+ "_type": "aay",
+ "_value": [
+ "/dev/disk/by-id/usb-Generic_Flash_Disk_61BCDDB6-0:0-part1",
+ "/dev/disk/by-path/platform-xhci-hcd.1.auto-usb-0:1.2:1.0-scsi-0:0:0:0-part1",
+ "/dev/disk/by-uuid/2802-1EDE"
+ ]
+ },
+ "DeviceNumber": 2065,
+ "Id": "by-uuid-2802-1EDE",
+ "Size": 67108864,
+ "ReadOnly": false,
+ "Drive": "/org/freedesktop/UDisks2/drives/Generic_Flash_Disk_61BCDDB6",
+ "MDRaid": "/",
+ "MDRaidMember": "/",
+ "IdUsage": "filesystem",
+ "IdType": "vfat",
+ "IdVersion": "FAT16",
+ "IdLabel": "",
+ "IdUUID": "2802-1EDE",
+ "Configuration": [],
+ "CryptoBackingDevice": "/",
+ "HintPartitionable": true,
+ "HintSystem": false,
+ "HintIgnore": false,
+ "HintAuto": true,
+ "HintName": "",
+ "HintIconName": "",
+ "HintSymbolicIconName": "",
+ "UserspaceMountOptions": []
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Block_zram1.json b/tests/fixtures/org_freedesktop_UDisks2_Block_zram1.json
new file mode 100644
index 000000000..060341f6b
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Block_zram1.json
@@ -0,0 +1,27 @@
+{
+ "Device": { "_type": "ay", "_value": "/dev/zram1" },
+ "PreferredDevice": { "_type": "ay", "_value": "/dev/zram1" },
+ "Symlinks": [],
+ "DeviceNumber": 64769,
+ "Id": "",
+ "Size": 33554432,
+ "ReadOnly": false,
+ "Drive": "/",
+ "MDRaid": "/",
+ "MDRaidMember": "/",
+ "IdUsage": "",
+ "IdType": "",
+ "IdVersion": "",
+ "IdLabel": "",
+ "IdUUID": "",
+ "Configuration": [],
+ "CryptoBackingDevice": "/",
+ "HintPartitionable": true,
+ "HintSystem": true,
+ "HintIgnore": false,
+ "HintAuto": false,
+ "HintName": "",
+ "HintIconName": "",
+ "HintSymbolicIconName": "",
+ "UserspaceMountOptions": []
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Drive_BJTD4R_0x97cde291.json b/tests/fixtures/org_freedesktop_UDisks2_Drive_BJTD4R_0x97cde291.json
new file mode 100644
index 000000000..6f91ffdb0
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Drive_BJTD4R_0x97cde291.json
@@ -0,0 +1,31 @@
+{
+ "Vendor": "",
+ "Model": "BJTD4R",
+ "Revision": "",
+ "Serial": "0x97cde291",
+ "WWN": "",
+ "Id": "BJTD4R-0x97cde291",
+ "Configuration": {},
+ "Media": "flash_mmc",
+ "MediaCompatibility": ["flash_mmc"],
+ "MediaRemovable": false,
+ "MediaAvailable": true,
+ "MediaChangeDetected": true,
+ "Size": 31268536320,
+ "TimeDetected": 1673981760067475,
+ "TimeMediaDetected": 1673981760067475,
+ "Optical": false,
+ "OpticalBlank": false,
+ "OpticalNumTracks": 0,
+ "OpticalNumAudioTracks": 0,
+ "OpticalNumDataTracks": 0,
+ "OpticalNumSessions": 0,
+ "RotationRate": 0,
+ "ConnectionBus": "sdio",
+ "Seat": "seat0",
+ "Removable": false,
+ "Ejectable": false,
+ "SortKey": "00coldplug/00fixed/mmcblk1",
+ "CanPowerOff": false,
+ "SiblingId": ""
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Drive_Generic_Flash_Disk_61BCDDB6.json b/tests/fixtures/org_freedesktop_UDisks2_Drive_Generic_Flash_Disk_61BCDDB6.json
new file mode 100644
index 000000000..f797c3f40
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Drive_Generic_Flash_Disk_61BCDDB6.json
@@ -0,0 +1,31 @@
+{
+ "Vendor": "Generic",
+ "Model": "Flash Disk",
+ "Revision": "8.07",
+ "Serial": "61BCDDB6",
+ "WWN": "",
+ "Id": "Generic-Flash-Disk-61BCDDB6",
+ "Configuration": {},
+ "Media": "",
+ "MediaCompatibility": [],
+ "MediaRemovable": true,
+ "MediaAvailable": true,
+ "MediaChangeDetected": true,
+ "Size": 8054112256,
+ "TimeDetected": 1675972756688073,
+ "TimeMediaDetected": 1675972756688073,
+ "Optical": false,
+ "OpticalBlank": false,
+ "OpticalNumTracks": 0,
+ "OpticalNumAudioTracks": 0,
+ "OpticalNumDataTracks": 0,
+ "OpticalNumSessions": 0,
+ "RotationRate": -1,
+ "ConnectionBus": "usb",
+ "Seat": "seat0",
+ "Removable": true,
+ "Ejectable": true,
+ "SortKey": "01hotplug/1675972756688073",
+ "CanPowerOff": true,
+ "SiblingId": "/sys/devices/platform/soc/ffe09000.usb/ff500000.usb/xhci-hcd.1.auto/usb1/1-1/1-1.2/1-1.2:1.0"
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Drive_SSK_SSK_Storage_DF56419883D56.json b/tests/fixtures/org_freedesktop_UDisks2_Drive_SSK_SSK_Storage_DF56419883D56.json
new file mode 100644
index 000000000..5f79b4d5d
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Drive_SSK_SSK_Storage_DF56419883D56.json
@@ -0,0 +1,31 @@
+{
+ "Vendor": "SSK",
+ "Model": "SSK Storage",
+ "Revision": "0206",
+ "Serial": "DF56419883D56",
+ "WWN": "",
+ "Id": "SSK-SSK-Storage-DF56419883D56",
+ "Configuration": {},
+ "Media": "",
+ "MediaCompatibility": [],
+ "MediaRemovable": false,
+ "MediaAvailable": true,
+ "MediaChangeDetected": true,
+ "Size": 250059350016,
+ "TimeDetected": 1675897304240492,
+ "TimeMediaDetected": 1675897304240492,
+ "Optical": false,
+ "OpticalBlank": false,
+ "OpticalNumTracks": 0,
+ "OpticalNumAudioTracks": 0,
+ "OpticalNumDataTracks": 0,
+ "OpticalNumSessions": 0,
+ "RotationRate": 0,
+ "ConnectionBus": "usb",
+ "Seat": "seat0",
+ "Removable": true,
+ "Ejectable": false,
+ "SortKey": "01hotplug/1675897304240492",
+ "CanPowerOff": true,
+ "SiblingId": "/sys/devices/platform/soc/ffe09000.usb/ff500000.usb/xhci-hcd.1.auto/usb2/2-1/2-1.4/2-1.4:1.0"
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Filesystem_mmcblk1p1.json b/tests/fixtures/org_freedesktop_UDisks2_Filesystem_mmcblk1p1.json
new file mode 100644
index 000000000..bb25968e2
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Filesystem_mmcblk1p1.json
@@ -0,0 +1 @@
+{ "MountPoints": { "_type": "aay", "_value": ["/mnt/boot"] }, "Size": 0 }
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Filesystem_mmcblk1p3.json b/tests/fixtures/org_freedesktop_UDisks2_Filesystem_mmcblk1p3.json
new file mode 100644
index 000000000..ecd2c14dc
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Filesystem_mmcblk1p3.json
@@ -0,0 +1,22 @@
+{
+ "MountPoints": {
+ "_type": "aay",
+ "_value": [
+ "/etc/NetworkManager/system-connections",
+ "/etc/dropbear",
+ "/etc/hostname",
+ "/etc/hosts",
+ "/etc/modprobe.d",
+ "/etc/modules-load.d",
+ "/etc/systemd/timesyncd.conf",
+ "/etc/udev/rules.d",
+ "/mnt/overlay",
+ "/root/.docker",
+ "/root/.ssh",
+ "/var/lib/NetworkManager",
+ "/var/lib/bluetooth",
+ "/var/lib/systemd"
+ ]
+ },
+ "Size": 100663296
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Filesystem_sda1.json b/tests/fixtures/org_freedesktop_UDisks2_Filesystem_sda1.json
new file mode 100644
index 000000000..850a0cfa4
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Filesystem_sda1.json
@@ -0,0 +1 @@
+{ "MountPoints": [], "Size": 250058113024 }
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Filesystem_sdb1.json b/tests/fixtures/org_freedesktop_UDisks2_Filesystem_sdb1.json
new file mode 100644
index 000000000..de74fd872
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Filesystem_sdb1.json
@@ -0,0 +1,7 @@
+{
+ "MountPoints": {
+ "_type": "aay",
+ "_value": ["/mnt/data/supervisor/media/ext"]
+ },
+ "Size": 67108864
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Filesystem_zram1.json b/tests/fixtures/org_freedesktop_UDisks2_Filesystem_zram1.json
new file mode 100644
index 000000000..311b066d0
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Filesystem_zram1.json
@@ -0,0 +1 @@
+{ "MountPoints": { "_type": "aay", "_value": ["/var"] }, "Size": 0 }
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Manager-GetBlockDevices.json b/tests/fixtures/org_freedesktop_UDisks2_Manager-GetBlockDevices.json
new file mode 100644
index 000000000..9229c9b08
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Manager-GetBlockDevices.json
@@ -0,0 +1,12 @@
+[
+ "/org/freedesktop/UDisks2/block_devices/loop0",
+ "/org/freedesktop/UDisks2/block_devices/mmcblk1",
+ "/org/freedesktop/UDisks2/block_devices/mmcblk1p1",
+ "/org/freedesktop/UDisks2/block_devices/mmcblk1p2",
+ "/org/freedesktop/UDisks2/block_devices/mmcblk1p3",
+ "/org/freedesktop/UDisks2/block_devices/sda",
+ "/org/freedesktop/UDisks2/block_devices/sda1",
+ "/org/freedesktop/UDisks2/block_devices/sdb",
+ "/org/freedesktop/UDisks2/block_devices/sdb1",
+ "/org/freedesktop/UDisks2/block_devices/zram1"
+]
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Manager-ResolveDevice.json b/tests/fixtures/org_freedesktop_UDisks2_Manager-ResolveDevice.json
new file mode 100644
index 000000000..8cf64f720
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Manager-ResolveDevice.json
@@ -0,0 +1 @@
+["/org/freedesktop/UDisks2/block_devices/sda1"]
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Manager.json b/tests/fixtures/org_freedesktop_UDisks2_Manager.json
new file mode 100644
index 000000000..b2ab031ce
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Manager.json
@@ -0,0 +1,6 @@
+{
+ "Version": "2.9.2",
+ "SupportedFilesystems": ["ext4", "vfat", "ntfs", "exfat", "swap"],
+ "SupportedEncryptionTypes": ["luks1", "luks2"],
+ "DefaultEncryptionType": "luks1"
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_Manager.xml b/tests/fixtures/org_freedesktop_UDisks2_Manager.xml
new file mode 100644
index 000000000..421dda6f0
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_Manager.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/fixtures/org_freedesktop_UDisks2_PartitionTable_mmcblk1.json b/tests/fixtures/org_freedesktop_UDisks2_PartitionTable_mmcblk1.json
new file mode 100644
index 000000000..182637b75
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_PartitionTable_mmcblk1.json
@@ -0,0 +1,8 @@
+{
+ "Partitions": [
+ "/org/freedesktop/UDisks2/block_devices/mmcblk1p1",
+ "/org/freedesktop/UDisks2/block_devices/mmcblk1p2",
+ "/org/freedesktop/UDisks2/block_devices/mmcblk1p3"
+ ],
+ "Type": "dos"
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_PartitionTable_sda.json b/tests/fixtures/org_freedesktop_UDisks2_PartitionTable_sda.json
new file mode 100644
index 000000000..8caa09619
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_PartitionTable_sda.json
@@ -0,0 +1,4 @@
+{
+ "Partitions": ["/org/freedesktop/UDisks2/block_devices/sda1"],
+ "Type": "gpt"
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_PartitionTable_sdb.json b/tests/fixtures/org_freedesktop_UDisks2_PartitionTable_sdb.json
new file mode 100644
index 000000000..aa5617f2d
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_PartitionTable_sdb.json
@@ -0,0 +1,4 @@
+{
+ "Partitions": ["/org/freedesktop/UDisks2/block_devices/sdb1"],
+ "Type": "gpt"
+}
diff --git a/tests/fixtures/org_freedesktop_UDisks2_block_devices_loop0.xml b/tests/fixtures/org_freedesktop_UDisks2_block_devices_loop0.xml
new file mode 100644
index 000000000..b6b5d8a2d
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_block_devices_loop0.xml
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/org_freedesktop_UDisks2_block_devices_mmcblk1.xml b/tests/fixtures/org_freedesktop_UDisks2_block_devices_mmcblk1.xml
new file mode 100644
index 000000000..21db403e1
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_block_devices_mmcblk1.xml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/fixtures/org_freedesktop_UDisks2_block_devices_mmcblk1p1.xml b/tests/fixtures/org_freedesktop_UDisks2_block_devices_mmcblk1p1.xml
new file mode 100644
index 000000000..727d4648c
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_block_devices_mmcblk1p1.xml
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/fixtures/org_freedesktop_UDisks2_block_devices_mmcblk1p2.xml b/tests/fixtures/org_freedesktop_UDisks2_block_devices_mmcblk1p2.xml
new file mode 100644
index 000000000..4de0bfb72
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_block_devices_mmcblk1p2.xml
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/fixtures/org_freedesktop_UDisks2_block_devices_mmcblk1p3.xml b/tests/fixtures/org_freedesktop_UDisks2_block_devices_mmcblk1p3.xml
new file mode 100644
index 000000000..727d4648c
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_block_devices_mmcblk1p3.xml
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda-CreatePartition.json b/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda-CreatePartition.json
new file mode 100644
index 000000000..4ed34baef
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda-CreatePartition.json
@@ -0,0 +1 @@
+"/org/freedesktop/UDisks2/block_devices/sda2"
diff --git a/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda.xml b/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda.xml
new file mode 100644
index 000000000..21db403e1
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda.xml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda1-Check.json b/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda1-Check.json
new file mode 100644
index 000000000..27ba77dda
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda1-Check.json
@@ -0,0 +1 @@
+true
diff --git a/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda1-Mount.json b/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda1-Mount.json
new file mode 100644
index 000000000..95d2424e2
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda1-Mount.json
@@ -0,0 +1 @@
+"/run/media/dev/hassos_data"
diff --git a/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda1-Repair.json b/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda1-Repair.json
new file mode 100644
index 000000000..27ba77dda
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda1-Repair.json
@@ -0,0 +1 @@
+true
diff --git a/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda1.xml b/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda1.xml
new file mode 100644
index 000000000..1cd7c03b0
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_block_devices_sda1.xml
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/fixtures/org_freedesktop_UDisks2_block_devices_sdb.xml b/tests/fixtures/org_freedesktop_UDisks2_block_devices_sdb.xml
new file mode 100644
index 000000000..21db403e1
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_block_devices_sdb.xml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/fixtures/org_freedesktop_UDisks2_block_devices_sdb1.xml b/tests/fixtures/org_freedesktop_UDisks2_block_devices_sdb1.xml
new file mode 100644
index 000000000..727d4648c
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_block_devices_sdb1.xml
@@ -0,0 +1,166 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/fixtures/org_freedesktop_UDisks2_block_devices_zram1.xml b/tests/fixtures/org_freedesktop_UDisks2_block_devices_zram1.xml
new file mode 100644
index 000000000..2b9a3ba0a
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_block_devices_zram1.xml
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/fixtures/org_freedesktop_UDisks2_drives_BJTD4R_0x97cde291.xml b/tests/fixtures/org_freedesktop_UDisks2_drives_BJTD4R_0x97cde291.xml
new file mode 100644
index 000000000..95c47a414
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_drives_BJTD4R_0x97cde291.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/fixtures/org_freedesktop_UDisks2_drives_Generic_Flash_Disk_61BCDDB6.xml b/tests/fixtures/org_freedesktop_UDisks2_drives_Generic_Flash_Disk_61BCDDB6.xml
new file mode 100644
index 000000000..95c47a414
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_drives_Generic_Flash_Disk_61BCDDB6.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/fixtures/org_freedesktop_UDisks2_drives_SSK_SSK_Storage_DF56419883D56.xml b/tests/fixtures/org_freedesktop_UDisks2_drives_SSK_SSK_Storage_DF56419883D56.xml
new file mode 100644
index 000000000..95c47a414
--- /dev/null
+++ b/tests/fixtures/org_freedesktop_UDisks2_drives_SSK_SSK_Storage_DF56419883D56.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/host/test_manager.py b/tests/host/test_manager.py
index 168821598..679092ef8 100644
--- a/tests/host/test_manager.py
+++ b/tests/host/test_manager.py
@@ -1,15 +1,18 @@
"""Test host manager."""
from unittest.mock import PropertyMock, patch
+from awesomeversion import AwesomeVersion
import pytest
from supervisor.coresys import CoreSys
from supervisor.dbus.agent import OSAgent
from supervisor.dbus.const import MulticastProtocolEnabled
from supervisor.dbus.hostname import Hostname
+from supervisor.dbus.manager import DBusManager
from supervisor.dbus.resolved import Resolved
from supervisor.dbus.systemd import Systemd
from supervisor.dbus.timedate import TimeDate
+from supervisor.dbus.udisks2 import UDisks2
@pytest.fixture(name="coresys_dbus")
@@ -20,13 +23,15 @@ async def fixture_coresys_dbus(
timedate: TimeDate,
os_agent: OSAgent,
resolved: Resolved,
+ udisks2: UDisks2,
) -> CoreSys:
"""Coresys with all dbus interfaces mock loaded."""
- type(coresys.dbus).hostname = PropertyMock(return_value=hostname)
- type(coresys.dbus).systemd = PropertyMock(return_value=systemd)
- type(coresys.dbus).timedate = PropertyMock(return_value=timedate)
- type(coresys.dbus).agent = PropertyMock(return_value=os_agent)
- type(coresys.dbus).resolved = PropertyMock(return_value=resolved)
+ DBusManager.hostname = PropertyMock(return_value=hostname)
+ DBusManager.systemd = PropertyMock(return_value=systemd)
+ DBusManager.timedate = PropertyMock(return_value=timedate)
+ DBusManager.agent = PropertyMock(return_value=os_agent)
+ DBusManager.resolved = PropertyMock(return_value=resolved)
+ DBusManager.udisks2 = PropertyMock(return_value=udisks2)
yield coresys
@@ -47,6 +52,7 @@ async def test_load(coresys_dbus: CoreSys, dbus: list[str]):
assert coresys.dbus.resolved.multicast_dns == MulticastProtocolEnabled.RESOLVE
assert coresys.dbus.agent.apparmor.version == "2.13.2"
assert len(coresys.host.logs.default_identifiers) > 0
+ assert coresys.dbus.udisks2.version == AwesomeVersion("2.9.2")
sound_update.assert_called_once()