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