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