Commits

Floris Bruynooghe committed 584bd5d

Initial import

  • Participants

Comments (0)

Files changed (13)

File pysnmp_eventlet/__init__.py

Empty file added.

File pysnmp_eventlet/carrier/__init__.py

Empty file added.

File pysnmp_eventlet/carrier/eventlet/__init__.py

Empty file added.

File pysnmp_eventlet/carrier/eventlet/base.py

+# -*- coding: utf-8 -*-
+# Copyright (C) 2013 Abilisoft Ltd.
+# http://abilisoft.com
+
+"""Defines the pysnmp API to eventlet-based transport
+
+XXX It seems odd this is not defined in pysnmp.carrier.base.
+"""
+
+from __future__ import (absolute_import,
+                        unicode_literals, print_function, division)
+
+import pysnmp.carrier.error
+
+
+class AbstractEventletTransport(object):
+    """The API pysnmp expects from a transport"""
+
+    def openClientMode(self, iface=None):
+        pysnmp.carrier.error.CarrierError('Method not implemented')
+
+    def openServerMode(self, iface=None):
+        pysnmp.carrier.error.CarrierError('Method not implemented')
+
+    def sendMessage(self, iface=None):
+        pysnmp.carrier.error.CarrierError('Method not implemented')
+
+    def registerCbFun(self, cbFun):
+        pysnmp.carrier.error.CarrierError('Method not implemented')
+
+    def unregisterCbFun(self):
+        pysnmp.carrier.error.CarrierError('Method not implemented')
+
+    def closeTransport(self):
+        pysnmp.carrier.error.CarrierError('Method not implemented')

File pysnmp_eventlet/carrier/eventlet/dgram/__init__.py

Empty file added.

File pysnmp_eventlet/carrier/eventlet/dgram/base.py

+# -*- coding: utf-8 -*-
+# Copyright (C) 2013 Abilisoft Ltd.
+# http://abilisoft.com
+
+"""Base datagram implementation for pysnmp eventlet transport
+
+This contains the base implementation which can be subclassed to
+create the udp and udp6 transports.
+"""
+
+from __future__ import (absolute_import,
+                        unicode_literals, print_function, division)
+
+import eventlet
+import greenlet
+from eventlet.green import socket
+from pysnmp.carrier.error import CarrierError
+from pysnmp import debug
+
+
+snmpUDPDomain = (1, 3, 6, 1, 6, 1, 1)
+snmpUDP6Domain = (1, 3, 6, 1, 2, 1, 100, 1, 2)
+
+
+class BaseUdpEventletTransport(object):
+    RECV_SIZE = 65535           # IPv4 theoretical max packet size
+    FAMILY = socket.AF_INET
+
+    def __init__(self, sock=None):
+        self._recv_greenlet = None
+        if sock is None:
+            try:
+                sock = socket.socket(self.FAMILY, socket.SOCK_DGRAM)
+            except socket.error as e:
+                raise CarrierError('socket() failed: {}'.format(e))
+        self._sock = sock
+
+    def openClientMode(self, iface=None):
+        if iface is not None:
+            try:
+                self._sock.bind(iface)
+            except socket.error as e:
+                raise CarrierError('bind() for {} failed: {}'.format(iface, e))
+        return self
+
+    def openServerMode(self, iface):
+        try:
+            self._sock.bind(iface)
+        except socket.error as e:
+            raise CarrierError('bind() for {} failed: {}'.format(iface, e))
+        return self
+
+    def sendMessage(self, outgoingMessage, transportAddress):
+        """Send binary data to a specific socket address"""
+        self.checkRecv()
+        debug.logger & debug.flagIO and debug.logger(
+            'sendMessage: transportAddress %r -> %s outgoingMessage %s',
+            self._sock.getsockname(),
+            transportAddress, debug.hexdump(outgoingMessage))
+        if not transportAddress:
+            debug.logger & debug.flagIO and debug.logger(
+                'handle_write: missing dst address, losing outgoing msg')
+            return
+        try:
+            self._sock.sendto(outgoingMessage, transportAddress)
+        except socket.error as e:
+            raise CarrierError('sendto() failed for {}: {}'
+                               .format(transportAddress, e))
+
+    def registerCbFun(self, cbFun):
+        """Register a callback function to receive incoming data
+
+        This hooks up the internal recv function to the eventlet hub
+        ensuring it can read data from the network.
+
+        This normally gets called by the transport dispatcher which
+        will register it's callback to the transport when the
+        transport is being registered to the dispatcher using
+        AbstractTransportDispatcher.registerTransport().  This in turn
+        is traditionally called from
+        pysnmp.entity.config.addSocketTransport().
+        """
+        def recv():
+            while True:
+                try:
+                    data, addr = self._sock.recvfrom(self.RECV_SIZE)
+                    debug.logger & debug.flagIO and debug.logger(
+                        'recv: addr {!r} -> {!r} data {}'
+                        .format(addr, self._sock.getsockname(),
+                                debug.hexdump(data)))
+                    if not data:
+                        return
+                    cbFun(self, addr, data)
+                except socket.error as e:
+                    raise CarrierError('recvfrom() failed: {}'.format(e))
+        self._recv_greenlet = eventlet.spawn(recv)
+
+    def checkRecv(self):
+        """Check the recv greenlet for errors
+
+        If you call this periodically you will be alerted of any
+        exceptions happening in the recv() side of this transport.
+        This is required since otherwise the transport will simply
+        stop receiving data and you won't notice.  It is automatically
+        called each time .sendMessage() is called too.
+        """
+        if self._recv_greenlet is not None and self._recv_greenlet.dead:
+            try:
+                self._recv_greenlet.wait()
+            except greenlet.GreenletExit:
+                pass
+
+    def unregisterCbFun(self):
+        """Unregister the callback function - idempotent
+
+        Thus stopping the transport from receiving data from the
+        network.  As with .registerCbFun() this API is normally
+        invoked by AbstractTransportDispatcher.unregisterTransport().
+        """
+        try:
+            self._recv_greenlet.kill()
+        except AttributeError:
+            return              # _recv_greenlet was None
+        try:
+            self._recv_greenlet.wait()
+        except greenlet.GreenletExit:
+            pass
+        finally:
+            self._recv_greenlet = None
+
+    def closeTransport(self):
+        try:
+            self.unregisterCbFun()
+        finally:
+            self._sock.close()
+            self._sock = None  # CPython 2.x hack, closing is not enough

