Srikanth Vavilapalli | 1725e49 | 2014-12-01 17:50:52 -0800 | [diff] [blame^] | 1 | #!/usr/bin/env python |
| 2 | # |
| 3 | # Copyright (c) 2013 Big Switch Networks, Inc. |
| 4 | # |
| 5 | # Licensed under the Eclipse Public License, Version 1.0 (the |
| 6 | # "License"); you may not use this file except in compliance with the |
| 7 | # License. You may obtain a copy of the License at |
| 8 | # |
| 9 | # http://www.eclipse.org/legal/epl-v10.html |
| 10 | # |
| 11 | # Unless required by applicable law or agreed to in writing, software |
| 12 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
| 14 | # implied. See the License for the specific language governing |
| 15 | # permissions and limitations under the License. |
| 16 | # |
| 17 | |
| 18 | # |
| 19 | # Copyright (c) 2011,2012 Big Switch Networks, Inc. |
| 20 | # All rights reserved. |
| 21 | # |
| 22 | |
| 23 | from sys import stderr |
| 24 | from re import compile, match, search |
| 25 | from optparse import OptionParser |
| 26 | from collections import defaultdict |
| 27 | from subprocess import Popen, PIPE, STDOUT |
| 28 | |
| 29 | import socket, fcntl, struct |
| 30 | def get_ipaddr(ifname): |
| 31 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| 32 | ipaddr = fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', ifname[:15])) # SIOCGIFADDR |
| 33 | return socket.inet_ntoa(ipaddr[20:24]) |
| 34 | |
| 35 | import urllib, json |
| 36 | def get_aliases(controller='localhost', controller_port=6633, ifname='eth0'): |
| 37 | url = "http://%s/rest/v1/model/switch/" % controller |
| 38 | aliases = {get_ipaddr(ifname)+':%d'%controller_port: 'controller'} |
| 39 | for switch in json.loads(urllib.urlopen(url).read()): |
| 40 | socketaddr = switch.get('socket-address') |
| 41 | if socketaddr: |
| 42 | socketaddr = socketaddr[1:] |
| 43 | alias = switch.get('alias') |
| 44 | if not alias: |
| 45 | alias = switch.get('dpid') |
| 46 | aliases[socketaddr] = alias |
| 47 | return aliases |
| 48 | |
| 49 | DEBUG=False |
| 50 | |
| 51 | class OpenFlowTracer(object): |
| 52 | # Formats |
| 53 | SUMMARY = "summary" |
| 54 | DETAIL = "detail" |
| 55 | ONELINE = "oneline" |
| 56 | |
| 57 | def __init__(self, msg_filters=None, tcpdump_filter='', fmt=SUMMARY, alias=False, printer=None): |
| 58 | self.msg_filters = msg_filters |
| 59 | self.tcpdump_filter = tcpdump_filter |
| 60 | self.detail = (fmt != self.SUMMARY) |
| 61 | self.oneline = (fmt == self.ONELINE) |
| 62 | self.printer = self.prn |
| 63 | if printer: |
| 64 | self.printer = printer |
| 65 | |
| 66 | self.pkt = defaultdict(str) |
| 67 | self.reading_detail = False |
| 68 | self.alias = alias |
| 69 | self.aliases = None |
| 70 | if self.alias: |
| 71 | self.aliases = get_aliases() |
| 72 | |
| 73 | def run(self): |
| 74 | self.tcpdump_trace(self.tcpdump_open()) |
| 75 | return |
| 76 | |
| 77 | # filter packets |
| 78 | def filter(self): |
| 79 | msg_type = self.pkt['of-msg'] |
| 80 | if not msg_type: |
| 81 | return False |
| 82 | if self.msg_filters: |
| 83 | return any(map(lambda pattern: search(pattern, msg_type), self.msg_filters)) |
| 84 | return True |
| 85 | |
| 86 | # format packet output |
| 87 | def fmt(self): |
| 88 | pkt = self.pkt |
| 89 | sep = '\n ' |
| 90 | if self.oneline: sep = ', ' |
| 91 | |
| 92 | src = '%s:%s' % (pkt['src-ip'], pkt['src-port']) |
| 93 | dst = '%s:%s' % (pkt['dst-ip'], pkt['dst-port']) |
| 94 | if self.alias: |
| 95 | if not self.aliases.get(src) or not self.aliases.get(dst): |
| 96 | self.aliases = get_aliases() |
| 97 | src = self.aliases.get(src, src) |
| 98 | dst = self.aliases.get(dst, dst) |
| 99 | |
| 100 | ret = '%-18s %-18s [ %s -> %s ]' % ( |
| 101 | pkt['timestamp'], pkt['of-msg'], src, dst) |
| 102 | if self.detail and pkt['of-data']: |
| 103 | for data in pkt['of-data'].split(','): |
| 104 | data = data.strip() |
| 105 | if data: |
| 106 | ret += (sep + data) |
| 107 | return ret |
| 108 | |
| 109 | def prn(self, x): |
| 110 | print x |
| 111 | |
| 112 | def tcpdump_open(self): |
| 113 | tcpdump_cmd = 'sudo /usr/local/sbin/tcpdump -i any \'%s\' %s' |
| 114 | tcpdump_args = '-l -nn -vv' |
| 115 | self.process = Popen(tcpdump_cmd % (self.tcpdump_filter, tcpdump_args), |
| 116 | shell=True, bufsize=1, stdout=PIPE, stderr=STDOUT) |
| 117 | return self.process.stdout |
| 118 | |
| 119 | def tcpdump_trace(self, fd): |
| 120 | # tcpdump decoder patterns |
| 121 | empty_line = compile('^$') |
| 122 | tcpdump_line = compile('^tcpdump.*$') |
| 123 | ip_line = compile('^(\d{2}\:\d{2}:\d{2}\.\d+) IP .*$') |
| 124 | tcp_line = compile('^\s*(\d+\.\d+\.\d+\.\d+)\.(\d+) \> (\d+\.\d+\.\d+\.\d+)\.(\d+)\:.*length \d+(.*)$') |
| 125 | of_hdr = compile('^([^ ]*) \(xid\=([^\)]*)\)\:(.*)$') |
| 126 | of_line = compile('^(.*in_port\=.*)$') |
| 127 | if_cont_line = compile('^\s+current\:') |
| 128 | inline_err = compile('^\s*\(\*\*\*.*$') |
| 129 | pkt_count = compile('^\d+ packets ') |
| 130 | |
| 131 | # helper fn |
| 132 | def of_data(pkt, data): |
| 133 | if pkt['of-msg'] == 'packet_out': |
| 134 | data = data.replace(' ', ',') |
| 135 | pkt['of-data'] += (',' + data) |
| 136 | |
| 137 | # decode packet data |
| 138 | line = fd.readline() |
| 139 | while line: |
| 140 | try: |
| 141 | line = line.strip() |
| 142 | pkt = self.pkt |
| 143 | if empty_line.match(line): |
| 144 | pass |
| 145 | elif pkt_count.match(line): |
| 146 | print>>stderr, line |
| 147 | elif tcpdump_line.match(line): |
| 148 | pass |
| 149 | elif ip_line.match(line): |
| 150 | if self.filter(): self.printer(self.fmt()) |
| 151 | self.pkt = defaultdict(str) |
| 152 | self.reading_detail = False |
| 153 | pkt = self.pkt |
| 154 | pkt['timestamp'] = ip_line.match(line).groups()[0] |
| 155 | elif tcp_line.match(line): |
| 156 | m = tcp_line.match(line).groups() |
| 157 | pkt['src-ip'] = m[0] |
| 158 | pkt['src-port'] = m[1] |
| 159 | pkt['dst-ip'] = m[2] |
| 160 | pkt['dst-port'] = m[3] |
| 161 | if of_hdr.match(m[4]): |
| 162 | self.reading_detail = True |
| 163 | m = of_hdr.match(m[4]).groups() |
| 164 | pkt['of-msg'] = m[0].strip() |
| 165 | pkt['of-xid'] = m[1] |
| 166 | data = m[2] |
| 167 | if not inline_err.match(m[2]): |
| 168 | of_data(pkt, data) |
| 169 | else: |
| 170 | if DEBUG: |
| 171 | print>>stderr, 'Inline Error', data |
| 172 | elif of_line.match(line): |
| 173 | of_data(pkt, of_line.match(line).groups()[0]) |
| 174 | elif if_cont_line.match(line): |
| 175 | line = line.replace(',', '; ') |
| 176 | of_data(pkt, '; '+line) |
| 177 | elif self.reading_detail: |
| 178 | line = line.replace(',', '; ') |
| 179 | of_data(pkt, line) |
| 180 | else: |
| 181 | if DEBUG: |
| 182 | print>>stderr, 'Unexpected: Line', line |
| 183 | line = fd.readline() |
| 184 | except KeyboardInterrupt: |
| 185 | print |
| 186 | if self.process: |
| 187 | self.process.terminate() |
| 188 | self.process = None |
| 189 | else: |
| 190 | break |
| 191 | |
| 192 | def main(): |
| 193 | usage = "usage: %prog [--detail] [--oneline] [--filter <tcpdump-filter>] [<msg-type> ...]" |
| 194 | optparser = OptionParser(usage=usage) |
| 195 | optparser.add_option("-f", "--filter", dest="tcpdump_filter", |
| 196 | action="store", type="string", default='((tcp) and (port 6633))', |
| 197 | help="tcpdump filter rule", metavar="FILTER") |
| 198 | optparser.add_option("-d", "--detail", dest="detail", |
| 199 | action="store_true", default=False, |
| 200 | help="print message detail") |
| 201 | optparser.add_option("-o", "--oneline", dest="oneline", |
| 202 | action="store_true", default=False, |
| 203 | help="print message detail on one line") |
| 204 | optparser.add_option("-a", "--alias", dest="alias", |
| 205 | action="store_true", default=False, |
| 206 | help="query controller to resolve address to switch-dpid/alias") |
| 207 | (options, msg_filters) = optparser.parse_args() |
| 208 | |
| 209 | try: |
| 210 | print 'Starting openflow trace, use ^C to quit' |
| 211 | fmt = OpenFlowTracer.SUMMARY |
| 212 | if options.detail: fmt = OpenFlowTracer.DETAIL |
| 213 | if options.oneline: fmt = OpenFlowTracer.ONELINE |
| 214 | tracer = OpenFlowTracer(msg_filters, options.tcpdump_filter, fmt=fmt, alias=options.alias) |
| 215 | tracer.run() |
| 216 | except KeyboardInterrupt: |
| 217 | pass |
| 218 | |
| 219 | if __name__ == '__main__': |
| 220 | main() |