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