From 8de097b7cbdffb36ed9d10b93ce14da226a69c39 Mon Sep 17 00:00:00 2001
From: David Lehman <dlehman@redhat.com>
Date: Mon, 2 Feb 2015 18:41:52 -0600
Subject: [PATCH 23/26] Create an event handling class hierarchy.

---
 blivet/devicetree.py |  88 +++++++------------
 blivet/errors.py     |   9 ++
 blivet/event.py      | 240 +++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 278 insertions(+), 59 deletions(-)
 create mode 100644 blivet/event.py

diff --git a/blivet/devicetree.py b/blivet/devicetree.py
index 268cf69..9650621 100644
--- a/blivet/devicetree.py
+++ b/blivet/devicetree.py
@@ -27,9 +27,9 @@ import shutil
 import pprint
 import copy
 from six import add_metaclass
-import pyudev
+import time
 
-from .errors import CryptoError, DeviceError, DeviceTreeError, DiskLabelCommitError, FSError, InvalidDiskLabelError, LUKSError, MDRaidError, StorageError, UnusableConfigurationError
+from .errors import CryptoError, DeviceError, DeviceTreeError, DiskLabelCommitError, EventQueueEmptyError, FSError, InvalidDiskLabelError, LUKSError, MDRaidError, StorageError, UnusableConfigurationError
 from .devices import BTRFSDevice, BTRFSSubVolumeDevice, BTRFSVolumeDevice, BTRFSSnapShotDevice
 from .devices import DASDDevice, DMDevice, DMLinearDevice, DMRaidArrayDevice, DiskDevice
 from .devices import FcoeDiskDevice, FileDevice, LoopDevice, LUKSDevice
@@ -58,6 +58,7 @@ from .storage_log import log_exception_info, log_method_call, log_method_return
 from .i18n import _
 from .size import Size
 from .threads import blivet_lock, SynchronizedMeta
+from .event import UdevEventHandler
 
 import logging
 log = logging.getLogger("blivet")
@@ -153,8 +154,7 @@ class DeviceTree(object):
 
         self._cleanup = False
 
-        self.ueventNotifyCB = None
-        self._pyudev_observer = None
+        self.eventHandler = UdevEventHandler(handler_cb=self.handleUevent)
 
     def setDiskImages(self, images):
         """ Set the disk images and reflect them in exclusiveDisks.
@@ -358,7 +358,7 @@ class DeviceTree(object):
         :type callbacks: :class:`~.callbacks.DoItCallbacks`
 
         """
-
+        self.eventHandler.enable()
         self._preProcessActions()
 
         for action in self._actions[:]:
@@ -389,61 +389,36 @@ class DeviceTree(object):
 
                 self._completed_actions.append(self._actions.pop(0))
 
+            udev.settle()
+
+            # give up some cycles to handle events
+            time.sleep(0.2)
+
         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
+    def handleUevent(self):
+        """ Handle the next uevent in the queue. """
+        log_method_call(self)
+        try:
+            event = self.eventHandler.next_event()
+        except EventQueueEmptyError:
+            log.debug("uevent queue is empty")
+            return
+
+        log.debug("event: %s", event)
+        if event.action == "add":
+            self.deviceAddedCB(event.info)
             # TODO: update self.roots if in installer_mode
-        elif action == "remove":
-            device = self.deviceRemovedCB(info)
-            notify = True
+        elif event.action == "remove":
+            self.deviceRemovedCB(event.info)
             # TODO: update self.roots
-        elif action == "change":
-            device = self.deviceChangedCB(info)
-            notify = True
+        elif event.action == "change":
+            self.deviceChangedCB(event.info)
         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
+            log.info("unknown event: %s", event)
 
     def deviceAddedCB(self, info, force=False):
         """ Handle an "add" uevent on a block device.
@@ -475,8 +450,6 @@ class DeviceTree(object):
         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))
@@ -677,8 +650,6 @@ class DeviceTree(object):
 
             # TODO: update mountopts?
 
-        return device
-
     def deviceRemovedCB(self, info):
         """ Handle a "remove" uevent on a block device.
 
