From 4c2d72964695ddf4c21b3d6901ae87e7bc7ac222 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 15 Feb 2023 02:17:29 -0500 Subject: [PATCH] Add udisks2 dbus support (#3848) * Add udisks2 dbus support * assert mountpoints * Comment * Add reference links * docstring * fix type * fix type * add typing extensions as import * isort * additional changes * Simplify classes and conversions, fix bugs * More simplification * Fix imports * fix pip * Add additional properties and fix requirements * fix tests maybe * Handle optionality of certain configuration details * black * connect to devices before returning them * Refactor for latest dbus work * Not .items * fix mountpoints logic * use variants * Use variants for options too * isort * Switch to dbus fast * Move import to parent * Add some fixture data * Add another fixture and reduce the block devices list * Implement changes discussed with mike * Add property fixtures * update object path * Fix get_block_devices call * Tests and refactor to minimize dbus reconnects * Call super init in DBusInterfaceProxy * Fix permissions on introspection files --------- Co-authored-by: Mike Degatano --- requirements.txt | 1 + requirements_tests.txt | 1 + supervisor/api/const.py | 25 +- supervisor/api/hardware.py | 71 ++++- supervisor/dbus/agent/__init__.py | 2 +- supervisor/dbus/agent/apparmor.py | 5 - supervisor/dbus/agent/boards/__init__.py | 4 +- supervisor/dbus/agent/boards/interface.py | 5 +- supervisor/dbus/agent/datadisk.py | 5 - supervisor/dbus/const.py | 35 +++ supervisor/dbus/hostname.py | 5 - supervisor/dbus/interface.py | 25 +- supervisor/dbus/manager.py | 8 + supervisor/dbus/network/__init__.py | 4 +- supervisor/dbus/network/accesspoint.py | 5 +- supervisor/dbus/network/connection.py | 3 +- supervisor/dbus/network/dns.py | 3 +- supervisor/dbus/network/interface.py | 3 +- supervisor/dbus/network/ip_configuration.py | 4 +- supervisor/dbus/network/wireless.py | 3 +- supervisor/dbus/rauc.py | 4 +- supervisor/dbus/resolved.py | 5 - supervisor/dbus/systemd.py | 5 - supervisor/dbus/timedate.py | 5 - supervisor/dbus/udisks2/__init__.py | 146 +++++++++ supervisor/dbus/udisks2/block.py | 206 ++++++++++++ supervisor/dbus/udisks2/const.py | 37 +++ supervisor/dbus/udisks2/data.py | 294 ++++++++++++++++++ supervisor/dbus/udisks2/drive.py | 127 ++++++++ supervisor/dbus/udisks2/filesystem.py | 78 +++++ supervisor/dbus/udisks2/partition_table.py | 63 ++++ supervisor/host/const.py | 5 +- supervisor/host/manager.py | 6 + supervisor/utils/dbus.py | 10 + tests/api/test_hardware.py | 29 +- tests/api/test_host.py | 8 + tests/conftest.py | 41 ++- tests/dbus/test_interface.py | 40 ++- tests/dbus/udisks2/__init__.py | 1 + tests/dbus/udisks2/test_block.py | 70 +++++ tests/dbus/udisks2/test_drive.py | 44 +++ tests/dbus/udisks2/test_filesystem.py | 84 +++++ tests/dbus/udisks2/test_manager.py | 111 +++++++ tests/dbus/udisks2/test_partition_table.py | 79 +++++ .../org_freedesktop_UDisks2_Block_loop0.json | 27 ++ ...org_freedesktop_UDisks2_Block_mmcblk1.json | 33 ++ ...g_freedesktop_UDisks2_Block_mmcblk1p1.json | 37 +++ ...g_freedesktop_UDisks2_Block_mmcblk1p2.json | 34 ++ ...g_freedesktop_UDisks2_Block_mmcblk1p3.json | 36 +++ .../org_freedesktop_UDisks2_Block_sda.json | 39 +++ .../org_freedesktop_UDisks2_Block_sda1.json | 37 +++ .../org_freedesktop_UDisks2_Block_sdb.json | 33 ++ .../org_freedesktop_UDisks2_Block_sdb1.json | 34 ++ .../org_freedesktop_UDisks2_Block_zram1.json | 27 ++ ...sktop_UDisks2_Drive_BJTD4R_0x97cde291.json | 31 ++ ...ks2_Drive_Generic_Flash_Disk_61BCDDB6.json | 31 ++ ...2_Drive_SSK_SSK_Storage_DF56419883D56.json | 31 ++ ...edesktop_UDisks2_Filesystem_mmcblk1p1.json | 1 + ...edesktop_UDisks2_Filesystem_mmcblk1p3.json | 22 ++ ...g_freedesktop_UDisks2_Filesystem_sda1.json | 1 + ...g_freedesktop_UDisks2_Filesystem_sdb1.json | 7 + ..._freedesktop_UDisks2_Filesystem_zram1.json | 1 + ...sktop_UDisks2_Manager-GetBlockDevices.json | 12 + ...desktop_UDisks2_Manager-ResolveDevice.json | 1 + .../org_freedesktop_UDisks2_Manager.json | 6 + .../org_freedesktop_UDisks2_Manager.xml | 89 ++++++ ...esktop_UDisks2_PartitionTable_mmcblk1.json | 8 + ...reedesktop_UDisks2_PartitionTable_sda.json | 4 + ...reedesktop_UDisks2_PartitionTable_sdb.json | 4 + ...reedesktop_UDisks2_block_devices_loop0.xml | 117 +++++++ ...edesktop_UDisks2_block_devices_mmcblk1.xml | 127 ++++++++ ...esktop_UDisks2_block_devices_mmcblk1p1.xml | 166 ++++++++++ ...esktop_UDisks2_block_devices_mmcblk1p2.xml | 136 ++++++++ ...esktop_UDisks2_block_devices_mmcblk1p3.xml | 166 ++++++++++ ...ks2_block_devices_sda-CreatePartition.json | 1 + ..._freedesktop_UDisks2_block_devices_sda.xml | 127 ++++++++ ...ktop_UDisks2_block_devices_sda1-Check.json | 1 + ...ktop_UDisks2_block_devices_sda1-Mount.json | 1 + ...top_UDisks2_block_devices_sda1-Repair.json | 1 + ...freedesktop_UDisks2_block_devices_sda1.xml | 166 ++++++++++ ..._freedesktop_UDisks2_block_devices_sdb.xml | 127 ++++++++ ...freedesktop_UDisks2_block_devices_sdb1.xml | 166 ++++++++++ ...reedesktop_UDisks2_block_devices_zram1.xml | 135 ++++++++ ...sktop_UDisks2_drives_BJTD4R_0x97cde291.xml | 78 +++++ ...ks2_drives_Generic_Flash_Disk_61BCDDB6.xml | 78 +++++ ...2_drives_SSK_SSK_Storage_DF56419883D56.xml | 78 +++++ tests/host/test_manager.py | 16 +- 87 files changed, 3914 insertions(+), 74 deletions(-) create mode 100644 supervisor/dbus/udisks2/__init__.py create mode 100644 supervisor/dbus/udisks2/block.py create mode 100644 supervisor/dbus/udisks2/const.py create mode 100644 supervisor/dbus/udisks2/data.py create mode 100644 supervisor/dbus/udisks2/drive.py create mode 100644 supervisor/dbus/udisks2/filesystem.py create mode 100644 supervisor/dbus/udisks2/partition_table.py create mode 100644 tests/dbus/udisks2/__init__.py create mode 100644 tests/dbus/udisks2/test_block.py create mode 100644 tests/dbus/udisks2/test_drive.py create mode 100644 tests/dbus/udisks2/test_filesystem.py create mode 100644 tests/dbus/udisks2/test_manager.py create mode 100644 tests/dbus/udisks2/test_partition_table.py create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Block_loop0.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Block_mmcblk1.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Block_mmcblk1p1.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Block_mmcblk1p2.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Block_mmcblk1p3.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Block_sda.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Block_sda1.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Block_sdb.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Block_sdb1.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Block_zram1.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Drive_BJTD4R_0x97cde291.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Drive_Generic_Flash_Disk_61BCDDB6.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Drive_SSK_SSK_Storage_DF56419883D56.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Filesystem_mmcblk1p1.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Filesystem_mmcblk1p3.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Filesystem_sda1.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Filesystem_sdb1.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Filesystem_zram1.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Manager-GetBlockDevices.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Manager-ResolveDevice.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Manager.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_Manager.xml create mode 100644 tests/fixtures/org_freedesktop_UDisks2_PartitionTable_mmcblk1.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_PartitionTable_sda.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_PartitionTable_sdb.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_block_devices_loop0.xml create mode 100644 tests/fixtures/org_freedesktop_UDisks2_block_devices_mmcblk1.xml create mode 100644 tests/fixtures/org_freedesktop_UDisks2_block_devices_mmcblk1p1.xml create mode 100644 tests/fixtures/org_freedesktop_UDisks2_block_devices_mmcblk1p2.xml create mode 100644 tests/fixtures/org_freedesktop_UDisks2_block_devices_mmcblk1p3.xml create mode 100644 tests/fixtures/org_freedesktop_UDisks2_block_devices_sda-CreatePartition.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_block_devices_sda.xml create mode 100644 tests/fixtures/org_freedesktop_UDisks2_block_devices_sda1-Check.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_block_devices_sda1-Mount.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_block_devices_sda1-Repair.json create mode 100644 tests/fixtures/org_freedesktop_UDisks2_block_devices_sda1.xml create mode 100644 tests/fixtures/org_freedesktop_UDisks2_block_devices_sdb.xml create mode 100644 tests/fixtures/org_freedesktop_UDisks2_block_devices_sdb1.xml create mode 100644 tests/fixtures/org_freedesktop_UDisks2_block_devices_zram1.xml create mode 100644 tests/fixtures/org_freedesktop_UDisks2_drives_BJTD4R_0x97cde291.xml create mode 100644 tests/fixtures/org_freedesktop_UDisks2_drives_Generic_Flash_Disk_61BCDDB6.xml create mode 100644 tests/fixtures/org_freedesktop_UDisks2_drives_SSK_SSK_Storage_DF56419883D56.xml 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()