]> git.proxmox.com Git - mirror_frr.git/blame - tests/topotests/lib/topogen.py
topotest: fix a regression in version_cmp
[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
7547ebd8 43import logging
a40daddc 44import json
edd2bdf6 45import ConfigParser
13e1fc49 46import glob
f6899d4d 47import grp
007e7313
RZ
48import platform
49import pwd
007e7313 50import subprocess
1fca63c1
RZ
51
52from mininet.net import Mininet
53from mininet.log import setLogLevel
54from mininet.cli import CLI
55
56from lib import topotest
36d1dc45 57from lib.topolog import logger, logger_config
1fca63c1 58
edd2bdf6
RZ
59CWD = os.path.dirname(os.path.realpath(__file__))
60
1fca63c1
RZ
61# pylint: disable=C0103
62# Global Topogen variable. This is being used to keep the Topogen available on
63# all test functions without declaring a test local variable.
64global_tgen = None
65
66def get_topogen(topo=None):
67 """
68 Helper function to retrieve Topogen. Must be called with `topo` when called
69 inside the build() method of Topology class.
70 """
71 if topo is not None:
72 global_tgen.topo = topo
73 return global_tgen
74
75def set_topogen(tgen):
76 "Helper function to set Topogen"
77 # pylint: disable=W0603
78 global global_tgen
79 global_tgen = tgen
80
81#
82# Main class: topology builder
83#
84
007e7313
RZ
85# Topogen configuration defaults
86tgen_defaults = {
87 'verbosity': 'info',
88 'frrdir': '/usr/lib/frr',
89 'quaggadir': '/usr/lib/quagga',
90 'routertype': 'frr',
91 'memleak_path': None,
92}
93
1fca63c1
RZ
94class Topogen(object):
95 "A topology test builder helper."
96
edd2bdf6
RZ
97 CONFIG_SECTION = 'topogen'
98
13e1fc49
RZ
99 def __init__(self, cls, modname='unnamed'):
100 """
101 Topogen initialization function, takes the following arguments:
102 * `cls`: the topology class that is child of mininet.topo
103 * `modname`: module name must be a unique name to identify logs later.
104 """
edd2bdf6 105 self.config = None
1fca63c1
RZ
106 self.topo = None
107 self.net = None
108 self.gears = {}
109 self.routern = 1
110 self.switchn = 1
13e1fc49 111 self.modname = modname
1eb633c0
RZ
112 self.errorsd = {}
113 self.errors = ''
19ccab57 114 self.peern = 1
1fca63c1 115 self._init_topo(cls)
13e1fc49 116 logger.info('loading topology: {}'.format(self.modname))
1fca63c1
RZ
117
118 @staticmethod
119 def _mininet_reset():
120 "Reset the mininet environment"
121 # Clean up the mininet environment
122 os.system('mn -c > /dev/null 2>&1')
123
124 def _init_topo(self, cls):
125 """
126 Initialize the topogily provided by the user. The user topology class
127 must call get_topogen() during build() to get the topogen object.
128 """
129 # Set the global variable so the test cases can access it anywhere
130 set_topogen(self)
131
edd2bdf6
RZ
132 # Load the default topology configurations
133 self._load_config()
134
1fca63c1
RZ
135 # Initialize the API
136 self._mininet_reset()
137 cls()
138 self.net = Mininet(controller=None, topo=self.topo)
139 for gear in self.gears.values():
140 gear.net = self.net
141
edd2bdf6
RZ
142 def _load_config(self):
143 """
144 Loads the configuration file `pytest.ini` located at the root dir of
145 topotests.
146 """
007e7313 147 self.config = ConfigParser.ConfigParser(tgen_defaults)
edd2bdf6
RZ
148 pytestini_path = os.path.join(CWD, '../pytest.ini')
149 self.config.read(pytestini_path)
150
2ab85530 151 def add_router(self, name=None, cls=topotest.Router, **params):
1fca63c1
RZ
152 """
153 Adds a new router to the topology. This function has the following
154 options:
edd2bdf6
RZ
155 * `name`: (optional) select the router name
156 * `daemondir`: (optional) custom daemon binary directory
157 * `routertype`: (optional) `quagga` or `frr`
1fca63c1
RZ
158 Returns a TopoRouter.
159 """
160 if name is None:
31bfa9df 161 name = 'r{}'.format(self.routern)
1fca63c1
RZ
162 if name in self.gears:
163 raise KeyError('router already exists')
164
edd2bdf6
RZ
165 params['frrdir'] = self.config.get(self.CONFIG_SECTION, 'frrdir')
166 params['quaggadir'] = self.config.get(self.CONFIG_SECTION, 'quaggadir')
c540096e 167 params['memleak_path'] = self.config.get(self.CONFIG_SECTION, 'memleak_path')
edd2bdf6
RZ
168 if not params.has_key('routertype'):
169 params['routertype'] = self.config.get(self.CONFIG_SECTION, 'routertype')
170
2ab85530 171 self.gears[name] = TopoRouter(self, cls, name, **params)
1fca63c1
RZ
172 self.routern += 1
173 return self.gears[name]
174
175 def add_switch(self, name=None, cls=topotest.LegacySwitch):
176 """
177 Adds a new switch to the topology. This function has the following
178 options:
179 name: (optional) select the switch name
180 Returns the switch name and number.
181 """
182 if name is None:
31bfa9df 183 name = 's{}'.format(self.switchn)
1fca63c1
RZ
184 if name in self.gears:
185 raise KeyError('switch already exists')
186
187 self.gears[name] = TopoSwitch(self, cls, name)
188 self.switchn += 1
189 return self.gears[name]
190
19ccab57
RZ
191 def add_exabgp_peer(self, name, ip, defaultRoute):
192 """
193 Adds a new ExaBGP peer to the topology. This function has the following
194 parameters:
195 * `ip`: the peer address (e.g. '1.2.3.4/24')
196 * `defaultRoute`: the peer default route (e.g. 'via 1.2.3.1')
197 """
198 if name is None:
199 name = 'peer{}'.format(self.peern)
200 if name in self.gears:
201 raise KeyError('exabgp peer already exists')
202
203 self.gears[name] = TopoExaBGP(self, name, ip=ip, defaultRoute=defaultRoute)
204 self.peern += 1
205 return self.gears[name]
206
1fca63c1
RZ
207 def add_link(self, node1, node2, ifname1=None, ifname2=None):
208 """
209 Creates a connection between node1 and node2. The nodes can be the
210 following:
211 * TopoGear
212 * TopoRouter
213 * TopoSwitch
214 """
215 if not isinstance(node1, TopoGear):
216 raise ValueError('invalid node1 type')
217 if not isinstance(node2, TopoGear):
218 raise ValueError('invalid node2 type')
219
220 if ifname1 is None:
8c3fdf62 221 ifname1 = node1.new_link()
1fca63c1 222 if ifname2 is None:
8c3fdf62
RZ
223 ifname2 = node2.new_link()
224
225 node1.register_link(ifname1, node2, ifname2)
226 node2.register_link(ifname2, node1, ifname1)
1fca63c1
RZ
227 self.topo.addLink(node1.name, node2.name,
228 intfName1=ifname1, intfName2=ifname2)
229
19ccab57
RZ
230 def get_gears(self, geartype):
231 """
232 Returns a dictionary of all gears of type `geartype`.
233
234 Normal usage:
235 * Dictionary iteration:
236 ```py
237 tgen = get_topogen()
238 router_dict = tgen.get_gears(TopoRouter)
239 for router_name, router in router_dict.iteritems():
240 # Do stuff
241 ```
242 * List iteration:
243 ```py
244 tgen = get_topogen()
245 peer_list = tgen.get_gears(TopoExaBGP).values()
246 for peer in peer_list:
247 # Do stuff
248 ```
249 """
250 return dict((name, gear) for name, gear in self.gears.iteritems()
251 if isinstance(gear, geartype))
252
1fca63c1
RZ
253 def routers(self):
254 """
255 Returns the router dictionary (key is the router name and value is the
256 router object itself).
257 """
19ccab57
RZ
258 return self.get_gears(TopoRouter)
259
260 def exabgp_peers(self):
261 """
262 Returns the exabgp peer dictionary (key is the peer name and value is
263 the peer object itself).
264 """
265 return self.get_gears(TopoExaBGP)
1fca63c1 266
edd2bdf6 267 def start_topology(self, log_level=None):
1fca63c1
RZ
268 """
269 Starts the topology class. Possible `log_level`s are:
270 'debug': all information possible
271 'info': informational messages
272 'output': default logging level defined by Mininet
273 'warning': only warning, error and critical messages
274 'error': only error and critical messages
275 'critical': only critical messages
276 """
edd2bdf6
RZ
277 # If log_level is not specified use the configuration.
278 if log_level is None:
13e1fc49 279 log_level = self.config.get(self.CONFIG_SECTION, 'verbosity')
edd2bdf6 280
36d1dc45
RZ
281 # Set python logger level
282 logger_config.set_log_level(log_level)
283
1fca63c1 284 # Run mininet
13e1fc49
RZ
285 if log_level == 'debug':
286 setLogLevel(log_level)
287
288 logger.info('starting topology: {}'.format(self.modname))
1fca63c1
RZ
289 self.net.start()
290
291 def start_router(self, router=None):
292 """
293 Call the router startRouter method.
294 If no router is specified it is called for all registred routers.
295 """
296 if router is None:
297 # pylint: disable=r1704
298 for _, router in self.routers().iteritems():
299 router.start()
300 else:
301 if isinstance(router, str):
302 router = self.gears[router]
303
304 router.start()
305
306 def stop_topology(self):
4cfdff1a
RZ
307 """
308 Stops the network topology. This function will call the stop() function
309 of all gears before calling the mininet stop function, so they can have
310 their oportunity to do a graceful shutdown.
311 """
13e1fc49 312 logger.info('stopping topology: {}'.format(self.modname))
4cfdff1a
RZ
313
314 for gear in self.gears.values():
315 gear.stop()
316
1fca63c1
RZ
317 self.net.stop()
318
319 def mininet_cli(self):
320 """
321 Interrupt the test and call the command line interface for manual
322 inspection. Should be only used on non production code.
323 """
324 if not sys.stdin.isatty():
325 raise EnvironmentError(
326 'you must run pytest with \'-s\' in order to use mininet CLI')
327
328 CLI(self.net)
329
13e1fc49
RZ
330 def is_memleak_enabled(self):
331 "Returns `True` if memory leak report is enable, otherwise `False`."
78ed6123
RZ
332 # On router failure we can't run the memory leak test
333 if self.routers_have_failure():
334 return False
335
e8469297
RZ
336 memleak_file = (os.environ.get('TOPOTESTS_CHECK_MEMLEAK') or
337 self.config.get(self.CONFIG_SECTION, 'memleak_path'))
13e1fc49
RZ
338 if memleak_file is None:
339 return False
340 return True
341
342 def report_memory_leaks(self, testname=None):
343 "Run memory leak test and reports."
344 if not self.is_memleak_enabled():
345 return
346
347 # If no name was specified, use the test module name
348 if testname is None:
349 testname = self.modname
350
351 router_list = self.routers().values()
352 for router in router_list:
353 router.report_memory_leaks(self.modname)
354
393ca0fa
RZ
355 def set_error(self, message, code=None):
356 "Sets an error message and signal other tests to skip."
357 logger.error(message)
358
359 # If no code is defined use a sequential number
360 if code is None:
1eb633c0 361 code = len(self.errorsd)
393ca0fa 362
1eb633c0
RZ
363 self.errorsd[code] = message
364 self.errors += '\n{}: {}'.format(code, message)
393ca0fa
RZ
365
366 def has_errors(self):
367 "Returns whether errors exist or not."
1eb633c0 368 return len(self.errorsd) > 0
13e1fc49 369
78ed6123
RZ
370 def routers_have_failure(self):
371 "Runs an assertion to make sure that all routers are running."
372 if self.has_errors():
373 return True
374
375 errors = ''
376 router_list = self.routers().values()
377 for router in router_list:
378 result = router.check_router_running()
379 if result != '':
380 errors += result + '\n'
381
382 if errors != '':
383 self.set_error(errors, 'router_error')
384 assert errors != '', errors
385 return True
386 return False
387
1fca63c1
RZ
388#
389# Topology gears (equipment)
390#
391
392class TopoGear(object):
393 "Abstract class for type checking"
394
395 def __init__(self):
396 self.tgen = None
397 self.name = None
398 self.cls = None
8c3fdf62 399 self.links = {}
1fca63c1
RZ
400 self.linkn = 0
401
7326ea11
RZ
402 def __str__(self):
403 links = ''
404 for myif, dest in self.links.iteritems():
405 _, destif = dest
406 if links != '':
407 links += ','
408 links += '"{}"<->"{}"'.format(myif, destif)
409
410 return 'TopoGear<name="{}",links=[{}]>'.format(self.name, links)
411
4cfdff1a
RZ
412 def start(self):
413 "Basic start function that just reports equipment start"
414 logger.info('starting "{}"'.format(self.name))
415
416 def stop(self):
417 "Basic start function that just reports equipment stop"
418 logger.info('stopping "{}"'.format(self.name))
419
8c3fdf62
RZ
420 def run(self, command):
421 """
422 Runs the provided command string in the router and returns a string
423 with the response.
424 """
425 return self.tgen.net[self.name].cmd(command)
426
1fca63c1
RZ
427 def add_link(self, node, myif=None, nodeif=None):
428 """
429 Creates a link (connection) between myself and the specified node.
430 Interfaces name can be speficied with:
431 myif: the interface name that will be created in this node
432 nodeif: the target interface name that will be created on the remote node.
433 """
434 self.tgen.add_link(self, node, myif, nodeif)
435
8c3fdf62 436 def link_enable(self, myif, enabled=True):
1fca63c1 437 """
8c3fdf62
RZ
438 Set this node interface administrative state.
439 myif: this node interface name
440 enabled: whether we should enable or disable the interface
1fca63c1 441 """
8c3fdf62
RZ
442 if myif not in self.links.keys():
443 raise KeyError('interface doesn\'t exists')
444
445 if enabled is True:
446 operation = 'up'
447 else:
448 operation = 'down'
449
13e1fc49
RZ
450 logger.info('setting node "{}" link "{}" to state "{}"'.format(
451 self.name, myif, operation
452 ))
8c3fdf62
RZ
453 return self.run('ip link set dev {} {}'.format(myif, operation))
454
455 def peer_link_enable(self, myif, enabled=True):
456 """
457 Set the peer interface administrative state.
458 myif: this node interface name
459 enabled: whether we should enable or disable the interface
460
461 NOTE: this is used to simulate a link down on this node, since when the
462 peer disables their interface our interface status changes to no link.
463 """
464 if myif not in self.links.keys():
465 raise KeyError('interface doesn\'t exists')
466
467 node, nodeif = self.links[myif]
468 node.link_enable(nodeif, enabled)
1fca63c1 469
8c3fdf62
RZ
470 def new_link(self):
471 """
472 Generates a new unique link name.
473
474 NOTE: This function should only be called by Topogen.
475 """
476 ifname = '{}-eth{}'.format(self.name, self.linkn)
1fca63c1 477 self.linkn += 1
1fca63c1
RZ
478 return ifname
479
8c3fdf62
RZ
480 def register_link(self, myif, node, nodeif):
481 """
482 Register link between this node interface and outside node.
483
484 NOTE: This function should only be called by Topogen.
485 """
486 if myif in self.links.keys():
487 raise KeyError('interface already exists')
488
489 self.links[myif] = (node, nodeif)
490
1fca63c1
RZ
491class TopoRouter(TopoGear):
492 """
d9ea1cda 493 Router abstraction.
1fca63c1
RZ
494 """
495
496 # The default required directories by Quagga/FRR
497 PRIVATE_DIRS = [
498 '/etc/frr',
499 '/etc/quagga',
500 '/var/run/frr',
501 '/var/run/quagga',
502 '/var/log'
503 ]
504
505 # Router Daemon enumeration definition.
7326ea11 506 RD_ZEBRA = 1
1fca63c1
RZ
507 RD_RIP = 2
508 RD_RIPNG = 3
509 RD_OSPF = 4
510 RD_OSPF6 = 5
511 RD_ISIS = 6
512 RD_BGP = 7
513 RD_LDP = 8
514 RD_PIM = 9
c267e5b1
RZ
515 RD_EIGRP = 10
516 RD_NHRP = 11
1fca63c1
RZ
517 RD = {
518 RD_ZEBRA: 'zebra',
519 RD_RIP: 'ripd',
520 RD_RIPNG: 'ripngd',
521 RD_OSPF: 'ospfd',
522 RD_OSPF6: 'ospf6d',
523 RD_ISIS: 'isisd',
524 RD_BGP: 'bgpd',
525 RD_PIM: 'pimd',
526 RD_LDP: 'ldpd',
c267e5b1
RZ
527 RD_EIGRP: 'eigrpd',
528 RD_NHRP: 'nhrpd',
1fca63c1
RZ
529 }
530
2ab85530 531 def __init__(self, tgen, cls, name, **params):
d9ea1cda
RZ
532 """
533 The constructor has the following parameters:
534 * tgen: Topogen object
535 * cls: router class that will be used to instantiate
536 * name: router name
537 * daemondir: daemon binary directory
538 * routertype: 'quagga' or 'frr'
539 """
1fca63c1
RZ
540 super(TopoRouter, self).__init__()
541 self.tgen = tgen
542 self.net = None
543 self.name = name
544 self.cls = cls
c540096e 545 self.options = {}
f6899d4d 546 self.routertype = params.get('routertype', 'frr')
2ab85530
RZ
547 if not params.has_key('privateDirs'):
548 params['privateDirs'] = self.PRIVATE_DIRS
c540096e
RZ
549
550 self.options['memleak_path'] = params.get('memleak_path', None)
13e1fc49
RZ
551
552 # Create new log directory
553 self.logdir = '/tmp/topotests/{}'.format(self.tgen.modname)
554 # Clean up before starting new log files: avoids removing just created
555 # log files.
556 self._prepare_tmpfiles()
557 # Propagate the router log directory
558 params['logdir'] = self.logdir
559
560 # Open router log file
561 logfile = '{}/{}.log'.format(self.logdir, name)
562 self.logger = logger_config.get_logger(name=name, target=logfile)
2ab85530 563 self.tgen.topo.addNode(self.name, cls=self.cls, **params)
1fca63c1 564
7326ea11
RZ
565 def __str__(self):
566 gear = super(TopoRouter, self).__str__()
567 gear += ' TopoRouter<>'
568 return gear
569
13e1fc49
RZ
570 def _prepare_tmpfiles(self):
571 # Create directories if they don't exist
572 try:
573 os.makedirs(self.logdir, 0755)
574 except OSError:
575 pass
576
f6899d4d
RZ
577 # Allow unprivileged daemon user (frr/quagga) to create log files
578 try:
579 # Only allow group, if it exist.
580 gid = grp.getgrnam(self.routertype)[2]
581 os.chown(self.logdir, 0, gid)
582 os.chmod(self.logdir, 0775)
583 except KeyError:
584 # Allow anyone, but set the sticky bit to avoid file deletions
585 os.chmod(self.logdir, 01777)
586
13e1fc49
RZ
587 # Try to find relevant old logfiles in /tmp and delete them
588 map(os.remove, glob.glob('{}/*{}*.log'.format(self.logdir, self.name)))
589 # Remove old core files
590 map(os.remove, glob.glob('{}/{}*.dmp'.format(self.logdir, self.name)))
591
1fca63c1
RZ
592 def load_config(self, daemon, source=None):
593 """
594 Loads daemon configuration from the specified source
595 Possible daemon values are: TopoRouter.RD_ZEBRA, TopoRouter.RD_RIP,
596 TopoRouter.RD_RIPNG, TopoRouter.RD_OSPF, TopoRouter.RD_OSPF6,
597 TopoRouter.RD_ISIS, TopoRouter.RD_BGP, TopoRouter.RD_LDP,
598 TopoRouter.RD_PIM.
599 """
600 daemonstr = self.RD.get(daemon)
13e1fc49 601 self.logger.info('loading "{}" configuration: {}'.format(daemonstr, source))
1fca63c1
RZ
602 self.tgen.net[self.name].loadConf(daemonstr, source)
603
604 def check_router_running(self):
605 """
606 Run a series of checks and returns a status string.
607 """
13e1fc49 608 self.logger.info('checking if daemons are running')
1fca63c1
RZ
609 return self.tgen.net[self.name].checkRouterRunning()
610
611 def start(self):
612 """
613 Start router:
614 * Load modules
615 * Clean up files
616 * Configure interfaces
617 * Start daemons (e.g. FRR/Quagga)
f6899d4d 618 * Configure daemon logging files
1fca63c1 619 """
13e1fc49 620 self.logger.debug('starting')
f6899d4d
RZ
621 nrouter = self.tgen.net[self.name]
622 result = nrouter.startRouter()
623
624 # Enable all daemon logging files and set them to the logdir.
625 for daemon, enabled in nrouter.daemons.iteritems():
626 if enabled == 0:
627 continue
628 self.vtysh_cmd('configure terminal\nlog file {}/{}-{}.log'.format(
f9b48d8b 629 self.logdir, self.name, daemon), daemon=daemon)
f6899d4d 630
57c5075b
RZ
631 if result != '':
632 self.tgen.set_error(result)
633
f6899d4d 634 return result
1fca63c1 635
13e1fc49
RZ
636 def stop(self):
637 """
638 Stop router:
639 * Kill daemons
640 """
641 self.logger.debug('stopping')
642 return self.tgen.net[self.name].stopRouter()
643
f9b48d8b 644 def vtysh_cmd(self, command, isjson=False, daemon=None):
1fca63c1
RZ
645 """
646 Runs the provided command string in the vty shell and returns a string
647 with the response.
648
649 This function also accepts multiple commands, but this mode does not
650 return output for each command. See vtysh_multicmd() for more details.
651 """
652 # Detect multi line commands
653 if command.find('\n') != -1:
f9b48d8b
RZ
654 return self.vtysh_multicmd(command, daemon=daemon)
655
656 dparam = ''
657 if daemon is not None:
658 dparam += '-d {}'.format(daemon)
659
660 vtysh_command = 'vtysh {} -c "{}" 2>/dev/null'.format(dparam, command)
1fca63c1 661
a40daddc 662 output = self.run(vtysh_command)
13e1fc49
RZ
663 self.logger.info('\nvtysh command => {}\nvtysh output <= {}'.format(
664 command, output))
a40daddc
RZ
665 if isjson is False:
666 return output
667
7b093d84
RZ
668 try:
669 return json.loads(output)
670 except ValueError:
671 logger.warning('vtysh_cmd: failed to convert json output')
672 return {}
1fca63c1 673
f9b48d8b 674 def vtysh_multicmd(self, commands, pretty_output=True, daemon=None):
1fca63c1
RZ
675 """
676 Runs the provided commands in the vty shell and return the result of
677 execution.
678
679 pretty_output: defines how the return value will be presented. When
680 True it will show the command as they were executed in the vty shell,
681 otherwise it will only show lines that failed.
682 """
683 # Prepare the temporary file that will hold the commands
684 fname = topotest.get_file(commands)
685
f9b48d8b
RZ
686 dparam = ''
687 if daemon is not None:
688 dparam += '-d {}'.format(daemon)
689
1fca63c1
RZ
690 # Run the commands and delete the temporary file
691 if pretty_output:
f9b48d8b 692 vtysh_command = 'vtysh {} < {}'.format(dparam, fname)
1fca63c1 693 else:
f9b48d8b 694 vtysh_command = 'vtysh {} -f {}'.format(dparam, fname)
1fca63c1
RZ
695
696 res = self.run(vtysh_command)
697 os.unlink(fname)
698
13e1fc49
RZ
699 self.logger.info('\nvtysh command => "{}"\nvtysh output <= "{}"'.format(
700 vtysh_command, res))
701
1fca63c1
RZ
702 return res
703
38c39932
RZ
704 def report_memory_leaks(self, testname):
705 """
706 Runs the router memory leak check test. Has the following parameter:
707 testname: the test file name for identification
708
709 NOTE: to run this you must have the environment variable
c540096e 710 TOPOTESTS_CHECK_MEMLEAK set or memleak_path configured in `pytest.ini`.
38c39932 711 """
c540096e 712 memleak_file = os.environ.get('TOPOTESTS_CHECK_MEMLEAK') or self.options['memleak_path']
38c39932 713 if memleak_file is None:
38c39932
RZ
714 return
715
13e1fc49
RZ
716 self.stop()
717 self.logger.info('running memory leak report')
38c39932
RZ
718 self.tgen.net[self.name].report_memory_leaks(memleak_file, testname)
719
6ca2411e
RZ
720 def version_info(self):
721 "Get equipment information from 'show version'."
722 output = self.vtysh_cmd('show version').split('\n')[0]
723 columns = topotest.normalize_text(output).split(' ')
b3b1b1d1
RZ
724 try:
725 return {
726 'type': columns[0],
727 'version': columns[1],
728 }
729 except IndexError:
730 return {
731 'type': None,
732 'version': None,
733 }
6ca2411e
RZ
734
735 def has_version(self, cmpop, version):
736 """
737 Compares router version using operation `cmpop` with `version`.
738 Valid `cmpop` values:
739 * `>=`: has the same version or greater
740 * '>': has greater version
741 * '=': has the same version
742 * '<': has a lesser version
743 * '<=': has the same version or lesser
744
745 Usage example: router.has_version('>', '1.0')
746 """
747 rversion = self.version_info()['version']
b3b1b1d1
RZ
748 if rversion is None:
749 return False
750
6ca2411e
RZ
751 result = topotest.version_cmp(rversion, version)
752 if cmpop == '>=':
753 return result >= 0
754 if cmpop == '>':
755 return result > 0
756 if cmpop == '=':
757 return result == 0
758 if cmpop == '<':
759 return result < 0
760 if cmpop == '<':
761 return result < 0
762 if cmpop == '<=':
763 return result <= 0
764
765 def has_type(self, rtype):
766 """
767 Compares router type with `rtype`. Returns `True` if the type matches,
768 otherwise `false`.
769 """
770 curtype = self.version_info()['type']
771 return rtype == curtype
772
1fca63c1
RZ
773class TopoSwitch(TopoGear):
774 """
775 Switch abstraction. Has the following properties:
776 * cls: switch class that will be used to instantiate
777 * name: switch name
778 """
779 # pylint: disable=too-few-public-methods
780
781 def __init__(self, tgen, cls, name):
782 super(TopoSwitch, self).__init__()
783 self.tgen = tgen
784 self.net = None
785 self.name = name
786 self.cls = cls
787 self.tgen.topo.addSwitch(name, cls=self.cls)
7326ea11
RZ
788
789 def __str__(self):
790 gear = super(TopoSwitch, self).__str__()
791 gear += ' TopoSwitch<>'
792 return gear
19ccab57
RZ
793
794class TopoHost(TopoGear):
795 "Host abstraction."
796 # pylint: disable=too-few-public-methods
797
798 def __init__(self, tgen, name, **params):
799 """
800 Mininet has the following known `params` for hosts:
801 * `ip`: the IP address (string) for the host interface
802 * `defaultRoute`: the default route that will be installed
803 (e.g. 'via 10.0.0.1')
804 * `privateDirs`: directories that will be mounted on a different domain
805 (e.g. '/etc/important_dir').
806 """
807 super(TopoHost, self).__init__()
808 self.tgen = tgen
809 self.net = None
810 self.name = name
811 self.options = params
812 self.tgen.topo.addHost(name, **params)
813
814 def __str__(self):
815 gear = super(TopoHost, self).__str__()
816 gear += ' TopoHost<ip="{}",defaultRoute="{}",privateDirs="{}">'.format(
817 self.options['ip'], self.options['defaultRoute'],
818 str(self.options['privateDirs']))
819 return gear
820
821class TopoExaBGP(TopoHost):
822 "ExaBGP peer abstraction."
823 # pylint: disable=too-few-public-methods
824
825 PRIVATE_DIRS = [
826 '/etc/exabgp',
827 '/var/run/exabgp',
828 '/var/log',
829 ]
830
831 def __init__(self, tgen, name, **params):
832 """
833 ExaBGP usually uses the following parameters:
834 * `ip`: the IP address (string) for the host interface
835 * `defaultRoute`: the default route that will be installed
836 (e.g. 'via 10.0.0.1')
837
838 Note: the different between a host and a ExaBGP peer is that this class
839 has a privateDirs already defined and contains functions to handle ExaBGP
840 things.
841 """
842 params['privateDirs'] = self.PRIVATE_DIRS
843 super(TopoExaBGP, self).__init__(tgen, name, **params)
844 self.tgen.topo.addHost(name, **params)
845
846 def __str__(self):
847 gear = super(TopoExaBGP, self).__str__()
848 gear += ' TopoExaBGP<>'.format()
849 return gear
850
851 def start(self, peer_dir, env_file=None):
852 """
853 Start running ExaBGP daemon:
854 * Copy all peer* folder contents into /etc/exabgp
855 * Copy exabgp env file if specified
856 * Make all python files runnable
857 * Run ExaBGP with env file `env_file` and configuration peer*/exabgp.cfg
858 """
859 self.run('mkdir /etc/exabgp')
860 self.run('chmod 755 /etc/exabgp')
861 self.run('cp {}/* /etc/exabgp/'.format(peer_dir))
862 if env_file is not None:
863 self.run('cp {} /etc/exabgp/exabgp.env'.format(env_file))
864 self.run('chmod 644 /etc/exabgp/*')
865 self.run('chmod a+x /etc/exabgp/*.py')
866 self.run('chown -R exabgp:exabgp /etc/exabgp')
867 self.run('exabgp -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg')
868
869 def stop(self):
870 "Stop ExaBGP peer and kill the daemon"
871 self.run('kill `cat /var/run/exabgp/exabgp.pid`')
007e7313
RZ
872
873
874#
875# Diagnostic function
876#
877
878# Disable linter branch warning. It is expected to have these here.
879# pylint: disable=R0912
880def diagnose_env():
881 """
882 Run diagnostics in the running environment. Returns `True` when everything
883 is ok, otherwise `False`.
884 """
885 ret = True
7547ebd8 886
fcfbc769
RZ
887 # Test log path exists before installing handler.
888 if not os.path.isdir('/tmp'):
889 logger.warning('could not find /tmp for logs')
890 else:
891 os.system('mkdir /tmp/topotests')
892 # Log diagnostics to file so it can be examined later.
893 fhandler = logging.FileHandler(filename='/tmp/topotests/diagnostics.txt')
894 fhandler.setLevel(logging.DEBUG)
895 fhandler.setFormatter(
896 logging.Formatter(fmt='%(asctime)s %(levelname)s: %(message)s')
897 )
898 logger.addHandler(fhandler)
7547ebd8 899
007e7313
RZ
900 logger.info('Running environment diagnostics')
901
902 # Load configuration
903 config = ConfigParser.ConfigParser(tgen_defaults)
904 pytestini_path = os.path.join(CWD, '../pytest.ini')
905 config.read(pytestini_path)
906
907 # Assert that we are running as root
908 if os.getuid() != 0:
909 logger.error('you must run topotest as root')
910 ret = False
911
912 # Assert that we have mininet
913 if os.system('which mn >/dev/null 2>/dev/null') != 0:
914 logger.error('could not find mininet binary (mininet is not installed)')
915 ret = False
916
917 # Assert that we have iproute installed
918 if os.system('which ip >/dev/null 2>/dev/null') != 0:
919 logger.error('could not find ip binary (iproute is not installed)')
920 ret = False
921
922 # Assert that we have gdb installed
923 if os.system('which gdb >/dev/null 2>/dev/null') != 0:
924 logger.error('could not find gdb binary (gdb is not installed)')
925 ret = False
926
927 # Assert that FRR utilities exist
928 frrdir = config.get('topogen', 'frrdir')
812e38a9 929 hasfrr = False
007e7313
RZ
930 if not os.path.isdir(frrdir):
931 logger.error('could not find {} directory'.format(frrdir))
932 ret = False
933 else:
812e38a9 934 hasfrr = True
007e7313
RZ
935 try:
936 pwd.getpwnam('frr')[2]
937 except KeyError:
938 logger.warning('could not find "frr" user')
939
940 try:
941 grp.getgrnam('frr')[2]
942 except KeyError:
943 logger.warning('could not find "frr" group')
944
945 try:
946 if 'frr' not in grp.getgrnam('frrvty').gr_mem:
947 logger.error('"frr" user and group exist, but user is not under "frrvty"')
948 except KeyError:
949 logger.warning('could not find "frrvty" group')
950
951 for fname in ['zebra', 'ospfd', 'ospf6d', 'bgpd', 'ripd', 'ripngd',
952 'isisd', 'pimd', 'ldpd']:
953 path = os.path.join(frrdir, fname)
954 if not os.path.isfile(path):
955 # LDPd is an exception
956 if fname == 'ldpd':
957 logger.info('could not find {} in {}'.format(fname, frrdir) +
958 '(LDPd tests will not run)')
959 continue
960
961 logger.warning('could not find {} in {}'.format(fname, frrdir))
962 ret = False
d34f6134
RZ
963 else:
964 if fname != 'zebra':
965 continue
966
967 os.system(
968 '{} -v 2>&1 >/tmp/topotests/frr_zebra.txt'.format(path)
969 )
007e7313
RZ
970
971 # Assert that Quagga utilities exist
972 quaggadir = config.get('topogen', 'quaggadir')
812e38a9
RZ
973 if hasfrr:
974 # if we have frr, don't check for quagga
975 pass
976 elif not os.path.isdir(quaggadir):
007e7313
RZ
977 logger.info('could not find {} directory (quagga tests will not run)'.format(quaggadir))
978 else:
812e38a9 979 ret = True
007e7313
RZ
980 try:
981 pwd.getpwnam('quagga')[2]
982 except KeyError:
983 logger.info('could not find "quagga" user')
984
985 try:
986 grp.getgrnam('quagga')[2]
987 except KeyError:
988 logger.info('could not find "quagga" group')
989
990 try:
991 if 'quagga' not in grp.getgrnam('quaggavty').gr_mem:
992 logger.error('"quagga" user and group exist, but user is not under "quaggavty"')
993 except KeyError:
812e38a9 994 logger.warning('could not find "quaggavty" group')
007e7313
RZ
995
996 for fname in ['zebra', 'ospfd', 'ospf6d', 'bgpd', 'ripd', 'ripngd',
997 'isisd', 'pimd']:
998 path = os.path.join(quaggadir, fname)
999 if not os.path.isfile(path):
1000 logger.warning('could not find {} in {}'.format(fname, quaggadir))
1001 ret = False
d34f6134
RZ
1002 else:
1003 if fname != 'zebra':
1004 continue
1005
1006 os.system(
1007 '{} -v 2>&1 >/tmp/topotests/quagga_zebra.txt'.format(path)
1008 )
007e7313 1009
007e7313
RZ
1010 # Test MPLS availability
1011 krel = platform.release()
1012 if topotest.version_cmp(krel, '4.5') < 0:
1013 logger.info('LDPd tests will not run (have kernel "{}", but it requires 4.5)'.format(krel))
1014
c11c4cc7
MW
1015 # Test for MPLS Kernel modules available
1016 if os.system('/sbin/modprobe -n mpls-router') != 0:
1017 logger.info('LDPd tests will not run (missing mpls-router kernel module)')
1018 if os.system('/sbin/modprobe -n mpls-iptunnel') != 0:
1019 logger.info('LDPd tests will not run (missing mpls-iptunnel kernel module)')
1020
007e7313
RZ
1021 # TODO remove me when we start supporting exabgp >= 4
1022 try:
1023 output = subprocess.check_output(['exabgp', '-v'])
1024 line = output.split('\n')[0]
1025 version = line.split(' ')[2]
1026 if topotest.version_cmp(version, '4') >= 0:
1027 logger.warning('BGP topologies are still using exabgp version 3, expect failures')
1028
1029 # We want to catch all exceptions
1030 # pylint: disable=W0702
1031 except:
1032 logger.warning('failed to find exabgp or returned error')
1033
7547ebd8
RZ
1034 # After we logged the output to file, remove the handler.
1035 logger.removeHandler(fhandler)
1036
007e7313 1037 return ret