@@ -2328,6 +2299,7 @@ class DeviceTree(object):
                                      sysfsPath=loop_sysfs,
                                      exists=True)
                 loopdev.setup()
+                loopdev.updateSize()
                 log.debug("%s", loopdev)
                 dmdev = DMLinearDevice(name,
                                        dmUuid="ANACONDA-%s" % name,
@@ -2335,6 +2307,7 @@ class DeviceTree(object):
                                        exists=True)
                 dmdev.setup()
                 dmdev.updateSysfsPath()
+                dmdev.updateSize()
                 log.debug("%s", dmdev)
             except (ValueError, DeviceError) as e:
                 log.error("failed to set up disk image: %s", e)
@@ -2991,9 +2964,6 @@ 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):
diff --git a/blivet/errors.py b/blivet/errors.py
index 34d6fd1..2e56c73 100644
--- a/blivet/errors.py
+++ b/blivet/errors.py
@@ -192,3 +192,12 @@ class UnknownSourceDeviceError(StorageError):
 # factories
 class DeviceFactoryError(StorageError):
     pass
+
+class EventHandlerError(StorageError):
+    pass
+
+class EventQueueEmptyError(StorageError):
+    pass
+
+class EventParamError(StorageError):
+    pass
diff --git a/blivet/event.py b/blivet/event.py
new file mode 100644
index 0000000..95771ef
--- /dev/null
+++ b/blivet/event.py
@@ -0,0 +1,240 @@
+# event.py
+# Event management classes.
+#
+# Copyright (C) 2015  Red Hat, Inc.
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
+# Public License for more details.  You should have received a copy of the
+# GNU General Public License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
+# source code or documentation are not subject to the GNU General Public
+# License and may only be used or replicated with the express permission of
+# Red Hat, Inc.
+#
+# Red Hat Author(s): David Lehman <dlehman@redhat.com>
+#
+
+import abc
+from collections import deque
+from threading import RLock, Timer
+import pyudev
+from six import add_metaclass
+
+from . import udev
+from . import util
+from .errors import EventHandlerError, EventParamError, EventQueueEmptyError
+
+##
+## Event
+##
+@add_metaclass(abc.ABCMeta)
+class Event(object):
+    def __init__(self, action, info):
+        """
+            :param str action: a string describing the type of event
+            :param info: information about the device
+
+            The info parameter can be of any type provided that the subclass
+            using it does so appropriately.
+        """
+        self.action = action
+        self.info = info
+
+    @abc.abstractproperty
+    def device(self):
+        """ Basename (friendly) of device this event acted upon. """
+        return None
+
+    def __str__(self):
+        return "%s %s" % (self.action, self.device)
+
+class UdevEvent(Event):
+    def __init__(self, action, info):
+        """
+            :param str action: a string describing the type of event
+            :param :class:`pyudev.Device` info: udev db entry
+        """
+        super(UdevEvent, self).__init__(action, info)
+
+    @property
+    def device(self):
+        return udev.device_get_name(self.info)
+
+##
+## EventQueue
+##
+class EventQueue(object):
+    def __init__(self):
+        self._queue = deque()
+        self._lock = RLock()
+
+    def enqueue(self, event):
+        with self._lock:
+            self._queue.append(event)
+
+    def dequeue(self):
+        """ Dequeue and return the next event.
+
+            :returns: the next uevent
+            :rtype: :class:`~.Event`
+            :raises class:`~.errors.EventQueueEmptyError` if the queue is empty
+        """
+        with self._lock:
+            if not self._queue:
+                raise EventQueueEmptyError()
+
+            return self._queue.popleft()
+
+    def __list__(self):
+        return list(self._queue)
+
+##
+## EventHandler
+##
+@add_metaclass(abc.ABCMeta)
+class EventHandler(object):
+    _event_queue_class = EventQueue
+    _default_delay_sec = 0
+
+    def __init__(self, handler_cb=None, delay=None, notify_cb=None):
+        self._handler_cb = None
+        self._notify_cb = None
+        self._delay = self._default_delay_sec
+
+        if handler_cb is not None:
+            self.handler_cb = handler_cb
+
+        if delay is not None:
+            self.delay = delay
+
+        if notify_cb is not None:
+            self.notify_cb = notify_cb
+
+        self._queue = self._event_queue_class()
+
+    #
+    # handler_cb is the main event handler
+    #
+    def _set_handler_cb(self, cb):
+        if not callable(cb):
+            raise EventParamError("handler must be callable")
+
+        self._handler_cb = cb
+
+    def _get_handler_cb(self):
+        return self._handler_cb
+
+    handler_cb = property(lambda h: h._get_handler_cb(),
+                          lambda h, cb: h._set_handler_cb(cb))
+
+    #
+    # notify_cb is a notification handler that runs after the main handler
+    #
+    def _set_notify_cb(self, cb):
+        if not callable(cb) or cb.func_code.argcount < 1:
+            raise EventParamError("callback function must accept at least one arg")
+
+        self._notify_cb = cb
+
+    def _get_notify_cb(self):
+        return self._notify_cb
+
+    notify_cb = property(lambda h: h._get_notify_cb(),
+                         lambda h, cb: h._set_notify_cb(cb))
+
+    #
+    # delay in seconds before handling a new event
+    #
+    def _set_delay(self, delay):
+        if not isinstance(delay, (int, float)):
+            raise EventParamError("delay must be int or float")
+
+        self._delay = delay
+
+    def _get_delay(self):
+        return self._delay
+
+    delay = property(lambda h: h._get_delay(),
+                     lambda h,d: h._set_delay(d),
+                     doc="delay in seconds before running callback")
+
+    @abc.abstractproperty
+    def enabled(self):
+        return False
+
+    @abc.abstractmethod
+    def enable(self):
+        """ Enable monitoring and handling of events.
+
+            :raises: :class:`~.errors.EventHandlerError` if no callback defined
+        """
+        if self.handler_cb is None:
+            raise EventHandlerError("cannot enable handler with no callback")
+
+    @abc.abstractmethod
+    def disable(self):
+        """ Disable monitoring and handling of events. """
+        pass
+
+    def next_event(self):
+        return self._queue.dequeue()
+
+    @property
+    def events_pending(self):
+        return len(self._queue._queue)
+
+    @abc.abstractmethod
+    def enqueue_event(self, *args, **kwargs):
+        """ Convert an event into :class:`~.Event` and enqueue it. """
+        pass
+
+    def handle_event(self, *args, **kwargs):
+        """ Enqueue an event and handle it after the configured delay. """
+        self.enqueue_event(*args, **kwargs)
+        Timer(self.delay, self.handler_cb).start()
+
+class UdevEventHandler(EventHandler):
+    def __init__(self, handler_cb, delay=None, notify_cb=None):
+        super(UdevEventHandler, self).__init__(handler_cb, delay=delay,
+                                               notify_cb=notify_cb)
+        self._pyudev_observer = None
+
+    def __deepcopy__(self, memo):
+        return util.variable_copy(self, memo, shallow=('_pyudev_observer'))
+
+    @property
+    def enabled(self):
+        return self._pyudev_observer and self._pyudev_observer.monitor.started
+
+    def enable(self):
+        """ Enable monitoring and handling of block device uevents. """
+        super(UdevEventHandler, self).enable()
+        monitor = pyudev.Monitor.from_netlink(udev.global_udev)
+        monitor.filter_by("block")
+        self._pyudev_observer = pyudev.MonitorObserver(monitor,
+                                                       self.handle_event)
+        self._pyudev_observer.start()
+
+    def disable(self):
+        """ Disable monitoring and handling of block device uevents. """
+        if self._pyudev_observer:
+            self._pyudev_observer.stop()
+            self._pyudev_observer = None
+
+    def enqueue_event(self, *args, **kwargs):
+        if args[0] == "add" and (udev.device_is_dm(args[1]) or
+                                 udev.device_is_md(args[1])):
+            return
+
+        event = UdevEvent(args[0], args[1])
+        self._queue.enqueue(event)
+
+    def handle_event(self, *args, **kwargs):
+        """ Enqueue a uevent and handle it after the configured delay. """
+        super(UdevEventHandler, self).handle_event(args[0], args[1])
-- 
1.9.3

