From 8c5ed14d377619f8ddbb8ed834dd346f9df2b5b5 Mon Sep 17 00:00:00 2001
From: David Lehman <dlehman@redhat.com>
Date: Mon, 12 Aug 2013 16:32:51 -0500
Subject: [PATCH 6/9] Add support for listening to uevents on block devices.
 (#870373)

TODO:
 - locking
 - conflict resolution, race conditions, &c
---
 blivet/__init__.py   |  75 ++++++++++++++++++++++++
 blivet/devicetree.py | 158 +++++++++++++++++++++++++++++++++++++++++++++++++++
 scripts/udev.py      |  72 +++++++++++++++++++++++
 3 files changed, 305 insertions(+)
 create mode 100644 scripts/udev.py

diff --git a/blivet/__init__.py b/blivet/__init__.py
index 8f6e82a..15759b3 100644
--- a/blivet/__init__.py
+++ b/blivet/__init__.py
@@ -60,6 +60,7 @@ except ImportError:
     nss = None
 
 import parted
+import pyudev
 
 from pykickstart.constants import *
 
@@ -77,6 +78,7 @@ from devicelibs.dm import name_from_dm_node
 from devicelibs.crypto import generateBackupPassphrase
 from devicelibs.edd import get_edd_dict
 from udev import udev_trigger
+from udev import global_udev
 import iscsi
 import fcoe
 import zfcp
@@ -306,6 +308,59 @@ class Blivet(object):
         self.roots = []
         self.services = set()
 
+        self.ueventNotifyCB = None
+        self._pyudev_observer = None
+
+    def ueventCB(self, action, info):
+        notify = False
+        device = None
+        if action == "add":
+            device = self.devicetree.deviceAddedCB(info)
+            notify = True
+            # TODO: update self.roots if in installer_mode
+        elif action == "remove":
+            device = self.devicetree.deviceRemovedCB(info)
+            notify = True
+            # TODO: update self.roots
+        elif action == "change":
+            device = self.devicetree.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(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 doIt(self):
         self.devicetree.processActions()
         if not flags.installer_mode:
@@ -1965,6 +2020,26 @@ class Blivet(object):
         log.debug("finished Blivet copy")
         return new
 
+    def __deepcopy__(self, memo):
+        """ Create a deep copy of a Blivet instance.
+
+            We can't do copy.deepcopy on parted objects, which is okay.
+            For these parted objects, we just do a shallow copy.
+        """
+        new = self.__class__.__new__(self.__class__)
+        memo[id(self)] = new
+        dont_copy_attrs = []
+        shallow_copy_attrs = ['_pyudev_observer']
+        for (attr, value) in self.__dict__.items():
+            if attr in dont_copy_attrs:
+                setattr(new, attr, value)
+            elif attr in shallow_copy_attrs:
+                setattr(new, attr, copy.copy(value))
+            else:
+                setattr(new, attr, copy.deepcopy(value, memo))
+
+        return new
+
     def getActiveMounts(self):
         """ Reflect active mounts in the appropriate devices' formats. """
         log.info("collecting information about active mounts")
diff --git a/blivet/devicetree.py b/blivet/devicetree.py
index 277df9a..119f299 100644
--- a/blivet/devicetree.py
+++ b/blivet/devicetree.py
@@ -255,6 +255,135 @@ class DeviceTree(object):
         for disk in (d for d in self.devices if d.partitioned):
             disk.originalFormat = copy.deepcopy(disk.format)
 
+    def _checkForMountPoint(self, device):
+        if not device:
+            return
+
+        # This is bananas, yo! Adding a partition to a disk generates a remove
+        # event for every partition on the disk, followed by add events for all
+        # of them including the newly added one. So when we find formatting for
+        # a device we check the completed actions to see if we just created this
+        # device and, if so, we crib the mountpoint from the old StorageDevice's
+        # format.
+        for action in reversed(self._completed_actions):
+            if action.isCreate and action.isDevice and \
+               action.device.sysfsPath == device.sysfsPath and \
+               action.device.format.type == device.format.type:
+                if getattr(action.device.format, "mountpoint"):
+                    device.format.mountpoint = action.device.format.mountpoint
+
+    def deviceAddedCB(self, info):
+        """ Handle an "add" uevent on a block device.
+
+            This can be a new device or one we already know about becoming
+            active.
+        """
+        sysfs_path = udev_device_get_sysfs_path(info)
+        log.debug("device added: %s" % sysfs_path)
+        if info.subsystem != "block":
+            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)
+        self._checkForMountPoint(device)
+        self._applyDiskFilters()
+        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
+
+        device = self.getDeviceBySysfsPath(sysfs_path, hidden=True)
+        if not device:
+            log.error("failed to look up device: %s" % sysfs_path)
+            return
+
+        if not device.format.exists:
+            log.warning("received change uevent on device with reformat scheduled")
+            return
+
+        with device.lock:
+            uuid = udev_device_get_uuid(new_info)
+            label = udev_device_get_label(new_info)
+
+            uuid_changed = (device.format.uuid != uuid)
+            label_changed = (getattr(device.format, "label", None) != label)
+            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)
+
+            if type_changed or partitioned:
+                # XXX this device might be hidden
+                self.handleUdevDeviceFormat(new_info, device)
+
+                # this is just for partitions since the formatting may not be
+                # detected when the add event occurs, necessitating one of the
+                # many subsequent change events
+                self._checkForMountPoint(device)
+            else:
+                if device.format.type == "btrfs":
+                    device.format.uuid = new_info.get("ID_FS_UUID_SUB")
+                    device.format.volUUID = uuid
+                else:
+                    device.format.uuid = uuid
+
+                if hasattr(device.format, "label"):
+                    device.format.label = label
+
+                # TODO: update mountopts?
+
+                # FIXME: update all of these after creating vg/md
+                # not in udev:
+                #
+                # lvmpv: vgName, vgUuid, peStart
+                # mdmember: mdUuid
+
+        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.
+        """
+        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):
         """ Add a device to the tree.
 
