]>
Commit | Line | Data |
---|---|---|
586e15c4 MW |
1 | #!/usr/bin/env python |
2 | ||
3 | # | |
4 | # test_bgp_multiview_topo1.py | |
5 | # Part of NetDEF Topology Tests | |
6 | # | |
7 | # Copyright (c) 2016 by | |
8 | # Network Device Education Foundation, Inc. ("NetDEF") | |
9 | # | |
10 | # Permission to use, copy, modify, and/or distribute this software | |
11 | # for any purpose with or without fee is hereby granted, provided | |
12 | # that the above copyright notice and this permission notice appear | |
13 | # in all copies. | |
14 | # | |
15 | # THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES | |
16 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |
17 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR | |
18 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY | |
19 | # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, | |
20 | # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS | |
21 | # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE | |
22 | # OF THIS SOFTWARE. | |
23 | # | |
24 | ||
25 | """ | |
26 | test_bgp_multiview_topo1.py: Simple Quagga Route-Server Test | |
27 | ||
28 | See Topology Diagram bgp-routeserver-1.pdf | |
29 | """ | |
30 | ||
31 | import os | |
32 | import re | |
33 | import sys | |
34 | import difflib | |
35 | import StringIO | |
2cda38e4 MW |
36 | import glob |
37 | import subprocess | |
586e15c4 MW |
38 | |
39 | from mininet.topo import Topo | |
40 | from mininet.net import Mininet | |
41 | from mininet.node import Node, OVSSwitch, Host | |
42 | from mininet.log import setLogLevel, info | |
43 | from mininet.cli import CLI | |
44 | from mininet.link import Intf | |
45 | ||
46 | from functools import partial | |
47 | from time import sleep | |
48 | ||
49 | import pytest | |
50 | ||
2cda38e4 MW |
51 | fatal_error = "" |
52 | ||
586e15c4 MW |
53 | def int2dpid(dpid): |
54 | "Converting Integer to DPID" | |
55 | ||
56 | try: | |
57 | dpid = hex(dpid)[2:] | |
58 | dpid = '0'*(16-len(dpid))+dpid | |
59 | return dpid | |
60 | except IndexError: | |
61 | raise Exception('Unable to derive default datapath ID - ' | |
62 | 'please either specify a dpid or use a ' | |
63 | 'canonical switch name such as s23.') | |
64 | ||
65 | class LinuxRouter(Node): | |
66 | "A Node with IPv4/IPv6 forwarding enabled." | |
67 | ||
68 | def config(self, **params): | |
69 | super(LinuxRouter, self).config(**params) | |
70 | # Enable forwarding on the router | |
71 | self.cmd('sysctl net.ipv4.ip_forward=1') | |
72 | self.cmd('sysctl net.ipv6.conf.all.forwarding=1') | |
73 | def terminate(self): | |
74 | """ | |
75 | Terminate generic LinuxRouter Mininet instance | |
76 | """ | |
77 | self.cmd('sysctl net.ipv4.ip_forward=0') | |
78 | self.cmd('sysctl net.ipv6.conf.all.forwarding=0') | |
79 | super(LinuxRouter, self).terminate() | |
80 | ||
81 | class QuaggaRouter(Node): | |
82 | "A Node with IPv4/IPv6 forwarding enabled and Quagga as Routing Engine" | |
83 | ||
84 | def config(self, **params): | |
85 | super(QuaggaRouter, self).config(**params) | |
86 | # Enable forwarding on the router | |
87 | self.cmd('sysctl net.ipv4.ip_forward=1') | |
88 | self.cmd('sysctl net.ipv6.conf.all.forwarding=1') | |
2cda38e4 MW |
89 | # Enable coredumps |
90 | self.cmd('sysctl kernel.core_uses_pid=1') | |
91 | self.cmd('sysctl fs.suid_dumpable=2') | |
92 | self.cmd("sysctl kernel.core_pattern=/tmp/%s_%%e_core-sig_%%s-pid_%%p.dmp" % self.name) | |
5ead87b4 | 93 | self.cmd('ulimit -c unlimited') |
2cda38e4 | 94 | # Set ownership of config files |
586e15c4 MW |
95 | self.cmd('chown quagga:quaggavty /etc/quagga') |
96 | self.daemons = {'zebra': 0, 'ripd': 0, 'ripngd': 0, 'ospfd': 0, | |
97 | 'ospf6d': 0, 'isisd': 0, 'bgpd': 0, 'pimd': 0} | |
98 | def terminate(self): | |
99 | # Delete Running Quagga Daemons | |
100 | rundaemons = self.cmd('ls -1 /var/run/quagga/*.pid') | |
101 | for d in StringIO.StringIO(rundaemons): | |
102 | self.cmd('kill -7 `cat %s`' % d.rstrip()) | |
103 | self.waitOutput() | |
104 | # Disable forwarding | |
105 | self.cmd('sysctl net.ipv4.ip_forward=0') | |
106 | self.cmd('sysctl net.ipv6.conf.all.forwarding=0') | |
107 | super(QuaggaRouter, self).terminate() | |
108 | def removeIPs(self): | |
109 | for interface in self.intfNames(): | |
110 | self.cmd('ip address flush', interface) | |
111 | def loadConf(self, daemon, source=None): | |
112 | # print "Daemons before:", self.daemons | |
113 | if daemon in self.daemons.keys(): | |
114 | self.daemons[daemon] = 1 | |
115 | if source is None: | |
116 | self.cmd('touch /etc/quagga/%s.conf' % daemon) | |
117 | self.waitOutput() | |
118 | else: | |
119 | self.cmd('cp %s /etc/quagga/%s.conf' % (source, daemon)) | |
120 | self.waitOutput() | |
121 | self.cmd('chmod 640 /etc/quagga/%s.conf' % daemon) | |
122 | self.waitOutput() | |
123 | self.cmd('chown quagga:quagga /etc/quagga/%s.conf' % daemon) | |
124 | self.waitOutput() | |
125 | else: | |
126 | print("No daemon %s known" % daemon) | |
127 | # print "Daemons after:", self.daemons | |
128 | def startQuagga(self): | |
129 | # Disable integrated-vtysh-config | |
130 | self.cmd('echo "no service integrated-vtysh-config" > /etc/quagga/vtysh.conf') | |
131 | with open("/etc/quagga/vtysh.conf", "w") as vtyshfile: | |
132 | vtyshfile.write('no service integrated-vtysh-config') | |
133 | self.cmd('chown quagga:quaggavty /etc/quagga/vtysh.conf') | |
2cda38e4 MW |
134 | # Try to find relevant old logfiles in /tmp and delete them |
135 | map(os.remove, glob.glob("/tmp/*%s*.log" % self.name)) | |
136 | # Remove old core files | |
137 | map(os.remove, glob.glob("/tmp/%s*.dmp" % self.name)) | |
586e15c4 MW |
138 | # Remove IP addresses from OS first - we have them in zebra.conf |
139 | self.removeIPs() | |
140 | # Start Zebra first | |
141 | if self.daemons['zebra'] == 1: | |
142 | self.cmd('/usr/lib/quagga/zebra -d') | |
143 | self.waitOutput() | |
144 | print('%s: zebra started' % self) | |
145 | sleep(1) | |
146 | # Fix Link-Local Addresses | |
147 | # Somehow (on Mininet only), Zebra removes the IPv6 Link-Local addresses on start. Fix this | |
148 | self.cmd('for i in `ls /sys/class/net/` ; do mac=`cat /sys/class/net/$i/address`; IFS=\':\'; set $mac; unset IFS; ip address add dev $i scope link fe80::$(printf %02x $((0x$1 ^ 2)))$2:${3}ff:fe$4:$5$6/64; done') | |
149 | # Now start all the other daemons | |
150 | for daemon in self.daemons: | |
151 | if (self.daemons[daemon] == 1) and (daemon != 'zebra'): | |
152 | self.cmd('/usr/lib/quagga/%s -d' % daemon) | |
153 | self.waitOutput() | |
154 | print('%s: %s started' % (self, daemon)) | |
155 | def checkQuaggaRunning(self): | |
2cda38e4 MW |
156 | global fatal_error |
157 | ||
586e15c4 MW |
158 | daemonsRunning = self.cmd('vtysh -c "show log" | grep "Logging configuration for"') |
159 | for daemon in self.daemons: | |
2cda38e4 MW |
160 | if (self.daemons[daemon] == 1) and not (daemon in daemonsRunning): |
161 | sys.stderr.write("%s: Daemon %s not running\n" % (self.name, daemon)) | |
162 | # Look for core file | |
163 | corefiles = glob.glob("/tmp/%s_%s_core*.dmp" % (self.name, daemon)) | |
164 | if (len(corefiles) > 0): | |
5ead87b4 MW |
165 | backtrace = subprocess.check_output(["gdb /usr/lib/quagga/%s %s --batch -ex bt 2> /dev/null" % (daemon, corefiles[0])], shell=True) |
166 | sys.stderr.write("\n%s: %s crashed. Core file found - Backtrace follows:\n" % (self.name, daemon)) | |
2cda38e4 MW |
167 | sys.stderr.write("%s\n" % backtrace) |
168 | else: | |
169 | # No core found - If we find matching logfile in /tmp, then print last 20 lines from it. | |
170 | if os.path.isfile("/tmp/%s-%s.log" % (self.name, daemon)): | |
5ead87b4 MW |
171 | log_tail = subprocess.check_output(["tail -n20 /tmp/%s-%s.log 2> /dev/null" % (self.name, daemon)], shell=True) |
172 | sys.stderr.write("\nFrom quagga %s %s log file:\n" % (self.name, daemon)) | |
2cda38e4 MW |
173 | sys.stderr.write("%s\n" % log_tail) |
174 | ||
175 | fatal_error = "%s: Daemon %s not running" % (self.name, daemon) | |
176 | assert False, "%s: Daemon %s not running" % (self.name, daemon) | |
586e15c4 MW |
177 | |
178 | class LegacySwitch(OVSSwitch): | |
179 | "A Legacy Switch without OpenFlow" | |
180 | ||
181 | def __init__(self, name, **params): | |
182 | OVSSwitch.__init__(self, name, failMode='standalone', **params) | |
183 | self.switchIP = None | |
184 | ||
185 | ||
186 | ##################################################### | |
187 | ## | |
188 | ## Network Topology Definition | |
189 | ## | |
190 | ##################################################### | |
191 | ||
192 | class NetworkTopo(Topo): | |
193 | "A LinuxRouter connecting three IP subnets" | |
194 | ||
195 | def build(self, **_opts): | |
196 | ||
197 | quaggaPrivateDirs = ['/etc/quagga', | |
198 | '/var/run/quagga', | |
199 | '/var/log', | |
200 | '/var/run/ssh'] | |
201 | exabgpPrivateDirs = ['/etc/exabgp', | |
202 | '/var/run/exabgp', | |
203 | '/var/log'] | |
204 | ||
205 | # Setup Routers | |
206 | quagga = {} | |
207 | for i in range(1, 2): | |
208 | quagga[i] = self.addNode('r%s' % i, cls=QuaggaRouter, | |
209 | privateDirs=quaggaPrivateDirs) | |
210 | ||
211 | # Setup Provider BGP peers | |
212 | peer = {} | |
213 | for i in range(1, 9): | |
214 | peer[i] = self.addHost('peer%s' % i, ip='172.16.1.%s/24' % i, | |
215 | defaultRoute='via 172.16.1.254', | |
216 | privateDirs=exabgpPrivateDirs) | |
217 | ||
218 | # Setup Switches | |
219 | switch = {} | |
220 | # First switch is for a dummy interface (for local network) | |
221 | switch[0] = self.addSwitch('sw0', cls=LegacySwitch) | |
222 | self.addLink(switch[0], quagga[1], intfName2='r1-stub') | |
223 | # Second switch is for connection to all peering routers | |
224 | switch[1] = self.addSwitch('sw1', cls=LegacySwitch) | |
225 | self.addLink(switch[1], quagga[1], intfName2='r1-eth0') | |
226 | for j in range(1, 9): | |
227 | self.addLink(switch[1], peer[j], intfName2='peer%s-eth0' % j) | |
228 | ||
229 | ||
230 | ##################################################### | |
231 | ## | |
232 | ## Tests starting | |
233 | ## | |
234 | ##################################################### | |
235 | ||
236 | def setup_module(module): | |
237 | global topo, net | |
238 | ||
239 | print("\n\n** %s: Setup Topology" % module.__name__) | |
240 | print("******************************************\n") | |
241 | ||
242 | print("Cleanup old Mininet runs") | |
243 | os.system('sudo mn -c > /dev/null 2>&1') | |
244 | ||
245 | thisDir = os.path.dirname(os.path.realpath(__file__)) | |
246 | topo = NetworkTopo() | |
247 | ||
248 | net = Mininet(controller=None, topo=topo) | |
249 | net.start() | |
250 | ||
251 | # Starting Routers | |
252 | for i in range(1, 2): | |
253 | net['r%s' % i].loadConf('zebra', '%s/r%s/zebra.conf' % (thisDir, i)) | |
254 | net['r%s' % i].loadConf('bgpd', '%s/r%s/bgpd.conf' % (thisDir, i)) | |
255 | net['r%s' % i].startQuagga() | |
256 | ||
257 | # Starting PE Hosts and init ExaBGP on each of them | |
258 | print('*** Starting BGP on all 8 Peers in 10s') | |
259 | sleep(10) | |
260 | for i in range(1, 9): | |
261 | net['peer%s' % i].cmd('cp %s/exabgp.env /etc/exabgp/exabgp.env' % thisDir) | |
262 | net['peer%s' % i].cmd('cp %s/peer%s/* /etc/exabgp/' % (thisDir, i)) | |
263 | net['peer%s' % i].cmd('chmod 644 /etc/exabgp/*') | |
264 | net['peer%s' % i].cmd('chmod 755 /etc/exabgp/*.py') | |
265 | net['peer%s' % i].cmd('chown -R exabgp:exabgp /etc/exabgp') | |
266 | net['peer%s' % i].cmd('exabgp -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg') | |
267 | print('peer%s' % i), | |
268 | print('') | |
269 | ||
270 | # For debugging after starting Quagga daemons, uncomment the next line | |
271 | # CLI(net) | |
272 | ||
273 | def teardown_module(module): | |
274 | global net | |
275 | ||
276 | print("\n\n** %s: Shutdown Topology" % module.__name__) | |
277 | print("******************************************\n") | |
278 | ||
279 | # Shutdown - clean up everything | |
280 | print('*** Killing BGP on Peer routers') | |
281 | # Killing ExaBGP | |
282 | for i in range(1, 9): | |
283 | net['peer%s' % i].cmd('kill `cat /var/run/exabgp/exabgp.pid`') | |
284 | ||
285 | # End - Shutdown network | |
286 | net.stop() | |
287 | ||
288 | def test_quagga_running(): | |
2cda38e4 | 289 | global fatal_error |
586e15c4 MW |
290 | global net |
291 | ||
2cda38e4 MW |
292 | # Skip if previous fatal error condition is raised |
293 | if (fatal_error != ""): | |
294 | pytest.skip(fatal_error) | |
295 | ||
586e15c4 MW |
296 | print("\n\n** Check if Quagga is running on each Router node") |
297 | print("******************************************\n") | |
298 | sleep(5) | |
299 | ||
300 | # Starting Routers | |
301 | for i in range(1, 2): | |
302 | net['r%s' % i].checkQuaggaRunning() | |
303 | ||
304 | def test_bgp_converge(): | |
305 | "Check for BGP converged on all peers and BGP views" | |
306 | ||
2cda38e4 | 307 | global fatal_error |
586e15c4 MW |
308 | global net |
309 | ||
2cda38e4 MW |
310 | # Skip if previous fatal error condition is raised |
311 | if (fatal_error != ""): | |
312 | pytest.skip(fatal_error) | |
313 | ||
586e15c4 MW |
314 | # Wait for BGP to converge (All Neighbors in either Full or TwoWay State) |
315 | print("\n\n** Verify for BGP to converge") | |
316 | print("******************************************\n") | |
317 | timeout = 60 | |
318 | while timeout > 0: | |
319 | print("Timeout in %s: " % timeout), | |
320 | sys.stdout.flush() | |
321 | # Look for any node not yet converged | |
322 | for i in range(1, 2): | |
323 | for view in range(1, 4): | |
324 | notConverged = net['r%s' % i].cmd('vtysh -c "show ip bgp view %s summary" 2> /dev/null | grep ^[0-9] | grep -v " 11$"' % view) | |
325 | if notConverged: | |
326 | print('Waiting for r%s, view %s' % (i, view)) | |
327 | sys.stdout.flush() | |
328 | break | |
329 | if notConverged: | |
330 | break | |
331 | if notConverged: | |
332 | sleep(5) | |
333 | timeout -= 5 | |
334 | else: | |
335 | print('Done') | |
336 | break | |
337 | else: | |
338 | # Bail out with error if a router fails to converge | |
339 | bgpStatus = net['r%s' % i].cmd('show ip bgp view %s summary"') | |
586e15c4 MW |
340 | assert False, "BGP did not converge:\n%s" % bgpStatus |
341 | ||
342 | print("BGP converged.") | |
343 | ||
344 | # if timeout < 60: | |
345 | # # Only wait if we actually went through a convergence | |
346 | # print("\nwaiting 15s for routes to populate") | |
347 | # sleep(15) | |
348 | ||
349 | # For debugging after starting Quagga daemons, uncomment the next line | |
350 | # CLI(net) | |
351 | ||
352 | def test_bgp_routingTable(): | |
2cda38e4 | 353 | global fatal_error |
586e15c4 MW |
354 | global net |
355 | ||
2cda38e4 MW |
356 | # Skip if previous fatal error condition is raised |
357 | if (fatal_error != ""): | |
358 | pytest.skip(fatal_error) | |
359 | ||
586e15c4 MW |
360 | thisDir = os.path.dirname(os.path.realpath(__file__)) |
361 | ||
362 | # Verify OSPFv3 Routing Table | |
363 | print("\n\n** Verifing BGP Routing Tables") | |
364 | print("******************************************\n") | |
365 | failures = 0 | |
366 | for i in range(1, 2): | |
367 | for view in range(1, 4): | |
368 | refTableFile = '%s/r%s/show_ip_bgp_view_%s.ref' % (thisDir, i, view) | |
369 | if os.path.isfile(refTableFile): | |
370 | # Read expected result from file | |
371 | expected = open(refTableFile).read().rstrip() | |
372 | # Fix newlines (make them all the same) | |
373 | expected = ('\n'.join(expected.splitlines()) + '\n').splitlines(1) | |
374 | ||
375 | # Actual output from router | |
376 | actual = net['r%s' % i].cmd('vtysh -c "show ip bgp view %s" 2> /dev/null' % view).rstrip() | |
377 | ||
378 | # Fix inconsitent spaces between 0.99.24 and newer versions of Quagga... | |
379 | actual = re.sub('0 0', '0 0', actual) | |
380 | actual = re.sub(r'([0-9]) 32768', r'\1 32768', actual) | |
381 | # Remove summary line (changed recently) | |
382 | actual = re.sub(r'Total number.*', '', actual) | |
383 | actual = re.sub(r'Displayed.*', '', actual) | |
384 | actual = actual.rstrip() | |
385 | ||
386 | # Fix newlines (make them all the same) | |
387 | actual = ('\n'.join(actual.splitlines()) + '\n').splitlines(1) | |
388 | ||
389 | # Generate Diff | |
390 | diff = ''.join(difflib.unified_diff(actual, expected)) | |
391 | # Empty string if it matches, otherwise diff contains unified diff | |
392 | ||
393 | if diff: | |
394 | sys.stderr.write('r%s failed Routing Table Check for view %s:\n%s\n' | |
395 | % (i, view, diff)) | |
396 | failures += 1 | |
397 | else: | |
398 | print("r%s ok" % i) | |
399 | ||
400 | assert failures == 0, "Routing Table verification failed for router r%s, view %s:\n%s" % (i, view, diff) | |
401 | ||
402 | # For debugging after starting Quagga daemons, uncomment the next line | |
403 | # CLI(net) | |
404 | ||
405 | ||
406 | if __name__ == '__main__': | |
407 | ||
408 | setLogLevel('info') | |
2cda38e4 MW |
409 | # To suppress tracebacks, either use the following pytest call or add "--tb=no" to cli |
410 | # retval = pytest.main(["-s", "--tb=no"]) | |
586e15c4 MW |
411 | retval = pytest.main(["-s"]) |
412 | sys.exit(retval) |