From 704918cc7d0efbcda098de894ba620e4e300bab4 Mon Sep 17 00:00:00 2001
From: David Lehman <dlehman@redhat.com>
Date: Tue, 10 Feb 2015 17:57:14 -0600
Subject: [PATCH 06/10] Enable event synchronization for container devices.

Container devices vary, but typically for create, destroy, setup, and
teardown uevents will be generated for the member devices and not
necessarily for the container itself.

In the case of lvmvg and btrfs volume this is because there is no device
to represent the container.

It is possible to create both mdarrays and lvmlvs without activating the
new device, so we must rely on change events for the member devices for
creation while looking for events on the device itself for setup and
teardown. Similarly, it is impossible to differentiate teardown from
destroy when looking at an event on the device itself, so we must use
change events on the members to determine destroy has occurred.

For btrfs subvolumes, like volumes, everything depends on events on the
member devices.
---
 blivet/devices/btrfs.py     |   3 +
 blivet/devices/container.py |  25 +++++++-
 blivet/devices/lvm.py       |   4 ++
 blivet/devices/md.py        |   4 ++
 blivet/devices/partition.py |  22 +++----
 blivet/devices/storage.py   | 105 +++++++++++++++++++++++++--------
 blivet/devicetree.py        | 141 ++++++++++++++++++++++++++++++--------------
 blivet/formats/__init__.py  |  21 ++++---
 blivet/formats/disklabel.py |  32 +++++-----
 blivet/formats/fs.py        |  55 ++++++++---------
 blivet/formats/luks.py      |  20 +++----
 blivet/formats/lvmpv.py     |  16 ++---
 blivet/formats/prepboot.py  |   8 +--
 blivet/formats/swap.py      |  26 ++++----
 blivet/threads.py           |  62 ++++++++++++++++---
 15 files changed, 370 insertions(+), 174 deletions(-)

diff --git a/blivet/devices/btrfs.py b/blivet/devices/btrfs.py
index 09aa207..be755cf 100644
--- a/blivet/devices/btrfs.py
+++ b/blivet/devices/btrfs.py
@@ -562,6 +562,9 @@ class BTRFSSubVolumeDevice(BTRFSDevice, RaidDevice):
         if self not in self.volume.subvolumes:
             self.volume._addSubVolume(self)
 
+    controlSync = property(lambda d: d.volume.controlSync)
+    modifySync = property(lambda d: d.volume.modifySync)
+
     def populateKSData(self, data):
         super(BTRFSSubVolumeDevice, self).populateKSData(data)
         data.subvol = True
diff --git a/blivet/devices/container.py b/blivet/devices/container.py
index 8050513..20ccf0e 100644
--- a/blivet/devices/container.py
+++ b/blivet/devices/container.py
@@ -26,7 +26,7 @@ from six import add_metaclass
 from .. import errors
 from ..storage_log import log_method_call
 from ..formats import get_device_format_class
-from ..threads import SynchronizedABCMeta
+from ..threads import SynchronizedABCMeta, StorageEventSynchronizerSet
 
 import logging
 log = logging.getLogger("blivet")
@@ -61,6 +61,7 @@ class ContainerDevice(StorageDevice):
         if not self.formatClass:
             raise errors.StorageError("cannot find '%s' class" % self._formatClassName)
 
+        self._parentSyncSet = None
         super(ContainerDevice, self).__init__(*args, **kwargs)
 
     def _verifyMemberUuid(self, member, expect_equality=True, require_existence=True):
@@ -123,6 +124,11 @@ class ContainerDevice(StorageDevice):
             raise ValueError("cannot add member with mismatched UUID")
 
         super(ContainerDevice, self)._addParent(member)
+        self._clearEventSyncs()
+
+    def _removeParent(self, member):
+        super(ContainerDevice, self)._removeParent(member)
+        self._clearEventSyncs()
 
     @abc.abstractmethod
     def _add(self, member):
@@ -188,3 +194,20 @@ class ContainerDevice(StorageDevice):
 
         if member in self.parents:
             self.parents.remove(member)
+
+    # Container device operations manifest as events on member devices. We use
+    # the parents' controlSync since it monitors events on the device itself
+    # instead of delegating to parents whenever possible (IFF the device has a
+    # device node).
+    def _getParentSyncSet(self):
+        if self._parentSyncSet is None:
+            synchronizers = [p.controlSync for p in self.parents]
+            self._parentSyncSet = StorageEventSynchronizerSet(synchronizers)
+
+        return self._parentSyncSet
+
+    def _clearEventSyncs(self):
+        self._parentSyncSet = None
+
+    controlSync = property(lambda c: c._getParentSyncSet())
+    modifySync = property(lambda c: c._getParentSyncSet())
diff --git a/blivet/devices/lvm.py b/blivet/devices/lvm.py
index 0ceb28c..37bbd27 100644
--- a/blivet/devices/lvm.py
+++ b/blivet/devices/lvm.py
@@ -752,6 +752,10 @@ class LVMLogicalVolumeDevice(DMDevice):
         if self not in self.vg.lvs:
             self.vg._addLogVol(self)
 
+    # lv create/destroy manifest as events on PVs
+    modifySync = property(lambda d: d.vg.modifySync,
+                          doc="control synchronization for create/destroy")
+
     def populateKSData(self, data):
         super(LVMLogicalVolumeDevice, self).populateKSData(data)
         data.vgname = self.vg.name
diff --git a/blivet/devices/md.py b/blivet/devices/md.py
index b15c4f8..39f7253 100644
--- a/blivet/devices/md.py
+++ b/blivet/devices/md.py
@@ -539,6 +539,10 @@ class MDRaidArrayDevice(ContainerDevice, RaidDevice):
     def model(self):
         return self.description
 
