]> git.proxmox.com Git - mirror_frr.git/blob - tests/topotests/lib/topogen.py
5c8d1ddf36e53a45761474047b28538b17774736
[mirror_frr.git] / tests / topotests / lib / topogen.py
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 """
24 Topogen (Topology Generator) is an abstraction around Topotest and Mininet to
25 help reduce boilerplate code and provide a stable interface to build topology
26 tests on.
27
28 Basic 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
41 import os
42 import sys
43 import logging
44 import json
45 import ConfigParser
46 import glob
47 import grp
48 import platform
49 import pwd
50 import subprocess
51 import pytest
52
53 from mininet.net import Mininet
54 from mininet.log import setLogLevel
55 from mininet.cli import CLI
56
57 from lib import topotest
58 from lib.topolog import logger, logger_config
59
60 CWD = os.path.dirname(os.path.realpath(__file__))
61
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.
65 global_tgen = None
66
67 def 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
76 def 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
86 # Topogen configuration defaults
87 tgen_defaults = {
88 'verbosity': 'info',
89 'frrdir': '/usr/lib/frr',
90 'quaggadir': '/usr/lib/quagga',
91 'routertype': 'frr',
92 'memleak_path': None,
93 }
94
95 class Topogen(object):
96 "A topology test builder helper."
97
98 CONFIG_SECTION = 'topogen'
99
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 """
106 self.config = None
107 self.topo = None
108 self.net = None
109 self.gears = {}
110 self.routern = 1
111 self.switchn = 1
112 self.modname = modname
113 self.errorsd = {}
114 self.errors = ''
115 self.peern = 1
116 self._init_topo(cls)
117 logger.info('loading topology: {}'.format(self.modname))
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
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
141 # Load the default topology configurations
142 self._load_config()
143
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
151 def _load_config(self):
152 """
153 Loads the configuration file `pytest.ini` located at the root dir of
154 topotests.
155 """
156 self.config = ConfigParser.ConfigParser(tgen_defaults)
157 pytestini_path = os.path.join(CWD, '../pytest.ini')
158 self.config.read(pytestini_path)
159
160 def add_router(self, name=None, cls=topotest.Router, **params):
161 """
162 Adds a new router to the topology. This function has the following
163 options:
164 * `name`: (optional) select the router name
165 * `daemondir`: (optional) custom daemon binary directory
166 * `routertype`: (optional) `quagga` or `frr`
167 Returns a TopoRouter.
168 """
169 if name is None:
170 name = 'r{}'.format(self.routern)
171 if name in self.gears:
172 raise KeyError('router already exists')
173
174 params['frrdir'] = self.config.get(self.CONFIG_SECTION, 'frrdir')
175 params['quaggadir'] = self.config.get(self.CONFIG_SECTION, 'quaggadir')
176 params['memleak_path'] = self.config.get(self.CONFIG_SECTION, 'memleak_path')
177 if not params.has_key('routertype'):
178 params['routertype'] = self.config.get(self.CONFIG_SECTION, 'routertype')
179
180 self.gears[name] = TopoRouter(self, cls, name, **params)
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:
192 name = 's{}'.format(self.switchn)
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
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
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:
230 ifname1 = node1.new_link()
231 if ifname2 is None:
232 ifname2 = node2.new_link()
233
234 node1.register_link(ifname1, node2, ifname2)
235 node2.register_link(ifname2, node1, ifname1)
236 self.topo.addLink(node1.name, node2.name,
237 intfName1=ifname1, intfName2=ifname2)
238
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
262 def routers(self):
263 """
264 Returns the router dictionary (key is the router name and value is the
265 router object itself).
266 """
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)
275
276 def start_topology(self, log_level=None):
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 """
286 # If log_level is not specified use the configuration.
287 if log_level is None:
288 log_level = self.config.get(self.CONFIG_SECTION, 'verbosity')
289
290 # Set python logger level
291 logger_config.set_log_level(log_level)
292
293 # Run mininet
294 if log_level == 'debug':
295 setLogLevel(log_level)
296
297 logger.info('starting topology: {}'.format(self.modname))
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):
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
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.
322 """
323 logger.info('stopping topology: {}'.format(self.modname))
324
325 for gear in self.gears.values():
326 gear.stop(False)
327 for gear in self.gears.values():
328 gear.stop(True)
329
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
343 def is_memleak_enabled(self):
344 "Returns `True` if memory leak report is enable, otherwise `False`."
345 # On router failure we can't run the memory leak test
346 if self.routers_have_failure():
347 return False
348
349 memleak_file = (os.environ.get('TOPOTESTS_CHECK_MEMLEAK') or
350 self.config.get(self.CONFIG_SECTION, 'memleak_path'))
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
368 def set_error(self, message, code=None):
369 "Sets an error message and signal other tests to skip."
370 logger.info(message)
371
372 # If no code is defined use a sequential number
373 if code is None:
374 code = len(self.errorsd)
375
376 self.errorsd[code] = message
377 self.errors += '\n{}: {}'.format(code, message)
378
379 def has_errors(self):
380 "Returns whether errors exist or not."
381 return len(self.errorsd) > 0
382
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')
397 assert False, errors
398 return True
399 return False
400
401 #
402 # Topology gears (equipment)
403 #
404
405 class 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
412 self.links = {}
413 self.linkn = 0
414
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
425 def start(self):
426 "Basic start function that just reports equipment start"
427 logger.info('starting "{}"'.format(self.name))
428
429 def stop(self, wait=True):
430 "Basic start function that just reports equipment stop"
431 logger.info('stopping "{}"'.format(self.name))
432
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
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
449 def link_enable(self, myif, enabled=True):
450 """
451 Set this node interface administrative state.
452 myif: this node interface name
453 enabled: whether we should enable or disable the interface
454 """
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
463 logger.info('setting node "{}" link "{}" to state "{}"'.format(
464 self.name, myif, operation
465 ))
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)
482
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)
490 self.linkn += 1
491 return ifname
492
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
504 class TopoRouter(TopoGear):
505 """
506 Router abstraction.
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.
519 RD_ZEBRA = 1
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
528 RD_EIGRP = 10
529 RD_NHRP = 11
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',
540 RD_EIGRP: 'eigrpd',
541 RD_NHRP: 'nhrpd',
542 }
543
544 def __init__(self, tgen, cls, name, **params):
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 """
553 super(TopoRouter, self).__init__()
554 self.tgen = tgen
555 self.net = None
556 self.name = name
557 self.cls = cls
558 self.options = {}
559 self.routertype = params.get('routertype', 'frr')
560 if not params.has_key('privateDirs'):
561 params['privateDirs'] = self.PRIVATE_DIRS
562
563 self.options['memleak_path'] = params.get('memleak_path', None)
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
573 #setup the per node directory
574 dir = '{}/{}'.format(self.logdir, self.name)
575 os.system('mkdir -p ' + dir)
576 os.system('chmod 775 ' + dir)
577 os.system('chgrp {} {}'.format(self.routertype, dir))
578
579 # Open router log file
580 logfile = '{0}/{1}.log'.format(dir, name)
581
582 self.logger = logger_config.get_logger(name=name, target=logfile)
583 self.tgen.topo.addNode(self.name, cls=self.cls, **params)
584
585 def __str__(self):
586 gear = super(TopoRouter, self).__str__()
587 gear += ' TopoRouter<>'
588 return gear
589
590 def _prepare_tmpfiles(self):
591 # Create directories if they don't exist
592 try:
593 os.makedirs(self.logdir, 0755)
594 except OSError:
595 pass
596
597 # Allow unprivileged daemon user (frr/quagga) to create log files
598 try:
599 # Only allow group, if it exist.
600 gid = grp.getgrnam(self.routertype)[2]
601 os.chown(self.logdir, 0, gid)
602 os.chmod(self.logdir, 0775)
603 except KeyError:
604 # Allow anyone, but set the sticky bit to avoid file deletions
605 os.chmod(self.logdir, 01777)
606
607 # Try to find relevant old logfiles in /tmp and delete them
608 map(os.remove, glob.glob('{}/{}/*.log'.format(self.logdir, self.name)))
609 # Remove old core files
610 map(os.remove, glob.glob('{}/{}/*.dmp'.format(self.logdir, self.name)))
611
612 def check_capability(self, daemon, param):
613 """
614 Checks a capability daemon against an argument option
615 Return True if capability available. False otherwise
616 """
617 daemonstr = self.RD.get(daemon)
618 self.logger.info('check capability {} for "{}"'.format(param, daemonstr))
619 return self.tgen.net[self.name].checkCapability(daemonstr, param)
620
621 def load_config(self, daemon, source=None, param=None):
622 """
623 Loads daemon configuration from the specified source
624 Possible daemon values are: TopoRouter.RD_ZEBRA, TopoRouter.RD_RIP,
625 TopoRouter.RD_RIPNG, TopoRouter.RD_OSPF, TopoRouter.RD_OSPF6,
626 TopoRouter.RD_ISIS, TopoRouter.RD_BGP, TopoRouter.RD_LDP,
627 TopoRouter.RD_PIM.
628 """
629 daemonstr = self.RD.get(daemon)
630 self.logger.info('loading "{}" configuration: {}'.format(daemonstr, source))
631 self.tgen.net[self.name].loadConf(daemonstr, source, param)
632
633 def check_router_running(self):
634 """
635 Run a series of checks and returns a status string.
636 """
637 self.logger.info('checking if daemons are running')
638 return self.tgen.net[self.name].checkRouterRunning()
639
640 def start(self):
641 """
642 Start router:
643 * Load modules
644 * Clean up files
645 * Configure interfaces
646 * Start daemons (e.g. FRR/Quagga)
647 * Configure daemon logging files
648 """
649 self.logger.debug('starting')
650 nrouter = self.tgen.net[self.name]
651 result = nrouter.startRouter(self.tgen)
652
653 # Enable all daemon command logging, logging files
654 # and set them to the start dir.
655 for daemon, enabled in nrouter.daemons.iteritems():
656 if enabled == 0:
657 continue
658 self.vtysh_cmd('configure terminal\nlog commands\nlog file {}.log'.format(
659 daemon), daemon=daemon)
660
661 if result != '':
662 self.tgen.set_error(result)
663
664 return result
665
666 def stop(self, wait=True):
667 """
668 Stop router:
669 * Kill daemons
670 """
671 self.logger.debug('stopping')
672 return self.tgen.net[self.name].stopRouter(wait)
673
674 def vtysh_cmd(self, command, isjson=False, daemon=None):
675 """
676 Runs the provided command string in the vty shell and returns a string
677 with the response.
678
679 This function also accepts multiple commands, but this mode does not
680 return output for each command. See vtysh_multicmd() for more details.
681 """
682 # Detect multi line commands
683 if command.find('\n') != -1:
684 return self.vtysh_multicmd(command, daemon=daemon)
685
686 dparam = ''
687 if daemon is not None:
688 dparam += '-d {}'.format(daemon)
689
690 vtysh_command = 'vtysh {} -c "{}" 2>/dev/null'.format(dparam, command)
691
692 output = self.run(vtysh_command)
693 self.logger.info('\nvtysh command => {}\nvtysh output <= {}'.format(
694 command, output))
695 if isjson is False:
696 return output
697
698 try:
699 return json.loads(output)
700 except ValueError:
701 logger.warning('vtysh_cmd: failed to convert json output')
702 return {}
703
704 def vtysh_multicmd(self, commands, pretty_output=True, daemon=None):
705 """
706 Runs the provided commands in the vty shell and return the result of
707 execution.
708
709 pretty_output: defines how the return value will be presented. When
710 True it will show the command as they were executed in the vty shell,
711 otherwise it will only show lines that failed.
712 """
713 # Prepare the temporary file that will hold the commands
714 fname = topotest.get_file(commands)
715
716 dparam = ''
717 if daemon is not None:
718 dparam += '-d {}'.format(daemon)
719
720 # Run the commands and delete the temporary file
721 if pretty_output:
722 vtysh_command = 'vtysh {} < {}'.format(dparam, fname)
723 else:
724 vtysh_command = 'vtysh {} -f {}'.format(dparam, fname)
725
726 res = self.run(vtysh_command)
727 os.unlink(fname)
728
729 self.logger.info('\nvtysh command => "{}"\nvtysh output <= "{}"'.format(
730 vtysh_command, res))
731
732 return res
733
734 def report_memory_leaks(self, testname):
735 """
736 Runs the router memory leak check test. Has the following parameter:
737 testname: the test file name for identification
738
739 NOTE: to run this you must have the environment variable
740 TOPOTESTS_CHECK_MEMLEAK set or memleak_path configured in `pytest.ini`.
741 """
742 memleak_file = os.environ.get('TOPOTESTS_CHECK_MEMLEAK') or self.options['memleak_path']
743 if memleak_file is None:
744 return
745
746 self.stop()
747 self.logger.info('running memory leak report')
748 self.tgen.net[self.name].report_memory_leaks(memleak_file, testname)
749
750 def version_info(self):
751 "Get equipment information from 'show version'."
752 output = self.vtysh_cmd('show version').split('\n')[0]
753 columns = topotest.normalize_text(output).split(' ')
754 try:
755 return {
756 'type': columns[0],
757 'version': columns[1],
758 }
759 except IndexError:
760 return {
761 'type': None,
762 'version': None,
763 }
764
765 def has_version(self, cmpop, version):
766 """
767 Compares router version using operation `cmpop` with `version`.
768 Valid `cmpop` values:
769 * `>=`: has the same version or greater
770 * '>': has greater version
771 * '=': has the same version
772 * '<': has a lesser version
773 * '<=': has the same version or lesser
774
775 Usage example: router.has_version('>', '1.0')
776 """
777 rversion = self.version_info()['version']
778 if rversion is None:
779 return False
780
781 result = topotest.version_cmp(rversion, version)
782 if cmpop == '>=':
783 return result >= 0
784 if cmpop == '>':
785 return result > 0
786 if cmpop == '=':
787 return result == 0
788 if cmpop == '<':
789 return result < 0
790 if cmpop == '<':
791 return result < 0
792 if cmpop == '<=':
793 return result <= 0
794
795 def has_type(self, rtype):
796 """
797 Compares router type with `rtype`. Returns `True` if the type matches,
798 otherwise `false`.
799 """
800 curtype = self.version_info()['type']
801 return rtype == curtype
802
803 def has_mpls(self):
804 nrouter = self.tgen.net[self.name]
805 return nrouter.hasmpls
806
807 class TopoSwitch(TopoGear):
808 """
809 Switch abstraction. Has the following properties:
810 * cls: switch class that will be used to instantiate
811 * name: switch name
812 """
813 # pylint: disable=too-few-public-methods
814
815 def __init__(self, tgen, cls, name):
816 super(TopoSwitch, self).__init__()
817 self.tgen = tgen
818 self.net = None
819 self.name = name
820 self.cls = cls
821 self.tgen.topo.addSwitch(name, cls=self.cls)
822
823 def __str__(self):
824 gear = super(TopoSwitch, self).__str__()
825 gear += ' TopoSwitch<>'
826 return gear
827
828 class TopoHost(TopoGear):
829 "Host abstraction."
830 # pylint: disable=too-few-public-methods
831
832 def __init__(self, tgen, name, **params):
833 """
834 Mininet has the following known `params` for hosts:
835 * `ip`: the IP address (string) for the host interface
836 * `defaultRoute`: the default route that will be installed
837 (e.g. 'via 10.0.0.1')
838 * `privateDirs`: directories that will be mounted on a different domain
839 (e.g. '/etc/important_dir').
840 """
841 super(TopoHost, self).__init__()
842 self.tgen = tgen
843 self.net = None
844 self.name = name
845 self.options = params
846 self.tgen.topo.addHost(name, **params)
847
848 def __str__(self):
849 gear = super(TopoHost, self).__str__()
850 gear += ' TopoHost<ip="{}",defaultRoute="{}",privateDirs="{}">'.format(
851 self.options['ip'], self.options['defaultRoute'],
852 str(self.options['privateDirs']))
853 return gear
854
855 class TopoExaBGP(TopoHost):
856 "ExaBGP peer abstraction."
857 # pylint: disable=too-few-public-methods
858
859 PRIVATE_DIRS = [
860 '/etc/exabgp',
861 '/var/run/exabgp',
862 '/var/log',
863 ]
864
865 def __init__(self, tgen, name, **params):
866 """
867 ExaBGP usually uses the following parameters:
868 * `ip`: the IP address (string) for the host interface
869 * `defaultRoute`: the default route that will be installed
870 (e.g. 'via 10.0.0.1')
871
872 Note: the different between a host and a ExaBGP peer is that this class
873 has a privateDirs already defined and contains functions to handle ExaBGP
874 things.
875 """
876 params['privateDirs'] = self.PRIVATE_DIRS
877 super(TopoExaBGP, self).__init__(tgen, name, **params)
878 self.tgen.topo.addHost(name, **params)
879
880 def __str__(self):
881 gear = super(TopoExaBGP, self).__str__()
882 gear += ' TopoExaBGP<>'.format()
883 return gear
884
885 def start(self, peer_dir, env_file=None):
886 """
887 Start running ExaBGP daemon:
888 * Copy all peer* folder contents into /etc/exabgp
889 * Copy exabgp env file if specified
890 * Make all python files runnable
891 * Run ExaBGP with env file `env_file` and configuration peer*/exabgp.cfg
892 """
893 self.run('mkdir /etc/exabgp')
894 self.run('chmod 755 /etc/exabgp')
895 self.run('cp {}/* /etc/exabgp/'.format(peer_dir))
896 if env_file is not None:
897 self.run('cp {} /etc/exabgp/exabgp.env'.format(env_file))
898 self.run('chmod 644 /etc/exabgp/*')
899 self.run('chmod a+x /etc/exabgp/*.py')
900 self.run('chown -R exabgp:exabgp /etc/exabgp')
901 output = self.run('exabgp -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg')
902 if output == None or len(output) == 0:
903 output = '<none>'
904 logger.info('{} exabgp started, output={}'.format(self.name, output))
905
906 def stop(self, wait=True):
907 "Stop ExaBGP peer and kill the daemon"
908 self.run('kill `cat /var/run/exabgp/exabgp.pid`')
909
910
911 #
912 # Diagnostic function
913 #
914
915 # Disable linter branch warning. It is expected to have these here.
916 # pylint: disable=R0912
917 def diagnose_env():
918 """
919 Run diagnostics in the running environment. Returns `True` when everything
920 is ok, otherwise `False`.
921 """
922 ret = True
923
924 # Test log path exists before installing handler.
925 if not os.path.isdir('/tmp'):
926 logger.warning('could not find /tmp for logs')
927 else:
928 os.system('mkdir /tmp/topotests')
929 # Log diagnostics to file so it can be examined later.
930 fhandler = logging.FileHandler(filename='/tmp/topotests/diagnostics.txt')
931 fhandler.setLevel(logging.DEBUG)
932 fhandler.setFormatter(
933 logging.Formatter(fmt='%(asctime)s %(levelname)s: %(message)s')
934 )
935 logger.addHandler(fhandler)
936
937 logger.info('Running environment diagnostics')
938
939 # Load configuration
940 config = ConfigParser.ConfigParser(tgen_defaults)
941 pytestini_path = os.path.join(CWD, '../pytest.ini')
942 config.read(pytestini_path)
943
944 # Assert that we are running as root
945 if os.getuid() != 0:
946 logger.error('you must run topotest as root')
947 ret = False
948
949 # Assert that we have mininet
950 if os.system('which mn >/dev/null 2>/dev/null') != 0:
951 logger.error('could not find mininet binary (mininet is not installed)')
952 ret = False
953
954 # Assert that we have iproute installed
955 if os.system('which ip >/dev/null 2>/dev/null') != 0:
956 logger.error('could not find ip binary (iproute is not installed)')
957 ret = False
958
959 # Assert that we have gdb installed
960 if os.system('which gdb >/dev/null 2>/dev/null') != 0:
961 logger.error('could not find gdb binary (gdb is not installed)')
962 ret = False
963
964 # Assert that FRR utilities exist
965 frrdir = config.get('topogen', 'frrdir')
966 hasfrr = False
967 if not os.path.isdir(frrdir):
968 logger.error('could not find {} directory'.format(frrdir))
969 ret = False
970 else:
971 hasfrr = True
972 try:
973 pwd.getpwnam('frr')[2]
974 except KeyError:
975 logger.warning('could not find "frr" user')
976
977 try:
978 grp.getgrnam('frr')[2]
979 except KeyError:
980 logger.warning('could not find "frr" group')
981
982 try:
983 if 'frr' not in grp.getgrnam('frrvty').gr_mem:
984 logger.error('"frr" user and group exist, but user is not under "frrvty"')
985 except KeyError:
986 logger.warning('could not find "frrvty" group')
987
988 for fname in ['zebra', 'ospfd', 'ospf6d', 'bgpd', 'ripd', 'ripngd',
989 'isisd', 'pimd', 'ldpd']:
990 path = os.path.join(frrdir, fname)
991 if not os.path.isfile(path):
992 # LDPd is an exception
993 if fname == 'ldpd':
994 logger.info('could not find {} in {}'.format(fname, frrdir) +
995 '(LDPd tests will not run)')
996 continue
997
998 logger.warning('could not find {} in {}'.format(fname, frrdir))
999 ret = False
1000 else:
1001 if fname != 'zebra':
1002 continue
1003
1004 os.system(
1005 '{} -v 2>&1 >/tmp/topotests/frr_zebra.txt'.format(path)
1006 )
1007
1008 # Assert that Quagga utilities exist
1009 quaggadir = config.get('topogen', 'quaggadir')
1010 if hasfrr:
1011 # if we have frr, don't check for quagga
1012 pass
1013 elif not os.path.isdir(quaggadir):
1014 logger.info('could not find {} directory (quagga tests will not run)'.format(quaggadir))
1015 else:
1016 ret = True
1017 try:
1018 pwd.getpwnam('quagga')[2]
1019 except KeyError:
1020 logger.info('could not find "quagga" user')
1021
1022 try:
1023 grp.getgrnam('quagga')[2]
1024 except KeyError:
1025 logger.info('could not find "quagga" group')
1026
1027 try:
1028 if 'quagga' not in grp.getgrnam('quaggavty').gr_mem:
1029 logger.error('"quagga" user and group exist, but user is not under "quaggavty"')
1030 except KeyError:
1031 logger.warning('could not find "quaggavty" group')
1032
1033 for fname in ['zebra', 'ospfd', 'ospf6d', 'bgpd', 'ripd', 'ripngd',
1034 'isisd', 'pimd']:
1035 path = os.path.join(quaggadir, fname)
1036 if not os.path.isfile(path):
1037 logger.warning('could not find {} in {}'.format(fname, quaggadir))
1038 ret = False
1039 else:
1040 if fname != 'zebra':
1041 continue
1042
1043 os.system(
1044 '{} -v 2>&1 >/tmp/topotests/quagga_zebra.txt'.format(path)
1045 )
1046
1047 # Test MPLS availability
1048 krel = platform.release()
1049 if topotest.version_cmp(krel, '4.5') < 0:
1050 logger.info('LDPd tests will not run (have kernel "{}", but it requires 4.5)'.format(krel))
1051
1052 # Test for MPLS Kernel modules available
1053 if os.system('/sbin/modprobe -n mpls-router') != 0:
1054 logger.info('LDPd tests will not run (missing mpls-router kernel module)')
1055 if os.system('/sbin/modprobe -n mpls-iptunnel') != 0:
1056 logger.info('LDPd tests will not run (missing mpls-iptunnel kernel module)')
1057
1058 # TODO remove me when we start supporting exabgp >= 4
1059 try:
1060 output = subprocess.check_output(['exabgp', '-v'])
1061 line = output.split('\n')[0]
1062 version = line.split(' ')[2]
1063 if topotest.version_cmp(version, '4') >= 0:
1064 logger.warning('BGP topologies are still using exabgp version 3, expect failures')
1065
1066 # We want to catch all exceptions
1067 # pylint: disable=W0702
1068 except:
1069 logger.warning('failed to find exabgp or returned error')
1070
1071 # After we logged the output to file, remove the handler.
1072 logger.removeHandler(fhandler)
1073
1074 return ret