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