File pysnmp_eventlet/carrier/eventlet/dgram/base_test.py

+# -*- coding: utf-8 -*-
+# Copyright (C) 2013 Abilisoft Ltd.
+# http://abilisoft.com
+
+from __future__ import (absolute_import,
+                        unicode_literals, print_function, division)
+
+import eventlet
+import greenlet
+import pytest
+from eventlet.green import socket
+from eventlet.green import threading
+
+from . import base
+
+
+def reapgt(gt):
+    """Reap an eventlet greenthread"""
+    gt.kill()
+    try:
+        gt.wait()
+    except greenlet.GreenletExit:
+        pass
+
+
+class TestTrans(base.BaseUdpEventletTransport):
+    """Transport instance for tests"""
+
+    def getaddr(self, iface='', port=0):
+        """Return the socket address tuple for a given port number"""
+        if self.FAMILY == socket.AF_INET and iface == '':
+            return ('0.0.0.0', port)
+        elif self.FAMILY == socket.AF_INET and iface == 'lo':
+            return ('127.0.0.1', port)
+        elif self.FAMILY == socket.AF_INET6 and iface == '':
+            return ('::', port, 0, 0)
+        elif self.FAMILY == socket.AF_INET6 and iface == 'lo':
+            return ('::1', port, 0, 0)
+
+    def close(self):
+        try:
+            self._sock.close()
+        except Exception:
+            pass
+        self._sock = None
+
+
+class V4Trans(TestTrans):
+    FAMILY = socket.AF_INET
+
+
+class V6Trans(TestTrans):
+    FAMILY = socket.AF_INET6
+
+
+@pytest.fixture(params=[V4Trans, V6Trans])
+def trans(request, pytestconfig):
+    if not pytestconfig.ipv6 and request.param.FAMILY == socket.AF_INET6:
+        pytest.skip('No working IPv6 stack available')
+    t = request.param()
+    request.addfinalizer(t.close)
+    return t
+
+
+# WTF: On OSX the socket address of a wildcard IPv6/UDP socket appears
+#      as '0.0.0.0' instead of '::'.
+@pytest.mark.platxfail('*-osx*')
+def test_openClientMode(trans):
+    r = trans.openClientMode()
+    assert r is trans
+    assert trans._sock.getsockname() == trans.getaddr()
+
+
+def test_openClientMode_iface(trans):
+    addr = trans.getaddr('lo')
+    trans.openClientMode(addr)
+    assert trans._sock.getsockname()[0] == addr[0]
+
+
+# WTF: On OSX the socket address of a wildcard IPv6/UDP socket appears
+#      as '0.0.0.0' instead of '::'.
+@pytest.mark.platxfail('*-osx*')
+def test_openServerMode(trans):
+    addr = trans.getaddr(port=1162)
+    r = trans.openServerMode(addr)
+    assert r is trans
+    assert trans._sock.getsockname() == addr
+
+
+def test_openServerMode_iface(trans):
+    addr = trans.getaddr('lo', 1162)
+    trans.openServerMode(addr)
+    assert trans._sock.getsockname() == addr
+
+
+def test_sendMessage(trans, monkeypatch):
+    monkeypatch.setattr(trans, 'checkRecv', pytest.Mock())
+    addr = trans.getaddr('lo')
+    dst_sock = socket.socket(trans.FAMILY, socket.SOCK_DGRAM)
+    dst_sock.bind(addr)
+
+    def recv():
+        return dst_sock.recvfrom(4096)
+
+    trans.sendMessage(b'spam', dst_sock.getsockname())
+    gt = eventlet.spawn(recv)
+    data, addr = gt.wait()
+    assert data == b'spam'
+    assert trans.checkRecv.called
+
+
+def test_registerCbFun_server(request, trans):
+    recv_buf = []
+    recv_evt = threading.Event()
+
+    def cb(trans, addr, data):
+        recv_buf.append((trans, addr, data))
+        recv_evt.set()
+
+    addr = trans.getaddr('lo')
+    trans.openServerMode(addr)
+    trans.registerCbFun(cb)
+    clt_sock = socket.socket(trans.FAMILY, socket.SOCK_DGRAM)
+    clt_sock.sendto(b'spam', trans._sock.getsockname())
+
+    recv_evt.wait(1)
+    request.addfinalizer(lambda: reapgt(trans._recv_greenlet))
+
+    (trans, addr, data), = recv_buf
+    assert trans is trans
+    assert addr == (addr[0],) + clt_sock.getsockname()[1:]
+    assert data == b'spam'
+
+
+def test_registerCbFun_client(request, trans):
+    recv_buf = []
+    recv_evt = threading.Event()
+
+    def cb(trans, addr, data):
+        recv_buf.append((trans, addr, data))
+        recv_evt.set()
+
+    addr = trans.getaddr('lo')
+    trans.openClientMode()
+    trans.registerCbFun(cb)
+    srv_sock = socket.socket(trans.FAMILY, socket.SOCK_DGRAM)
+    srv_sock.bind(addr)
+
+    def srv():
+        data, addr = srv_sock.recvfrom(4096)
+        srv_sock.sendto(data, addr)
+
+    srv_gt = eventlet.spawn(srv)
+    trans._sock.sendto(b'spam', srv_sock.getsockname())
+    recv_evt.wait(1)
+    request.addfinalizer(lambda: reapgt(srv_gt))
+    request.addfinalizer(lambda: reapgt(trans._recv_greenlet))
+
+    (trans, addr, data), = recv_buf
+    assert trans is trans
+    assert addr == srv_sock.getsockname()
+    assert data == b'spam'
+
+
+def test_checkRecv_no_gt():
+    t = base.BaseUdpEventletTransport()
+    t.checkRecv()
+
+
+def test_checkRecv_gt_not_started(monkeypatch):
+    t = base.BaseUdpEventletTransport()
+    t.registerCbFun(lambda *x: None)  # Create greenthread
+    monkeypatch.setattr(t._recv_greenlet, 'wait', pytest.Mock())
+    t.checkRecv()
+    assert not t._recv_greenlet.dead
+    assert not t._recv_greenlet.wait.called
+
+
+def test_checkRecv_gt_alive(monkeypatch):
+    t = base.BaseUdpEventletTransport()
+    t.registerCbFun(lambda *x: None)  # Create greenthread
+    eventlet.sleep(0)                 # Start greenthread
+    monkeypatch.setattr(t._recv_greenlet, 'wait', pytest.Mock())
+    t.checkRecv()
+    assert not t._recv_greenlet.dead
+    assert not t._recv_greenlet.wait.called
+
+
+def test_checkRecv_gt_exc(monkeypatch):
+    t = base.BaseUdpEventletTransport()
+    t.registerCbFun(lambda *x: None)  # Create greenthread
+    eventlet.sleep(0)                 # Start greenthread
+    t._recv_greenlet.kill(Exception('foo'))
+    with pytest.raises(Exception) as e:
+        t.checkRecv()
+    assert e.value.args == ('foo',)
+    assert t._recv_greenlet.dead
+
+
+def test_checkRecv_gt_kill(monkeypatch):
+    t = base.BaseUdpEventletTransport()
+    t.registerCbFun(lambda *x: None)  # Create greenthread
+    eventlet.sleep(0)                 # Start greenthread
+    t._recv_greenlet.kill()
+    t.checkRecv()
+    assert t._recv_greenlet.dead
+
+
+def test_unregisterCbFun():
+    t = base.BaseUdpEventletTransport()
+    t.registerCbFun(lambda *x: None)  # Create greenthread
+    eventlet.sleep(0)                 # Start greenthread
+    assert t._recv_greenlet is not None
+    gt = t._recv_greenlet
+    t.unregisterCbFun()
+    assert t._recv_greenlet is None
+    assert gt.dead
+
+
+def test_closeTransport(monkeypatch):
+    t = base.BaseUdpEventletTransport()
+    sock = t._sock
+    monkeypatch.setattr(t, 'unregisterCbFun', pytest.Mock())
+    monkeypatch.setattr(t._sock, 'close', pytest.Mock())
+    t.closeTransport()
+    assert t._sock is None
+    assert sock.close.called
+    assert t.unregisterCbFun.called

