]> git.proxmox.com Git - mirror_frr.git/blob - tests/topotests/lib/topogen.py
tests: cleanup infra
[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()
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 is 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("setting error msg: %s", 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_MGMTD = 20
717 RD = {
718 RD_FRR: "frr",
719 RD_ZEBRA: "zebra",
720 RD_RIP: "ripd",
721 RD_RIPNG: "ripngd",
722 RD_OSPF: "ospfd",
723 RD_OSPF6: "ospf6d",
724 RD_ISIS: "isisd",
725 RD_BGP: "bgpd",
726 RD_PIM: "pimd",
727 RD_PIM6: "pim6d",
728 RD_LDP: "ldpd",
729 RD_EIGRP: "eigrpd",
730 RD_NHRP: "nhrpd",
731 RD_STATIC: "staticd",
732 RD_BFD: "bfdd",
733 RD_SHARP: "sharpd",
734 RD_BABEL: "babeld",
735 RD_PBRD: "pbrd",
736 RD_PATH: "pathd",
737 RD_SNMP: "snmpd",
738 RD_MGMTD: "mgmtd",
739 }
740
741 def __init__(self, tgen, cls, name, **params):
742 """
743 The constructor has the following parameters:
744 * tgen: Topogen object
745 * cls: router class that will be used to instantiate
746 * name: router name
747 * daemondir: daemon binary directory
748 * routertype: 'frr'
749 """
750 super(TopoRouter, self).__init__(tgen, name, **params)
751 self.routertype = params.get("routertype", "frr")
752 if "privateDirs" not in params:
753 params["privateDirs"] = self.PRIVATE_DIRS
754
755 # Propagate the router log directory
756 logfile = self._setup_tmpdir()
757 params["logdir"] = self.logdir
758
759 self.logger = topolog.get_logger(name, log_level="debug", target=logfile)
760 params["logger"] = self.logger
761 tgen.net.add_host(self.name, cls=cls, **params)
762 topotest.fix_netns_limits(tgen.net[name])
763
764 # Mount gear log directory on a common path
765 self.net.bind_mount(self.gearlogdir, "/tmp/gearlogdir")
766
767 # Ensure pid file
768 with open(os.path.join(self.logdir, self.name + ".pid"), "w") as f:
769 f.write(str(self.net.pid) + "\n")
770
771 def __str__(self):
772 gear = super(TopoRouter, self).__str__()
773 gear += " TopoRouter<>"
774 return gear
775
776 def check_capability(self, daemon, param):
777 """
778 Checks a capability daemon against an argument option
779 Return True if capability available. False otherwise
780 """
781 daemonstr = self.RD.get(daemon)
782 self.logger.info('check capability {} for "{}"'.format(param, daemonstr))
783 return self.net.checkCapability(daemonstr, param)
784
785 def load_frr_config(self, source, daemons=None):
786 """
787 Loads the unified configuration file source
788 Start the daemons in the list
789 If daemons is None, try to infer daemons from the config file
790 """
791 self.load_config(self.RD_FRR, source)
792 if not daemons:
793 # Always add zebra
794 self.load_config(self.RD_ZEBRA)
795 for daemon in self.RD:
796 # This will not work for all daemons
797 daemonstr = self.RD.get(daemon).rstrip("d")
798 if daemonstr == "pim":
799 grep_cmd = "grep 'ip {}' {}".format(daemonstr, source)
800 else:
801 grep_cmd = "grep 'router {}' {}".format(daemonstr, source)
802 result = self.run(grep_cmd).strip()
803 if result:
804 self.load_config(daemon)
805 else:
806 for daemon in daemons:
807 self.load_config(daemon)
808
809 def load_config(self, daemon, source=None, param=None):
810 """Loads daemon configuration from the specified source
811 Possible daemon values are: TopoRouter.RD_ZEBRA, TopoRouter.RD_RIP,
812 TopoRouter.RD_RIPNG, TopoRouter.RD_OSPF, TopoRouter.RD_OSPF6,
813 TopoRouter.RD_ISIS, TopoRouter.RD_BGP, TopoRouter.RD_LDP,
814 TopoRouter.RD_PIM, TopoRouter.RD_PIM6, TopoRouter.RD_PBR,
815 TopoRouter.RD_SNMP, TopoRouter.RD_MGMTD.
816
817 Possible `source` values are `None` for an empty config file, a path name which is
818 used directly, or a file name with no path components which is first looked for
819 directly and then looked for under a sub-directory named after router.
820
821 This API unfortunately allows for source to not exist for any and
822 all routers.
823 """
824 daemonstr = self.RD.get(daemon)
825 self.logger.debug('loading "{}" configuration: {}'.format(daemonstr, source))
826 self.net.loadConf(daemonstr, source, param)
827
828 def check_router_running(self):
829 """
830 Run a series of checks and returns a status string.
831 """
832 self.logger.info("checking if daemons are running")
833 return self.net.checkRouterRunning()
834
835 def start(self):
836 """
837 Start router:
838 * Load modules
839 * Clean up files
840 * Configure interfaces
841 * Start daemons (e.g. FRR)
842 * Configure daemon logging files
843 """
844
845 nrouter = self.net
846 result = nrouter.startRouter(self.tgen)
847
848 # Enable command logging
849
850 # Enable all daemon command logging, logging files
851 # and set them to the start dir.
852 for daemon, enabled in nrouter.daemons.items():
853 if enabled and daemon != "snmpd":
854 self.vtysh_cmd(
855 "\n".join(
856 [
857 "clear log cmdline-targets",
858 "conf t",
859 "log file {}.log debug".format(daemon),
860 "log commands",
861 "log timestamp precision 3",
862 ]
863 ),
864 daemon=daemon,
865 )
866
867 if result != "":
868 self.tgen.set_error(result)
869 elif nrouter.daemons["ldpd"] == 1 or nrouter.daemons["pathd"] == 1:
870 # Enable MPLS processing on all interfaces.
871 for interface in self.links:
872 topotest.sysctl_assure(
873 nrouter, "net.mpls.conf.{}.input".format(interface), 1
874 )
875
876 return result
877
878 def stop(self):
879 """
880 Stop router cleanly:
881 * Signal daemons twice, once with SIGTERM, then with SIGKILL.
882 """
883 self.logger.debug("stopping (no assert)")
884 return self.net.stopRouter(False)
885
886 def startDaemons(self, daemons):
887 """
888 Start Daemons: to start specific daemon(user defined daemon only)
889 * Start daemons (e.g. FRR)
890 * Configure daemon logging files
891 """
892 self.logger.debug("starting")
893 nrouter = self.net
894 result = nrouter.startRouterDaemons(daemons)
895
896 if daemons is None:
897 daemons = nrouter.daemons.keys()
898
899 # Enable all daemon command logging, logging files
900 # and set them to the start dir.
901 for daemon in daemons:
902 enabled = nrouter.daemons[daemon]
903 if enabled and daemon != "snmpd":
904 self.vtysh_cmd(
905 "\n".join(
906 [
907 "clear log cmdline-targets",
908 "conf t",
909 "log file {}.log debug".format(daemon),
910 "log commands",
911 "log timestamp precision 3",
912 ]
913 ),
914 daemon=daemon,
915 )
916
917 if result != "":
918 self.tgen.set_error(result)
919
920 return result
921
922 def killDaemons(self, daemons, wait=True, assertOnError=True):
923 """
924 Kill specific daemon(user defined daemon only)
925 forcefully using SIGKILL
926 """
927 self.logger.debug("Killing daemons using SIGKILL..")
928 return self.net.killRouterDaemons(daemons, wait, assertOnError)
929
930 def vtysh_cmd(self, command, isjson=False, daemon=None):
931 """
932 Runs the provided command string in the vty shell and returns a string
933 with the response.
934
935 This function also accepts multiple commands, but this mode does not
936 return output for each command. See vtysh_multicmd() for more details.
937 """
938 # Detect multi line commands
939 if command.find("\n") != -1:
940 return self.vtysh_multicmd(command, daemon=daemon)
941
942 dparam = ""
943 if daemon is not None:
944 dparam += "-d {}".format(daemon)
945
946 vtysh_command = 'vtysh {} -c "{}" 2>/dev/null'.format(dparam, command)
947
948 self.logger.debug('vtysh command => "{}"'.format(command))
949 output = self.run(vtysh_command)
950
951 dbgout = output.strip()
952 if dbgout:
953 if "\n" in dbgout:
954 dbgout = dbgout.replace("\n", "\n\t")
955 self.logger.debug("vtysh result:\n\t{}".format(dbgout))
956 else:
957 self.logger.debug('vtysh result: "{}"'.format(dbgout))
958
959 if isjson is False:
960 return output
961
962 try:
963 return json.loads(output)
964 except ValueError as error:
965 logger.warning(
966 "vtysh_cmd: %s: failed to convert json output: %s: %s",
967 self.name,
968 str(output),
969 str(error),
970 )
971 return {}
972
973 def vtysh_multicmd(self, commands, pretty_output=True, daemon=None):
974 """
975 Runs the provided commands in the vty shell and return the result of
976 execution.
977
978 pretty_output: defines how the return value will be presented. When
979 True it will show the command as they were executed in the vty shell,
980 otherwise it will only show lines that failed.
981 """
982 # Prepare the temporary file that will hold the commands
983 fname = topotest.get_file(commands)
984
985 dparam = ""
986 if daemon is not None:
987 dparam += "-d {}".format(daemon)
988
989 # Run the commands and delete the temporary file
990 if pretty_output:
991 vtysh_command = "vtysh {} < {}".format(dparam, fname)
992 else:
993 vtysh_command = "vtysh {} -f {}".format(dparam, fname)
994
995 dbgcmds = commands if is_string(commands) else "\n".join(commands)
996 dbgcmds = "\t" + dbgcmds.replace("\n", "\n\t")
997 self.logger.debug("vtysh command => FILE:\n{}".format(dbgcmds))
998
999 res = self.run(vtysh_command)
1000 os.unlink(fname)
1001
1002 dbgres = res.strip()
1003 if dbgres:
1004 if "\n" in dbgres:
1005 dbgres = dbgres.replace("\n", "\n\t")
1006 self.logger.debug("vtysh result:\n\t{}".format(dbgres))
1007 else:
1008 self.logger.debug('vtysh result: "{}"'.format(dbgres))
1009 return res
1010
1011 def report_memory_leaks(self, testname):
1012 """
1013 Runs the router memory leak check test. Has the following parameter:
1014 testname: the test file name for identification
1015
1016 NOTE: to run this you must have the environment variable
1017 TOPOTESTS_CHECK_MEMLEAK set or memleak_path configured in `pytest.ini`.
1018 """
1019 memleak_file = (
1020 os.environ.get("TOPOTESTS_CHECK_MEMLEAK") or self.params["memleak_path"]
1021 )
1022 if memleak_file == "" or memleak_file is None:
1023 return
1024
1025 self.stop()
1026
1027 self.logger.info("running memory leak report")
1028 self.net.report_memory_leaks(memleak_file, testname)
1029
1030 def version_info(self):
1031 "Get equipment information from 'show version'."
1032 output = self.vtysh_cmd("show version").split("\n")[0]
1033 columns = topotest.normalize_text(output).split(" ")
1034 try:
1035 return {
1036 "type": columns[0],
1037 "version": columns[1],
1038 }
1039 except IndexError:
1040 return {
1041 "type": None,
1042 "version": None,
1043 }
1044
1045 def has_version(self, cmpop, version):
1046 """
1047 Compares router version using operation `cmpop` with `version`.
1048 Valid `cmpop` values:
1049 * `>=`: has the same version or greater
1050 * '>': has greater version
1051 * '=': has the same version
1052 * '<': has a lesser version
1053 * '<=': has the same version or lesser
1054
1055 Usage example: router.has_version('>', '1.0')
1056 """
1057 return self.net.checkRouterVersion(cmpop, version)
1058
1059 def has_type(self, rtype):
1060 """
1061 Compares router type with `rtype`. Returns `True` if the type matches,
1062 otherwise `false`.
1063 """
1064 curtype = self.version_info()["type"]
1065 return rtype == curtype
1066
1067 def has_mpls(self):
1068 return self.net.hasmpls
1069
1070
1071 class TopoSwitch(TopoGear):
1072 """
1073 Switch abstraction. Has the following properties:
1074 * cls: switch class that will be used to instantiate
1075 * name: switch name
1076 """
1077
1078 # pylint: disable=too-few-public-methods
1079
1080 def __init__(self, tgen, name, **params):
1081 super(TopoSwitch, self).__init__(tgen, name, **params)
1082 tgen.net.add_switch(name)
1083
1084 def __str__(self):
1085 gear = super(TopoSwitch, self).__str__()
1086 gear += " TopoSwitch<>"
1087 return gear
1088
1089
1090 class TopoHost(TopoGear):
1091 "Host abstraction."
1092 # pylint: disable=too-few-public-methods
1093
1094 def __init__(self, tgen, name, **params):
1095 """
1096 Mininet has the following known `params` for hosts:
1097 * `ip`: the IP address (string) for the host interface
1098 * `defaultRoute`: the default route that will be installed
1099 (e.g. 'via 10.0.0.1')
1100 * `privateDirs`: directories that will be mounted on a different domain
1101 (e.g. '/etc/important_dir').
1102 """
1103 super(TopoHost, self).__init__(tgen, name, **params)
1104
1105 # Propagate the router log directory
1106 logfile = self._setup_tmpdir()
1107 params["logdir"] = self.logdir
1108
1109 # Odd to have 2 logfiles for each host
1110 self.logger = topolog.get_logger(name, log_level="debug", target=logfile)
1111 params["logger"] = self.logger
1112 tgen.net.add_host(name, **params)
1113 topotest.fix_netns_limits(tgen.net[name])
1114
1115 # Mount gear log directory on a common path
1116 self.net.bind_mount(self.gearlogdir, "/tmp/gearlogdir")
1117
1118 def __str__(self):
1119 gear = super(TopoHost, self).__str__()
1120 gear += ' TopoHost<ip="{}",defaultRoute="{}",privateDirs="{}">'.format(
1121 self.params["ip"],
1122 self.params["defaultRoute"],
1123 str(self.params["privateDirs"]),
1124 )
1125 return gear
1126
1127
1128 class TopoExaBGP(TopoHost):
1129 "ExaBGP peer abstraction."
1130 # pylint: disable=too-few-public-methods
1131
1132 PRIVATE_DIRS = [
1133 "/etc/exabgp",
1134 "/var/run/exabgp",
1135 "/var/log",
1136 ]
1137
1138 def __init__(self, tgen, name, **params):
1139 """
1140 ExaBGP usually uses the following parameters:
1141 * `ip`: the IP address (string) for the host interface
1142 * `defaultRoute`: the default route that will be installed
1143 (e.g. 'via 10.0.0.1')
1144
1145 Note: the different between a host and a ExaBGP peer is that this class
1146 has a privateDirs already defined and contains functions to handle ExaBGP
1147 things.
1148 """
1149 params["privateDirs"] = self.PRIVATE_DIRS
1150 super(TopoExaBGP, self).__init__(tgen, name, **params)
1151
1152 def __str__(self):
1153 gear = super(TopoExaBGP, self).__str__()
1154 gear += " TopoExaBGP<>".format()
1155 return gear
1156
1157 def start(self, peer_dir, env_file=None):
1158 """
1159 Start running ExaBGP daemon:
1160 * Copy all peer* folder contents into /etc/exabgp
1161 * Copy exabgp env file if specified
1162 * Make all python files runnable
1163 * Run ExaBGP with env file `env_file` and configuration peer*/exabgp.cfg
1164 """
1165 exacmd = self.tgen.get_exabgp_cmd()
1166 assert exacmd, "Can't find a usabel ExaBGP (must be < version 4)"
1167
1168 self.run("mkdir -p /etc/exabgp")
1169 self.run("chmod 755 /etc/exabgp")
1170 self.run("cp {}/exa-* /etc/exabgp/".format(CWD))
1171 self.run("cp {}/* /etc/exabgp/".format(peer_dir))
1172 if env_file is not None:
1173 self.run("cp {} /etc/exabgp/exabgp.env".format(env_file))
1174 self.run("chmod 644 /etc/exabgp/*")
1175 self.run("chmod a+x /etc/exabgp/*.py")
1176 self.run("chown -R exabgp:exabgp /etc/exabgp")
1177
1178 output = self.run(exacmd + " -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg")
1179 if output is None or len(output) == 0:
1180 output = "<none>"
1181
1182 logger.info("{} exabgp started, output={}".format(self.name, output))
1183
1184 def stop(self, wait=True, assertOnError=True):
1185 "Stop ExaBGP peer and kill the daemon"
1186 self.run("kill `cat /var/run/exabgp/exabgp.pid`")
1187 return ""
1188
1189
1190 #
1191 # Diagnostic function
1192 #
1193
1194 # Disable linter branch warning. It is expected to have these here.
1195 # pylint: disable=R0912
1196 def diagnose_env_linux(rundir):
1197 """
1198 Run diagnostics in the running environment. Returns `True` when everything
1199 is ok, otherwise `False`.
1200 """
1201 ret = True
1202
1203 # Load configuration
1204 config = configparser.ConfigParser(defaults=tgen_defaults)
1205 pytestini_path = os.path.join(CWD, "../pytest.ini")
1206 config.read(pytestini_path)
1207
1208 # Test log path exists before installing handler.
1209 os.system("mkdir -p " + rundir)
1210 # Log diagnostics to file so it can be examined later.
1211 fhandler = logging.FileHandler(filename="{}/diagnostics.txt".format(rundir))
1212 fhandler.setLevel(logging.DEBUG)
1213 fhandler.setFormatter(logging.Formatter(fmt=topolog.FORMAT))
1214 logger.addHandler(fhandler)
1215
1216 logger.info("Running environment diagnostics")
1217
1218 # Assert that we are running as root
1219 if os.getuid() != 0:
1220 logger.error("you must run topotest as root")
1221 ret = False
1222
1223 # Assert that we have mininet
1224 # if os.system("which mn >/dev/null 2>/dev/null") != 0:
1225 # logger.error("could not find mininet binary (mininet is not installed)")
1226 # ret = False
1227
1228 # Assert that we have iproute installed
1229 if os.system("which ip >/dev/null 2>/dev/null") != 0:
1230 logger.error("could not find ip binary (iproute is not installed)")
1231 ret = False
1232
1233 # Assert that we have gdb installed
1234 if os.system("which gdb >/dev/null 2>/dev/null") != 0:
1235 logger.error("could not find gdb binary (gdb is not installed)")
1236 ret = False
1237
1238 # Assert that FRR utilities exist
1239 frrdir = config.get("topogen", "frrdir")
1240 if not os.path.isdir(frrdir):
1241 logger.error("could not find {} directory".format(frrdir))
1242 ret = False
1243 else:
1244 try:
1245 pwd.getpwnam("frr")[2]
1246 except KeyError:
1247 logger.warning('could not find "frr" user')
1248
1249 try:
1250 grp.getgrnam("frr")[2]
1251 except KeyError:
1252 logger.warning('could not find "frr" group')
1253
1254 try:
1255 if "frr" not in grp.getgrnam("frrvty").gr_mem:
1256 logger.error(
1257 '"frr" user and group exist, but user is not under "frrvty"'
1258 )
1259 except KeyError:
1260 logger.warning('could not find "frrvty" group')
1261
1262 for fname in [
1263 "zebra",
1264 "ospfd",
1265 "ospf6d",
1266 "bgpd",
1267 "ripd",
1268 "ripngd",
1269 "isisd",
1270 "pimd",
1271 "pim6d",
1272 "ldpd",
1273 "pbrd",
1274 "mgmtd",
1275 ]:
1276 path = os.path.join(frrdir, fname)
1277 if not os.path.isfile(path):
1278 # LDPd is an exception
1279 if fname == "ldpd":
1280 logger.info(
1281 "could not find {} in {}".format(fname, frrdir)
1282 + "(LDPd tests will not run)"
1283 )
1284 continue
1285
1286 logger.error("could not find {} in {}".format(fname, frrdir))
1287 ret = False
1288 else:
1289 if fname != "zebra" or fname != "mgmtd":
1290 continue
1291
1292 os.system("{} -v 2>&1 >{}/frr_mgmtd.txt".format(path, rundir))
1293 os.system("{} -v 2>&1 >{}/frr_zebra.txt".format(path, rundir))
1294
1295 # Test MPLS availability
1296 krel = platform.release()
1297 if topotest.version_cmp(krel, "4.5") < 0:
1298 logger.info(
1299 'LDPd tests will not run (have kernel "{}", but it requires 4.5)'.format(
1300 krel
1301 )
1302 )
1303
1304 # Test for MPLS Kernel modules available
1305 if not topotest.module_present("mpls-router", load=False) != 0:
1306 logger.info("LDPd tests will not run (missing mpls-router kernel module)")
1307 if not topotest.module_present("mpls-iptunnel", load=False) != 0:
1308 logger.info("LDPd tests will not run (missing mpls-iptunnel kernel module)")
1309
1310 if not get_exabgp_cmd():
1311 logger.warning("Failed to find exabgp < 4")
1312
1313 logger.removeHandler(fhandler)
1314 fhandler.close()
1315
1316 return ret
1317
1318
1319 def diagnose_env_freebsd():
1320 return True
1321
1322
1323 def diagnose_env(rundir):
1324 if sys.platform.startswith("linux"):
1325 return diagnose_env_linux(rundir)
1326 elif sys.platform.startswith("freebsd"):
1327 return diagnose_env_freebsd()
1328
1329 return False