
from __future__ import absolute_import

import inspect
import logging
import threading

import pyudev

from collections import namedtuple

from vdsm.storage import devicemapper

MultipathEvent = namedtuple("MultipathEvent",
                            "type, mpath_uuid, path, valid_paths, dm_seqnum")

MPATH_REMOVED = "removed"
PATH_FAILED = "failed"
PATH_REINSTATED = "reinstated"


def create_observer(monitor, callback, name):
    argspec = inspect.getargspec(pyudev.MonitorObserver.__init__)
    if "callback" in argspec.args:
        return pyudev.MonitorObserver(monitor,
                                      callback=callback,
                                      name=name)
    else:
        def event_handler(action, device):
            callback(device)
        return pyudev.MonitorObserver(monitor, event_handler, name=name)


class MultipathMonitor(object):

    def start(self):
        """
        Called when starting multipath listener, after udev events were
        registered with the kernel, but the listener is not reading the events
        yet.

        When all registered monitors were started, the listener starts to
        receive udev events, and the montior handle() method may be called.

        Should be implemented by a monitor if it needs to check the current
        system state before receiving events.
        """

    def handle(self, event):
        raise NotImplementedError

    def stop(self):
        """
        Called when the listener was stopped.

        May be implemented by a montior if it wants to do some cleanup when
        stopping monitoring.
        """


class MultipathListener(object):
    log = logging.getLogger("storage.udev")

    def __init__(self):
        self._lock = threading.Lock()
        self._monitors = set()
        self._observer = None

    def start(self):
        self.log.info("Starting multipath event listener")
        with self._lock:
            if self._observer is not None:
                raise AssertionError("Listener already started")

            context = pyudev.Context()
            monitor = pyudev.Monitor.from_netlink(context)
            monitor.filter_by("block", device_type="disk")
            self._observer = create_observer(monitor,
                                             self._callback,
                                             name="mpathlistener")


            monitor.start()

            self._start_monitors(self._monitors)

            self._observer.start()

    def stop(self):
        self.log.info("Stopping multipath event listener")
        with self._lock:
            if self._observer is None:
                return
            self._observer.stop()
            self._observer = None
            self._stop_monitors(self._monitors)

    def register(self, monitor):
        self.log.info("Registering multipath event monitor %s", monitor)


        with self._lock:
            if monitor in self._monitors:
                raise AssertionError("Monitor %s already registered" % monitor)

            self._monitors.add(monitor)

            if self._observer is None:
                return

        self.log.debug("Starting monitor %s", monitor)
        try:
            monitor.start()
        except:
            with self._lock:
                self._monitors.remove(monitor)
            raise

    def unregister(self, monitor):
        self.log.info("Unregistering multipath event monitor %s", monitor)
        with self._lock:
            if monitor not in self._monitors:
                raise AssertionError("Monitor %s not registered" % monitor)
            self._monitors.remove(monitor)
            self._stop_monitors([monitor])

    def _callback(self, device):
        self.log.debug("Received udev event (action=%s, device=%s)",
                       device["ACTION"], device)
        try:
            event = self._detect_event(device)
        except Exception as e:
            self.log.exception("Error detecting udev event: %s", e)
            return

        if event:
            self.log.debug("Forwarding %s", event)
            self._forward_event(event)

    def _detect_event(self, device):
        mpath_uuid = device.get("DM_UUID", "")
        if not mpath_uuid.startswith("mpath-"):
            return None
        mpath_uuid = mpath_uuid[6:]

        if device["ACTION"] == "change":
            dm_action = device.get("DM_ACTION")
            if dm_action == "PATH_FAILED":
                event_type = PATH_FAILED
            elif dm_action == "PATH_REINSTATED":
                event_type = PATH_REINSTATED
            else:
                self.log.debug("Unsupported DM_ACTION %r", dm_action)
                return
            valid_paths = int(device.get("DM_NR_VALID_PATHS"))
            dm_seqnum = int(device.get("DM_SEQNUM"))
            path = devicemapper.device_name(device.get("DM_PATH"))
        elif device["ACTION"] == "remove":
            event_type = MPATH_REMOVED
            valid_paths = None
            path = None
            dm_seqnum = None
        else:
            return None

        return MultipathEvent(event_type, mpath_uuid, path, valid_paths,
                              dm_seqnum)

    def _forward_event(self, event):
        with self._lock:
            monitors = list(self._monitors)

        for m in monitors:
            try:
                m.handle(event)
            except Exception as e:
                self.log.exception("Unhandled exception in %s: %s", m, e)

    def _start_monitors(self, monitors):
        started = []
        try:
            for m in monitors:
                self.log.debug("Starting monitor %s", m)
                m.start()
                started.append(m)
        except:
            self._stop_monitors(started)
            raise

    def _stop_monitors(self, monitors):
        for m in monitors:
            self.log.debug("Stopping monitor %s", m)
            try:
                m.stop()
            except Exception:
                self.log.exception("Unhandled exception stopping %s", m)