File pysnmp_eventlet/carrier/eventlet/dgram/udp.py

+# -*- coding: utf-8 -*-
+# Copyright (C) 2013 Abilisoft Ltd.
+# http://abilisoft.com
+
+from __future__ import (absolute_import,
+                        unicode_literals, print_function, division)
+
+from eventlet.green import socket
+
+from . import base
+
+
+domainName = snmpUDPDomain = base.snmpUDPDomain
+
+
+class UdpEventletTransport(base.BaseUdpEventletTransport):
+    FAMILY = socket.AF_INET
+
+
+UdpTransport = UdpEventletTransport

File pysnmp_eventlet/carrier/eventlet/dgram/udp6.py

+# -*- coding: utf-8 -*-
+# Copyright (C) 2013 Abilisoft Ltd.
+# http://abilisoft.com
+
+from __future__ import (absolute_import,
+                        unicode_literals, print_function, division)
+
+from eventlet.green import socket
+
+from . import base
+
+
+domainName = snmpUDP6Domain = base.snmpUDP6Domain
+
+
+class Udp6EventletTransport(base.BaseUdpEventletTransport):
+    FAMILY = socket.AF_INET6
+
+
+Udp6Transport = Udp6EventletTransport

File pysnmp_eventlet/carrier/eventlet/dgram/udp6_test.py

