]> git.proxmox.com Git - mirror_frr.git/blame - tests/topotests/lib/topogen.py
topotest: implement json_cmp function
[mirror_frr.git] / tests / topotests / lib / topogen.py
CommitLineData
1fca63c1
RZ
1#
2# topogen.py
3# Library of helper functions for NetDEF Topology Tests
4#
5# Copyright (c) 2017 by
6# Network Device Education Foundation, Inc. ("NetDEF")
7#
8# Permission to use, copy, modify, and/or distribute this software
9# for any purpose with or without fee is hereby granted, provided
10# that the above copyright notice and this permission notice appear
11# in all copies.
12#
13# THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES
14# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
15# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR
16# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
17# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
18# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
19# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
20# OF THIS SOFTWARE.
21#
22
23"""
24Topogen (Topology Generator) is an abstraction around Topotest and Mininet to
25help reduce boilerplate code and provide a stable interface to build topology
26tests on.
27
28Basic usage instructions:
29
30* Define a Topology class with a build method using mininet.topo.Topo.
31 See examples/test_template.py.
32* Use Topogen inside the build() method with get_topogen.
33 e.g. get_topogen(self).
34* Start up your topology with: Topogen(YourTopology)
35* Initialize the Mininet with your topology with: tgen.start_topology()
36* Configure your routers/hosts and start them
37* Run your tests / mininet cli.
38* After running stop Mininet with: tgen.stop_topology()
39"""
40
41import os
42import sys
43
44from mininet.net import Mininet
45from mininet.log import setLogLevel
46from mininet.cli import CLI
47
48from lib import topotest
49
50# pylint: disable=C0103
51# Global Topogen variable. This is being used to keep the Topogen available on
52# all test functions without declaring a test local variable.
53global_tgen = None
54
55def get_topogen(topo=None):
56 """
57 Helper function to retrieve Topogen. Must be called with `topo` when called
58 inside the build() method of Topology class.
59 """
60 if topo is not None:
61 global_tgen.topo = topo
62 return global_tgen
63
64def set_topogen(tgen):
65 "Helper function to set Topogen"
66 # pylint: disable=W0603
67 global global_tgen
68 global_tgen = tgen
69
70#
71# Main class: topology builder
72#
73
74class Topogen(object):
75 "A topology test builder helper."
76
77 def __init__(self, cls):
78 self.topo = None
79 self.net = None
80 self.gears = {}
81 self.routern = 1
82 self.switchn = 1
83 self._init_topo(cls)
84
85 @staticmethod
86 def _mininet_reset():
87 "Reset the mininet environment"
88 # Clean up the mininet environment
89 os.system('mn -c > /dev/null 2>&1')
90
91 def _init_topo(self, cls):
92 """
93 Initialize the topogily provided by the user. The user topology class
94 must call get_topogen() during build() to get the topogen object.
95 """
96 # Set the global variable so the test cases can access it anywhere
97 set_topogen(self)
98
99 # Initialize the API
100 self._mininet_reset()
101 cls()
102 self.net = Mininet(controller=None, topo=self.topo)
103 for gear in self.gears.values():
104 gear.net = self.net
105
2ab85530 106 def add_router(self, name=None, cls=topotest.Router, **params):
1fca63c1
RZ
107 """
108 Adds a new router to the topology. This function has the following
109 options:
110 name: (optional) select the router name
111 Returns a TopoRouter.
112 """
113 if name is None:
31bfa9df 114 name = 'r{}'.format(self.routern)
1fca63c1
RZ
115 if name in self.gears:
116 raise KeyError('router already exists')
117
2ab85530 118 self.gears[name] = TopoRouter(self, cls, name, **params)
1fca63c1
RZ
119 self.routern += 1
120 return self.gears[name]
121
122 def add_switch(self, name=None, cls=topotest.LegacySwitch):
123 """
124 Adds a new switch to the topology. This function has the following
125 options:
126 name: (optional) select the switch name
127 Returns the switch name and number.
128 """
129 if name is None:
31bfa9df 130 name = 's{}'.format(self.switchn)
1fca63c1
RZ
131 if name in self.gears:
132 raise KeyError('switch already exists')
133
134 self.gears[name] = TopoSwitch(self, cls, name)
135 self.switchn += 1
136 return self.gears[name]
137
138 def add_link(self, node1, node2, ifname1=None, ifname2=None):
139 """
140 Creates a connection between node1 and node2. The nodes can be the
141 following:
142 * TopoGear
143 * TopoRouter
144 * TopoSwitch
145 """
146 if not isinstance(node1, TopoGear):
147 raise ValueError('invalid node1 type')
148 if not isinstance(node2, TopoGear):
149 raise ValueError('invalid node2 type')
150
151 if ifname1 is None:
8c3fdf62 152 ifname1 = node1.new_link()
1fca63c1 153 if ifname2 is None:
8c3fdf62
RZ
154 ifname2 = node2.new_link()
155
156 node1.register_link(ifname1, node2, ifname2)
157 node2.register_link(ifname2, node1, ifname1)
1fca63c1
RZ
158 self.topo.addLink(node1.name, node2.name,
159 intfName1=ifname1, intfName2=ifname2)
160
161 def routers(self):
162 """
163 Returns the router dictionary (key is the router name and value is the
164 router object itself).
165 """
166 return dict((rname, gear) for rname, gear in self.gears.iteritems()
167 if isinstance(gear, TopoRouter))
168
169 def start_topology(self, log_level='info'):
170 """
171 Starts the topology class. Possible `log_level`s are:
172 'debug': all information possible
173 'info': informational messages
174 'output': default logging level defined by Mininet
175 'warning': only warning, error and critical messages
176 'error': only error and critical messages
177 'critical': only critical messages
178 """
179 # Run mininet
180 setLogLevel(log_level)
181 self.net.start()
182
183 def start_router(self, router=None):
184 """
185 Call the router startRouter method.
186 If no router is specified it is called for all registred routers.
187 """
188 if router is None:
189 # pylint: disable=r1704
190 for _, router in self.routers().iteritems():
191 router.start()
192 else:
193 if isinstance(router, str):
194 router = self.gears[router]
195
196 router.start()
197
198 def stop_topology(self):
199 "Stops the network topology"
200 self.net.stop()
201
202 def mininet_cli(self):
203 """
204 Interrupt the test and call the command line interface for manual
205 inspection. Should be only used on non production code.
206 """
207 if not sys.stdin.isatty():
208 raise EnvironmentError(
209 'you must run pytest with \'-s\' in order to use mininet CLI')
210
211 CLI(self.net)
212
213#
214# Topology gears (equipment)
215#
216
217class TopoGear(object):
218 "Abstract class for type checking"
219
220 def __init__(self):
221 self.tgen = None
222 self.name = None
223 self.cls = None
8c3fdf62 224 self.links = {}
1fca63c1
RZ
225 self.linkn = 0
226
7326ea11
RZ
227 def __str__(self):
228 links = ''
229 for myif, dest in self.links.iteritems():
230 _, destif = dest
231 if links != '':
232 links += ','
233 links += '"{}"<->"{}"'.format(myif, destif)
234
235 return 'TopoGear<name="{}",links=[{}]>'.format(self.name, links)
236
8c3fdf62
RZ
237 def run(self, command):
238 """
239 Runs the provided command string in the router and returns a string
240 with the response.
241 """
242 return self.tgen.net[self.name].cmd(command)
243
1fca63c1
RZ
244 def add_link(self, node, myif=None, nodeif=None):
245 """
246 Creates a link (connection) between myself and the specified node.
247 Interfaces name can be speficied with:
248 myif: the interface name that will be created in this node
249 nodeif: the target interface name that will be created on the remote node.
250 """
251 self.tgen.add_link(self, node, myif, nodeif)
252
8c3fdf62 253 def link_enable(self, myif, enabled=True):
1fca63c1 254 """
8c3fdf62
RZ
255 Set this node interface administrative state.
256 myif: this node interface name
257 enabled: whether we should enable or disable the interface
1fca63c1 258 """
8c3fdf62
RZ
259 if myif not in self.links.keys():
260 raise KeyError('interface doesn\'t exists')
261
262 if enabled is True:
263 operation = 'up'
264 else:
265 operation = 'down'
266
267 return self.run('ip link set dev {} {}'.format(myif, operation))
268
269 def peer_link_enable(self, myif, enabled=True):
270 """
271 Set the peer interface administrative state.
272 myif: this node interface name
273 enabled: whether we should enable or disable the interface
274
275 NOTE: this is used to simulate a link down on this node, since when the
276 peer disables their interface our interface status changes to no link.
277 """
278 if myif not in self.links.keys():
279 raise KeyError('interface doesn\'t exists')
280
281 node, nodeif = self.links[myif]
282 node.link_enable(nodeif, enabled)
1fca63c1 283
8c3fdf62
RZ
284 def new_link(self):
285 """
286 Generates a new unique link name.
287
288 NOTE: This function should only be called by Topogen.
289 """
290 ifname = '{}-eth{}'.format(self.name, self.linkn)
1fca63c1 291 self.linkn += 1
1fca63c1
RZ
292 return ifname
293
8c3fdf62
RZ
294 def register_link(self, myif, node, nodeif):
295 """
296 Register link between this node interface and outside node.
297
298 NOTE: This function should only be called by Topogen.
299 """
300 if myif in self.links.keys():
301 raise KeyError('interface already exists')
302
303 self.links[myif] = (node, nodeif)
304
1fca63c1
RZ
305class TopoRouter(TopoGear):
306 """
d9ea1cda 307 Router abstraction.
1fca63c1
RZ
308 """
309
310 # The default required directories by Quagga/FRR
311 PRIVATE_DIRS = [
312 '/etc/frr',
313 '/etc/quagga',
314 '/var/run/frr',
315 '/var/run/quagga',
316 '/var/log'
317 ]
318
319 # Router Daemon enumeration definition.
7326ea11 320 RD_ZEBRA = 1
1fca63c1
RZ
321 RD_RIP = 2
322 RD_RIPNG = 3
323 RD_OSPF = 4
324 RD_OSPF6 = 5
325 RD_ISIS = 6
326 RD_BGP = 7
327 RD_LDP = 8
328 RD_PIM = 9
329 RD = {
330 RD_ZEBRA: 'zebra',
331 RD_RIP: 'ripd',
332 RD_RIPNG: 'ripngd',
333 RD_OSPF: 'ospfd',
334 RD_OSPF6: 'ospf6d',
335 RD_ISIS: 'isisd',
336 RD_BGP: 'bgpd',
337 RD_PIM: 'pimd',
338 RD_LDP: 'ldpd',
339 }
340
2ab85530 341 def __init__(self, tgen, cls, name, **params):
d9ea1cda
RZ
342 """
343 The constructor has the following parameters:
344 * tgen: Topogen object
345 * cls: router class that will be used to instantiate
346 * name: router name
347 * daemondir: daemon binary directory
348 * routertype: 'quagga' or 'frr'
349 """
1fca63c1
RZ
350 super(TopoRouter, self).__init__()
351 self.tgen = tgen
352 self.net = None
353 self.name = name
354 self.cls = cls
2ab85530
RZ
355 if not params.has_key('privateDirs'):
356 params['privateDirs'] = self.PRIVATE_DIRS
357 self.tgen.topo.addNode(self.name, cls=self.cls, **params)
1fca63c1 358
7326ea11
RZ
359 def __str__(self):
360 gear = super(TopoRouter, self).__str__()
361 gear += ' TopoRouter<>'
362 return gear
363
1fca63c1
RZ
364 def load_config(self, daemon, source=None):
365 """
366 Loads daemon configuration from the specified source
367 Possible daemon values are: TopoRouter.RD_ZEBRA, TopoRouter.RD_RIP,
368 TopoRouter.RD_RIPNG, TopoRouter.RD_OSPF, TopoRouter.RD_OSPF6,
369 TopoRouter.RD_ISIS, TopoRouter.RD_BGP, TopoRouter.RD_LDP,
370 TopoRouter.RD_PIM.
371 """
372 daemonstr = self.RD.get(daemon)
373 self.tgen.net[self.name].loadConf(daemonstr, source)
374
375 def check_router_running(self):
376 """
377 Run a series of checks and returns a status string.
378 """
379 return self.tgen.net[self.name].checkRouterRunning()
380
381 def start(self):
382 """
383 Start router:
384 * Load modules
385 * Clean up files
386 * Configure interfaces
387 * Start daemons (e.g. FRR/Quagga)
388 """
389 return self.tgen.net[self.name].startRouter()
390
1fca63c1
RZ
391 def vtysh_cmd(self, command):
392 """
393 Runs the provided command string in the vty shell and returns a string
394 with the response.
395
396 This function also accepts multiple commands, but this mode does not
397 return output for each command. See vtysh_multicmd() for more details.
398 """
399 # Detect multi line commands
400 if command.find('\n') != -1:
401 return self.vtysh_multicmd(command)
402
403 vtysh_command = 'vtysh -c "{}" 2>/dev/null'.format(command)
404 return self.run(vtysh_command)
405
406 def vtysh_multicmd(self, commands, pretty_output=True):
407 """
408 Runs the provided commands in the vty shell and return the result of
409 execution.
410
411 pretty_output: defines how the return value will be presented. When
412 True it will show the command as they were executed in the vty shell,
413 otherwise it will only show lines that failed.
414 """
415 # Prepare the temporary file that will hold the commands
416 fname = topotest.get_file(commands)
417
418 # Run the commands and delete the temporary file
419 if pretty_output:
420 vtysh_command = 'vtysh < {}'.format(fname)
421 else:
422 vtysh_command = 'vtysh -f {}'.format(fname)
423
424 res = self.run(vtysh_command)
425 os.unlink(fname)
426
427 return res
428
38c39932
RZ
429 def report_memory_leaks(self, testname):
430 """
431 Runs the router memory leak check test. Has the following parameter:
432 testname: the test file name for identification
433
434 NOTE: to run this you must have the environment variable
435 TOPOTESTS_CHECK_MEMLEAK set to the appropriated path.
436 """
437 memleak_file = os.environ.get('TOPOTESTS_CHECK_MEMLEAK')
438 if memleak_file is None:
439 print "SKIPPED check on Memory leaks: Disabled (TOPOTESTS_CHECK_MEMLEAK undefined)"
440 return
441
442 self.tgen.net[self.name].stopRouter()
443 self.tgen.net[self.name].report_memory_leaks(memleak_file, testname)
444
1fca63c1
RZ
445class TopoSwitch(TopoGear):
446 """
447 Switch abstraction. Has the following properties:
448 * cls: switch class that will be used to instantiate
449 * name: switch name
450 """
451 # pylint: disable=too-few-public-methods
452
453 def __init__(self, tgen, cls, name):
454 super(TopoSwitch, self).__init__()
455 self.tgen = tgen
456 self.net = None
457 self.name = name
458 self.cls = cls
459 self.tgen.topo.addSwitch(name, cls=self.cls)
7326ea11
RZ
460
461 def __str__(self):
462 gear = super(TopoSwitch, self).__str__()
463 gear += ' TopoSwitch<>'
464 return gear