[asterisk-commits] twilson: testsuite/asterisk/trunk r2914 - in /asterisk/trunk: ./ lib/python/ ...

SVN commits to the Asterisk project asterisk-commits at lists.digium.com
Fri Dec 16 10:25:54 CST 2011


Author: twilson
Date: Fri Dec 16 10:25:47 2011
New Revision: 2914

URL: http://svnview.digium.com/svn/testsuite?view=rev&rev=2914
Log:
Add pcap capture logging and testing

(closes issue ASTERISK-18440)
(closes issue ASTERISK-18438)
Review: https://reviewboard.asterisk.org/r/1623/

Added:
    asterisk/trunk/lib/python/pcap_listener.py   (with props)
    asterisk/trunk/lib/python/sip_message.py   (with props)
    asterisk/trunk/tests/channels/SIP/pcap_demo/
    asterisk/trunk/tests/channels/SIP/pcap_demo/configs/
    asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/
    asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/extensions.conf   (with props)
    asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/sip.conf   (with props)
    asterisk/trunk/tests/channels/SIP/pcap_demo/run-test   (with props)
    asterisk/trunk/tests/channels/SIP/pcap_demo/sipp/
    asterisk/trunk/tests/channels/SIP/pcap_demo/test-config.yaml   (with props)
Modified:
    asterisk/trunk/lib/python/asterisk/TestCase.py
    asterisk/trunk/lib/python/asterisk/TestConfig.py
    asterisk/trunk/runtests.py
    asterisk/trunk/tests/channels/SIP/tests.yaml

Modified: asterisk/trunk/lib/python/asterisk/TestCase.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/lib/python/asterisk/TestCase.py?view=diff&rev=2914&r1=2913&r2=2914
==============================================================================
--- asterisk/trunk/lib/python/asterisk/TestCase.py (original)
+++ asterisk/trunk/lib/python/asterisk/TestCase.py Fri Dec 16 10:25:47 2011
@@ -20,6 +20,12 @@
 from TestConfig import TestConfig
 from TestConditions import TestConditionController, TestCondition
 from ThreadTestCondition import ThreadPreTestCondition, ThreadPostTestCondition
+
+try:
+    from pcap_listener import PcapListener
+    PCAP_AVAILABLE = True
+except:
+    PCAP_AVAILABLE = False
 
 logger = logging.getLogger(__name__)
 
@@ -49,6 +55,11 @@
         self.global_config = TestConfig(os.getcwd())
         self.test_config = TestConfig(self.test_name, self.global_config)
         self.testStateController = None
+        self.pcap = None
+        self.pcapfilename = None
+        self.testlogdir = os.path.join(Asterisk.test_suite_root, self.base, str(os.getpid()))
+
+        os.makedirs(self.testlogdir)
 
         """ Set up logging """
         logConfigFile = os.path.join(os.getcwd(), "%s" % (self.defaultLogFileName))
@@ -61,6 +72,10 @@
         else:
             print "WARNING: no logging.conf file found; using default configuration"
             logging.basicConfig(level=self.defaultLogLevel)
+
+        if PCAP_AVAILABLE:
+            self.pcapfilename = os.path.join(self.testlogdir, "dumpfile.pcap")
+            self.pcap = self.create_pcap_listener(dumpfile=self.pcapfilename)
 
         self.testConditionController = TestConditionController(self.test_config, self.ast, self.stop_reactor)
         self.__setup_conditions()
@@ -162,6 +177,24 @@
             reactor.listenTCP(4573, self.fastagi_factory,
                     self.reactor_timeout, host)
 
