[master 27/30] Account for changes made in response to each event.
by dashea
From: David Lehman <dlehman(a)redhat.com>
Each event is handled in a separate thread, so we use thread-local
data to store an ordered list of the changes made while handling
each event.
---
blivet/actionlist.py | 3 ++
blivet/devicetree.py | 9 ++++
blivet/events/changes.py | 78 +++++++++++++++++++++++++++++
blivet/events/handler.py | 21 ++++++--
blivet/events/manager.py | 7 ++-
blivet/populator/helpers/formatpopulator.py | 16 ++++--
blivet/populator/helpers/lvm.py | 7 +++
blivet/populator/populator.py | 1 -
8 files changed, 133 insertions(+), 9 deletions(-)
create mode 100644 blivet/events/changes.py
diff --git a/blivet/actionlist.py b/blivet/actionlist.py
index ff00ab3..edec59a 100644
--- a/blivet/actionlist.py
+++ b/blivet/actionlist.py
@@ -28,6 +28,8 @@
from .devicelibs import lvm
from .devices import PartitionDevice
from .errors import DiskLabelCommitError, StorageError
+from .events.changes import data as event_data
+from .events.changes import ActionCanceled
from .flags import flags
from . import tsort
from .threads import blivet_lock, SynchronizedMeta
@@ -80,6 +82,7 @@ def remove(self, action):
action.cancel()
self._actions.remove(action)
+ event_data.changes.append(ActionCanceled(action=action))
log.info("canceled action %s", action)
def find(self, device=None, action_type=None, object_type=None,
diff --git a/blivet/devicetree.py b/blivet/devicetree.py
index 9172ebe..30481cc 100644
--- a/blivet/devicetree.py
+++ b/blivet/devicetree.py
@@ -36,6 +36,8 @@
from .devices import LVMLogicalVolumeDevice, LVMVolumeGroupDevice
from . import formats
from .devicelibs import lvm
+from .events.changes import data as event_data
+from .events.changes import AttributeChanged, DeviceAdded, DeviceRemoved
from .events.handler import EventHandlerMixin
from . import util
from .populator import PopulatorMixin
@@ -159,6 +161,7 @@ def _add_device(self, newdev, new=True):
newdev.type != "btrfs volume" and
newdev.name not in self.names):
self.names.append(newdev.name)
+ event_data.changes.append(DeviceAdded(device=newdev))
log.info("added %s %s (id %d) to device tree", newdev.type,
newdev.name,
newdev.id)
@@ -201,6 +204,7 @@ def _remove_device(self, dev, force=None, modparent=True):
device.update_name()
self._devices.remove(dev)
+ event_data.changes.append(DeviceRemoved(device=dev))
log.info("removed %s %s (id %d) from device tree", dev.type,
dev.name,
dev.id)
@@ -247,11 +251,16 @@ def recursive_remove(self, device, actions=True, remove_device=True, modparent=T
devices.remove(leaf)
if not device.format_immutable:
+ old_fmt = device.format
if actions:
self.actions.add(ActionDestroyFormat(device))
else:
device.format = None
+ if old_fmt.type and (not remove_device or device.is_disk):
+ event_data.changes.append(AttributeChanged(device=device, attr="format",
+ old=old_fmt, new=device.format))
+
if remove_device and not device.is_disk:
if actions:
self.actions.add(ActionDestroyDevice(device))
diff --git a/blivet/events/changes.py b/blivet/events/changes.py
new file mode 100644
index 0000000..a2b9f87
--- /dev/null
+++ b/blivet/events/changes.py
@@ -0,0 +1,78 @@
+# events/changes.py
+# Per-thread change accounting data structures.
+#
+# Copyright (C) 2016 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 Lesser 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 Lesser General Public License for more details. You should have
+# received a copy of the GNU Lesser 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 Lesser 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(a)redhat.com>
+#
+
+from threading import local
+
+from .. import util
+
+
+data = local()
+""" Thread-local data for event handler threads. """
+
+data.changes = list() # Saves checking if the list exists in the main thread.
+
+
+""" Accounting for changes made in response to an event.
+
+ We want to end up with a single, ordered list containing every change
+ that was made in response to a specific event.
+"""
+_DeviceAdded = util.default_namedtuple("DeviceAdded", ["device"])
+_DeviceRemoved = util.default_namedtuple("DeviceRemoved", ["device"])
+_ActionCanceled = util.default_namedtuple("ActionCanceled", ["action"])
+_ParentAdded = util.default_namedtuple("ListItemAdded", ["device", "item"])
+_ParentRemoved = util.default_namedtuple("ListItemRemoved", ["device", "item"])
+_AttributeChanged = util.default_namedtuple("AttrChanged", ["device", ("fmt", None), "attr", "old", "new"])
+
+
+class DeviceAdded(_DeviceAdded):
+ def __str__(self):
+ return "add device '%s'" % str(self.device)
+
+
+class DeviceRemoved(_DeviceRemoved):
+ def __str__(self):
+ return "remove device %s" % self.device.name
+
+
+class ActionCanceled(_ActionCanceled):
+ def __str__(self):
+ return "cancel action '%s'" % str(self.action)
+
+
+class ParentAdded(_ParentAdded):
+ def __str__(self):
+ return "add parent %s to %s" % (self.item.name, self.device.name)
+
+
+class ParentRemoved(_ParentRemoved):
+ def __str__(self):
+ return "remove parent %s from %s" % (self.item.name, self.device.name)
+
+
+class AttributeChanged(_AttributeChanged):
+ def __str__(self):
+ return "change attribute %s of %s%s from '%s' to '%s'" % (self.attr,
+ self.device.name,
+ " format" if self.fmt else "",
+ str(self.old),
+ str(self.new))
diff --git a/blivet/events/handler.py b/blivet/events/handler.py
index 2072c48..5601a7f 100644
--- a/blivet/events/handler.py
+++ b/blivet/events/handler.py
@@ -25,6 +25,8 @@
from .. import udev
from ..threads import SynchronizedMeta
+from .changes import data as event_data
+from .changes import AttributeChanged, ParentRemoved
from .manager import event_manager
import logging
@@ -121,7 +123,12 @@ def _handle_add_event(self, event):
elif device is not None and device.exists:
log.info("device %s was activated", device.name)
# device was activated from outside, so update the sysfs path
- device.sysfs_path = udev.device_get_sysfs_path(event.info)
+ sysfs_path = udev.device_get_sysfs_path(event.info)
+ if device.sysfs_path != sysfs_path:
+ old_sysfs_path = device.sysfs_path
+ device.sysfs_path = sysfs_path
+ event_data.changes.append(AttributeChanged(device=device, attr="sysfs_path",
+ old=old_sysfs_path, new=sysfs_path))
def _handle_format_change(self, event, device):
helper_class = self._get_format_helper(event.info, device) # pylint: disable=no-member
@@ -158,6 +165,8 @@ def _handle_format_change(self, event, device):
device.name, container.name, str(e))
raise EventHandlingError("reformatted container member")
+ event_data.changes.append(ParentRemoved(device=container, attr="parents", item=device))
+
self.recursive_remove(device, actions=False, remove_device=False)
helper.run()
@@ -221,6 +230,10 @@ def _handle_remove_event(self, event):
log.info("disk %s was removed", device.name)
self._remove_device(device)
else:
- log.info("device %s was deactivated", device.name)
- # device was deactivated from outside, so clear the sysfs path
- device.sysfs_path = ''
+ if device.sysfs_path:
+ old_sysfs_path = device.sysfs_path
+ log.info("device %s was deactivated", device.name)
+ # device was deactivated from outside, so clear the sysfs path
+ device.sysfs_path = ''
+ event_data.changes.append(AttributeChanged(device=device, attr="sysfs_path",
+ old=old_sysfs_path, new=''))
diff --git a/blivet/events/manager.py b/blivet/events/manager.py
index ddb3037..14ebb90 100644
--- a/blivet/events/manager.py
+++ b/blivet/events/manager.py
@@ -21,7 +21,7 @@
#
import abc
-from threading import RLock, Thread, current_thread
+from threading import current_thread, RLock, Thread
import pyudev
import sys
import time
@@ -33,6 +33,8 @@
from ..errors import EventManagerError, EventParamError
from ..flags import flags
+from .changes import data
+
import logging
event_log = logging.getLogger("blivet.event")
@@ -247,6 +249,9 @@ def handle_event(self, *args, **kwargs):
def _run_event_handler(self, event):
""" Run the event handler and account for unhandled exceptions. """
+ # initialize thread-local data attribute for change accounting
+ data.changes = list()
+
if self.handler_cb is None:
return
diff --git a/blivet/populator/helpers/formatpopulator.py b/blivet/populator/helpers/formatpopulator.py
index 8032b05..001d34f 100644
--- a/blivet/populator/helpers/formatpopulator.py
+++ b/blivet/populator/helpers/formatpopulator.py
@@ -20,6 +20,8 @@
# Red Hat Author(s): David Lehman <dlehman(a)redhat.com>
#
+from ...events.changes import data as event_data
+from ...events.changes import AttributeChanged
from ... import formats
from ... import udev
from ...errors import FSError
@@ -81,6 +83,7 @@ def run(self):
""" Create a format instance and associate it with the device instance. """
kwargs = self._get_kwargs()
type_spec = self.type_spec
+ old_fmt = self.device.format
try:
log.info("type detected on '%s' is '%s'", self.device.name, type_spec)
self.device.format = formats.get_format(type_spec, **kwargs)
@@ -88,8 +91,15 @@ def run(self):
log.warning("type '%s' on '%s' invalid, assuming no format",
type_spec, self.device.name)
self.device.format = formats.DeviceFormat(device=self.device.path, exists=True)
- return
+
+ if old_fmt.type != self.device.format.type or old_fmt.uuid != self.device.format.uuid:
+ event_data.changes.append(AttributeChanged(device=self.device, attr="format",
+ old=old_fmt, new=self.device.format))
def update(self):
- if hasattr(self.device.format, "label"):
- self.device.format.label = udev.device_get_label(self.data)
+ label = udev.device_get_label(self.data)
+ if hasattr(self.device.format, "label") and self.device.format.label != label:
+ old_label = self.device.format.label
+ self.device.format.label = label
+ event_data.changes.append(AttributeChanged(device=self.device, fmt=self.device.format,
+ attr="label", old=old_label, new=label))
diff --git a/blivet/populator/helpers/lvm.py b/blivet/populator/helpers/lvm.py
index 7ab6c7c..2ad091d 100644
--- a/blivet/populator/helpers/lvm.py
+++ b/blivet/populator/helpers/lvm.py
@@ -31,6 +31,8 @@
from ...devices import LVMThinPoolDevice, LVMThinSnapShotDevice, LVMThinLogicalVolumeDevice
from ...devices.lvm import get_internal_lv_class
from ...errors import DeviceTreeError, DuplicateVGError
+from ...events.changes import data as event_data
+from ...events.changes import AttributeChanged, ParentAdded, ParentRemoved
from ...flags import flags
from ...size import Size
from ...storage_log import log_method_call
@@ -187,7 +189,10 @@ def add_lv(lv):
# lvresize can operate on an inactive lv, in which case
# the only notification we will receive is a change uevent
# for the pv(s)
+ old_size = lv_device._size
lv_device.update_size(newsize=lv_size)
+ event_data.changes.append(AttributeChanged(device=lv_device, attr="size",
+ old=old_size, new=lv_size))
self._devicetree.cancel_disk_actions(vg_device.disks)
return
@@ -359,6 +364,7 @@ def _add_vg_device(self):
vg_device = self._devicetree.get_device_by_uuid(vg_uuid, incomplete=True)
if vg_device and self.device not in vg_device.parents:
vg_device.parents.append(self.device)
+ event_data.changes.append(ParentAdded(device=vg_device, item=self.device))
elif vg_device is None:
same_name = self._devicetree.get_device_by_name(vg_name)
if isinstance(same_name, LVMVolumeGroupDevice):
@@ -433,6 +439,7 @@ def update(self):
vg_device = self.device.children[0]
if len(vg_device.parents) > 1:
vg_device.parents.remove(self.device)
+ event_data.changes.append(ParentRemoved(device=vg_device, item=self.device))
else:
self._devicetree.recursive_remove(vg_device, actions=False)
return
diff --git a/blivet/populator/populator.py b/blivet/populator/populator.py
index 7e3595f..fa7ab92 100644
--- a/blivet/populator/populator.py
+++ b/blivet/populator/populator.py
@@ -577,4 +577,3 @@ def handle_nodev_filesystems(self):
n = len([d for d in self.devices if d.format.type == fstype])
device._name += ".%d" % n
self._add_device(device)
-
--
To view this commit on github, visit https://github.com/rhinstaller/blivet/commit/5711603d98f265edd99d1504854b...
8 years, 4 months
[master 26/30] Add facilities for reporting exceptions that occur in
threads.
by dashea
From: David Lehman <dlehman(a)redhat.com>
Callers can register a callback which blivet can use to report
exceptions that occur in event handling threads. If they do not
register such a callback, the following will happen: 1) The exception
and traceback will be logged using the ERROR facility. 2) The next time
a method that has been wrapped using the SynchronizedMeta metaclass is
called in the main thread, the exception will be re-raised.
---
blivet/errors.py | 5 ++++
blivet/events/manager.py | 62 +++++++++++++++++++++++++++++++++++++++++++-----
blivet/threads.py | 39 +++++++++++++++++++++++++++++-
3 files changed, 99 insertions(+), 7 deletions(-)
diff --git a/blivet/errors.py b/blivet/errors.py
index 885e105..aa94c81 100644
--- a/blivet/errors.py
+++ b/blivet/errors.py
@@ -273,3 +273,8 @@ class EventParamError(StorageError):
class EventHandlingError(StorageError):
pass
+
+
+class ThreadError(StorageError):
+ """ An error occurred in a non-main thread. """
+ pass
diff --git a/blivet/events/manager.py b/blivet/events/manager.py
index aa85890..ddb3037 100644
--- a/blivet/events/manager.py
+++ b/blivet/events/manager.py
@@ -21,15 +21,17 @@
#
import abc
-from threading import RLock, Thread
+from threading import RLock, Thread, current_thread
import pyudev
+import sys
import time
+import traceback
+from .. import threads
from .. import udev
from .. import util
from ..errors import EventManagerError, EventParamError
from ..flags import flags
-from ..threads import blivet_lock
import logging
event_log = logging.getLogger("blivet.event")
@@ -99,9 +101,15 @@ def match(self, event):
# EventManager
#
class EventManager(object, metaclass=abc.ABCMeta):
- def __init__(self, handler_cb=None, notify_cb=None):
+ def __init__(self, handler_cb=None, notify_cb=None, error_cb=None):
self._handler_cb = None
+ """ event handler """
+
self._notify_cb = None
+ """ notification callback """
+
+ self._error_cb = None
+ """ exception handler """
if handler_cb is not None:
self.handler_cb = handler_cb
@@ -109,6 +117,9 @@ def __init__(self, handler_cb=None, notify_cb=None):
if notify_cb is not None:
self.notify_cb = notify_cb
+ if error_cb is not None:
+ self.error_cb = error_cb
+
self._mask_list = list()
"""List of masks specifying events that should be ignored."""
@@ -139,6 +150,18 @@ def notify_cb(self, cb):
self._notify_cb = cb
+ @property
+ def error_cb(self):
+ """ callback to run when an exception occurrs in a thread. """
+ return self._error_cb
+
+ @error_cb.setter
+ def error_cb(self, cb):
+ if not callable(cb) or cb.func_code.argcount < 1:
+ raise EventParamError("callback function must accept at least one arg")
+
+ self._error_cb = cb
+
@abc.abstractproperty
def enabled(self):
return False
@@ -196,6 +219,18 @@ def handle_event(self, *args, **kwargs):
Currently the handler is run in a separate thread. This removes any
threading-related expectations about the behavior of whatever is
telling us about the events.
+
+ Unhandled exceptions in event handler threads present a bit of a
+ challenge. Generally, an unhandled exception in an event handler
+ thread should be fatal because of the high likelihood that the
+ error left the :class:`~.DeviceTree` in an inconsistent state.
+ Since exceptions that occur in threads are not reported to the
+ main thread, we have to try to notify it ourselves. Ideally, the
+ calling application will have registered an :attr:`error_cb`,
+ which we can use to notify it of the exception. If no callback
+ has been set we can only wait until the next time the calling
+ application makes a call into blivet, at which point we can
+ raise the original exception.
"""
event = self._create_event(*args, **kwargs)
event_log.debug("new event: %s", event)
@@ -204,12 +239,27 @@ def handle_event(self, *args, **kwargs):
event_log.debug("ignoring masked event %s", event)
return
- t = Thread(target=self.handler_cb,
+ t = Thread(target=self._run_event_handler,
name="event%d" % event.id,
kwargs={"event": event},
daemon=True)
t.start()
+ def _run_event_handler(self, event):
+ """ Run the event handler and account for unhandled exceptions. """
+ if self.handler_cb is None:
+ return
+
+ try:
+ self.handler_cb(event) # pylint: disable=not-callable
+ except Exception: # pylint: disable=broad-except
+ event_log.error(traceback.format_exc())
+ exc_info = sys.exc_info()
+ if self.error_cb is not None:
+ self.error_cb(exc_info) # pylint: disable=not-callable
+ else:
+ threads.save_thread_exception(current_thread(), exc_info)
+
class UdevEventManager(EventManager):
def __init__(self, handler_cb=None, notify_cb=None):
@@ -229,7 +279,7 @@ def enable(self):
callback=self.handle_event,
name="monitor")
self._pyudev_observer.start()
- with blivet_lock:
+ with threads.blivet_lock:
flags.uevents = True
def disable(self):
@@ -239,7 +289,7 @@ def disable(self):
self._pyudev_observer.stop()
self._pyudev_observer = None
- with blivet_lock:
+ with threads.blivet_lock:
flags.uevents = False
def __call__(self, *args, **kwargs):
diff --git a/blivet/threads.py b/blivet/threads.py
index 3178fc2..d0ed97d 100644
--- a/blivet/threads.py
+++ b/blivet/threads.py
@@ -20,11 +20,12 @@
# Red Hat Author(s): David Lehman <dlehman(a)redhat.com>
#
-from threading import RLock
+from threading import RLock, current_thread, main_thread
from functools import wraps
from types import FunctionType
from abc import ABCMeta
+from .errors import ThreadError
from .flags import flags
blivet_lock = RLock(verbose=flags.debug_threads)
@@ -35,6 +36,12 @@ def exclusive(m):
@wraps(m)
def run_with_lock(*args, **kwargs):
with blivet_lock:
+ if current_thread() == main_thread():
+ exn_info = get_thread_exception()
+ if exn_info[1]:
+ clear_thread_exception()
+ raise ThreadError("raising queued exception") from exn_info[1]
+
return m(*args, **kwargs)
return run_with_lock
@@ -70,3 +77,33 @@ def __new__(cls, name, bases, dct):
class SynchronizedABCMeta(SynchronizedMeta, ABCMeta):
pass
+
+
+#
+# Facilities for storing/retrieving information about an unhandled exception in a thread.
+#
+_exception_thread = None
+_thread_exception = None
+
+
+def save_thread_exception(thread, exc_info):
+ global _exception_thread
+ global _thread_exception
+
+ if _exception_thread is None or _thread_exception is None:
+ return
+
+ _exception_thread = thread
+ _thread_exception = exc_info
+
+
+def clear_thread_exception():
+ global _exception_thread
+ global _thread_exception
+
+ _exception_thread = None
+ _thread_exception = None
+
+
+def get_thread_exception():
+ return (_exception_thread, _thread_exception)
--
To view this commit on github, visit https://github.com/rhinstaller/blivet/commit/a89fdc3d8704c7e6c3142c1490e9...
8 years, 4 months
[master 25/30] Add event handlers via a DeviceTree mixin class.
by dashea
From: David Lehman <dlehman(a)redhat.com>
---
blivet/devicetree.py | 4 +-
blivet/errors.py | 4 +
blivet/events/handler.py | 226 ++++++++++++++++++++++++++++++++++++++++++
blivet/populator/populator.py | 1 +
4 files changed, 234 insertions(+), 1 deletion(-)
create mode 100644 blivet/events/handler.py
diff --git a/blivet/devicetree.py b/blivet/devicetree.py
index 2b750af..9172ebe 100644
--- a/blivet/devicetree.py
+++ b/blivet/devicetree.py
@@ -36,6 +36,7 @@
from .devices import LVMLogicalVolumeDevice, LVMVolumeGroupDevice
from . import formats
from .devicelibs import lvm
+from .events.handler import EventHandlerMixin
from . import util
from .populator import PopulatorMixin
from .storage_log import log_method_call, log_method_return
@@ -925,10 +926,11 @@ def _hide_ignored_disks(self):
self.hide(disk)
-class DeviceTree(DeviceTreeBase, PopulatorMixin):
+class DeviceTree(DeviceTreeBase, PopulatorMixin, EventHandlerMixin):
def __init__(self, conf=None, passphrase=None, luks_dict=None):
DeviceTreeBase.__init__(self, conf=conf)
PopulatorMixin.__init__(self, passphrase=passphrase, luks_dict=luks_dict)
+ EventHandlerMixin.__init__(self)
# pylint: disable=arguments-differ
def reset(self, conf=None, passphrase=None, luks_dict=None):
diff --git a/blivet/errors.py b/blivet/errors.py
index be130f1..885e105 100644
--- a/blivet/errors.py
+++ b/blivet/errors.py
@@ -269,3 +269,7 @@ class EventManagerError(StorageError):
class EventParamError(StorageError):
pass
+
+
+class EventHandlingError(StorageError):
+ pass
diff --git a/blivet/events/handler.py b/blivet/events/handler.py
new file mode 100644
index 0000000..2072c48
--- /dev/null
+++ b/blivet/events/handler.py
@@ -0,0 +1,226 @@
+# events/handler.py
+# Event handler mixin class.
+#
+# Copyright (C) 2015-2016 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 Lesser 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 Lesser General Public License for more details. You should have
+# received a copy of the GNU Lesser 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 Lesser 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(a)redhat.com>
+#
+
+from ..errors import DeviceError, EventHandlingError
+from ..devices import DM_MAJORS, MD_MAJORS
+from .. import udev
+from ..threads import SynchronizedMeta
+
+from .manager import event_manager
+
+import logging
+log = logging.getLogger("blivet")
+event_log = logging.getLogger("blivet.event")
+
+
+class EventHandlerMixin(metaclass=SynchronizedMeta):
+ def __init__(self):
+ event_manager.handler_cb = self.handle_event
+
+ def handle_event(self, event):
+ """ Handle an event on a block device.
+
+ :param :class:`~.event.Event` event: information about the event
+
+ TODO: Rename all this stuff so it's explicit that it only handles uevents.
+ """
+ # delegate event to appropriate handler
+ handlers = {"add": self._handle_add_event,
+ "change": self._handle_change_event,
+ "remove": self._handle_remove_event}
+
+ handler = handlers.get(event.action)
+ if handler is not None:
+ handler(event)
+
+ def _event_device_is_dm(self, event):
+ """ Return True if event operand is a dm device.
+
+ Since this may be run on add events it does not require the dm/
+ subdirectory be present in the device's sysfs root, unlike
+ udev.device_is_dm.
+
+ XXX Should this replace udev.device_is_dm?
+ """
+ return udev.device_get_major(event.info) in DM_MAJORS
+
+ def _event_device_is_md(self, event):
+ """ Return True if event operand is a dm device.
+
+ Since this may be run on add events it does not require the md/
+ subdirectory be present in the device's sysfs root, unlike
+ udev.device_is_md.
+
+ XXX Should this replace udev.device_is_md?
+ """
+ return udev.device_get_major(event.info) in MD_MAJORS
+
+ def _should_ignore_add_event(self, event):
+ """ Return True if event is an add event that should be ignored.
+
+ add events on md and dm devices should be ignored in general. When the
+ operation is complete there will be a change event.
+ """
+ # udev.device_is_md and udev.device_is_dm are not sufficient for this purpose
+ # because there are sometimes events when dm/ or md/ subdir does not exist.
+ return (event.action == "add" and
+ (self._event_device_is_dm(event) or self._event_device_is_md(event)))
+
+ def _event_device_is_physical_disk(self, event):
+ """ Return True if event device is a physical disk. """
+ # udev.device_is_md and udev.device_is_dm are not sufficient for this purpose
+ # because there are sometimes events when dm/ or md/ subdir does not exist.
+ return (self._udev_device_is_disk(event.info) and
+ not self._event_device_is_dm(event) and
+ not self._event_device_is_md(event))
+
+ def _handle_add_event(self, event):
+ """ Handle an "add" event.
+
+ Add events should correlate to activation rather than creation in
+ most cases. When a device is added, there is generally a change
+ event on the new device's parent(s). The obvious exception to this
+ rule is the addition of a new disk, which has no parents.
+ """
+ # ignore add on dm and md devices
+ if self._should_ignore_add_event(event):
+ return
+
+ # Try to look up the device. Do not look it up by sysfs path since in all
+ # likelihood blivet thinks the device is not active and therefore has no
+ # sysfs path.
+ # XXX We need an existing device here, so we should also be checking destroy actions.
+ device = self.get_device_by_name(udev.device_get_name(event.info), hidden=True)
+ if device is None:
+ device = self.get_device_by_uuid(event.info.get("UUID_SUB", udev.device_get_uuid(event.info)),
+ hidden=True)
+
+ # XXX Don't change anything (except simple attributes?) if actions are being executed.
+ if device is None and self._event_device_is_physical_disk(event):
+ log.info("disk %s was added", udev.device_get_name(event.info))
+ self.handle_device(event.info)
+ elif device is not None and device.exists:
+ log.info("device %s was activated", device.name)
+ # device was activated from outside, so update the sysfs path
+ device.sysfs_path = udev.device_get_sysfs_path(event.info)
+
+ def _handle_format_change(self, event, device):
+ helper_class = self._get_format_helper(event.info, device) # pylint: disable=no-member
+ helper = helper_class(self, event.info, device=device)
+ new_type = helper.type_spec
+ old_type = device.format.type
+
+ new_uuid = helper._get_kwargs().get("uuid")
+ old_uuid = device.format.uuid
+
+ # If the type has changed it has been reformatted. Easy.
+ # If the type is unchanged and the UUID has changed, it could be a change to
+ # the UUID or a reformat. Is it worthwhile to try to differentiate?
+ log.debug("old_type=%-8s ; old_uuid=%-16s", old_type, old_uuid)
+ log.debug("new_type=%-8s ; new_uuid=%-16s", new_type, new_uuid)
+ if new_type == old_type and new_uuid == old_uuid and not self.actions.processing:
+ helper.update()
+ return
+
+ if self.actions.processing:
+ return
+
+ self.cancel_disk_actions(device.disks)
+
+ # The device was reformatted, but we can't blindly remove all children since
+ # it could have been a member of a still-intact container.
+ if getattr(device.format, "container_uuid", None):
+ container = device.children[0]
+ if len(container.parents) > 1:
+ try:
+ container.parents.remove(device)
+ except DeviceError as e:
+ log.error("error removing member %s from container %s: %s",
+ device.name, container.name, str(e))
+ raise EventHandlingError("reformatted container member")
+
+ self.recursive_remove(device, actions=False, remove_device=False)
+
+ helper.run()
+
+ def _handle_change_event(self, event):
+ """ Handle a "change" event.
+
+ This could be any number of things, including activation of md/dm
+ devices.
+ """
+ name = udev.device_get_name(event.info)
+ if (name.startswith("dm-") or name.startswith("md")) and name == event.info.sys_name:
+ log.debug("ignoring event on virtual device %s with no symbolic name", name)
+ return
+
+ if self._event_device_is_md(event) or self._event_device_is_dm(event):
+ self._handle_add_event(event)
+
+ # Try to look up the device in the devicetree.
+ # We're expecting the device to already be active, so we try the lookup
+ # by sysfs path first.
+ device = self.get_device_by_sysfs_path(udev.device_get_sysfs_path(event.info))
+ if device is None:
+ # Lookup by sysfs path failed. Try looking it up by name.
+ device = self.get_device_by_name(udev.device_get_name(event.info))
+
+ if device is None:
+ log.debug("failed to look up device")
+ return
+
+ if not device.exists:
+ log.debug("device lookup returned a non-existent device")
+ return
+
+ log.info("device %s was changed", device.name)
+
+ helper_class = self._get_device_helper(event.info) # pylint: disable=no-member
+ if helper_class is not None:
+ helper = helper_class(self, event.info, device=device)
+ helper.update()
+
+ # check for metadata changes, but do not make changes to the devicetree
+ # during action processing
+ self._handle_format_change(event, device)
+
+ def _handle_remove_event(self, event):
+ """ Handle a "remove" event.
+
+ Remove events correlate to deactivation or removal, but we will
+ use change events on parent devices to detect removal.
+ """
+ # XXX We need an existing device here.
+ device = self.get_device_by_sysfs_path(udev.device_get_sysfs_path(event.info))
+ if device is None:
+ device = self.get_device_by_name(udev.device_get_name(event.info))
+
+ if device is None:
+ return
+
+ if self._event_device_is_physical_disk(event):
+ log.info("disk %s was removed", device.name)
+ self._remove_device(device)
+ else:
+ log.info("device %s was deactivated", device.name)
+ # device was deactivated from outside, so clear the sysfs path
+ device.sysfs_path = ''
diff --git a/blivet/populator/populator.py b/blivet/populator/populator.py
index fa7ab92..7e3595f 100644
--- a/blivet/populator/populator.py
+++ b/blivet/populator/populator.py
@@ -577,3 +577,4 @@ def handle_nodev_filesystems(self):
n = len([d for d in self.devices if d.format.type == fstype])
device._name += ".%d" % n
self._add_device(device)
+
--
To view this commit on github, visit https://github.com/rhinstaller/blivet/commit/ceed2d42c1b3c960003fb80f624f...
8 years, 4 months
[master 24/30] Add methods to wrap helper getters.
by dashea
From: David Lehman <dlehman(a)redhat.com>
---
blivet/populator/populator.py | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/blivet/populator/populator.py b/blivet/populator/populator.py
index f0e1116..fa7ab92 100644
--- a/blivet/populator/populator.py
+++ b/blivet/populator/populator.py
@@ -269,6 +269,12 @@ def _update_exclusive_disks(self, device):
if parent.name not in self.exclusive_disks:
self.exclusive_disks.append(parent.name)
+ def _get_format_helper(self, info, device=None):
+ return get_format_helper(info, device=device)
+
+ def _get_device_helper(self, info):
+ return get_device_helper(info)
+
def handle_device(self, info, update_orig_fmt=False):
"""
:param :class:`pyudev.Device` info: udev info for the device
@@ -301,7 +307,7 @@ def handle_device(self, info, update_orig_fmt=False):
if device:
device_added = False
else:
- helper_class = get_device_helper(info)
+ helper_class = self._get_device_helper(info)
if helper_class is not None:
device = helper_class(self, info).run()
@@ -339,7 +345,7 @@ def handle_format(self, info, device):
log.debug("no type or existing type for %s, bailing", name)
return
- helper_class = get_format_helper(info, device=device)
+ helper_class = self._get_format_helper(info, device=device)
if helper_class is not None:
helper_class(self, info, device).run()
--
To view this commit on github, visit https://github.com/rhinstaller/blivet/commit/baaa4df4e5a97309fda152ca0af2...
8 years, 4 months
[master 23/30] Add PopulatorHelper method to update device/formats.
by dashea
From: David Lehman <dlehman(a)redhat.com>
The primary purpose of this is to be called from an event handler
for "change" uevents.
---
blivet/populator/helpers/devicepopulator.py | 17 +++++
blivet/populator/helpers/disklabel.py | 52 ++++++++++++++
blivet/populator/helpers/formatpopulator.py | 4 ++
blivet/populator/helpers/lvm.py | 107 +++++++++++++++++++++++++---
blivet/populator/helpers/mdraid.py | 4 ++
blivet/populator/helpers/populatorhelper.py | 8 +++
tests/populator_test.py | 20 ++++--
7 files changed, 200 insertions(+), 12 deletions(-)
diff --git a/blivet/populator/helpers/devicepopulator.py b/blivet/populator/helpers/devicepopulator.py
index f96acc3..ca7dedc 100644
--- a/blivet/populator/helpers/devicepopulator.py
+++ b/blivet/populator/helpers/devicepopulator.py
@@ -21,6 +21,7 @@
#
from .populatorhelper import PopulatorHelper
+from ... import udev
# pylint: disable=abstract-method
@@ -33,3 +34,19 @@ class DevicePopulator(PopulatorHelper):
@classmethod
def match(cls, data):
return False
+
+ def _handle_rename(self):
+ name = udev.device_get_name(self.data)
+ if self.device.name != name:
+ self.device.name = name
+ # TODO: update name registry -- better yet, generate the name list on demand
+
+ def _handle_resize(self):
+ old_size = self.device.current_size
+ self.device.update_size()
+ if old_size != self.device.current_size:
+ self._devicetree.cancel_disk_actions(self.device.disks)
+
+ def update(self):
+ self._handle_rename()
+ self._handle_resize()
diff --git a/blivet/populator/helpers/disklabel.py b/blivet/populator/helpers/disklabel.py
index 0bd6961..af2ae51 100644
--- a/blivet/populator/helpers/disklabel.py
+++ b/blivet/populator/helpers/disklabel.py
@@ -20,6 +20,7 @@
# Red Hat Author(s): David Lehman <dlehman(a)redhat.com>
#
+import os
import gi
gi.require_version("BlockDev", "1.0")
@@ -101,3 +102,54 @@ def run(self):
self.device.format = formats.get_format(_("Invalid Disk Label"))
else:
self.device.format = fmt
+
+ def update(self):
+ self.device.format.update_parted_disk()
+
+ # Reconcile the new partition list with the old.
+
+ # Remove any partitions that are no longer present from the devicetree.
+ # On GPT we will use the partition UUIDs.
+ # On DOS we will use the partition start/end sectors since the UUIDs are a joke.
+ #
+ # XXX We use the udev device list because we think it could be more current. Does
+ # this even make sense?
+ udev_devices = udev.get_devices()
+ parted_partitions = self.device.format.partitions
+ for partition in self.device.children[:]:
+ start_sector = partition.parted_partition.geometry.start
+ udev_device = None
+ if self.device.format.label_type == "gpt":
+ udev_device = next((ud for ud in udev_devices
+ if udev.device_get_partition_uuid(ud) == partition.uuid),
+ None)
+ else:
+ udev_device = next((ud for ud in udev_devices
+ if udev.device_get_partition_disk(ud) == self.device.name and
+ int(ud.get("ID_PART_ENTRY_OFFSET")) == start_sector),
+ None)
+
+ if udev_device is None:
+ self._devicetree.recursive_remove(partition, modparent=False, actions=False)
+ continue
+
+ parted_partition = next((pp for pp in parted_partitions
+ if os.path.basename(pp.path) == udev.device_get_name(udev_device)),
+ None)
+ log.debug("got parted_partition %s for partition %s",
+ parted_partition.path.split("/")[-1], partition.name)
+ partition.parted_partition = parted_partition
+
+ # Add any new partitions to the devicetree.
+ for parted_partition in self.device.format.partitions:
+ partition_name = os.path.basename(parted_partition.path)
+ start_sector = parted_partition.geometry.start
+ udev_device = next((ud for ud in udev_devices
+ if udev.device_get_name(ud) == partition_name and
+ int(ud.get("ID_PART_ENTRY_OFFSET")) == start_sector),
+ None)
+
+ if udev_device is None:
+ continue
+
+ self._devicetree.handle_device(udev_device)
diff --git a/blivet/populator/helpers/formatpopulator.py b/blivet/populator/helpers/formatpopulator.py
index 1f7c1fb..8032b05 100644
--- a/blivet/populator/helpers/formatpopulator.py
+++ b/blivet/populator/helpers/formatpopulator.py
@@ -89,3 +89,7 @@ def run(self):
type_spec, self.device.name)
self.device.format = formats.DeviceFormat(device=self.device.path, exists=True)
return
+
+ def update(self):
+ if hasattr(self.device.format, "label"):
+ self.device.format.label = udev.device_get_label(self.data)
diff --git a/blivet/populator/helpers/lvm.py b/blivet/populator/helpers/lvm.py
index ff18ea0..7ab6c7c 100644
--- a/blivet/populator/helpers/lvm.py
+++ b/blivet/populator/helpers/lvm.py
@@ -67,6 +67,17 @@ def run(self):
return self._devicetree.get_device_by_name(name)
+ def _handle_rename(self):
+ name = self.data.get("DM_LV_NAME")
+ if not name:
+ return
+
+ # device.name is of the form "%s-%s" % (vg_name, lv_name), while
+ # device.lvname is the name of the lv without the vg name.
+ if self.device.lvname != name:
+ self.device.name = name
+ # TODO: update name registry
+
class LVMFormatPopulator(FormatPopulator):
priority = 100
@@ -95,14 +106,30 @@ def _get_kwargs(self):
return kwargs
- def _handle_vg_lvs(self, vg_device):
- """ Handle setup of the LV's in the vg_device. """
+ def _get_vg_device(self):
+ return self._devicetree.get_device_by_uuid(self.device.format.container_uuid, incomplete=True)
+
+ def _update_lvs(self):
+ """ Handle setup of the LVs in the vg_device. """
+ log_method_call(self, pv=self.device.name)
+ vg_device = self._get_vg_device()
+ if vg_device is None:
+ # orphan pv
+ return
+
vg_name = vg_device.name
lv_info = dict((k, v) for (k, v) in iter(self._devicetree.lv_info.items())
if v.vg_name == vg_name)
+ # FIXME: This should account for added/removed LVs.
self._devicetree.names.extend(n for n in lv_info.keys() if n not in self._devicetree.names)
+ for lv_device in vg_device.lvs[:]:
+ if lv_device.name not in lv_info:
+ log.info("lv %s was removed", lv_device.name)
+ self._devicetree.cancel_disk_actions(vg_device.disks)
+ self._devicetree.recursive_remove(lv_device, actions=False)
+
if not vg_device.complete:
log.warning("Skipping LVs for incomplete VG %s", vg_name)
return
@@ -152,9 +179,17 @@ def add_lv(lv):
lv_kwargs = {}
name = "%s-%s" % (vg_name, lv_name)
- if self._devicetree.get_device_by_name(name):
+ lv_device = self._devicetree.get_device_by_name(name)
+ if lv_device is not None:
# some lvs may have been added on demand below
log.debug("already added %s", name)
+ if lv_size != lv_device.current_size:
+ # lvresize can operate on an inactive lv, in which case
+ # the only notification we will receive is a change uevent
+ # for the pv(s)
+ lv_device.update_size(newsize=lv_size)
+ self._devicetree.cancel_disk_actions(vg_device.disks)
+
return
if lv_attr[0] in 'Ss':
@@ -308,8 +343,7 @@ def create_internal_lv(lv):
else:
log.warning("Failed to determine parent LV for an internal LV '%s'", lv.name)
- def run(self):
- super().run()
+ def _add_vg_device(self):
pv_info = self._devicetree.pv_info.get(self.device.path, None)
if pv_info:
vg_name = pv_info.vg_name
@@ -323,9 +357,9 @@ def run(self):
return
vg_device = self._devicetree.get_device_by_uuid(vg_uuid, incomplete=True)
- if vg_device:
+ if vg_device and self.device not in vg_device.parents:
vg_device.parents.append(self.device)
- else:
+ elif vg_device is None:
same_name = self._devicetree.get_device_by_name(vg_name)
if isinstance(same_name, LVMVolumeGroupDevice):
raise DuplicateVGError("multiple LVM volume groups with the same name (%s)" % vg_name)
@@ -353,4 +387,61 @@ def run(self):
exists=True)
self._devicetree._add_device(vg_device)
- self._handle_vg_lvs(vg_device)
+ def run(self):
+ log_method_call(self, pv=self.device.name)
+ super().run()
+ self._add_vg_device()
+ self._update_lvs()
+
+ def _handle_vg_rename(self):
+ vg_device = self._get_vg_device()
+ if vg_device is None:
+ return
+
+ pv_info = self._devicetree.pv_info.get(self.device.path, None)
+ if not pv_info or not pv_info.vg_name:
+ return
+
+ vg_name = pv_info.vg_name
+ if vg_device.name != vg_name:
+ vg_device.name = vg_name
+ # TODO: update name registry
+
+ def _update_pv_format(self):
+ pv_info = self._devicetree.pv_info.get(self.device.path, None)
+ if not pv_info:
+ return
+
+ self.device.format.vg_name = pv_info.vg_name
+ self.device.format.vg_uuid = pv_info.vg_uuid
+ self.device.format.pe_start = Size(pv_info.pe_start)
+ self.device.format.pe_free = Size(pv_info.pv_free)
+
+ def update(self):
+ self._devicetree.drop_lvm_cache()
+ self._update_pv_format()
+ pv_info = self._devicetree.pv_info.get(self.device.path, None)
+ vg_device = self._get_vg_device()
+ if vg_device is None:
+ # The VG device isn't in the tree. The PV might have just been
+ # added to a VG.
+ if pv_info and pv_info.vg_name:
+ # Handle adding orphan pv to a vg.
+ self._add_vg_device()
+ elif self.device.children:
+ # The PV was removed from its VG or the VG was removed.
+ vg_device = self.device.children[0]
+ if len(vg_device.parents) > 1:
+ vg_device.parents.remove(self.device)
+ else:
+ self._devicetree.recursive_remove(vg_device, actions=False)
+ return
+ else:
+ # The VG device is in the tree. Check if the PV still belongs to it.
+ if pv_info and pv_info.vg_name:
+ # This is the "normal" case: the pv is still part of the
+ # vg it was part of last time we looked.
+ self._handle_vg_rename()
+
+ # handles vg rename, lv add, lv remove, lv resize (inactive lv)
+ self._update_lvs()
diff --git a/blivet/populator/helpers/mdraid.py b/blivet/populator/helpers/mdraid.py
index 22df189..fe9e63d 100644
--- a/blivet/populator/helpers/mdraid.py
+++ b/blivet/populator/helpers/mdraid.py
@@ -205,3 +205,7 @@ def run(self):
return
self._devicetree.handle_device(array_info, update_orig_fmt=True)
+
+ def update(self):
+ # update array based on current md data
+ pass
diff --git a/blivet/populator/helpers/populatorhelper.py b/blivet/populator/helpers/populatorhelper.py
index b2a0f59..f29cfd8 100644
--- a/blivet/populator/helpers/populatorhelper.py
+++ b/blivet/populator/helpers/populatorhelper.py
@@ -58,3 +58,11 @@ def run(self):
perform all processing related to the device's formatting.
"""
raise NotImplementedError()
+
+ def update(self):
+ """ Handle changes associated with an event.
+
+ This method should handle any changes to an existing format instance.
+ It should not handle reformatting.
+ """
+ pass
diff --git a/tests/populator_test.py b/tests/populator_test.py
index c6aa940..bd76289 100644
--- a/tests/populator_test.py
+++ b/tests/populator_test.py
@@ -895,18 +895,21 @@ def test_run(self, *args):
device.path = sentinel.pv_path
vg_device = Mock()
+ vg_device.parents = []
+ vg_device.lvs = []
get_device_by_uuid.return_value = vg_device
with patch("blivet.udev.device_get_format", return_value=self.udev_type):
helper = self.helper_class(devicetree, data, device)
+ self.assertFalse(device in vg_device.parents)
helper.run()
self.assertEqual(device.format.type,
self.blivet_type,
msg="Wrong format type after FormatPopulator.run on %s" % self.udev_type)
- self.assertEqual(get_device_by_uuid.call_count, 1)
+ self.assertEqual(get_device_by_uuid.call_count, 3)
get_device_by_uuid.assert_called_with(pv_info.vg_uuid, incomplete=True)
- vg_device.parents.append.assert_called_once_with(device) # pylint: disable=no-member
+ self.assertTrue(device in vg_device.parents)
get_device_by_uuid.reset_mock()
get_device_by_uuid.return_value = None
@@ -926,7 +929,7 @@ def test_run(self, *args):
self.blivet_type,
msg="Wrong format type after FormatPopulator.run on %s" % self.udev_type)
- self.assertEqual(get_device_by_uuid.call_count, 1)
+ self.assertEqual(get_device_by_uuid.call_count, 2)
get_device_by_uuid.assert_called_with(pv_info.vg_uuid, incomplete=True)
vg_device = devicetree.get_device_by_name(pv_info.vg_name)
self.assertTrue(vg_device is not None)
@@ -957,14 +960,23 @@ def test_run(self, *args):
lv2_name: lv2}
devicetree._lvs_cache = lv_info
+ device.format.container_uuid = pv_info.vg_uuid
+
+ def gdbu(uuid, **kwargs): # pylint: disable=unused-argument
+ # This version doesn't check format UUIDs
+ return next((d for d in devicetree.devices if d.uuid == uuid), None)
+ get_device_by_uuid.side_effect = gdbu
+
with patch("blivet.udev.device_get_format", return_value=self.udev_type):
+ self.assertEqual(devicetree.get_device_by_name(pv_info.vg_name, incomplete=True), None)
helper = self.helper_class(devicetree, data, device)
helper.run()
self.assertEqual(device.format.type,
self.blivet_type,
msg="Wrong format type after FormatPopulator.run on %s" % self.udev_type)
- self.assertEqual(get_device_by_uuid.call_count, 3) # one for vg and one for each lv
+ self.assertEqual(get_device_by_uuid.call_count, 4,
+ get_device_by_uuid.mock_calls) # two for vg and one for each lv
get_device_by_uuid.assert_has_calls([call(pv_info.vg_uuid, incomplete=True),
call(lv1.uuid),
call(lv2.uuid)],
--
To view this commit on github, visit https://github.com/rhinstaller/blivet/commit/3a139505f13b0353aebca9155ddd...
8 years, 4 months
[master 22/30] Add classes for managing external events.
by dashea
From: David Lehman <dlehman(a)redhat.com>
---
blivet/errors.py | 7 ++
blivet/events/__init__.py | 0
blivet/events/manager.py | 252 ++++++++++++++++++++++++++++++++++++++++++++++
blivet/flags.py | 5 +
tests/events_test.py | 73 ++++++++++++++
5 files changed, 337 insertions(+)
create mode 100644 blivet/events/__init__.py
create mode 100644 blivet/events/manager.py
create mode 100644 tests/events_test.py
diff --git a/blivet/errors.py b/blivet/errors.py
index c446b28..be130f1 100644
--- a/blivet/errors.py
+++ b/blivet/errors.py
@@ -261,4 +261,11 @@ class DeviceFactoryError(StorageError):
class AvailabilityError(StorageError):
""" Raised if problem determining availability of external resource. """
+
+
+class EventManagerError(StorageError):
+ pass
+
+
+class EventParamError(StorageError):
pass
diff --git a/blivet/events/__init__.py b/blivet/events/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/blivet/events/manager.py b/blivet/events/manager.py
new file mode 100644
index 0000000..aa85890
--- /dev/null
+++ b/blivet/events/manager.py
@@ -0,0 +1,252 @@
+# events/manager.py
+# Event management classes.
+#
+# Copyright (C) 2015-2016 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 Lesser 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 Lesser General Public License for more details. You should have
+# received a copy of the GNU Lesser 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 Lesser 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(a)redhat.com>
+#
+
+import abc
+from threading import RLock, Thread
+import pyudev
+import time
+
+from .. import udev
+from .. import util
+from ..errors import EventManagerError, EventParamError
+from ..flags import flags
+from ..threads import blivet_lock
+
+import logging
+event_log = logging.getLogger("blivet.event")
+
+
+#
+# Event
+#
+class Event(util.ObjectID):
+ """ An external event. """
+ def __init__(self, action, device, info=None):
+ """
+ :param str action: a string describing the type of event
+ :param str device: (friendly) basename of device event operated on
+ :param info: information about the device
+ """
+ self.initialized = time.time()
+ self.action = action
+ self.device = device
+ self.info = info
+
+ def __str__(self):
+ return "%s %s [%d]" % (self.action, self.device, self.id)
+
+
+class EventMask(util.ObjectID):
+ """ Specification of events to ignore. """
+ def __init__(self, device=None, action=None, partitions=False):
+ """
+ :keyword str device: basename of device to mask events on
+ :keyword str action: action type to mask events of
+ :keyword bool partitions: also match events on child partitions
+ """
+ self.device = device
+ self.action = action
+ self._partitions = partitions
+
+ def _device_match(self, event):
+ if self.device is None:
+ return True
+
+ if self.device == event.device:
+ return True
+
+ if (not self._partitions or
+ not (udev.device_is_partition(event.info) or udev.device_is_dm_partition(event.info))):
+ return False
+
+ disk = udev.device_get_partition_disk(event.info)
+ return disk and self.device == disk
+
+ def _action_match(self, event):
+ return self.action is None or self.action == event.action
+
+ def match(self, event):
+ """ Return True if this mask applies to the specified event.
+
+ ..note::
+
+ A mask whose device is a partitioned disk will match events
+ on its partitions.
+ """
+ return self._device_match(event) and self._action_match(event)
+
+
+#
+# EventManager
+#
+class EventManager(object, metaclass=abc.ABCMeta):
+ def __init__(self, handler_cb=None, notify_cb=None):
+ self._handler_cb = None
+ self._notify_cb = None
+
+ if handler_cb is not None:
+ self.handler_cb = handler_cb
+
+ if notify_cb is not None:
+ self.notify_cb = notify_cb
+
+ self._mask_list = list()
+ """List of masks specifying events that should be ignored."""
+
+ self._lock = RLock()
+ """Re-entrant lock to serialize access to mask list."""
+
+ @property
+ def handler_cb(self):
+ """ the main event handler """
+ return self._handler_cb
+
+ @handler_cb.setter
+ def handler_cb(self, cb):
+ if not callable(cb):
+ raise EventParamError("handler must be callable")
+
+ self._handler_cb = cb
+
+ @property
+ def notify_cb(self):
+ """ notification handler that runs after the main event handler """
+ return self._notify_cb
+
+ @notify_cb.setter
+ def 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
+
+ @abc.abstractproperty
+ def enabled(self):
+ return False
+
+ @abc.abstractmethod
+ def enable(self):
+ """ Enable monitoring and handling of events.
+
+ :raises: :class:`~.errors.EventManagerError` if no callback defined
+ """
+ if self.handler_cb is None:
+ raise EventManagerError("cannot enable handler with no callback")
+
+ event_log.info("enabling event handling")
+
+ @abc.abstractmethod
+ def disable(self):
+ """ Disable monitoring and handling of events. """
+ event_log.info("disabling event handling")
+
+ def _mask_event(self, event):
+ """ Return True if this event should be ignored """
+ with self._lock:
+ return next((m for m in self._mask_list if m.match(event)), None) is not None
+
+ def add_mask(self, device=None, action=None, partitions=False):
+ """ Add an event mask and return the new :class:`EventMask`.
+
+ :keyword str device: ignore events on the named device
+ :keyword str action: ignore events of the specified type
+ :keyword bool partitions: also match events on child partitions
+
+ device of None means mask events on all devices
+ action of None means mask all event types
+ """
+ em = EventMask(device=device, action=action, partitions=partitions)
+ with self._lock:
+ self._mask_list.append(em)
+ return em
+
+ def remove_mask(self, mask):
+ try:
+ with self._lock:
+ self._mask_list.remove(mask)
+ except ValueError:
+ pass
+
+ @abc.abstractmethod
+ def _create_event(self, *args, **kwargs):
+ pass
+
+ def handle_event(self, *args, **kwargs):
+ """ Handle an event by running the registered handler.
+
+ Currently the handler is run in a separate thread. This removes any
+ threading-related expectations about the behavior of whatever is
+ telling us about the events.
+ """
+ event = self._create_event(*args, **kwargs)
+ event_log.debug("new event: %s", event)
+
+ if self._mask_event(event):
+ event_log.debug("ignoring masked event %s", event)
+ return
+
+ t = Thread(target=self.handler_cb,
+ name="event%d" % event.id,
+ kwargs={"event": event},
+ daemon=True)
+ t.start()
+
+
+class UdevEventManager(EventManager):
+ def __init__(self, handler_cb=None, notify_cb=None):
+ super().__init__(handler_cb=handler_cb, notify_cb=notify_cb)
+ self._pyudev_observer = None
+
+ @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().enable()
+ monitor = pyudev.Monitor.from_netlink(udev.global_udev)
+ monitor.filter_by("block")
+ self._pyudev_observer = pyudev.MonitorObserver(monitor,
+ callback=self.handle_event,
+ name="monitor")
+ self._pyudev_observer.start()
+ with blivet_lock:
+ flags.uevents = True
+
+ def disable(self):
+ """ Disable monitoring and handling of block device uevents. """
+ super().disable()
+ if self.enabled:
+ self._pyudev_observer.stop()
+
+ self._pyudev_observer = None
+ with blivet_lock:
+ flags.uevents = False
+
+ def __call__(self, *args, **kwargs):
+ return self
+
+ def _create_event(self, *args, **kwargs):
+ return Event(args[0].action, udev.device_get_name(args[0]), args[0])
+
+
+event_manager = UdevEventManager()
diff --git a/blivet/flags.py b/blivet/flags.py
index 25e480d..c879572 100644
--- a/blivet/flags.py
+++ b/blivet/flags.py
@@ -36,6 +36,11 @@ def __init__(self):
self.debug = False
#
+ # minor modes
+ #
+ self.uevents = False
+
+ #
# minor modes (installer-specific)
#
self.automated_install = False
diff --git a/tests/events_test.py b/tests/events_test.py
new file mode 100644
index 0000000..511fb7f
--- /dev/null
+++ b/tests/events_test.py
@@ -0,0 +1,73 @@
+
+from unittest import TestCase
+from unittest.mock import Mock
+
+from blivet.events.manager import Event, EventManager
+
+
+class FakeEventManager(EventManager):
+ def enabled(self):
+ return False
+
+ def enable(self):
+ pass
+
+ def disable(self):
+ pass
+
+ def _create_event(self, *args, **kwargs):
+ return Event(*args, **kwargs)
+
+
+class EventManagerTest(TestCase):
+ def testEventMask(self):
+ handler_cb = Mock()
+ mgr = FakeEventManager(handler_cb=handler_cb)
+
+ device = "sdc"
+ action = "add"
+ mgr.handle_event(action, device)
+ self.assertEqual(handler_cb.call_count, 1)
+ event = handler_cb.call_args[1]["event"] # pylint: disable=unsubscriptable-object
+ self.assertEqual(event.device, device)
+ self.assertEqual(event.action, action)
+
+ # mask matches device but not action -> event is handled
+ handler_cb.reset_mock()
+ mask = mgr.add_mask(device=device, action=action + 'x')
+ mgr.handle_event(action, device)
+ self.assertEqual(handler_cb.call_count, 1)
+ event = handler_cb.call_args[1]["event"] # pylint: disable=unsubscriptable-object
+ self.assertEqual(event.device, device)
+ self.assertEqual(event.action, action)
+
+ # mask matches action but not device -> event is handled
+ handler_cb.reset_mock()
+ mask = mgr.add_mask(device=device + 'x', action=action)
+ mgr.handle_event(action, device)
+ self.assertEqual(handler_cb.call_count, 1)
+ event = handler_cb.call_args[1]["event"] # pylint: disable=unsubscriptable-object
+ self.assertEqual(event.device, device)
+ self.assertEqual(event.action, action)
+
+ # mask matches device and action -> event is ignored
+ handler_cb.reset_mock()
+ mgr.remove_mask(mask)
+ mask = mgr.add_mask(device=device, action=action)
+ mgr.handle_event(action, device)
+ self.assertEqual(handler_cb.call_count, 0)
+
+ # device-only mask matches -> event is ignored
+ handler_cb.reset_mock()
+ mgr.remove_mask(mask)
+ mask = mgr.add_mask(device=device)
+ mgr.handle_event(action, device)
+ self.assertEqual(handler_cb.call_count, 0)
+
+ # action-only mask matches -> event is ignored
+ handler_cb.reset_mock()
+ mgr.remove_mask(mask)
+ mask = mgr.add_mask(action=action)
+ mgr.handle_event(action, device)
+ self.assertEqual(handler_cb.call_count, 0)
+ mgr.remove_mask(mask)
--
To view this commit on github, visit https://github.com/rhinstaller/blivet/commit/a3f928bc21527345083c645e2825...
8 years, 4 months
[master 20/30] Allow passing a device to any PopulatorHelper
constructor.
by dashea
From: David Lehman <dlehman(a)redhat.com>
---
blivet/populator/helpers/formatpopulator.py | 10 ----------
blivet/populator/helpers/populatorhelper.py | 5 ++++-
blivet/populator/populator.py | 2 +-
3 files changed, 5 insertions(+), 12 deletions(-)
diff --git a/blivet/populator/helpers/formatpopulator.py b/blivet/populator/helpers/formatpopulator.py
index 014d6a5..1f7c1fb 100644
--- a/blivet/populator/helpers/formatpopulator.py
+++ b/blivet/populator/helpers/formatpopulator.py
@@ -34,16 +34,6 @@ class FormatPopulator(PopulatorHelper):
priority = 0
_type_specifier = None
- def __init__(self, devicetree, data, device):
- """
- :param :class:`blivet.devicetree.DeviceTree` devicetree: the calling devicetree
- :param :class:`pyudev.Device` data: udev data describing a device
- :param device: device instance corresponding to the udev data
- :type device: :class:`~.devices.StorageDevice`
- """
- super().__init__(devicetree, data)
- self.device = device
-
@classmethod
def match(cls, data, device): # pylint: disable=arguments-differ,unused-argument
""" Return True if this helper is appropriate for the given device.
diff --git a/blivet/populator/helpers/populatorhelper.py b/blivet/populator/helpers/populatorhelper.py
index fc7cbe8..b2a0f59 100644
--- a/blivet/populator/helpers/populatorhelper.py
+++ b/blivet/populator/helpers/populatorhelper.py
@@ -27,13 +27,16 @@ class PopulatorHelper:
priority = 100
""" Higher priority value gets checked for match first. """
- def __init__(self, devicetree, data):
+ def __init__(self, devicetree, data, device=None):
"""
:param :class:`~.DeviceTree` devicetree: the calling devicetree
:param :class:`pyudev.Device` data: udev data describing a device
+ :keyword device: device instance corresponding to the udev data
+ :type device: :class:`~.devices.StorageDevice`
"""
self._devicetree = devicetree
self.data = data
+ self.device = device
@classmethod
def match(cls, data):
diff --git a/blivet/populator/populator.py b/blivet/populator/populator.py
index 4718fe8..f0e1116 100644
--- a/blivet/populator/populator.py
+++ b/blivet/populator/populator.py
@@ -339,7 +339,7 @@ def handle_format(self, info, device):
log.debug("no type or existing type for %s, bailing", name)
return
- helper_class = get_format_helper(info, device)
+ helper_class = get_format_helper(info, device=device)
if helper_class is not None:
helper_class(self, info, device).run()
--
To view this commit on github, visit https://github.com/rhinstaller/blivet/commit/0c4565c14f97ffb6dab13ff68d0c...
8 years, 4 months
[master 19/30] Plumb modparent through DeviceTree.recursive_remove.
by dashea
From: David Lehman <dlehman(a)redhat.com>
When we're removing partitions to account for changes
made to disklabels outside of blivet we'll need to be
able to skip the remove_hook.
---
blivet/devicetree.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/blivet/devicetree.py b/blivet/devicetree.py
index e692330..2b750af 100644
--- a/blivet/devicetree.py
+++ b/blivet/devicetree.py
@@ -204,11 +204,12 @@ def _remove_device(self, dev, force=None, modparent=True):
dev.name,
dev.id)
- def recursive_remove(self, device, actions=True, remove_device=True):
+ def recursive_remove(self, device, actions=True, remove_device=True, modparent=True):
""" Remove a device after removing its dependent devices.
:param :class:`~.devices.StorageDevice` device: the device to remove
:keyword bool actions: whether to schedule actions for the removal
+ :keyword bool modparent: whether to update parent device upon removal
:keyword bool remove_device: whether to remove the root device
If the device is not a leaf, all of its dependents are removed
@@ -240,7 +241,7 @@ def recursive_remove(self, device, actions=True, remove_device=True):
else:
if not leaf.format_immutable:
leaf.format = None
- self._remove_device(leaf)
+ self._remove_device(leaf, modparent=modparent)
devices.remove(leaf)
@@ -254,7 +255,7 @@ def recursive_remove(self, device, actions=True, remove_device=True):
if actions:
self.actions.add(ActionDestroyDevice(device))
else:
- self._remove_device(device)
+ self._remove_device(device, modparent=modparent)
#
# Actions
--
To view this commit on github, visit https://github.com/rhinstaller/blivet/commit/8b76ece3809fd80fc0d03d03a459...
8 years, 4 months
[master 18/30] Implement DiskLabelFormatPopulator._get_kwargs.
by dashea
From: David Lehman <dlehman(a)redhat.com>
---
blivet/populator/helpers/disklabel.py | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/blivet/populator/helpers/disklabel.py b/blivet/populator/helpers/disklabel.py
index ed4e355..0bd6961 100644
--- a/blivet/populator/helpers/disklabel.py
+++ b/blivet/populator/helpers/disklabel.py
@@ -51,6 +51,11 @@ def match(cls, data, device):
not is_multipath_member and
udev.device_get_format(data) != "iso9660")
+ def _get_kwargs(self):
+ kwargs = super()._get_kwargs()
+ kwargs["uuid"] = udev.device_get_disklabel_uuid(self.data)
+ return kwargs
+
def run(self):
disklabel_type = udev.device_get_disklabel_type(self.data)
log_method_call(self, device=self.device.name, label_type=disklabel_type)
@@ -74,13 +79,12 @@ def run(self):
[self.device.name])
return
+ kwargs = self._get_kwargs()
+
# special handling for unsupported partitioned devices
if not self.device.partitionable:
try:
- fmt = formats.get_format("disklabel",
- device=self.device.path,
- label_type=disklabel_type,
- exists=True)
+ fmt = formats.get_format("disklabel", **kwargs)
except InvalidDiskLabelError:
log.warning("disklabel detected but not usable on %s",
self.device.name)
@@ -89,10 +93,7 @@ def run(self):
return
try:
- fmt = formats.get_format("disklabel",
- device=self.device.path,
- uuid=udev.device_get_disklabel_uuid(self.data),
- exists=True)
+ fmt = formats.get_format("disklabel", **kwargs)
except InvalidDiskLabelError as e:
log.info("no usable disklabel on %s", self.device.name)
if disklabel_type == "gpt":
--
To view this commit on github, visit https://github.com/rhinstaller/blivet/commit/7bab9329fbdf1a00bce8ad67cb69...
8 years, 4 months