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