+    def create_pcap_listener(self, device=None, bpf_filter=None, dumpfile=None):
+        """Create a single instance of a pcap listener.
+
+        Keyword arguments:
+        device -- The interface to listen on. Defaults to the first interface beginning with 'lo'.
+        bpf_filter -- BPF (filter) describing what packets to match, i.e. "port 5060"
+        dumpfile -- The filename at which to save a pcap capture
+
+        """
+
+        if not PCAP_AVAILABLE:
+            raise Exception("PCAP not available on this machine. Test config is missing pcap dependency.")
+
+        # TestCase will create a listener for logging purposes, and individual tests can
+        # create their own. Tests may only want to watch a specific port, while a general
+        # logger will want to watch more general traffic which can be filtered later.
+        return PcapListener(device, bpf_filter, dumpfile, self.__pcap_callback)
+
     def start_asterisk(self):
         """
         Start the instances of Asterisk that were previously created.  See
@@ -250,6 +283,16 @@
         self.ami[ami.id] = ami
         self.ami_connect(ami)
 
+    def pcap_callback(self, packet):
+        """
+        Hook method used to receive captured packets.
+        """
+        pass
+
+    def __pcap_callback(self, packet):
+        logger.debug("Received packet: %s\n" % (packet,))
+        self.pcap_callback(packet)
+
     def handleOriginateFailure(self, reason):
         """
         Convenience callback handler for twisted deferred errors for an AMI originate call.  Derived

Modified: asterisk/trunk/lib/python/asterisk/TestConfig.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/lib/python/asterisk/TestConfig.py?view=diff&rev=2914&r1=2913&r2=2914
==============================================================================
--- asterisk/trunk/lib/python/asterisk/TestConfig.py (original)
+++ asterisk/trunk/lib/python/asterisk/TestConfig.py Fri Dec 16 10:25:47 2011
@@ -147,6 +147,10 @@
         elif "buildoption" in dep:
             self.name = dep["buildoption"]
             self.met = self.__find_build_flag(self.name)
+        elif "pcap" in dep:
+            self.name = "pcap"
+            from TestCase import PCAP_AVAILABLE
+            self.met = PCAP_AVAILABLE
         else:
             print "Unknown dependency type specified:"
             for key in dep.keys():

Added: asterisk/trunk/lib/python/pcap_listener.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/lib/python/pcap_listener.py?view=auto&rev=2914
==============================================================================
--- asterisk/trunk/lib/python/pcap_listener.py (added)
+++ asterisk/trunk/lib/python/pcap_listener.py Fri Dec 16 10:25:47 2011
@@ -1,0 +1,60 @@
+from twisted.internet import abstract, protocol
+from yappcap import PcapLive, findalldevs
+
+class PcapFile(abstract.FileDescriptor):
+    """Treat a live pcap capture as a file for Twisted to call select() on"""
+    def __init__(self, protocol, interface, filter=None, dumpfile=None):
+        abstract.FileDescriptor.__init__(self)
+
+        p = PcapLive(interface, autosave=dumpfile)
+        p.activate()
+        p.blocking = False
+
+        if filter is not None:
+            p.setfilter(filter)
+
+        self.pcap = p
+        self.fd = p.fileno
+        self.protocol = protocol
+        self.protocol.makeConnection(self)
+        self.startReading()
+
+    def fileno(self):
+        return self.fd
+
+    def doRead(self):
+        pkt = self.pcap.next()
+
+        # we may not have a packet if something weird happens
+        if not pkt:
+            # according to the twisted docs 0 implies no write done
+            return 0
+
+        self.protocol.dataReceived(pkt)
+
+    def connectionLost(self, reason):
+        self.protocol.connectionLost(reason)
+
+
+class PcapListener(protocol.Protocol):
+    """A Twisted protocol wrapper for a pcap capture"""
+    def __init__(self, interface, bpf_filter=None, dumpfile=None, callback=None):
+        """Initialize a new PcapListener
+
+        interface - The name of an interface. If None, the first loopback interface
+        bpf_filter - A Berkeley packet filter, i.e. "udp port 5060"
+        dumpfile - The filename where to save the capture file
+        callback - A function that will receive a PcapPacket for each packet captured
+
+        """
+        if interface is None:
+            interface = [x.name for x in findalldevs() if x.loopback][0]
+        self.pf = PcapFile(self, interface, bpf_filter, dumpfile)
+        self.callback = callback
+
+    def makeConnection(self, transport):
+        self.connectionMade()
+
+    def dataReceived(self, data):
+        if self.callback:
+            self.callback(data)

Propchange: asterisk/trunk/lib/python/pcap_listener.py
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: asterisk/trunk/lib/python/pcap_listener.py
------------------------------------------------------------------------------
    svn:keywords = Author Date Id Revision

