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