+    # md arrays use events on the members for create/destroy and events on the
+    # array device for setup/teardown
+    controlSync = property(lambda d: d._getEventSync())
+
     def dracutSetupArgs(self):
         return set(["rd.md.uuid=%s" % self.mdadmFormatUUID])
 
diff --git a/blivet/devices/partition.py b/blivet/devices/partition.py
index 77818b7..c4a4283 100644
--- a/blivet/devices/partition.py
+++ b/blivet/devices/partition.py
@@ -542,16 +542,17 @@ class PartitionDevice(StorageDevice):
                "seek=%s" % start, "count=%s" % count]
         # Is it reasonable to be using another device's cv? If not, we'll just
         # move this method into DiskDevice.
-        self.disk.cv.changing = True
+        event_sync = self.disk.controlSync
+        event_sync.changing = True
         try:
             util.run_program(cmd)
         except OSError as e:
             log.error(str(e))
         else:
-            self.disk.cv.wait()
+            event_sync.wait()
         finally:
-            self.disk.cv.changing = False
-            self.disk.cv.notify()
+            event_sync.changing = False
+            event_sync.notify()
             # If a udev device is created with the watch option, then
             # a change uevent is synthesized and we need to wait for
             # things to settle.
@@ -564,9 +565,9 @@ class PartitionDevice(StorageDevice):
         self.disk.format.addPartition(self.partedPartition.geometry.start,
                                       self.partedPartition.geometry.end,
                                       self.partedPartition.type)
-        self.cv.creating = False
+        self.modifySync.creating = False
         self._wipe()
-        self.cv.creating = True
+        self.modifySync.creating = True
         try:
             self.disk.format.commit()
         except errors.DiskLabelCommitError:
@@ -644,16 +645,17 @@ class PartitionDevice(StorageDevice):
                                         start=geometry.start,
                                         end=geometry.end)
 
-        self.cv.resizing = True
+        event_sync = self.modifySync
+        event_sync.resizing = True
         try:
             self.disk.format.commit()
         except Exception:
             raise
         else:
-            self.cv.wait()
+            event_sync.wait()
         finally:
-            self.cv.resizing = False
-            self.cv.notify()
+            event_sync.resizing = False
+            event_sync.notify()
 
     def _preDestroy(self):
         StorageDevice._preDestroy(self)
diff --git a/blivet/devices/storage.py b/blivet/devices/storage.py
index a2727b9..10f8098 100644
--- a/blivet/devices/storage.py
+++ b/blivet/devices/storage.py
@@ -31,7 +31,7 @@ from ..storage_log import log_method_call
 from .. import udev
 from ..formats import getFormat
 from ..size import Size
-from ..threads import StorageSynchronizer
+from ..threads import StorageEventSynchronizer
 
 import logging
 log = logging.getLogger("blivet")
@@ -130,8 +130,8 @@ class StorageDevice(Device):
 
         self.deviceLinks = []
 
-        """ Condition used to synchronize access with uevent handlers. """
-        self.cv = StorageSynchronizer(passthrough=not self.__class__._udev)
+        """ Used to synchronize access with uevent handlers. """
+        self._eventSync = None # use the same instance for all operations
 
     def __str__(self):
         exist = "existing"
@@ -373,7 +373,7 @@ class StorageDevice(Device):
             return False
 
         self.setupParents(orig=orig)
-        self.cv.starting = True
+        self.controlSync.starting = True
         return True
 
     def _setup(self, orig=False):
@@ -390,17 +390,18 @@ class StorageDevice(Device):
         try:
             self._setup(orig=orig)
         except Exception:
-            self.cv.starting = False
+            self.controlSync.starting = False
             raise
         self._postSetup()
 
     def _postSetup(self):
         """ Perform post-setup operations. """
+        event_sync = self.controlSync
         if self.__class__._setup != StorageDevice._setup:
-            self.cv.wait()
+            event_sync.wait()
 
-        self.cv.starting = False
-        self.cv.notify()
+        event_sync.starting = False
+        event_sync.notify()
         if not flags.uevents:
             udev.settle()
 
@@ -430,7 +431,7 @@ class StorageDevice(Device):
         if not flags.uevents:
             udev.settle()
 
-        self.cv.stopping = True
+        self.controlSync.stopping = True
         return True
 
     def _teardown(self, recursive=None):
@@ -447,18 +448,19 @@ class StorageDevice(Device):
         try:
             self._teardown(recursive=recursive)
         except Exception:
-            self.cv.stopping = False
+            self.controlSync.stopping = False
             raise
 
         self._postTeardown(recursive=recursive)
 
     def _postTeardown(self, recursive=None):
         """ Perform post-teardown operations. """
+        event_sync = self.controlSync
         if self.__class__._teardown != StorageDevice._teardown:
-            self.cv.wait()
+            event_sync.wait(0.2) # vgchange may not generate change events
 
-        self.cv.stopping = False
-        self.cv.notify()
+        event_sync.stopping = False
+        event_sync.notify()
 
         if recursive:
             self.teardownParents(recursive=recursive)
@@ -472,7 +474,7 @@ class StorageDevice(Device):
             raise errors.DeviceError("device has already been created", self.name)
 
         self.setupParents()
-        self.cv.creating = True
+        self.modifySync.creating = True
 
     def _create(self):
         """ Perform device-specific create operations. """
@@ -485,7 +487,7 @@ class StorageDevice(Device):
         try:
             self._create()
         except Exception:
-            self.cv.creating = False
+            self.modifySync.creating = False
             raise
         self._postCreate()
 
@@ -493,13 +495,14 @@ class StorageDevice(Device):
         """ Perform post-create operations. """
         # XXX what about devices that don't get started automatically when
         #     they are created
+        event_sync = self.modifySync
         if self.__class__._create != StorageDevice._create:
-            self.cv.notify() # notify the handler we're ready to post-process
-            self.cv.wait() # wait for notification the event was received
+            event_sync.notify() # notify the handler we're ready to post-process
+            event_sync.wait() # wait for notification the event was received
 
-        self.cv.creating = False
+        event_sync.creating = False
         self.exists = True
-        self.cv.notify() # notify the event handler we're done post-processing
+        event_sync.notify() # notify event handler we're done post-processing
 
         self.setup() # this seems redundant
         if not flags.uevents:
@@ -524,7 +527,7 @@ class StorageDevice(Device):
             raise errors.DeviceError("Cannot destroy non-leaf device", self.name)
 
         self.teardown()
-        self.cv.destroying = True
+        self.modifySync.destroying = True
 
     def _destroy(self):
         """ Perform device-specific destruction operations. """
@@ -537,19 +540,20 @@ class StorageDevice(Device):
         try:
             self._destroy()
         except Exception:
-            self.cv.destroying = False
+            self.modifySync.destroying = False
             raise
         self._postDestroy()
 
     def _postDestroy(self):
         """ Perform post-destruction operations. """
+        event_sync = self.modifySync
         if self.__class__._destroy != StorageDevice._destroy:
-            self.cv.notify() # notify the handler we're ready to post-process
-            self.cv.wait() # wait for notification that the event was received
+            event_sync.notify() # notify the handler we're ready to post-process
+            event_sync.wait() # wait for notification that event was received
 
-        self.cv.destroying = False
+        event_sync.destroying = False
         self.exists = False
-        self.cv.notify() # notify the handler we're done post-processing
+        event_sync.notify() # notify the handler we're done post-processing
         self.sysfsPath = ""
 
     def setupParents(self, orig=False):
@@ -794,6 +798,57 @@ class StorageDevice(Device):
         if not new:
             map(lambda p: p.addChild(), self.parents)
 
+    def _getEventSync(self):
+        if self._eventSync is None:
+            self._eventSync = StorageEventSynchronizer(passthrough=not self._udev)
+
+        return self._eventSync
+
+    ##
+    ## Event synchronization managers
+    ##
+    # These manage synchronization between the device methods that initiate
+    # actions on the underlying device and uevent handlers. This allows us to
+    # know exactly when newly created devices appear, for example.
+    #
+    # There are two of them because of variations in behavior among different
+    # device types:
+    #
+    # * Disks and partitions only need one synchronization manager because all
+    #   operations on those devices manifest as uevents on the device itself.
+    # * LVM VGs are different in that there is no block device to represent the
+    #   VG. This means that actions on a VG manifest as uevents on the VG's PVs.
+    # * LVM LVs have a combination of the two aforementioned models. Controlling
+    #   actions (setup, teardown) manifest as uevents on the LV device, while
+    #   modifying actions (create, destroy) events manifest as uevents on the
+    #   PVs.
+    # * MD arrays are similar to LVM LVs except that there is no intermediate
+    #   VG layer (which is really only a passthrough anyway).
+    # * BTRFS volumes behave the same way as LVM VGs.
+    # * BTRFS subvolumes use their top-level volume's synchronization manager,
+    #   meaning they also rely on uevents on the physical member devices.
+    #
+    # It is counter-intuitive to use the controlSync for "changing" actions when
+    # "changing" is synonymous with "modify" (as in "modifySync"). We do this
+    # because we want "changing" to use events on the actual device if that
+    # device has a device node. In every case except for lvm vg and btrfs, the
+    # controlSync is connected to the device itself and is not delegating
+    # to parent devices.
+    controlSync = property(lambda d: d._getEventSync(),
+                           doc="controls synchronization for setup/teardown")
+    modifySync = property(lambda d: d._getEventSync(),
+                          doc="controls synchronization for create/destroy")
+
+    @property
+    def delegateModifyEvents(self):
+        """ Do modify actions for this device manifest on parent devices? """
+        return self.modifySync != self._getEventSync()
+
+    @property
+    def delegateControlEvents(self):
+        """ Do control actions for this device manifest on parent devices? """
+        return self.controlSync != self._getEventSync()
+
     def populateKSData(self, data):
         # the common pieces are basically the formatting
         self.format.populateKSData(data)
diff --git a/blivet/devicetree.py b/blivet/devicetree.py
index cbb7284..2c880b3 100644
--- a/blivet/devicetree.py
+++ b/blivet/devicetree.py
@@ -425,22 +425,12 @@ class DeviceTree(object):
 
             Methods :meth:`~.devices.StorageDevice.create`,
             :meth:`~.devices.StorageDevice.destroy`, and