Propchange: asterisk/trunk/lib/python/pcap_listener.py
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: asterisk/trunk/lib/python/sip_message.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/lib/python/sip_message.py?view=auto&rev=2914
==============================================================================
--- asterisk/trunk/lib/python/sip_message.py (added)
+++ asterisk/trunk/lib/python/sip_message.py Fri Dec 16 10:25:47 2011
@@ -1,0 +1,122 @@
+#!/usr/bin/env python
+import re
+
+class SIPParseError(Exception):
+    pass
+
+# This is not particularly efficient. I don't care.
+# Ok, I do, but I'm not going to do anything about it.
+class SIPMessage:
+    """ This generally parses out the first line, headers, and body in a SIP message.
+    It does not ensure that those parts are in any way valid"""
+    def __init__(self, data):
+        self.first_line = None
+        self.body = None
+        self.headers = None
+
+        # First, split the msg into header and body
+        try:
+            (data, self.body) = data.split("\r\n\r\n", 1)
+        except ValueError:
+            # No message body
+            pass
+
+        try:
+            # Now, seperate Request/Response line from the rest of the data
+            (self.first_line, rest) = data.split("\r\n", 1)
+
+            # Now convert any multi-line headers to a single line, and then split on newlines
+            header_array = re.sub(r'\r\n[ \t]+', ' ', rest).split("\r\n")
+
+            # Now convert the headers into an array of (field, val) tuples
+            self.headers = []
+            for h in header_array:
+                (field, val) = h.split(':', 1)
+                self.headers.append((field.rstrip(' \t').lower(), val.lstrip(' \t')))
+        except:
+            raise SIPParseError()
+
+    def get_header(self, header):
+        for h in self.headers:
+            if h[0] == header.lower():
+                return h[1]
+
+    def get_header_all(self, header):
+        res = []
+        for h in self.headers:
+            if h[0] == header.lower():
+                res.append(h[1])
+        return res
+
+    def __str__(self):
+        return "%s\r\n%s\r\n\r\n%s" % (self.first_line, "\r\n".join(["%s: %s" % (h[0].title(), h[1]) for h in self.headers]), self.body)
+
+
+class SIPMessageTest(object):
+    def __init__(self, pass_callback=None):
+        self.passed = False
+        self.matches_left = []
+        self.matches_passed = []
+        self.pass_callback = pass_callback
+
+    def add_required_match(self, match):
+        """Add a list of pattern matches that a single packet must match
+
+        match is an array of two-tuples, position 0 is either:
+            "first_line", "body", or a header name
+        position 1 is a regular expression to match against that portion
+        of the SIPMessage. For example, match any SIP INVITE that has a
+        Call-ID, match could be:
+            [("first_line", r'^INVITE', ('Call-ID', r'.*')]
+
+        For a packet to pass a match, all elements of the match must pass.
+        For the SIPMessageTest itself to pass, all added match arrays must
+        pass.
+
+        """
+        self.matches_left.append(match)
+
+    def _match_val(self, regex, arr):
+        for val in arr:
+            if re.search(regex, val) is not None:
+                return True
+        return False
+
+    def _match_match(self, sipmsg, match):
+        (key, regex) = match
+        if key == "first_line":
+            it = [sipmsg.first_line]
+        elif key == "body":
+            it = [sipmsg.body]
+        else:
+            it = sipmsg.get_header_all(key)
+
+        return self._match_val(regex, it)
+
+
+    def test_sip_msg(self, sipmsg):
+        if len(self.matches_left) > 0:
+            test = self.matches_left[0]
+            for match in test:
+                if self._match_match(sipmsg, match):
+                    self.matches_passed.append(test)
+                    del self.matches_left[0]
+                    if len(self.matches_left) == 0:
+                        self.passed = True
+                        if self.pass_callback is not None:
+                            self.pass_callback()
+                    break
+
+
+def main():
+    msg = """INVITE sip:123 at example.com SIP/2.0\r\nContact   : \tTerry Wilson\r\n   <terry at example.com>\r\nCall-ID:\r\n Whatever\r\nContact: New Contact\r\n\r\nData!!!!!"""
+    sipmsg = SIPMessage(msg)
+    print sipmsg
+    if sipmsg.get_header('CoNtact') is None:
+        return -1
+    if len(sipmsg.get_header_all('contact')) != 2:
+        return -1
+
+if __name__ == '__main__':
+    import sys
+    sys.exit(main())

