[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