Skip to content

Commit

Permalink
Add classes for managing external events.
Browse files Browse the repository at this point in the history
  • Loading branch information
dwlehman committed Jan 28, 2016
1 parent f5d34d2 commit c77de3b
Show file tree
Hide file tree
Showing 4 changed files with 338 additions and 0 deletions.
7 changes: 7 additions & 0 deletions blivet/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
253 changes: 253 additions & 0 deletions blivet/event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
# event.py
# Event management classes.
#
# Copyright (C) 2015 Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU 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@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

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."""

#
# handler_cb is the main event handler
#

This comment has been minimized.

Copy link
@vpodzime

vpodzime Jan 31, 2016

I think this comment should be a docstring of the handler_cb property. And the same applies to notify_cb.

@property
def handler_cb(self):
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

#
# notify_cb is a notification handler that runs after the main handler
#
@property
def notify_cb(self):
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()
flags.uevents = True

This comment has been minimized.

Copy link
@vpodzime

vpodzime Jan 31, 2016

I think this flag should be guarded with a lock acquired for enable and disable methods.


def disable(self):
""" Disable monitoring and handling of block device uevents. """
super().disable()
if self.enabled:
self._pyudev_observer.stop()

self._pyudev_observer = None
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()
5 changes: 5 additions & 0 deletions blivet/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ def __init__(self):
self.installer_mode = False
self.debug = False

#
# minor modes
#
self.uevents = False

#
# minor modes (installer-specific)
#
Expand Down
73 changes: 73 additions & 0 deletions tests/events_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@

from unittest import TestCase
from unittest.mock import Mock

from blivet.event 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)

0 comments on commit c77de3b

Please sign in to comment.