Propchange: asterisk/trunk/lib/python/sip_message.py
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: asterisk/trunk/lib/python/sip_message.py
------------------------------------------------------------------------------
    svn:keywords = Author Date Id Revision

Propchange: asterisk/trunk/lib/python/sip_message.py
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: asterisk/trunk/runtests.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/runtests.py?view=diff&rev=2914&r1=2913&r2=2914
==============================================================================
--- asterisk/trunk/runtests.py (original)
+++ asterisk/trunk/runtests.py Fri Dec 16 10:25:47 2011
@@ -56,6 +56,7 @@
             self.stdout += msg + "\n"
             p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                                  stderr=subprocess.STDOUT)
+            self.pid = p.pid
             try:
                 for l in p.stdout.readlines():
                     print l,
@@ -70,6 +71,7 @@
             if not self.passed:
                 self.__archive_ast_logs()
                 self.__archive_core_dump()
+                self.__archive_pcap_dump()
 
         else:
             print "FAILED TO EXECUTE %s, it must exist and be executable" % cmd
@@ -122,6 +124,19 @@
                 break
             i += 1
 
+    def __archive_pcap_dump(self):
+        filename = "dumpfile.pcap"
+        test_base = self.test_name.lstrip("tests/")
+        src = os.path.join(Asterisk.test_suite_root, test_base, str(self.pid), filename)
+        dst = os.path.join("logs", test_base, filename)
+        if os.path.exists(src):
+            try:
+                hardlink_or_copy(src, dst)
+            except Exception, e:
+                print "Exeception occured while archiving pcap file from %s to %s: %s" % (
+                    src, dst, e
+                )
+
     def __check_can_run(self, ast_version):
         """Check tags and dependencies in the test config."""
         if self.test_config.check_deps(ast_version) and \

Added: asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/extensions.conf
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/extensions.conf?view=auto&rev=2914
==============================================================================
--- asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/extensions.conf (added)
+++ asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/extensions.conf Fri Dec 16 10:25:47 2011
@@ -1,0 +1,2 @@
+[default]
+exten => blah,1,Answer

Propchange: asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/extensions.conf
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/extensions.conf
------------------------------------------------------------------------------
    svn:keywords = Author Date Id Revision

Propchange: asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/extensions.conf
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/sip.conf
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/sip.conf?view=auto&rev=2914
==============================================================================
--- asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/sip.conf (added)
+++ asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/sip.conf Fri Dec 16 10:25:47 2011
@@ -1,0 +1,2 @@
+[general]
+allowguest=yes

Propchange: asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/sip.conf
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/sip.conf
------------------------------------------------------------------------------
    svn:keywords = Author Date Id Revision