-            :meth:`~.devices.StorageDevice.setup` all use a condition variable
-            to synchronize the finalization/confirmation of their respective
-            operations. Since each device has only one condition variable, flags
-            are used to indicate which, if any, of these methods is waiting.
+            :meth:`~.devices.StorageDevice.setup` all use a synchronization
+            manager (:class:`~.threads.StorageEventSynchronizer`) to
+            synchronize the finalization/confirmation of their respective
+            operations. Flags within the manager are used to indicate which, if
+            any, of these methods is under way.
         """
-        def device_op_in_progress(device):
-            ret = device.cv.starting or device.cv.resizing or device.cv.changing
-            log.debug("device %s ops-in-progress: %s", device, ret)
-            return ret
-
-        def format_op_in_progress(fmt):
-            ret = (fmt.cv.creating or fmt.cv.resizing or fmt.cv.destroying or
-                   fmt.cv.starting or fmt.cv.stopping or fmt.cv.changing)
-            log.debug("format %s ops-in-progress: %s", fmt, ret)
-            return ret
-
         ret = False
         name = udev.device_get_name(info)
 
@@ -449,22 +439,51 @@ class DeviceTree(object):
         # meaningful sysfs path (for dm and md they aren't predictable).
         device = self.getDeviceByName(name)
 
-        if action in ("add", "change") and device and device.cv.creating:
+        if action in ("add", "change") and device and \
+           device.modifySync.creating and not device.delegateModifyEvents:
+            log.debug("* create %s", device.name)
+            ## device create without event delegation (eg: partition)
             device.sysfsPath = udev.device_get_sysfs_path(info)
-            device.cv.wait() # wait until the device says it's ready
-            device.cv.notify() # notify the device that we've received the event
-            device.cv.wait() # wait for the device to postprocess the event
+            event_sync = device.modifySync
+            # Only wait briefly since there is a possibility the device already
+            # notified the cv from _postCreate.
+            event_sync.wait(1) # wait until the device says it's ready
+            event_sync.notify() # notify the device that we received the event
+            event_sync.wait() # wait for the device to postprocess the event
             ret = True
-        elif action == "change":
+        elif action in ("add", "change"):
             # could be any number of operations on a device that may no longer
             # be in the tree
-            cv = None
-            if device and device_op_in_progress(device):
-                cv = device.cv
-            elif device and format_op_in_progress(device.format):
-                cv = device.format.cv
+            # device setup could be add or change event
+            # all other operations will be change event
+            event_sync = None
+            name = getattr(device, "name", None)
+            if device and device.controlSync.starting and \
+               not device.delegateControlEvents:
+                ## device setup
+                log.debug("* setup %s", device.name)
+                event_sync = device.controlSync
+            elif action == "change" and device and device.modifySync.resizing:
+                ## device resize
+                log.debug("* resize %s", device.name)
+                event_sync = device.modifySync
+            elif action == "change" and device and device.controlSync.changing:
+                ## device change (eg: event on pv for vg or lv creation)
+                log.debug("* change %s", device.name)
+                event_sync = device.controlSync
+                # There's an extra notify/wait pair in device create/destroy
+                # that the other branches in this block will not do.
+                # Only wait briefly since it's possible the device has already
+                # notified the CV from _postCreate/_postDestroy.
+                event_sync.wait(1)
+            elif action == "change" and device and \
+                 device.format.eventSync.active:
+                ## any change to a format
+                log.debug("* change %s format", device.name)
+                event_sync = device.format.eventSync
             elif self._actions:
-                # see if this action is on a device scheduled for removal
+                # see if this action is on a device or format that is scheduled
+                # for removal
                 devices = (a.device for a in self._actions if a.isDevice and
                                                               a.isDestroy)
                 for device in devices:
@@ -474,33 +493,60 @@ class DeviceTree(object):
                 else:
                     device = None
 
-                if not device:
+                if action == "change" and not device:
                     # see if this is a format op on a format scheduled for
                     # destruction
                     actions = (a for a in self._actions if a.isDestroy and
                                                            a.isFormat)
                     for action in actions:
                         if action.device.name == name and \
-                           format_op_in_progress(action.format):
-                            cv = action.format.cv
+                           action.format.eventSync.active:
+                            ## any change to a format scheduled for destruction
+                            ## on a device not scheduled for destruction
+                            log.debug("*** change %s format", action.device.name)
+                            event_sync = action.format.eventSync
+                            name = action.device.name
                             break
 
-                if device and not cv:
+                # If we haven't come up with an event_sync yet, try the same
+                # checks on the device being operated on by the current action,
+                # if any.
+                # XXX None of the sync manager's flags should be set if an
+                #     action is not being executed.
+                if device and not event_sync and self._actions:
                     current_action = self._actions[0]
-                    if device_op_in_progress(current_action.device):
-                        cv = current_action.device.cv
-                    elif format_op_in_progress(current_action.format):
-                        cv = current_action.format.cv
-
-            if cv:
-                cv.notify()
-                cv.wait()
+                    if current_action.device.controlSync.starting and \
+                       not current_action.device.delegateControlEvents:
+                        ## setup of device scheduled for destruction
+                        log.debug("** setup %s", current_action.device.name)
+                        event_sync = current_action.device.controlSync
+                    elif action == "change" and \
+                         current_action.device.controlSync.resizing or \
+                         current_action.device.controlSync.changing:
+                        ## change to device scheduled for destruction (eg: event
+                        ## on pv when removing a vg or lv)
+                        log.debug("** change %s", current_action.device.name)
+                        event_sync = current_action.device.controlSync
+                    elif action == "change" and \
+                         current_action.format.eventSync.active:
+                        ## any change to a format on a device scheduled for
+                        ## destruction
+                        log.debug("** change %s format", current_action.device.name)
+                        event_sync = current_action.format.eventSync
+
+            if event_sync:
+                event_sync.notify()
+                event_sync.wait()
                 ret = True
-        elif action == "remove" and device and device.cv.stopping:
-            device.cv.notify()
-            device.cv.wait()
+        elif action == "remove" and device and device.controlSync.stopping and \
+             not device.delegateControlEvents:
+            ## device teardown
+            log.debug("* teardown %s", device.name)
+            device.controlSync.notify()
+            device.controlSync.wait()
             ret = True
         elif action == "remove":
+            ## device destroy
             current_action = None
             if self._actions:
                 current_action = self._actions[0]
@@ -508,11 +554,16 @@ class DeviceTree(object):
             if current_action and \
                current_action.isDestroy and current_action.isDevice and \
                current_action.device.exists and \
-               current_action.device.cv.destroying:
+               current_action.device.modifySync.destroying and \
+               not current_action.device.delegateModifyEvents:
                 device = current_action.device
-                device.cv.wait() # wait until the device says it's ready
-                device.cv.notify() # notify device that we received the event
-                device.cv.wait() # wait for the device to postprocess the event
+                log.debug("* destroy %s", device.name)
+                event_sync = device.modifySync
+                # Only wait briefly since it's possible the device already
+                # notified the cv from _postDestroy.
+                event_sync.wait(1) # wait until the device says it's ready
+                event_sync.notify() # notify device that we received the event
+                event_sync.wait() # wait for the device to postprocess the event
                 ret = True
 
         return ret
diff --git a/blivet/formats/__init__.py b/blivet/formats/__init__.py
index 6641b48..8b4f468 100644
--- a/blivet/formats/__init__.py
+++ b/blivet/formats/__init__.py
@@ -34,7 +34,7 @@ from ..devicelibs.dm import dm_node_from_name
 from ..devicelibs.mdraid import md_node_from_name
 from ..i18n import N_
 from ..size import Size
-from ..threads import SynchronizedMeta, StorageSynchronizer
+from ..threads import SynchronizedMeta, StorageEventSynchronizer
 from ..flags import flags
 
 import logging
@@ -197,7 +197,8 @@ class DeviceFormat(ObjectID):
         #if self.__class__ is DeviceFormat:
         #    self.exists = True
 
-        self.cv = StorageSynchronizer()
+        self.eventSync = StorageEventSynchronizer()
+        self.updateSyncPassthrough()
 
     def __repr__(self):
         s = ("%(classname)s instance (%(id)s) object id %(object_id)d--\n"
@@ -306,11 +307,17 @@ class DeviceFormat(ObjectID):
        doc="fstab entry option string"
     )
 
+    def updateSyncPassthrough(self):
+        self.eventSync.passthrough = (self.device and
+                               (not os.path.exists(self.device) or
+                                not stat.S_ISBLK(os.stat(self.device).st_mode)))
+
     def _setDevice(self, devspec):
         if devspec and not devspec.startswith("/"):
             raise ValueError("device must be a fully qualified path")
         self._device = devspec
-        self.cv.passthrough = not stat.S_ISBLK(os.stat(self._device).st_mode)
+        if hasattr(self, "eventSync"):
+            self.updateSyncPassthrough()
 
     def _getDevice(self):
         return self._device
@@ -367,7 +374,7 @@ class DeviceFormat(ObjectID):
 
         notify = kwargs.pop("notify", True)
         if notify:
-            self.cv.destroying = True
+            self.eventSync.destroying = True
         try:
             rc = run_program(["wipefs", "-f", "-a", self.device])
         except OSError as e:
@@ -380,13 +387,13 @@ class DeviceFormat(ObjectID):
                 wait_args = [2]
 
             if notify:
-                self.cv.wait(*wait_args)
+                self.eventSync.wait(*wait_args)
             if not err:
                 self.exists = False
         finally:
             if notify:
-                self.cv.destroying = False
-                self.cv.notify()
+                self.eventSync.destroying = False
+                self.eventSync.notify()
 
         if err:
             msg = "error wiping old signatures from %s: %s" % (self.device, err)
diff --git a/blivet/formats/disklabel.py b/blivet/formats/disklabel.py
index dc5febf..26d173a 100644
--- a/blivet/formats/disklabel.py
+++ b/blivet/formats/disklabel.py
@@ -251,17 +251,17 @@ class DiskLabel(DeviceFormat):
         # could ensure a fresh disklabel by setting self._partedDisk to
         # None right before calling self.commit(), but that might hide
         # other problems.
-        self.cv.creating = True
+        self.eventSync.creating = True
         try:
             self.commit(notify=False)
         except Exception:
             raise
         else:
             self.exists = True
-            self.cv.wait()
+            self.eventSync.wait()
         finally:
-            self.cv.creating = False
-            self.cv.notify()
+            self.eventSync.creating = False
+            self.eventSync.notify()
 
     def destroy(self, **kwargs):
         """ Wipe the disklabel from the device. """
@@ -273,17 +273,17 @@ class DiskLabel(DeviceFormat):
         if not os.access(self.device, os.W_OK):
             raise DeviceFormatError("device path does not exist")
 
-        self.cv.destroying = True
+        self.eventSync.destroying = True
         try:
             self.partedDevice.clobber()
         except Exception:
             raise
         else:
-            self.cv.wait()
+            self.eventSync.wait()
             self.exists = False
         finally:
-            self.cv.destroying = False
-            self.cv.notify()
+            self.eventSync.destroying = False
+            self.eventSync.notify()
 
     def commit(self, notify=True):
         """ Commit the current partition table to disk and notify the OS.