+# -*- coding: utf-8 -*-
+# Copyright (C) 2013 Abilisoft Ltd.
+# http://abilisoft.com
+
+from __future__ import (absolute_import,
+                        unicode_literals, print_function, division)
+
+
+from . import udp6
+
+
+def test_domainName():
+    assert udp6.domainName == (1, 3, 6, 1, 2, 1, 100, 1, 2)
+    assert udp6.snmpUDP6Domain == (1, 3, 6, 1, 2, 1, 100, 1, 2)
+
+
+def test_UdpTransport():
+    assert udp6.Udp6EventletTransport is udp6.Udp6Transport

File pysnmp_eventlet/carrier/eventlet/dgram/udp_test.py

+# -*- coding: utf-8 -*-
+# Copyright (C) 2013 Abilisoft Ltd.
+# http://abilisoft.com
+
+from __future__ import (absolute_import,
+                        unicode_literals, print_function, division)
+
+
+from . import udp
+
+
+def test_domainName():
+    assert udp.domainName == udp.snmpUDPDomain == (1, 3, 6, 1, 6, 1, 1)
+
+
+def test_UdpTransport():
+    assert udp.UdpEventletTransport is udp.UdpTransport

File pysnmp_eventlet/carrier/eventlet/dispatch.py

+# '#-*- coding: utf-8 -*-
+# Copyright (C) 2013 Abilisoft Ltd.
+# http://abilisoft.com
+
+"""Eventlet transport dispatcher
+
+This dispatcher needs to be registered with the SnmpEngine using
+.registerTransportDispatcher() in order for an engine to use the
+eventlet I/O loop.  Once you use an EventletDispatcher you will have
+to use a matching *EventletTransport class in the call to
+pysnmp.entity.config.addSocketTransport() or directly call
+EventletDispatcher.registerTransport() with the matching transport.
+So a typical setup looks like::
+
+   from pysnmp.carrier.eventlet.dgram import udp
+   from pysnmp.carrier.eventlet.dispatch import EventletDispatcher
+   from pysnmp.entity.engine import SnmpEngine
+
+   engine = SnmpEngine()
+   engine.registerTransportDispatcher(EventletDispatcher())
+   engine.transportDispatcher.registerTransport(
+       udp.domainName,
+       udp.UdpEventletTransport().openServerMode(('127.0.0.1', 162)))
+"""
+
+from __future__ import (absolute_import,
+                        unicode_literals, print_function, division)
+
+import time
+
+import eventlet
+from pysnmp.carrier.base import AbstractTransportDispatcher
+
+
+class EventletDispatcher(AbstractTransportDispatcher):
+    """Eventlet dispatcher for pysnmp
+
+    A pysnmp SnmpEngine needs to have a dispatcher with transports for
+    it's network I/O.  This provides such a dispatcher which can be
+    used with eventlet-based transports to do the I/O on the eventlet
+    hub.
+
+    The pysnmp .runDispatcher() API is probably not very convenient
+    when using eventlet.  It is expected you will run the equivalent
+    of this in a greenthread instead, maybe with added exception
+    handling though::
+
+       while True:
+           eventlet.sleep(dispatcher.getTimerResolution())
+           dispatcher.handleTimerTick(time.time())
+    """
+
+    def handleTimerTick(self, timeNow):
+        """Deliver a timer tick to do pysnmp housekeeping
+
+        This also checks for unexpected exceptions in any transports.
+        """
+        __transports = self._AbstractTransportDispatcher__transports
+        for transport in __transports.values():
+            transport.checkRecv()
+        AbstractTransportDispatcher.handleTimerTick(self, timeNow)
+
+    def runDispatcher(self, timeout=0.0):
+        """Run the dispatcher until all jobs are finished
+
+        This is a bit of an odd API but is compatible with the normal
+        pysnmp API.  Since the eventlet hub runs in the background
+        automatically anyway (and you can not stop it) you are
+        probably more interested in the .start() and .stop() methods
+        as they have the benefit of continuously delivering timer
+        ticks to pysnmp.
+
+        However one major benefit from this call is that you will get
+        any exceptions happening in the dispatcher or transport
+        bubbling out since this is a blocking call.
+        """
+        if not timeout or timeout < 0:
+            timeout = self.getTimerResolution()
+        while self.jobsArePending():
+            eventlet.sleep(timeout)
+            self.handleTimerTick(time.time())
+
+    def start(self):
+        """Run this dispatcher forever
+
+        This starts a greenlet running the dispatcher.  This is
+        essentially no more then periodically calling
+        .handleTimerTick() as the eventlet hub will do the required
+        I/O work for the transports.
+        """
+        pass
+
+    def stop(self):
+        """Stop running the dispatcher
+
+        This will stop the greenlet running the dispatcher and call
+        .closeDispatcher(), which means it will close all the
+        transports and unregister the dispatcher's recv and timer
+        callbacks from XXX.
+        """
+        pass