Propchange: asterisk/trunk/tests/channels/SIP/pcap_demo/configs/ast1/sip.conf
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: asterisk/trunk/tests/channels/SIP/pcap_demo/run-test
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/tests/channels/SIP/pcap_demo/run-test?view=auto&rev=2914
==============================================================================
--- asterisk/trunk/tests/channels/SIP/pcap_demo/run-test (added)
+++ asterisk/trunk/tests/channels/SIP/pcap_demo/run-test Fri Dec 16 10:25:47 2011
@@ -1,0 +1,76 @@
+#!/usr/bin/env python
+
+import re
+import sys
+import os
+import logging
+
+sys.path.append("lib/python")
+
+from asterisk.TestCase import TestCase
+from asterisk.syncami import SyncAMI
+from sip_message import SIPMessage, SIPMessageTest
+
+from twisted.internet import reactor
+from construct.protocols.ipstack import ip_stack
+
+logger = logging.getLogger(__name__)
+test1 = [
+    ("first_line", r'^INVITE'),
+    ('Via', r'branch=z9hG4bK'),
+    ('Call-ID', r'.*')
+]
+test2 = [("first_line", r'^SIP/2.0 200')]
+
+DUMP_FILE = '/tmp/dumpfile.pcap'
+
+class PcapSIPTest(TestCase):
+    def __init__(self):
+        super(PcapSIPTest, self).__init__()
+        self.passed = False
+        self.create_asterisk()
+        self.listener = self.create_pcap_listener(None, "port 5060", dumpfile=DUMP_FILE)
+
+        # Creates a SIP matching test based on regular expression matching
+        # The required matches must be matched in order
+        self.siptest = SIPMessageTest(self.on_pcap_complete)
+        self.siptest.add_required_match(test1)
+        self.siptest.add_required_match(test2)
+
+        if not self.listener.pf.pcap.datalink == "EN10MB":
+            raise Exception("Unknown datalink")
+
+    def run(self):
+        super(PcapSIPTest, self).run()
+        self.ast[0].cli_exec("channel originate SIP/blah at 127.0.0.1 application Hangup")
+
+    def pcap_callback(self, pkt):
+        # Note, this only works because we are doing UDP. TCP-stream reassembly would
+        # need to be done for TCP data. We also don't handle fragmented IP here.
+        obj = ip_stack.parse(pkt.data)
+        # Ethernet->IP->UDP->Application
+        data = obj.next.next.next
+        try:
+            sipmsg = SIPMessage(data)
+            logger.info(sipmsg)
+            self.siptest.test_sip_msg(sipmsg)
+        except SIPParseError:
+            logger.warning("Got a packet we didn't think was SIP");
+
+
+    def on_pcap_complete(self):
+        logger.info("Test passed")
+        self.passed = True
+        reactor.stop()
+
+def main():
+    t = PcapSIPTest()
+    t.start_asterisk()
+    reactor.run()
+    t.stop_asterisk()
+    if t.passed:
+        return 0
+    return 1
+
+if __name__ == '__main__':
+    sys.exit(main())

Propchange: asterisk/trunk/tests/channels/SIP/pcap_demo/run-test
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: asterisk/trunk/tests/channels/SIP/pcap_demo/run-test
------------------------------------------------------------------------------
    svn:executable = *

Propchange: asterisk/trunk/tests/channels/SIP/pcap_demo/run-test
------------------------------------------------------------------------------
    svn:keywords = Author Date Id Revision

Propchange: asterisk/trunk/tests/channels/SIP/pcap_demo/run-test
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: asterisk/trunk/tests/channels/SIP/pcap_demo/test-config.yaml
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/tests/channels/SIP/pcap_demo/test-config.yaml?view=auto&rev=2914
==============================================================================
--- asterisk/trunk/tests/channels/SIP/pcap_demo/test-config.yaml (added)
+++ asterisk/trunk/tests/channels/SIP/pcap_demo/test-config.yaml Fri Dec 16 10:25:47 2011
@@ -1,0 +1,10 @@
+testinfo:
+    summary:    'Demo of PCAP and SIPMessage tests'
+    description: |
+        'Make a simple call and verify via pcap that an INVITE and a 200 happen'
+properties:
+    minversion: '1.4'
+    dependencies:
+        - python : 'twisted'
+        - python : 'construct'
+        - 'pcap'

Propchange: asterisk/trunk/tests/channels/SIP/pcap_demo/test-config.yaml
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: asterisk/trunk/tests/channels/SIP/pcap_demo/test-config.yaml
------------------------------------------------------------------------------
    svn:keywords = Author Date Id Revision

Propchange: asterisk/trunk/tests/channels/SIP/pcap_demo/test-config.yaml
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: asterisk/trunk/tests/channels/SIP/tests.yaml
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/tests/channels/SIP/tests.yaml?view=diff&rev=2914&r1=2913&r2=2914
==============================================================================
--- asterisk/trunk/tests/channels/SIP/tests.yaml (original)
+++ asterisk/trunk/tests/channels/SIP/tests.yaml Fri Dec 16 10:25:47 2011
@@ -30,3 +30,4 @@
     - test: 'realtime_nosipregs'
     - test: 'codec_negotiation'
     - test: 'nat_supertest'
+    - test: 'pcap_demo'




More information about the asterisk-commits mailing list