]>
Commit | Line | Data |
---|---|---|
594b1259 MW |
1 | #!/usr/bin/env python |
2 | ||
3 | # | |
4 | # topotest.py | |
5 | # Library of helper functions for 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 | import os | |
26 | import re | |
27 | import sys | |
28 | import glob | |
29 | import StringIO | |
30 | import subprocess | |
31 | import platform | |
32 | ||
33 | from mininet.topo import Topo | |
34 | from mininet.net import Mininet | |
35 | from mininet.node import Node, OVSSwitch, Host | |
36 | from mininet.log import setLogLevel, info | |
37 | from mininet.cli import CLI | |
38 | from mininet.link import Intf | |
39 | ||
40 | from time import sleep | |
41 | ||
42 | def int2dpid(dpid): | |
43 | "Converting Integer to DPID" | |
44 | ||
45 | try: | |
46 | dpid = hex(dpid)[2:] | |
47 | dpid = '0'*(16-len(dpid))+dpid | |
48 | return dpid | |
49 | except IndexError: | |
50 | raise Exception('Unable to derive default datapath ID - ' | |
51 | 'please either specify a dpid or use a ' | |
52 | 'canonical switch name such as s23.') | |
53 | ||
54 | def addRouter(topo, name): | |
55 | "Adding a FreeRangeRouter (or Quagga) to Topology" | |
56 | ||
57 | MyPrivateDirs = ['/etc/frr', | |
58 | '/etc/quagga', | |
59 | '/var/run/frr', | |
60 | '/var/run/quagga', | |
61 | '/var/log'] | |
62 | return topo.addNode(name, cls=Router, privateDirs=MyPrivateDirs) | |
63 | ||
64 | class LinuxRouter(Node): | |
65 | "A Node with IPv4/IPv6 forwarding enabled." | |
66 | ||
67 | def config(self, **params): | |
68 | super(LinuxRouter, self).config(**params) | |
69 | # Enable forwarding on the router | |
70 | self.cmd('sysctl net.ipv4.ip_forward=1') | |
71 | self.cmd('sysctl net.ipv6.conf.all.forwarding=1') | |
72 | def terminate(self): | |
73 | """ | |
74 | Terminate generic LinuxRouter Mininet instance | |
75 | """ | |
76 | self.cmd('sysctl net.ipv4.ip_forward=0') | |
77 | self.cmd('sysctl net.ipv6.conf.all.forwarding=0') | |
78 | super(LinuxRouter, self).terminate() | |
79 | ||
80 | class Router(Node): | |
81 | "A Node with IPv4/IPv6 forwarding enabled and Quagga as Routing Engine" | |
82 | ||
83 | def config(self, **params): | |
84 | super(Router, self).config(**params) | |
85 | ||
86 | # Check if Quagga or FRR is installed | |
87 | if os.path.isfile('/usr/lib/frr/zebra'): | |
88 | self.routertype = 'frr' | |
89 | elif os.path.isfile('/usr/lib/quagga/zebra'): | |
90 | self.routertype = 'quagga' | |
91 | else: | |
92 | raise Exception('No FRR or Quagga found in ususal location') | |
93 | # Enable forwarding on the router | |
94 | self.cmd('sysctl net.ipv4.ip_forward=1') | |
95 | self.cmd('sysctl net.ipv6.conf.all.forwarding=1') | |
96 | # Enable coredumps | |
97 | self.cmd('sysctl kernel.core_uses_pid=1') | |
98 | self.cmd('sysctl fs.suid_dumpable=2') | |
99 | self.cmd("sysctl kernel.core_pattern=/tmp/%s_%%e_core-sig_%%s-pid_%%p.dmp" % self.name) | |
100 | self.cmd('ulimit -c unlimited') | |
101 | # Set ownership of config files | |
102 | self.cmd('chown %s:%svty /etc/%s' % (self.routertype, self.routertype, self.routertype)) | |
103 | self.daemons = {'zebra': 0, 'ripd': 0, 'ripngd': 0, 'ospfd': 0, | |
104 | 'ospf6d': 0, 'isisd': 0, 'bgpd': 0, 'pimd': 0, | |
105 | 'ldpd': 0} | |
106 | def terminate(self): | |
107 | # Delete Running Quagga or FRR Daemons | |
99561211 MW |
108 | self.stopRouter() |
109 | # rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype) | |
110 | # for d in StringIO.StringIO(rundaemons): | |
111 | # self.cmd('kill -7 `cat %s`' % d.rstrip()) | |
112 | # self.waitOutput() | |
594b1259 MW |
113 | # Disable forwarding |
114 | self.cmd('sysctl net.ipv4.ip_forward=0') | |
115 | self.cmd('sysctl net.ipv6.conf.all.forwarding=0') | |
116 | super(Router, self).terminate() | |
99561211 MW |
117 | def stopRouter(self): |
118 | # Stop Running Quagga or FRR Daemons | |
119 | rundaemons = self.cmd('ls -1 /var/run/%s/*.pid' % self.routertype) | |
120 | if rundaemons is not None: | |
121 | for d in StringIO.StringIO(rundaemons): | |
122 | self.cmd('kill -7 `cat %s`' % d.rstrip()) | |
123 | self.waitOutput() | |
594b1259 MW |
124 | def removeIPs(self): |
125 | for interface in self.intfNames(): | |
126 | self.cmd('ip address flush', interface) | |
127 | def loadConf(self, daemon, source=None): | |
128 | # print "Daemons before:", self.daemons | |
129 | if daemon in self.daemons.keys(): | |
130 | self.daemons[daemon] = 1 | |
131 | if source is None: | |
132 | self.cmd('touch /etc/%s/%s.conf' % (self.routertype, daemon)) | |
133 | self.waitOutput() | |
134 | else: | |
135 | self.cmd('cp %s /etc/%s/%s.conf' % (source, self.routertype, daemon)) | |
136 | self.waitOutput() | |
137 | self.cmd('chmod 640 /etc/%s/%s.conf' % (self.routertype, daemon)) | |
138 | self.waitOutput() | |
139 | self.cmd('chown %s:%s /etc/%s/%s.conf' % (self.routertype, self.routertype, self.routertype, daemon)) | |
140 | self.waitOutput() | |
141 | else: | |
142 | print("No daemon %s known" % daemon) | |
143 | # print "Daemons after:", self.daemons | |
144 | def startRouter(self): | |
145 | # Disable integrated-vtysh-config | |
146 | with open('/etc/%s/vtysh.conf' % self.routertype, "w") as vtyshfile: | |
147 | vtyshfile.write('no service integrated-vtysh-config') | |
148 | self.cmd('chown %s:%svty /etc/%s/vtysh.conf' % (self.routertype, self.routertype, self.routertype)) | |
149 | # Try to find relevant old logfiles in /tmp and delete them | |
150 | map(os.remove, glob.glob("/tmp/*%s*.log" % self.name)) | |
151 | # Remove old core files | |
152 | map(os.remove, glob.glob("/tmp/%s*.dmp" % self.name)) | |
153 | # Remove IP addresses from OS first - we have them in zebra.conf | |
154 | self.removeIPs() | |
155 | # If ldp is used, check for LDP to be compiled and Linux Kernel to be 4.5 or higher | |
156 | # No error - but return message and skip all the tests | |
157 | if self.daemons['ldpd'] == 1: | |
158 | if not os.path.isfile('/usr/lib/%s/ldpd' % self.routertype): | |
159 | print("LDP Test, but no ldpd compiled or installed") | |
160 | return "LDP Test, but no ldpd compiled or installed" | |
161 | kernel_version = re.search(r'([0-9]+\.[0-9]+).*', platform.release()) | |
162 | if kernel_version: | |
163 | if float(kernel_version.group(1)) < 4.5: | |
164 | print("LDP Test need Linux Kernel 4.5 minimum") | |
165 | return "LDP Test need Linux Kernel 4.5 minimum" | |
166 | # Add mpls modules to kernel if we use LDP | |
167 | if self.daemons['ldpd'] == 1: | |
168 | self.cmd('/sbin/modprobe mpls-router') | |
169 | self.cmd('/sbin/modprobe mpls-iptunnel') | |
170 | self.cmd('echo 100000 > /proc/sys/net/mpls/platform_labels') | |
99561211 MW |
171 | # Init done - now restarting daemons |
172 | self.restartRouter() | |
173 | return "" | |
174 | def restartRouter(self): | |
175 | # Starts actuall daemons without init (ie restart) | |
594b1259 MW |
176 | # Start Zebra first |
177 | if self.daemons['zebra'] == 1: | |
99561211 MW |
178 | # self.cmd('/usr/lib/%s/zebra -d' % self.routertype) |
179 | self.cmd('/usr/lib/%s/zebra > /tmp/%s-zebra.out 2> /tmp/%s-zebra.err &' % (self.routertype, self.name, self.name)) | |
594b1259 MW |
180 | self.waitOutput() |
181 | print('%s: %s zebra started' % (self, self.routertype)) | |
182 | sleep(1) | |
183 | # Fix Link-Local Addresses | |
184 | # Somehow (on Mininet only), Zebra removes the IPv6 Link-Local addresses on start. Fix this | |
185 | 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') | |
186 | # Now start all the other daemons | |
187 | for daemon in self.daemons: | |
188 | if (self.daemons[daemon] == 1) and (daemon != 'zebra'): | |
99561211 MW |
189 | # self.cmd('/usr/lib/%s/%s -d' % (self.routertype, daemon)) |
190 | self.cmd('/usr/lib/%s/%s > /tmp/%s-%s.out 2> /tmp/%s-%s.err &' % (self.routertype, daemon, self.name, daemon, self.name, daemon)) | |
594b1259 MW |
191 | self.waitOutput() |
192 | print('%s: %s %s started' % (self, self.routertype, daemon)) | |
99561211 MW |
193 | def getStdErr(self, daemon): |
194 | return self.getLog('err', daemon) | |
195 | def getStdOut(self, daemon): | |
196 | return self.getLog('out', daemon) | |
197 | def getLog(self, log, daemon): | |
198 | return self.cmd('cat /tmp/%s-%s.%s' % (self.name, daemon, log) ) | |
594b1259 MW |
199 | def checkRouterRunning(self): |
200 | global fatal_error | |
201 | ||
202 | daemonsRunning = self.cmd('vtysh -c "show log" | grep "Logging configuration for"') | |
203 | for daemon in self.daemons: | |
204 | if (self.daemons[daemon] == 1) and not (daemon in daemonsRunning): | |
205 | sys.stderr.write("%s: Daemon %s not running\n" % (self.name, daemon)) | |
206 | # Look for core file | |
207 | corefiles = glob.glob("/tmp/%s_%s_core*.dmp" % (self.name, daemon)) | |
208 | if (len(corefiles) > 0): | |
209 | backtrace = subprocess.check_output(["gdb /usr/lib/%s/%s %s --batch -ex bt 2> /dev/null" % (self.routertype, daemon, corefiles[0])], shell=True) | |
210 | sys.stderr.write("\n%s: %s crashed. Core file found - Backtrace follows:\n" % (self.name, daemon)) | |
211 | sys.stderr.write("%s\n" % backtrace) | |
212 | else: | |
213 | # No core found - If we find matching logfile in /tmp, then print last 20 lines from it. | |
214 | if os.path.isfile("/tmp/%s-%s.log" % (self.name, daemon)): | |
215 | log_tail = subprocess.check_output(["tail -n20 /tmp/%s-%s.log 2> /dev/null" % (self.name, daemon)], shell=True) | |
216 | sys.stderr.write("\nFrom %s %s %s log file:\n" % (self.routertype, self.name, daemon)) | |
217 | sys.stderr.write("%s\n" % log_tail) | |
218 | ||
219 | return "%s: Daemon %s not running" % (self.name, daemon) | |
220 | return "" | |
221 | def get_ipv6_linklocal(self): | |
222 | "Get LinkLocal Addresses from interfaces" | |
223 | ||
224 | linklocal = [] | |
225 | ||
226 | ifaces = self.cmd('ip -6 address') | |
227 | # Fix newlines (make them all the same) | |
228 | ifaces = ('\n'.join(ifaces.splitlines()) + '\n').splitlines() | |
229 | interface="" | |
230 | ll_per_if_count=0 | |
231 | for line in ifaces: | |
232 | m = re.search('[0-9]+: ([^:@]+)[@if0-9:]+ <', line) | |
233 | if m: | |
234 | interface = m.group(1) | |
235 | ll_per_if_count = 0 | |
236 | m = re.search('inet6 (fe80::[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+)[/0-9]* scope link', line) | |
237 | if m: | |
238 | local = m.group(1) | |
239 | ll_per_if_count += 1 | |
240 | if (ll_per_if_count > 1): | |
241 | linklocal += [["%s-%s" % (interface, ll_per_if_count), local]] | |
242 | else: | |
243 | linklocal += [[interface, local]] | |
244 | return linklocal | |
245 | ||
246 | class LegacySwitch(OVSSwitch): | |
247 | "A Legacy Switch without OpenFlow" | |
248 | ||
249 | def __init__(self, name, **params): | |
250 | OVSSwitch.__init__(self, name, failMode='standalone', **params) | |
251 | self.switchIP = None | |
252 |