]> git.proxmox.com Git - mirror_frr.git/blame - tests/topotests/lib/topogen.py
Merge pull request #3899 from ton31337/fix/remove_private_as_with_local_as
[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
f2d6ce41 135 if not topotest.module_present('mpls-router'):
9711fc7e 136 logger.info('MPLS tests will not run (missing mpls-router kernel module)')
f2d6ce41 137 elif not topotest.module_present('mpls-iptunnel'):
9711fc7e
LB
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))
95460a6b 324 errors = ""
4cfdff1a 325 for gear in self.gears.values():
95460a6b 326 gear.stop(False, False)
3a568b9c 327 for gear in self.gears.values():
95460a6b
LB
328 errors += gear.stop(True, False)
329 if len(errors) > 0:
330 assert "Errors found post shutdown - details follow:" == 0, errors
4cfdff1a 331
1fca63c1
RZ
332 self.net.stop()
333
334 def mininet_cli(self):
335 """
336 Interrupt the test and call the command line interface for manual
337 inspection. Should be only used on non production code.
338 """
339 if not sys.stdin.isatty():
340 raise EnvironmentError(
341 'you must run pytest with \'-s\' in order to use mininet CLI')
342
343 CLI(self.net)
344
13e1fc49
RZ
345 def is_memleak_enabled(self):
346 "Returns `True` if memory leak report is enable, otherwise `False`."
78ed6123
RZ
347 # On router failure we can't run the memory leak test
348 if self.routers_have_failure():
349 return False
350
e8469297
RZ
351 memleak_file = (os.environ.get('TOPOTESTS_CHECK_MEMLEAK') or
352 self.config.get(self.CONFIG_SECTION, 'memleak_path'))
13e1fc49
RZ
353 if memleak_file is None:
354 return False
355 return True
356
357 def report_memory_leaks(self, testname=None):
358 "Run memory leak test and reports."
359 if not self.is_memleak_enabled():
360 return
361
362 # If no name was specified, use the test module name
363 if testname is None:
364 testname = self.modname
365
366 router_list = self.routers().values()
367 for router in router_list:
368 router.report_memory_leaks(self.modname)
369
393ca0fa
RZ
370 def set_error(self, message, code=None):
371 "Sets an error message and signal other tests to skip."
222ea88b 372 logger.info(message)
393ca0fa
RZ
373
374 # If no code is defined use a sequential number
375 if code is None:
1eb633c0 376 code = len(self.errorsd)
393ca0fa 377
1eb633c0
RZ
378 self.errorsd[code] = message
379 self.errors += '\n{}: {}'.format(code, message)
393ca0fa
RZ
380
381 def has_errors(self):
382 "Returns whether errors exist or not."
1eb633c0 383 return len(self.errorsd) > 0
13e1fc49 384
78ed6123
RZ
385 def routers_have_failure(self):
386 "Runs an assertion to make sure that all routers are running."
387 if self.has_errors():
388 return True
389
390 errors = ''
391 router_list = self.routers().values()
392 for router in router_list:
393 result = router.check_router_running()
394 if result != '':
395 errors += result + '\n'
396
397 if errors != '':
398 self.set_error(errors, 'router_error')
46325763 399 assert False, errors
78ed6123
RZ
400 return True
401 return False
402
1fca63c1
RZ
403#
404# Topology gears (equipment)
405#
406
407class TopoGear(object):
408 "Abstract class for type checking"
409
410 def __init__(self):
411 self.tgen = None
412 self.name = None
413 self.cls = None
8c3fdf62 414 self.links = {}
1fca63c1
RZ
415 self.linkn = 0
416
7326ea11
RZ
417 def __str__(self):
418 links = ''
419 for myif, dest in self.links.iteritems():
420 _, destif = dest
421 if links != '':
422 links += ','
423 links += '"{}"<->"{}"'.format(myif, destif)
424
425 return 'TopoGear<name="{}",links=[{}]>'.format(self.name, links)
426
4cfdff1a
RZ
427 def start(self):
428 "Basic start function that just reports equipment start"
429 logger.info('starting "{}"'.format(self.name))
430
95460a6b 431 def stop(self, wait=True, assertOnError=True):
4cfdff1a
RZ
432 "Basic start function that just reports equipment stop"
433 logger.info('stopping "{}"'.format(self.name))
95460a6b 434 return ""
4cfdff1a 435
8c3fdf62
RZ
436 def run(self, command):
437 """
438 Runs the provided command string in the router and returns a string
439 with the response.
440 """
441 return self.tgen.net[self.name].cmd(command)
442
1fca63c1
RZ
443 def add_link(self, node, myif=None, nodeif=None):
444 """
445 Creates a link (connection) between myself and the specified node.
446 Interfaces name can be speficied with:
447 myif: the interface name that will be created in this node
448 nodeif: the target interface name that will be created on the remote node.
449 """
450 self.tgen.add_link(self, node, myif, nodeif)
451
0ab7733f 452 def link_enable(self, myif, enabled=True, netns=None):
1fca63c1 453 """
8c3fdf62
RZ
454 Set this node interface administrative state.
455 myif: this node interface name
456 enabled: whether we should enable or disable the interface
1fca63c1 457 """
8c3fdf62
RZ
458 if myif not in self.links.keys():
459 raise KeyError('interface doesn\'t exists')
460
461 if enabled is True:
462 operation = 'up'
463 else:
464 operation = 'down'
465
13e1fc49
RZ
466 logger.info('setting node "{}" link "{}" to state "{}"'.format(
467 self.name, myif, operation
468 ))
0ab7733f
PG
469 extract=''
470 if netns is not None:
471 extract = 'ip netns exec {} '.format(netns)
472 return self.run('{}ip link set dev {} {}'.format(extract, myif, operation))
8c3fdf62 473
0ab7733f 474 def peer_link_enable(self, myif, enabled=True, netns=None):
8c3fdf62
RZ
475 """
476 Set the peer interface administrative state.
477 myif: this node interface name
478 enabled: whether we should enable or disable the interface
479
480 NOTE: this is used to simulate a link down on this node, since when the
481 peer disables their interface our interface status changes to no link.
482 """
483 if myif not in self.links.keys():
484 raise KeyError('interface doesn\'t exists')
485
486 node, nodeif = self.links[myif]
0ab7733f 487 node.link_enable(nodeif, enabled, netns)
1fca63c1 488
8c3fdf62
RZ
489 def new_link(self):
490 """
491 Generates a new unique link name.
492
493 NOTE: This function should only be called by Topogen.
494 """
495 ifname = '{}-eth{}'.format(self.name, self.linkn)
1fca63c1 496 self.linkn += 1
1fca63c1
RZ
497 return ifname
498
8c3fdf62
RZ
499 def register_link(self, myif, node, nodeif):
500 """
501 Register link between this node interface and outside node.
502
503 NOTE: This function should only be called by Topogen.
504 """
505 if myif in self.links.keys():
506 raise KeyError('interface already exists')
507
508 self.links[myif] = (node, nodeif)
509
1fca63c1
RZ
510class TopoRouter(TopoGear):
511 """
d9ea1cda 512 Router abstraction.
1fca63c1
RZ
513 """
514
515 # The default required directories by Quagga/FRR
516 PRIVATE_DIRS = [
517 '/etc/frr',
518 '/etc/quagga',
519 '/var/run/frr',
520 '/var/run/quagga',
521 '/var/log'
522 ]
523
524 # Router Daemon enumeration definition.
7326ea11 525 RD_ZEBRA = 1
1fca63c1
RZ
526 RD_RIP = 2
527 RD_RIPNG = 3
528 RD_OSPF = 4
529 RD_OSPF6 = 5
530 RD_ISIS = 6
531 RD_BGP = 7
532 RD_LDP = 8
533 RD_PIM = 9
c267e5b1
RZ
534 RD_EIGRP = 10
535 RD_NHRP = 11
a2a1134c 536 RD_STATIC = 12
4d45d6d3 537 RD_BFD = 13
1fca63c1
RZ
538 RD = {
539 RD_ZEBRA: 'zebra',
540 RD_RIP: 'ripd',
541 RD_RIPNG: 'ripngd',
542 RD_OSPF: 'ospfd',
543 RD_OSPF6: 'ospf6d',
544 RD_ISIS: 'isisd',
545 RD_BGP: 'bgpd',
546 RD_PIM: 'pimd',
547 RD_LDP: 'ldpd',
c267e5b1
RZ
548 RD_EIGRP: 'eigrpd',
549 RD_NHRP: 'nhrpd',
a2a1134c 550 RD_STATIC: 'staticd',
4d45d6d3 551 RD_BFD: 'bfdd',
1fca63c1
RZ
552 }
553
2ab85530 554 def __init__(self, tgen, cls, name, **params):
d9ea1cda
RZ
555 """
556 The constructor has the following parameters:
557 * tgen: Topogen object
558 * cls: router class that will be used to instantiate
559 * name: router name
560 * daemondir: daemon binary directory
561 * routertype: 'quagga' or 'frr'
562 """
1fca63c1
RZ
563 super(TopoRouter, self).__init__()
564 self.tgen = tgen
565 self.net = None
566 self.name = name
567 self.cls = cls
c540096e 568 self.options = {}
f6899d4d 569 self.routertype = params.get('routertype', 'frr')
2ab85530
RZ
570 if not params.has_key('privateDirs'):
571 params['privateDirs'] = self.PRIVATE_DIRS
c540096e
RZ
572
573 self.options['memleak_path'] = params.get('memleak_path', None)
13e1fc49
RZ
574
575 # Create new log directory
576 self.logdir = '/tmp/topotests/{}'.format(self.tgen.modname)
577 # Clean up before starting new log files: avoids removing just created
578 # log files.
579 self._prepare_tmpfiles()
580 # Propagate the router log directory
581 params['logdir'] = self.logdir
582
e1dfa45e
LB
583 #setup the per node directory
584 dir = '{}/{}'.format(self.logdir, self.name)
585 os.system('mkdir -p ' + dir)
b0f0d980 586 os.system('chmod -R go+rw /tmp/topotests')
e1dfa45e 587
13e1fc49 588 # Open router log file
87ba6e1e 589 logfile = '{0}/{1}.log'.format(self.logdir, name)
13e1fc49 590 self.logger = logger_config.get_logger(name=name, target=logfile)
87ba6e1e 591
2ab85530 592 self.tgen.topo.addNode(self.name, cls=self.cls, **params)
1fca63c1 593
7326ea11
RZ
594 def __str__(self):
595 gear = super(TopoRouter, self).__str__()
596 gear += ' TopoRouter<>'
597 return gear
598
13e1fc49
RZ
599 def _prepare_tmpfiles(self):
600 # Create directories if they don't exist
601 try:
602 os.makedirs(self.logdir, 0755)
603 except OSError:
604 pass
605
f6899d4d
RZ
606 # Allow unprivileged daemon user (frr/quagga) to create log files
607 try:
608 # Only allow group, if it exist.
609 gid = grp.getgrnam(self.routertype)[2]
610 os.chown(self.logdir, 0, gid)
611 os.chmod(self.logdir, 0775)
612 except KeyError:
613 # Allow anyone, but set the sticky bit to avoid file deletions
614 os.chmod(self.logdir, 01777)
615
13e1fc49 616 # Try to find relevant old logfiles in /tmp and delete them
e1dfa45e 617 map(os.remove, glob.glob('{}/{}/*.log'.format(self.logdir, self.name)))
13e1fc49 618 # Remove old core files
e1dfa45e 619 map(os.remove, glob.glob('{}/{}/*.dmp'.format(self.logdir, self.name)))
13e1fc49 620
8dd5077d
PG
621 def check_capability(self, daemon, param):
622 """
623 Checks a capability daemon against an argument option
624 Return True if capability available. False otherwise
625 """
626 daemonstr = self.RD.get(daemon)
627 self.logger.info('check capability {} for "{}"'.format(param, daemonstr))
628 return self.tgen.net[self.name].checkCapability(daemonstr, param)
629
630 def load_config(self, daemon, source=None, param=None):
1fca63c1
RZ
631 """
632 Loads daemon configuration from the specified source
633 Possible daemon values are: TopoRouter.RD_ZEBRA, TopoRouter.RD_RIP,
634 TopoRouter.RD_RIPNG, TopoRouter.RD_OSPF, TopoRouter.RD_OSPF6,
635 TopoRouter.RD_ISIS, TopoRouter.RD_BGP, TopoRouter.RD_LDP,
636 TopoRouter.RD_PIM.
637 """
638 daemonstr = self.RD.get(daemon)
13e1fc49 639 self.logger.info('loading "{}" configuration: {}'.format(daemonstr, source))
8dd5077d 640 self.tgen.net[self.name].loadConf(daemonstr, source, param)
1fca63c1
RZ
641
642 def check_router_running(self):
643 """
644 Run a series of checks and returns a status string.
645 """
13e1fc49 646 self.logger.info('checking if daemons are running')
1fca63c1
RZ
647 return self.tgen.net[self.name].checkRouterRunning()
648
649 def start(self):
650 """
651 Start router:
652 * Load modules
653 * Clean up files
654 * Configure interfaces
655 * Start daemons (e.g. FRR/Quagga)
f6899d4d 656 * Configure daemon logging files
1fca63c1 657 """
13e1fc49 658 self.logger.debug('starting')
f6899d4d 659 nrouter = self.tgen.net[self.name]
9711fc7e 660 result = nrouter.startRouter(self.tgen)
f6899d4d 661
deb4cef0
LB
662 # Enable all daemon command logging, logging files
663 # and set them to the start dir.
f6899d4d
RZ
664 for daemon, enabled in nrouter.daemons.iteritems():
665 if enabled == 0:
666 continue
deb4cef0
LB
667 self.vtysh_cmd('configure terminal\nlog commands\nlog file {}.log'.format(
668 daemon), daemon=daemon)
f6899d4d 669
57c5075b
RZ
670 if result != '':
671 self.tgen.set_error(result)
672
f6899d4d 673 return result
1fca63c1 674
95460a6b 675 def stop(self, wait=True, assertOnError=True):
13e1fc49
RZ
676 """
677 Stop router:
678 * Kill daemons
679 """
680 self.logger.debug('stopping')
95460a6b 681 return self.tgen.net[self.name].stopRouter(wait, assertOnError)
13e1fc49 682
f9b48d8b 683 def vtysh_cmd(self, command, isjson=False, daemon=None):
1fca63c1
RZ
684 """
685 Runs the provided command string in the vty shell and returns a string
686 with the response.
687
688 This function also accepts multiple commands, but this mode does not
689 return output for each command. See vtysh_multicmd() for more details.
690 """
691 # Detect multi line commands
692 if command.find('\n') != -1:
f9b48d8b
RZ
693 return self.vtysh_multicmd(command, daemon=daemon)
694
695 dparam = ''
696 if daemon is not None:
697 dparam += '-d {}'.format(daemon)
698
699 vtysh_command = 'vtysh {} -c "{}" 2>/dev/null'.format(dparam, command)
1fca63c1 700
a40daddc 701 output = self.run(vtysh_command)
13e1fc49
RZ
702 self.logger.info('\nvtysh command => {}\nvtysh output <= {}'.format(
703 command, output))
a40daddc
RZ
704 if isjson is False:
705 return output
706
7b093d84
RZ
707 try:
708 return json.loads(output)
709 except ValueError:
710 logger.warning('vtysh_cmd: failed to convert json output')
711 return {}
1fca63c1 712
f9b48d8b 713 def vtysh_multicmd(self, commands, pretty_output=True, daemon=None):
1fca63c1
RZ
714 """
715 Runs the provided commands in the vty shell and return the result of
716 execution.
717
718 pretty_output: defines how the return value will be presented. When
719 True it will show the command as they were executed in the vty shell,
720 otherwise it will only show lines that failed.
721 """
722 # Prepare the temporary file that will hold the commands
723 fname = topotest.get_file(commands)
724
f9b48d8b
RZ
725 dparam = ''
726 if daemon is not None:
727 dparam += '-d {}'.format(daemon)
728
1fca63c1
RZ
729 # Run the commands and delete the temporary file
730 if pretty_output:
f9b48d8b 731 vtysh_command = 'vtysh {} < {}'.format(dparam, fname)
1fca63c1 732 else:
f9b48d8b 733 vtysh_command = 'vtysh {} -f {}'.format(dparam, fname)
1fca63c1
RZ
734
735 res = self.run(vtysh_command)
736 os.unlink(fname)
737
13e1fc49
RZ
738 self.logger.info('\nvtysh command => "{}"\nvtysh output <= "{}"'.format(
739 vtysh_command, res))
740
1fca63c1
RZ
741 return res
742
38c39932
RZ
743 def report_memory_leaks(self, testname):
744 """
745 Runs the router memory leak check test. Has the following parameter:
746 testname: the test file name for identification
747
748 NOTE: to run this you must have the environment variable
c540096e 749 TOPOTESTS_CHECK_MEMLEAK set or memleak_path configured in `pytest.ini`.
38c39932 750 """
c540096e 751 memleak_file = os.environ.get('TOPOTESTS_CHECK_MEMLEAK') or self.options['memleak_path']
38c39932 752 if memleak_file is None:
38c39932
RZ
753 return
754
13e1fc49
RZ
755 self.stop()
756 self.logger.info('running memory leak report')
38c39932
RZ
757 self.tgen.net[self.name].report_memory_leaks(memleak_file, testname)
758
6ca2411e
RZ
759 def version_info(self):
760 "Get equipment information from 'show version'."
761 output = self.vtysh_cmd('show version').split('\n')[0]
762 columns = topotest.normalize_text(output).split(' ')
b3b1b1d1
RZ
763 try:
764 return {
765 'type': columns[0],
766 'version': columns[1],
767 }
768 except IndexError:
769 return {
770 'type': None,
771 'version': None,
772 }
6ca2411e
RZ
773
774 def has_version(self, cmpop, version):
775 """
776 Compares router version using operation `cmpop` with `version`.
777 Valid `cmpop` values:
778 * `>=`: has the same version or greater
779 * '>': has greater version
780 * '=': has the same version
781 * '<': has a lesser version
782 * '<=': has the same version or lesser
783
784 Usage example: router.has_version('>', '1.0')
785 """
fb80b81b 786 return self.tgen.net[self.name].checkRouterVersion(cmpop, version)
6ca2411e
RZ
787
788 def has_type(self, rtype):
789 """
790 Compares router type with `rtype`. Returns `True` if the type matches,
791 otherwise `false`.
792 """
793 curtype = self.version_info()['type']
794 return rtype == curtype
795
447f2d5a
LB
796 def has_mpls(self):
797 nrouter = self.tgen.net[self.name]
798 return nrouter.hasmpls
799
1fca63c1
RZ
800class TopoSwitch(TopoGear):
801 """
802 Switch abstraction. Has the following properties:
803 * cls: switch class that will be used to instantiate
804 * name: switch name
805 """
806 # pylint: disable=too-few-public-methods
807
808 def __init__(self, tgen, cls, name):
809 super(TopoSwitch, self).__init__()
810 self.tgen = tgen
811 self.net = None
812 self.name = name
813 self.cls = cls
814 self.tgen.topo.addSwitch(name, cls=self.cls)
7326ea11
RZ
815
816 def __str__(self):
817 gear = super(TopoSwitch, self).__str__()
818 gear += ' TopoSwitch<>'
819 return gear
19ccab57
RZ
820
821class TopoHost(TopoGear):
822 "Host abstraction."
823 # pylint: disable=too-few-public-methods
824
825 def __init__(self, tgen, name, **params):
826 """
827 Mininet has the following known `params` for hosts:
828 * `ip`: the IP address (string) for the host interface
829 * `defaultRoute`: the default route that will be installed
830 (e.g. 'via 10.0.0.1')
831 * `privateDirs`: directories that will be mounted on a different domain
832 (e.g. '/etc/important_dir').
833 """
834 super(TopoHost, self).__init__()
835 self.tgen = tgen
836 self.net = None
837 self.name = name
838 self.options = params
839 self.tgen.topo.addHost(name, **params)
840
841 def __str__(self):
842 gear = super(TopoHost, self).__str__()
843 gear += ' TopoHost<ip="{}",defaultRoute="{}",privateDirs="{}">'.format(
844 self.options['ip'], self.options['defaultRoute'],
845 str(self.options['privateDirs']))
846 return gear
847
848class TopoExaBGP(TopoHost):
849 "ExaBGP peer abstraction."
850 # pylint: disable=too-few-public-methods
851
852 PRIVATE_DIRS = [
853 '/etc/exabgp',
854 '/var/run/exabgp',
855 '/var/log',
856 ]
857
858 def __init__(self, tgen, name, **params):
859 """
860 ExaBGP usually uses the following parameters:
861 * `ip`: the IP address (string) for the host interface
862 * `defaultRoute`: the default route that will be installed
863 (e.g. 'via 10.0.0.1')
864
865 Note: the different between a host and a ExaBGP peer is that this class
866 has a privateDirs already defined and contains functions to handle ExaBGP
867 things.
868 """
869 params['privateDirs'] = self.PRIVATE_DIRS
870 super(TopoExaBGP, self).__init__(tgen, name, **params)
871 self.tgen.topo.addHost(name, **params)
872
873 def __str__(self):
874 gear = super(TopoExaBGP, self).__str__()
875 gear += ' TopoExaBGP<>'.format()
876 return gear
877
878 def start(self, peer_dir, env_file=None):
879 """
880 Start running ExaBGP daemon:
881 * Copy all peer* folder contents into /etc/exabgp
882 * Copy exabgp env file if specified
883 * Make all python files runnable
884 * Run ExaBGP with env file `env_file` and configuration peer*/exabgp.cfg
885 """
886 self.run('mkdir /etc/exabgp')
887 self.run('chmod 755 /etc/exabgp')
888 self.run('cp {}/* /etc/exabgp/'.format(peer_dir))
889 if env_file is not None:
890 self.run('cp {} /etc/exabgp/exabgp.env'.format(env_file))
891 self.run('chmod 644 /etc/exabgp/*')
892 self.run('chmod a+x /etc/exabgp/*.py')
893 self.run('chown -R exabgp:exabgp /etc/exabgp')
87d5e16a
LB
894 output = self.run('exabgp -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg')
895 if output == None or len(output) == 0:
896 output = '<none>'
897 logger.info('{} exabgp started, output={}'.format(self.name, output))
19ccab57 898
95460a6b 899 def stop(self, wait=True, assertOnError=True):
19ccab57
RZ
900 "Stop ExaBGP peer and kill the daemon"
901 self.run('kill `cat /var/run/exabgp/exabgp.pid`')
95460a6b 902 return ""
007e7313
RZ
903
904
905#
906# Diagnostic function
907#
908
909# Disable linter branch warning. It is expected to have these here.
910# pylint: disable=R0912
af99f19e 911def diagnose_env_linux():
007e7313
RZ
912 """
913 Run diagnostics in the running environment. Returns `True` when everything
914 is ok, otherwise `False`.
915 """
916 ret = True
7547ebd8 917
fcfbc769
RZ
918 # Test log path exists before installing handler.
919 if not os.path.isdir('/tmp'):
920 logger.warning('could not find /tmp for logs')
921 else:
922 os.system('mkdir /tmp/topotests')
923 # Log diagnostics to file so it can be examined later.
924 fhandler = logging.FileHandler(filename='/tmp/topotests/diagnostics.txt')
925 fhandler.setLevel(logging.DEBUG)
926 fhandler.setFormatter(
927 logging.Formatter(fmt='%(asctime)s %(levelname)s: %(message)s')
928 )
929 logger.addHandler(fhandler)
7547ebd8 930
007e7313
RZ
931 logger.info('Running environment diagnostics')
932
933 # Load configuration
934 config = ConfigParser.ConfigParser(tgen_defaults)
935 pytestini_path = os.path.join(CWD, '../pytest.ini')
936 config.read(pytestini_path)
937
938 # Assert that we are running as root
939 if os.getuid() != 0:
940 logger.error('you must run topotest as root')
941 ret = False
942
943 # Assert that we have mininet
944 if os.system('which mn >/dev/null 2>/dev/null') != 0:
945 logger.error('could not find mininet binary (mininet is not installed)')
946 ret = False
947
948 # Assert that we have iproute installed
949 if os.system('which ip >/dev/null 2>/dev/null') != 0:
950 logger.error('could not find ip binary (iproute is not installed)')
951 ret = False
952
953 # Assert that we have gdb installed
954 if os.system('which gdb >/dev/null 2>/dev/null') != 0:
955 logger.error('could not find gdb binary (gdb is not installed)')
956 ret = False
957
958 # Assert that FRR utilities exist
959 frrdir = config.get('topogen', 'frrdir')
812e38a9 960 hasfrr = False
007e7313
RZ
961 if not os.path.isdir(frrdir):
962 logger.error('could not find {} directory'.format(frrdir))
963 ret = False
964 else:
812e38a9 965 hasfrr = True
007e7313
RZ
966 try:
967 pwd.getpwnam('frr')[2]
968 except KeyError:
969 logger.warning('could not find "frr" user')
970
971 try:
972 grp.getgrnam('frr')[2]
973 except KeyError:
974 logger.warning('could not find "frr" group')
975
976 try:
977 if 'frr' not in grp.getgrnam('frrvty').gr_mem:
978 logger.error('"frr" user and group exist, but user is not under "frrvty"')
979 except KeyError:
980 logger.warning('could not find "frrvty" group')
981
982 for fname in ['zebra', 'ospfd', 'ospf6d', 'bgpd', 'ripd', 'ripngd',
983 'isisd', 'pimd', 'ldpd']:
984 path = os.path.join(frrdir, fname)
985 if not os.path.isfile(path):
986 # LDPd is an exception
987 if fname == 'ldpd':
988 logger.info('could not find {} in {}'.format(fname, frrdir) +
989 '(LDPd tests will not run)')
990 continue
991
992 logger.warning('could not find {} in {}'.format(fname, frrdir))
993 ret = False
d34f6134
RZ
994 else:
995 if fname != 'zebra':
996 continue
997
998 os.system(
999 '{} -v 2>&1 >/tmp/topotests/frr_zebra.txt'.format(path)
1000 )
007e7313
RZ
1001
1002 # Assert that Quagga utilities exist
1003 quaggadir = config.get('topogen', 'quaggadir')
812e38a9
RZ
1004 if hasfrr:
1005 # if we have frr, don't check for quagga
1006 pass
1007 elif not os.path.isdir(quaggadir):
007e7313
RZ
1008 logger.info('could not find {} directory (quagga tests will not run)'.format(quaggadir))
1009 else:
812e38a9 1010 ret = True
007e7313
RZ
1011 try:
1012 pwd.getpwnam('quagga')[2]
1013 except KeyError:
1014 logger.info('could not find "quagga" user')
1015
1016 try:
1017 grp.getgrnam('quagga')[2]
1018 except KeyError:
1019 logger.info('could not find "quagga" group')
1020
1021 try:
1022 if 'quagga' not in grp.getgrnam('quaggavty').gr_mem:
1023 logger.error('"quagga" user and group exist, but user is not under "quaggavty"')
1024 except KeyError:
812e38a9 1025 logger.warning('could not find "quaggavty" group')
007e7313
RZ
1026
1027 for fname in ['zebra', 'ospfd', 'ospf6d', 'bgpd', 'ripd', 'ripngd',
1028 'isisd', 'pimd']:
1029 path = os.path.join(quaggadir, fname)
1030 if not os.path.isfile(path):
1031 logger.warning('could not find {} in {}'.format(fname, quaggadir))
1032 ret = False
d34f6134
RZ
1033 else:
1034 if fname != 'zebra':
1035 continue
1036
1037 os.system(
1038 '{} -v 2>&1 >/tmp/topotests/quagga_zebra.txt'.format(path)
1039 )
007e7313 1040
007e7313
RZ
1041 # Test MPLS availability
1042 krel = platform.release()
1043 if topotest.version_cmp(krel, '4.5') < 0:
1044 logger.info('LDPd tests will not run (have kernel "{}", but it requires 4.5)'.format(krel))
1045
c11c4cc7 1046 # Test for MPLS Kernel modules available
f2d6ce41 1047 if not topotest.module_present('mpls-router', load=False) != 0:
c11c4cc7 1048 logger.info('LDPd tests will not run (missing mpls-router kernel module)')
f2d6ce41 1049 if not topotest.module_present('mpls-iptunnel', load=False) != 0:
c11c4cc7
MW
1050 logger.info('LDPd tests will not run (missing mpls-iptunnel kernel module)')
1051
007e7313
RZ
1052 # TODO remove me when we start supporting exabgp >= 4
1053 try:
1054 output = subprocess.check_output(['exabgp', '-v'])
1055 line = output.split('\n')[0]
1056 version = line.split(' ')[2]
1057 if topotest.version_cmp(version, '4') >= 0:
1058 logger.warning('BGP topologies are still using exabgp version 3, expect failures')
1059
1060 # We want to catch all exceptions
1061 # pylint: disable=W0702
1062 except:
1063 logger.warning('failed to find exabgp or returned error')
1064
7547ebd8
RZ
1065 # After we logged the output to file, remove the handler.
1066 logger.removeHandler(fhandler)
1067
007e7313 1068 return ret
af99f19e
DS
1069
1070def diagnose_env_freebsd():
1071 return True
1072
1073def diagnose_env():
1074 if sys.platform.startswith("linux"):
1075 return diagnose_env_linux()
1076 elif sys.platform.startswith("freebsd"):
1077 return diagnose_env_freebsd()
1078
1079 return False