@@ -293,7 +293,7 @@ class DiskLabel(DeviceFormat):
         log_method_call(self, device=self.device,
                         numparts=len(self.partitions))
         if notify:
-            self.cv.changing = True
+            self.eventSync.changing = True
         try:
             self.partedDisk.commit()
         except parted.DiskException as msg:
@@ -303,17 +303,17 @@ class DiskLabel(DeviceFormat):
             if not flags.uevents:
                 udev.settle()
             if notify:
-                self.cv.wait()
+                self.eventSync.wait()
         finally:
             if notify:
-                self.cv.changing = False
-                self.cv.notify()
+                self.eventSync.changing = False
+                self.eventSync.notify()
 
     def commitToDisk(self):
         """ Commit the current partition table to disk. """
         log_method_call(self, device=self.device,
                         numparts=len(self.partitions))
-        self.cv.changing = True
+        self.eventSync.changing = True
         try:
             self.partedDisk.commitToDevice()
         except parted.DiskException as msg:
@@ -322,10 +322,10 @@ class DiskLabel(DeviceFormat):
             self.updateOrigPartedDisk()
             if not flags.uevents:
                 udev.settle()
-            self.cv.wait()
+            self.eventSync.wait()
         finally:
-            self.cv.changing = False
-            self.cv.notify()
+            self.eventSync.changing = False
+            self.eventSync.notify()
 
     def addPartition(self, start, end, ptype=None):
         """ Add a partition to the disklabel.
diff --git a/blivet/formats/fs.py b/blivet/formats/fs.py
index 8b4107b..baa3f9c 100644
--- a/blivet/formats/fs.py
+++ b/blivet/formats/fs.py
@@ -414,7 +414,7 @@ class FS(DeviceFormat):
         argv = self._getFormatOptions(options=options,
            do_labeling=not self.relabels())
 
-        self.cv.creating = True
+        self.eventSync.creating = True
 
         ret = 0
         try:
@@ -426,12 +426,12 @@ class FS(DeviceFormat):
             if not ret:
                 wait_args = [2]
 
-            self.cv.wait(*wait_args)
+            self.eventSync.wait(*wait_args)
             if not ret:
                 self.exists = True
         finally:
-            self.cv.creating = False
-            self.cv.notify()
+            self.eventSync.creating = False
+            self.eventSync.notify()
 
         if ret:
             raise FormatCreateError("format failed: %s" % ret, self.device)
@@ -519,17 +519,17 @@ class FS(DeviceFormat):
         else:
             self.targetSize = rounded
 
-        self.cv.resizing = True
+        self.eventSync.resizing = True
 
         try:
             ret = util.run_program([self.resizefsProg] + self.resizeArgs)
         except OSError as e:
             raise FSResizeError(e, self.device)
         else:
-            self.cv.wait()
+            self.eventSync.wait()
         finally:
-            self.cv.resizing = False
-            self.cv.notify()
+            self.eventSync.resizing = False
+            self.eventSync.notify()
 
         if ret:
             raise FSResizeError("resize failed: %s" % ret, self.device)
@@ -566,7 +566,7 @@ class FS(DeviceFormat):
         if not os.path.exists(self.device):
             raise FSError("device does not exist")
 
-        self.cv.changing = True
+        self.eventSync.changing = True
 
         try:
             ret = util.run_program([self.fsckProg] + self._getCheckArgs())
@@ -574,10 +574,10 @@ class FS(DeviceFormat):
             raise FSError("filesystem check failed: %s" % e)
         else:
             # it may not have been opened r/w, so just wait briefly
-            self.cv.wait(timeout=2)
+            self.eventSync.wait(timeout=2)
         finally:
-            self.cv.changing = False
-            self.cv.notify()
+            self.eventSync.changing = False
+            self.eventSync.notify()
 
         if self._fsckFailed(ret):
             hdr = _("%(type)s filesystem check failure on %(device)s: ") % \
@@ -671,7 +671,7 @@ class FS(DeviceFormat):
         if isinstance(self, BindFS):
             options = "bind," + options
 
-        self.cv.starting = True
+        self.eventSync.starting = True
         read_only = "ro" in options.split(",")
 
         try:
@@ -682,10 +682,10 @@ class FS(DeviceFormat):
             raise FSError("mount failed: %s" % e)
         else:
             if not rc and not read_only:
-                self.cv.wait()
+                self.eventSync.wait()
         finally:
-            self.cv.starting = False
-            self.cv.notify()
+            self.eventSync.starting = False
+            self.eventSync.notify()
 
         if rc:
             raise FSError("mount failed: %s" % rc)
@@ -719,7 +719,7 @@ class FS(DeviceFormat):
         if not flags.uevents:
             udev.settle()
 
-        self.cv.starting = True
+        self.eventSync.starting = True
 
         rc = 0
         try:
@@ -735,10 +735,10 @@ class FS(DeviceFormat):
                 wait_args = [2]
 
             if not self._mounted_read_only:
-                self.cv.wait(*wait_args)
+                self.eventSync.wait(*wait_args)
         finally:
-            self.cv.starting = False
-            self.cv.notify()
+            self.eventSync.starting = False
+            self.eventSync.notify()
 
         if rc:
             # try and catch whatever is causing the umount problem
@@ -801,7 +801,7 @@ class FS(DeviceFormat):
         if not os.path.exists(self.device):
             raise FSError("device does not exist")
 
-        self.cv.changing = True
+        self.eventSync.changing = True
         try:
             rc = util.run_program(self._labelfs.label_app.setLabelCommand(self))
         except Exception:
@@ -810,10 +810,10 @@ class FS(DeviceFormat):
             wait_args = []
             if rc:
                 wait_args = [2]
-            self.cv.wait(*wait_args)
+            self.eventSync.wait(*wait_args)
         finally:
-            self.cv.changing = False
-            self.cv.notify()
+            self.eventSync.changing = False
+            self.eventSync.notify()
 
         if rc:
             raise FSError("label failed")
@@ -1521,7 +1521,7 @@ class NFS(FS):
     def _setDevice(self, devspec):
         self._deviceCheck(devspec)
         self._device = devspec
-        self.cv.passthrough = True
+        self.eventSync.passthrough = True
 
     def _getDevice(self):
         return self._device
@@ -1561,7 +1561,7 @@ class NoDevFS(FS):
 
     def _setDevice(self, devspec):
         self._device = devspec
-        self.cv.passthrough = True
+        self.eventSync.passthrough = True
 
     @property
     def type(self):
@@ -1698,13 +1698,14 @@ class TmpFS(NoDevFS):
         """ All the tmpfs mounts use the same "tmpfs" device. """
         return self._type
 
+    # pylint: disable=unused-argument
     def _setDevice(self, value):
         # the DeviceFormat parent class does a
         # self.device = kwargs["device"]
         # assignment, so we need a setter for the
         # device property, but as the device is always the
         # same, nothing actually needs to be set
-        self.cv.passthrough = True
+        self.eventSync.passthrough = True
 
     device = property(_getDevice, _setDevice)
 
diff --git a/blivet/formats/luks.py b/blivet/formats/luks.py
index 8f2fa92..9f8e26a 100644
--- a/blivet/formats/luks.py
+++ b/blivet/formats/luks.py
@@ -188,7 +188,7 @@ class LUKS(DeviceFormat):
             return
 
         DeviceFormat.setup(self, **kwargs)
-        self.cv.starting = True
+        self.eventSync.starting = True
         try:
             crypto.luks_open(self.device, self.mapName,
                            passphrase=self.__passphrase,
@@ -196,9 +196,9 @@ class LUKS(DeviceFormat):
         except Exception:
             raise
         else:
-            self.cv.wait()
+            self.eventSync.wait()
         finally:
-            self.cv.starting = False
+            self.eventSync.starting = False
 
     def teardown(self):
         """ Close, or tear down, the format. """
@@ -210,7 +210,7 @@ class LUKS(DeviceFormat):
         if not self.status:
             return
 
-        self.cv.stopping = True
+        self.eventSync.stopping = True
 
         log.debug("unmapping %s", self.mapName)
         try:
@@ -218,9 +218,9 @@ class LUKS(DeviceFormat):
         except Exception:
             raise
         else:
-            self.cv.wait()
+            self.eventSync.wait()
         finally:
-            self.cv.starting = False
+            self.eventSync.starting = False
 
     def create(self, **kwargs):
         """ Write the formatting to the specified block device.
