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