From efa8e08eb7442015ba1ecc2d489a0617d34fb204 Mon Sep 17 00:00:00 2001
From: David Lehman <dlehman@redhat.com>
Date: Wed, 5 Nov 2014 17:43:26 -0600
Subject: [PATCH 16/18] WIP of adding support for listening to uevents.

---
 blivet/devicetree.py | 277 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 277 insertions(+)

diff --git a/blivet/devicetree.py b/blivet/devicetree.py
index e2f14ab..e7e9065 100644
--- a/blivet/devicetree.py
+++ b/blivet/devicetree.py
@@ -27,6 +27,7 @@ import shutil
 import pprint
 import copy
 from six import add_metaclass
+import pyudev
 
 from .errors import CryptoError, DeviceError, DeviceTreeError, DiskLabelCommitError, FSError, InvalidDiskLabelError, LUKSError, MDRaidError, StorageError, UnusableConfigurationError
 from .devices import BTRFSDevice, BTRFSSubVolumeDevice, BTRFSVolumeDevice, BTRFSSnapShotDevice
@@ -152,6 +153,9 @@ class DeviceTree(object):
 
         self._cleanup = False
 
+        self.ueventNotifyCB = None
+        self._pyudev_observer = None
+
     def setDiskImages(self, images):
         """ Set the disk images and reflect them in exclusiveDisks.
 
@@ -387,6 +391,271 @@ class DeviceTree(object):
 
         self._postProcessActions()
 
+    ##
+    ## uevent handling
+    ##
+    def ueventCB(self, action, info):
+        print action, udev.device_get_name(info)
+        notify = False
+        device = None
+        if action == "add":
+            device = self.deviceAddedCB(info)
+            notify = True
+            # TODO: update self.roots if in installer_mode
+        elif action == "remove":
+            device = self.deviceRemovedCB(info)
+            notify = True
+            # TODO: update self.roots
+        elif action == "change":
+            device = self.deviceChangedCB(info)
+            notify = True
+        else:
+            log.info("unknown uevent action: %s (%s)", action, info.sys_name)
+
+        if notify and self.ueventNotifyCB:
+            self.ueventNotifyCB(action, device)
+
+    def enableUeventMonitoring(self):
+        """ Enable monitoring and handling of block device uevents. """
+        monitor = pyudev.Monitor.from_netlink(udev.global_udev)
+        monitor.filter_by("block")
+        self._pyudev_observer = pyudev.MonitorObserver(monitor, self.ueventCB)
+        self._pyudev_observer.start()
+
+    def disableUeventMonitoring(self):
+        """ Disable monitoring and handling of block device uevents. """
+        if self._pyudev_observer:
+            self._pyudev_observer.stop()
+            self._pyudev_observer = None
+
+    def registerUeventNotifyCB(self, cb):
+        """ Register a uevent notification callback.
+
+            The callback will be run after processing the uevent, and will be
+            passed the action string and the StorageDevice instance that was
+            operated on.
+
+            Note that in the event of a remove action the returned device will
+            no longer be in the devicetree. Also, it is possible that the the
+            device will be hidden in the event of an add or remove action,
+            depending on the disk filtering settings of the devicetree.
+        """
+        if not callable(cb) or cb.func_code.co_argcount < 2:
+            raise ValueError("callback function must accept at least two args")
+
+        self.ueventNotifyCB = cb
+
+    def deviceAddedCB(self, info, force=False):
+        """ Handle an "add" uevent on a block device.
+
+            The device could be newly created or newly activated.
+        """
+        sysfs_path = udev.device_get_sysfs_path(info)
+        log.debug("device added: %s", sysfs_path)
+        if info.subsystem != "block":
+            return
+
+        # add events are usually not meaningful for dm and md devices, but the
+        # change event handler calls this method when a change event for such a
+        # device appears to signal an addition
+        if not force and (udev.device_is_md(info) or
+                          (udev.device_is_dm(info) or
+                           re.match(r'/dev/dm-\d+$', info['DEVNAME']))):
+            log.debug("ignoring add event for %s", sysfs_path)
+            return
+
+        new_info = udev.get_device(sysfs_path)
+        if not new_info or not new_info.is_initialized:
+            log.debug("new device not initialized -- not processing it")
+            return
+
+        self.addUdevDevice(new_info)
+        device = self.getDeviceBySysfsPath(udev.device_get_sysfs_path(new_info),
+                                           hidden=True)
+        if device:
+            self._hideIgnoredDisks()
+
+        return device
+
+    def deviceChangedCB(self, info):
+        """ Handle a "changed" uevent on a block device. """
+        log.debug("device changed: %s", udev.device_get_sysfs_path(info))
+        if info.subsystem != "block":
+            return
+
+        sysfs_path = udev.device_get_sysfs_path(info)
+        new_info = udev.get_device(sysfs_path)
+        if not new_info:
+            log.warning("failed to look up changed info")
+            return
+
+        # Would a lookup by UUID be more robust here?
+        device = self.getDeviceBySysfsPath(sysfs_path, hidden=True)
+        if (not device and
+            (udev.device_is_md(new_info) or udev.device_is_dm(new_info))):
+            return self.deviceAddedCB(info, force=True)
+
+        if not device.format.exists:
+            #
+            # A policy must be decided upon for external events that conflict
+            # with scheduled actions.
+            #
+            log.warning("ignoring change uevent on device with reformat scheduled")
+            return
+
+        with device.lock:
+            #
+            # Check for changes to the device itself.
+            #
+
+            # rename
+            name = new_info.get("DM_LV_NAME", udev.device_get_name(new_info))
+            if getattr(device, "lvname", "name") != name:
+                device.name = name
+
+            # resize
+            device.updateSize()
+
+            #
+            # Handle changes to the data it contains.
+            #
+            uuid = udev.device_get_uuid(new_info)
+            label = udev.device_get_label(new_info)
+
+            if device.format.type != "btrfs":
+                uuid_changed = (device.format.uuid != uuid)
+            else:
+                uuid_changed = (device.format.volUUID != uuid)
+            partitioned = (device.partitionable and
+                           new_info.get("ID_PART_TABLE_TYPE"))
+            new_type = getFormat(udev.device_get_format(new_info)).type
+            type_changed = (new_type != device.format.type and
+                            not
+                            (device.format.type == "disklabel" and partitioned))
+
+            if type_changed or uuid_changed:
+                # this device might be hidden
+                device.format = None
+                self.handleUdevDeviceFormat(new_info, device)
+            else:
+                # changes to existing metadata
+                if device.format.type == "btrfs":
+                    btrfs_changed = (device.format.volUUID != uuid)
+                    try:
+                        btrfs_vol = self.getChildren(device)[0]
+                    except IndexError:
+                        btrfs_vol = None
+
+                    if btrfs_changed:
+                        if btrfs_vol:
+                            self.recursiveRemove(btrfs_vol)
+
+                        device.format = None
+                        self.handleUdevDeviceFormat(new_info, device)
+                    else:
+                        device.format.uuid = new_info.get("ID_FS_UUID_SUB")
+                        device.format.volUUID = uuid
+
+                        # TODO: check for changes to subvol list
+                elif device.format.type == "lvmpv":
+                    self.dropLVMCache() # too heavy-handed?
+
+                    # TODO: vg name
+                    pv_info = self.pvInfo.get(device.path)
+
+                    new_vg_uuid = udev.device_get_vg_uuid(pv_info)
+                    vg_changed = (device.format.vgUuid != new_vg_uuid)
+                    new_vg_name = udev.device_get_vg_name(pv_info)
+                    try:
+                        vg_device = self.getChildren(device)[0]
+                    except IndexError:
+                        vg_device = None
+
+                    if vg_changed:
+                        if vg_device:
+                            self.recursiveRemove(vg_device)
+
+                        device.format = None
+                        self.handleUdevDeviceFormat(new_info, device)
+                    else:
+                        device.format.uuid = uuid
+                        device.format.vgUuid = new_vg_uuid
+                        device.format.vgName = new_vg_name
+                        device.format.peStart = udev.device_get_pv_pe_start(pv_info)
+                        if vg_device:
+                            vg_device.name = new_vg_name
+                            vg_device.uuid = new_vg_uuid
+                            self.handleVgLvs(vg_device)
+
+                    # the rest should be handled by events for the LVs
+                elif device.format.type == "mdmember":
+                    # update md info and reconcile it with our devices
+                    new_md_uuid = udev.device_get_uuid(new_info)
+                    md_changed = (device.format.mdUuid != new_md_uuid)
+                    try:
+                        md_device = self.getChildren(device)[0]
+                    except IndexError:
+                        md_device = None
+
+                    if md_changed:
+                        if md_device:
+                            self.recursiveRemove(md_device)
+
+                        device.format = None
+                        self.handleUdevDeviceFormat(new_info, device)
+
+                    # the rest should be handled by events for the md itself
+                    # TODO: raid level, members, spares, name
+                elif partitioned:
+                    # update the partition list
+                    device.format.updatePartedDisk()
+                    old_partitions = self.getChildren(device)
+                    for ppart in device.format.partitions:
+                        if devicePathToName(ppart.path)
+                else:
+                    # update filesystem label, &c in case they were changed
+                    device.format.uuid = uuid
+
+                    if hasattr(device.format, "label"):
+                        device.format.label = label
+
+                # TODO: md/lvm rename
+
+                # TODO: update mountopts?
+
+        return device
+
+    def deviceRemovedCB(self, info):
+        """ Handle a "remove" uevent on a block device.
+
+            This is generally going to be interpreted as a deactivation as
+            opposed to a removal since there is no consistent way to determine
+            which it is from the information given.
+
+            It seems sensible to interpret remove events as deactivations and
+            handle destruction via change events on parent devices.
+        """
+        log.debug("device removed: %s", udev.device_get_sysfs_path(info))
+        if info.subsystem != "block":
+            return
+
+        removed = None
+        for device in (d for d in self._devices if d.exists):
+            if device.sysfsPath == udev.device_get_sysfs_path(info):
+                # prefer devices with fewer ancestors since btrfs can have
+                # multiple "devices" with the same sys path, in which case we
+                # want to return the "real" device, eg: partition
+                if (removed is None or
+                    len(device.ancestors) < len(removed.ancestors)):
+                    removed = device
+
+                # the device is being removed, so any mounted filesystem on it
+                # must have been unmounted
+                if getattr(device.format, "_mountpoint", None):
+                    device.format._mountpoint = None
+
+        return removed
+
     def _addDevice(self, newdev, new=True):
         """ Add a device to the tree.
 
@@ -1334,6 +1603,11 @@ class DeviceTree(object):
 
         self.names.extend(n for n in lv_info.keys() if n not in self.names)
 
+        for lv_device in vg_device.lvs[:]:
+            if lv_device.name not in lv_info:
+                log.info("lv %s seems to have been removed", lv_device.name)
+                self.recursiveRemove(lv_device)
+
         if not vg_device.complete:
             log.warning("Skipping LVs for incomplete VG %s", vg_name)
             return
@@ -2664,6 +2938,9 @@ class DeviceTree(object):
                 device.format._mountpoint = mountpoint  # active mountpoint
                 device.format.mountopts = options
 
+    def __deepcopy__(self, memo):
+        return util.variable_copy(self, memo, shallow=('_pyudev_observer'))
+
     def __str__(self):
         done = []
         def get_depth(device):
-- 
1.9.3