@@ -241,7 +241,7 @@ class LUKS(DeviceFormat):
             raise LUKSError("luks device has no key/passphrase")
 
         DeviceFormat.create(self, **kwargs)
-        self.cv.creating = True
+        self.eventSync.creating = True
         try:
             crypto.luks_format(self.device,
                              passphrase=self.__passphrase,
@@ -252,14 +252,14 @@ class LUKS(DeviceFormat):
         except Exception:
             raise
         else:
-            self.cv.wait()
+            self.eventSync.wait()
             #self.uuid = crypto.luks_uuid(self.device)
             self.exists = True
             if flags.installer_mode:
                 self.mapName = "luks-%s" % self.uuid
         finally:
-            self.cv.creating = False
-            self.cv.notify()
+            self.eventSync.creating = False
+            self.eventSync.notify()
 
     def destroy(self, **kwargs):
         """ Remove the formatting from the associated block device.
diff --git a/blivet/formats/lvmpv.py b/blivet/formats/lvmpv.py
index 6d6e8cd..381e65b 100644
--- a/blivet/formats/lvmpv.py
+++ b/blivet/formats/lvmpv.py
@@ -117,17 +117,17 @@ class LVMPhysicalVolume(DeviceFormat):
             # lvm has issues with persistence of metadata, so here comes the
             # hammer...
             DeviceFormat.destroy(self, **kwargs)
-            self.cv.creating = True
+            self.eventSync.creating = True
             lvm.pvscan(self.device)
             lvm.pvcreate(self.device, data_alignment=self.dataAlignment)
         except Exception:
             raise
         else:
-            self.cv.wait()
+            self.eventSync.wait()
             self.exists = True
         finally:
-            self.cv.creating = False
-            self.cv.notify()
+            self.eventSync.creating = False
+            self.eventSync.notify()
             lvm.pvscan(self.device)
 
     def destroy(self, **kwargs):
@@ -144,7 +144,7 @@ class LVMPhysicalVolume(DeviceFormat):
         if self.status:
             raise PhysicalVolumeError("device is active")
 
-        self.cv.destroying = True
+        self.eventSync.destroying = True
 
         # FIXME: verify path exists?
         err = False
@@ -160,11 +160,11 @@ class LVMPhysicalVolume(DeviceFormat):
             wait_args = []
             if err:
                 wait_args = [2]
-            self.cv.wait(*wait_args)
-            self.cv.destroying = False
+            self.eventSync.wait(*wait_args)
+            self.eventSync.destroying = False
             if not err:
                 self.exists = False
-            self.cv.notify()
+            self.eventSync.notify()
             lvm.pvscan(self.device)
 
     @property
diff --git a/blivet/formats/prepboot.py b/blivet/formats/prepboot.py
index 60e0fde..77ce396 100644
--- a/blivet/formats/prepboot.py
+++ b/blivet/formats/prepboot.py
@@ -73,7 +73,7 @@ class PPCPRePBoot(DeviceFormat):
             raise FormatCreateError("PReP Boot format already exists")
 
         DeviceFormat.create(self, **kwargs)
-        self.cv.creating = True
+        self.eventSync.creating = True
         try:
             fd = os.open(self.device, os.O_RDWR)
             length = os.lseek(fd, 0, os.SEEK_END)
@@ -90,13 +90,13 @@ class PPCPRePBoot(DeviceFormat):
         except OSError as e:
             log.error("error zeroing out %s: %s", self.device, e)
         else:
-            self.cv.wait()
+            self.eventSync.wait()
         finally:
             if fd:
                 os.close(fd)
             self.exists = True
-            self.cv.creating = False
-            self.cv.notify()
+            self.eventSync.creating = False
+            self.eventSync.notify()
 
     @property
     def status(self):
diff --git a/blivet/formats/swap.py b/blivet/formats/swap.py
index 3b7cd38..546876d 100644
--- a/blivet/formats/swap.py
+++ b/blivet/formats/swap.py
@@ -158,16 +158,16 @@ class SwapSpace(DeviceFormat):
             return
 
         DeviceFormat.setup(self, **kwargs)
-        self.cv.starting = True
+        self.eventSync.starting = True
         try:
             swap.swapon(self.device, priority=self.priority)
         except Exception:
-            pass
+            raise
         else:
-            self.cv.wait()
+            self.eventSync.wait()
         finally:
-            self.cv.starting = False
-            self.cv.notify()
+            self.eventSync.starting = False
+            self.eventSync.notify()
 
     def teardown(self):
         """ Close, or tear down, a device. """
@@ -179,7 +179,7 @@ class SwapSpace(DeviceFormat):
         if not self.status:
             return
 
-        self.cv.stopping = True
+        self.eventSync.stopping = True
         try:
             swap.swapoff(self.device)
         except Exception:
@@ -188,10 +188,10 @@ class SwapSpace(DeviceFormat):
             # We don't need to worry about missing the change event triggered by
             # the swapoff call above because we hold the lock, preventing events
             # from being handled, until we call wait here.
-            self.cv.wait()
+            self.eventSync.wait()
         finally:
-            self.cv.stopping = False
-            self.cv.notify()
+            self.eventSync.stopping = False
+            self.eventSync.notify()
 
     def create(self, **kwargs):
         """ Write the formatting to the specified block device.
@@ -212,17 +212,17 @@ class SwapSpace(DeviceFormat):
             raise SwapSpaceError("format already exists")
 
         DeviceFormat.create(self, **kwargs)
-        self.cv.creating = True
+        self.eventSync.creating = True
         try:
             swap.mkswap(self.device, label=self.label)
         except Exception:
             raise
         else:
-            self.cv.wait()
+            self.eventSync.wait()
             self.exists = True
         finally:
-            self.cv.creating = False
-            self.cv.notify()
+            self.eventSync.creating = False
+            self.eventSync.notify()
 
 register_device_format(SwapSpace)
 
diff --git a/blivet/threads.py b/blivet/threads.py
index 3be0fd9..f170eb8 100644
--- a/blivet/threads.py
+++ b/blivet/threads.py
@@ -20,13 +20,14 @@
 # Red Hat Author(s): David Lehman <dlehman@redhat.com>
 #
 
-from threading import _RLock, Condition, currentThread
+from threading import Thread, _RLock, Condition, currentThread
 from functools import wraps
 from types import FunctionType
 from abc import ABCMeta
 import copy
 
 from .flags import flags
+from .util import ObjectID
 
 import logging
 log = logging.getLogger("blivet")
@@ -45,21 +46,22 @@ class NoisyRLock(_RLock):
 #blivet_lock = NoisyRLock()
 blivet_lock = _RLock(verbose=flags.debug_threads)
 
-class StorageSynchronizerFlags(object):
+class StorageEventFlags(ObjectID):
     _flag_names = ["starting", "stopping", "creating", "destroying", "resizing",
                    "changing"]
 
     def __init__(self):
         self._flags = {flag_name: False for flag_name in self._flag_names}
 
+    @property
+    def active(self):
+        return any(self._flags.values())
+
     def _get_flag(self, flag):
         return self._flags[flag]
 
     def _set_flag(self, flag, val):
-        print "setting %s=%s" % (flag, val)
-        if val and any(self._flags.values()):
-            for fl, v in self._flags.items():
-                print "%s=%s" % (fl,v)
+        if val and self.active:
             raise RuntimeError("only one flag can be active at a time")
 
         self._flags[flag] = val
@@ -77,7 +79,7 @@ class StorageSynchronizerFlags(object):
     changing = property(lambda s: s._get_flag("changing"),
                         lambda s,v: s._set_flag("changing", v))
 
-class StorageSynchronizer(StorageSynchronizerFlags):
+class StorageEventSynchronizer(StorageEventFlags):
     """ Manager for shared state related to storage operations.
 
         Each :class:`~.devices.StorageDevice` and
@@ -93,7 +95,7 @@ class StorageSynchronizer(StorageSynchronizerFlags):
         wrappers around the internal threading.Condition instance.
     """
     def __init__(self, passthrough=False):
-        super(StorageSynchronizer, self).__init__()
+        super(StorageEventSynchronizer, self).__init__()
         self._cv = Condition(blivet_lock, verbose=flags.debug_threads)
         self.passthrough = passthrough
 
@@ -123,6 +125,50 @@ class StorageSynchronizer(StorageSynchronizerFlags):
         args = [] if n is None else [n]
         self._cv.notify(*args)
 
+class StorageEventSynchronizerSet(StorageEventFlags):
+    def __init__(self, ss_list):
+        super(StorageEventSynchronizerSet, self).__init__()
+        self.ss_list = ss_list
+
+    def _get_flag(self, flag):
+        if flag in ("creating", "destroying"):
+            _flag = "changing"
+        else:
+            _flag = flag
+
+        # they have to all be the same value
+        vals = [getattr(ss, _flag) for ss in self.ss_list]
+        if not (all(vals) or not any(vals)):
+            raise RuntimeError("sync set members out of sync")
+        return super(StorageEventSynchronizerSet, self)._get_flag(flag)
+
+    def _set_flag(self, flag, val):
+        if flag in ("creating", "destroying"):
+            # create/destroy actions on parents manifest as generic change
+            # events, ie: not noted as related to a create/destroy
+            _flag = "changing"
+        else:
+            _flag = flag
+
+        vals = [getattr(ss, _flag) for ss in self.ss_list]
+        if not (all(vals) or not any(vals)):
+            raise RuntimeError("sync set members out of sync")
+        # pylint: disable=expression-not-assigned
+        [setattr(ss, _flag, val) for ss in self.ss_list]
+        super(StorageEventSynchronizerSet, self)._set_flag(flag, val)
+
+    def wait(self, timeout=None):
+        waiting = []
+        cv = Condition(blivet_lock)
+        for ss in self.ss_list:
+            ss.wait(timeout=timeout)
+
+        return
+
+    def notify(self, n=None):
+        for ss in self.ss_list:
+            ss.notify(n=n)
+
 def exclusive(m):
     """ Run a bound method after aqcuiring the instance's lock. """
     @wraps(m)
-- 
1.9.3

