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