@@ -270,6 +399,11 @@ class DeviceTree(object):
             if parent not in self._devices:
                 raise DeviceTreeError("parent device not in tree")
 
+        if newdev.exists:
+            conflict = self.getDeviceByName(newdev.name)
+            if conflict and not conflict.exists:
+                raise DeviceTreeError("device naming conflict")
+
         self._devices.append(newdev)
 
         # don't include "req%d" partition names
@@ -1010,6 +1144,9 @@ class DeviceTree(object):
                 # make sure any device we found is an md device
                 device = None
 
+        if device and not device.exists:
+            raise DeviceTreeError("device naming conflict")
+
         #
         # The first step is to either look up or create the device
         #
@@ -1730,6 +1867,27 @@ class DeviceTree(object):
             for pv in vg.pvs:
                 devicelibs.lvm.lvm_cc_addFilterRejectRegexp(pv.name)
 
+    def cancelDeviceActions(self, device, full=True):
+        """ Cancel all actions the depend on or operate on a given device.
+
+                full -- call self.cancelAction instead of action.cancel
+        """
+        for action in reversed(self._actions):
+            if not action.device.dependsOn(device) and action.device != device:
+                continue
+
+            log.debug("cancelling action: %s" % action)
+            try:
+                if full:
+                    self.cancelAction(action)
+                else:
+                    action.cancel()
+            except Exception as e:
+                log.error("failed to cancel action %s (for %s): %s"
+                            % (action, device.name, e))
+            finally:
+                self._actions.remove(action)
+
     def hide(self, device):
         if device in self._hidden:
             return
diff --git a/scripts/udev.py b/scripts/udev.py
new file mode 100644
index 0000000..5ca55c3
--- /dev/null
+++ b/scripts/udev.py
@@ -0,0 +1,72 @@
+import logging
+import sys
+import pyudev
+import time
+
+blivet_log = logging.getLogger("blivet")
+
+def set_up_logging():
+    """ Configure the blivet logger to use /tmp/blivet.log as its log file. """
+    blivet_log.setLevel(logging.DEBUG)
+    program_log = logging.getLogger("program")
+    program_log.setLevel(logging.DEBUG)
+    handler = logging.FileHandler("/tmp/blivet.log")
+    handler.setLevel(logging.DEBUG)
+    formatter = logging.Formatter("%(asctime)s,%(msecs)03d %(levelname)s %(name)s: %(message)s")
+    handler.setFormatter(formatter)
+    blivet_log.addHandler(handler)
+    program_log.addHandler(handler)
+
+
+# doing this before importing blivet gets the logging from format class
+# registrations and other stuff triggered by the import
+set_up_logging()
+
+import blivet
+
+blivet_log.info(sys.argv[0])
+
+#context = pyudev.Context()
+
+#def log_event(action, device):
+#    global context
+#    newdev = pyudev.Device.from_sys_path(context, device.sys_path)
+#    print "%s %s (%s/%s)" % (action, device.sys_name, device.is_initialized,
+#                             newdev.is_initialized)
+def uevent_cb(action, path):
+    global b
+    if action == "add":
+        print "ADD", path
+    elif action == "remove":
+        print "REMOVE", path
+    elif action == "change":
+        print "CHANGE", path
+        return
+    else:
+        print action.upper(), path
+        return
+
+    print [d.name for d in sorted(b.devices, key=lambda d: len(d.ancestors))]
+
+b = blivet.Blivet()   # create an instance of Blivet
+b.reset()             # detect system storage configuration
+
+print [d.name for d in sorted(b.devices, key=lambda d: len(d.ancestors))]
+b.registerUeventNotifyCB(uevent_cb)
+b.enableUeventMonitoring()
+
+#for device in sorted(b.devices, key=lambda d: len(d.ancestors)):
+#    print device    # this is a blivet.devices.StorageDevice instance
+
+#monitor = pyudev.Monitor.from_netlink(context)
+#monitor.filter_by("block")
+#observer = pyudev.MonitorObserver(monitor, uevent_cb)
+#observer.start()
+
+try:
+    while True:
+        time.sleep(1)
+except (SystemExit, KeyboardInterrupt):
+    #observer.stop()
+    pass
+
-- 
1.8.1.4