File pysnmp_eventlet/carrier/eventlet/dispatch_test.py

+# -*- coding: utf-8 -*-
+# Copyright (C) 2013 Abilisoft Ltd.
+# http://abilisoft.com
+
+from __future__ import (absolute_import,
+                        unicode_literals, print_function, division)
+
+import time
+
+import pytest
+
+from . import dispatch
+
+
+@pytest.fixture
+def ed():
+    return dispatch.EventletDispatcher()
+
+
+def test_timertick(ed, monkeypatch):
+    domain0 = pytest.Mock()
+    domain1 = pytest.Mock()
+    trans0 = pytest.Mock()
+    trans1 = pytest.Mock()
+    ed.registerTransport(domain0, trans0)
+    ed.registerTransport(domain1, trans1)
+    monkeypatch.setattr(dispatch.AbstractTransportDispatcher,
+                        'handleTimerTick', pytest.Mock())
+
+    ed.handleTimerTick(time.time())
+
+    assert trans0.checkRecv.called
+    assert trans1.checkRecv.called
+    assert dispatch.AbstractTransportDispatcher.handleTimerTick.called
+
+
+def test_rundispatcher_nojobs(ed, monkeypatch):
+    monkeypatch.setattr(ed, 'handleTimerTick', pytest.Mock())
+    ed.runDispatcher()
+    assert not ed.handleTimerTick.called
+
+
+
+def test_rundispatcher_one_job(ed, monkeypatch):
+    ed.jobStarted(1)
+    tick = lambda t: ed.jobFinished(1)
+    monkeypatch.setattr(ed, 'handleTimerTick', tick)
+    monkeypatch.setattr(dispatch.eventlet, 'sleep', pytest.Mock())
+    ed.runDispatcher()
+    assert not ed.jobsArePending()