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