]>
git.proxmox.com Git - mirror_frr.git/blob - tests/topotests/lib/topogen.py
1 # SPDX-License-Identifier: ISC
4 # Library of helper functions for NetDEF Topology Tests
6 # Copyright (c) 2017 by
7 # Network Device Education Foundation, Inc. ("NetDEF")
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
15 Basic usage instructions:
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()
40 from collections
import OrderedDict
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
48 from lib
import topotest
50 CWD
= os
.path
.dirname(os
.path
.realpath(__file__
))
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.
58 def get_topogen(topo
=None):
60 Helper function to retrieve Topogen. Must be called with `topo` when called
61 inside the build() method of Topology class.
64 global_tgen
.topo
= topo
68 def set_topogen(tgen
):
69 "Helper function to set Topogen"
70 # pylint: disable=W0603
76 """Return True if value is a string."""
78 return isinstance(value
, basestring
) # type: ignore
80 return isinstance(value
, str)
83 def get_exabgp_cmd(commander
=None):
84 """Return the command to use for ExaBGP version < 4."""
87 commander
= Commander("topogen")
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
)
96 if topotest
.version_cmp(version
, "4") >= 0:
97 logging
.debug("found exabgp version >= 4 in %s will keep looking", exacmd
)
99 logger
.info("Using ExaBGP version %s in %s", version
, exacmd
)
102 exacmd
= commander
.get_exec_path("exabgp")
103 if exacmd
and exacmd_version_ok(exacmd
):
105 py2_path
= commander
.get_exec_path("python2")
107 exacmd
= py2_path
+ " -m exabgp"
108 if exacmd_version_ok(exacmd
):
110 py2_path
= commander
.get_exec_path("python")
112 exacmd
= py2_path
+ " -m exabgp"
113 if exacmd_version_ok(exacmd
):
119 # Main class: topology builder
122 # Topogen configuration defaults
125 "frrdir": "/usr/lib/frr",
131 class Topogen(object):
132 "A topology test builder helper."
134 CONFIG_SECTION
= "topogen"
136 def __init__(self
, topodef
, modname
="unnamed"):
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.
149 self
.modname
= modname
154 self
.exabgp_cmd
= None
155 self
._init
_topo
(topodef
)
157 logger
.info("loading topology: {}".format(self
.modname
))
160 # def _mininet_reset():
161 # "Reset the mininet environment"
162 # # Clean up the mininet environment
163 # os.system("mn -c > /dev/null 2>&1")
168 def _init_topo(self
, topodef
):
170 Initialize the topogily provided by the user. The user topology class
171 must call get_topogen() during build() to get the topogen object.
173 # Set the global variable so the test cases can access it anywhere
176 # Increase host based limits
177 topotest
.fix_host_limits()
179 # Test for MPLS Kernel modules available
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)")
188 # Load the default topology configurations
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
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)
203 # Allow anyone, but set the sticky bit to avoid file deletions
204 os
.chmod(self
.logdir
, 0o1777)
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
)
212 self
.net
= Mininet(rundir
=self
.logdir
, pytestconfig
=topotest
.g_pytest_config
)
214 # Adjust the parent namespace
215 topotest
.fix_netns_limits(self
.net
)
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
221 if not inspect
.isclass(topodef
):
222 if callable(topodef
):
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
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()
236 self
.add_topology_from_dict(topodef
)
238 def add_topology_from_dict(self
, topodef
):
241 if isinstance(topodef
, OrderedDict
)
242 else sorted(topodef
.keys())
244 # ---------------------------
245 # Create all referenced hosts
246 # ---------------------------
247 for oname
in keylist
:
248 tup
= (topodef
[oname
],) if is_string(topodef
[oname
]) else topodef
[oname
]
252 if name
not in self
.gears
:
253 logging
.debug("Adding router: %s", name
)
254 self
.add_router(name
)
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
)
267 for oname
in keylist
:
270 tup
= (topodef
[oname
],) if is_string(topodef
[oname
]) else topodef
[oname
]
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
)
278 self
.net
.configure_hosts()
280 def _load_config(self
):
282 Loads the configuration file `pytest.ini` located at the root dir of
285 self
.config
= configparser
.ConfigParser(tgen_defaults
)
286 pytestini_path
= os
.path
.join(CWD
, "../pytest.ini")
287 self
.config
.read(pytestini_path
)
289 def add_router(self
, name
=None, cls
=None, **params
):
291 Adds a new router to the topology. This function has the following
293 * `name`: (optional) select the router name
294 * `daemondir`: (optional) custom daemon binary directory
295 * `routertype`: (optional) `frr`
296 Returns a TopoRouter.
299 cls
= topotest
.Router
301 name
= "r{}".format(self
.routern
)
302 if name
in self
.gears
:
303 raise KeyError("router already exists")
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")
310 self
.gears
[name
] = TopoRouter(self
, cls
, name
, **params
)
312 return self
.gears
[name
]
314 def add_switch(self
, name
=None):
316 Adds a new switch to the topology. This function has the following
318 name: (optional) select the switch name
319 Returns the switch name and number.
322 name
= "s{}".format(self
.switchn
)
323 if name
in self
.gears
:
324 raise KeyError("switch already exists")
326 self
.gears
[name
] = TopoSwitch(self
, name
)
328 return self
.gears
[name
]
330 def add_exabgp_peer(self
, name
, ip
, defaultRoute
):
332 Adds a new ExaBGP peer to the topology. This function has the following
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')
338 name
= "peer{}".format(self
.peern
)
339 if name
in self
.gears
:
340 raise KeyError("exabgp peer already exists")
342 self
.gears
[name
] = TopoExaBGP(self
, name
, ip
=ip
, defaultRoute
=defaultRoute
)
344 return self
.gears
[name
]
346 def add_host(self
, name
, ip
, defaultRoute
):
348 Adds a new host to the topology. This function has the following
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')
354 name
= "host{}".format(self
.peern
)
355 if name
in self
.gears
:
356 raise KeyError("host already exists")
358 self
.gears
[name
] = TopoHost(self
, name
, ip
=ip
, defaultRoute
=defaultRoute
)
360 return self
.gears
[name
]
362 def add_link(self
, node1
, node2
, ifname1
=None, ifname2
=None):
364 Creates a connection between node1 and node2. The nodes can be the
370 if not isinstance(node1
, TopoGear
):
371 raise ValueError("invalid node1 type")
372 if not isinstance(node2
, TopoGear
):
373 raise ValueError("invalid node2 type")
376 ifname1
= node1
.new_link()
378 ifname2
= node2
.new_link()
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
)
384 def get_gears(self
, geartype
):
386 Returns a dictionary of all gears of type `geartype`.
389 * Dictionary iteration:
392 router_dict = tgen.get_gears(TopoRouter)
393 for router_name, router in router_dict.items():
399 peer_list = tgen.get_gears(TopoExaBGP).values()
400 for peer in peer_list:
406 for name
, gear
in self
.gears
.items()
407 if isinstance(gear
, geartype
)
412 Returns the router dictionary (key is the router name and value is the
413 router object itself).
415 return self
.get_gears(TopoRouter
)
417 def exabgp_peers(self
):
419 Returns the exabgp peer dictionary (key is the peer name and value is
420 the peer object itself).
422 return self
.get_gears(TopoExaBGP
)
424 def start_topology(self
):
425 """Starts the topology class."""
426 logger
.info("starting topology: {}".format(self
.modname
))
429 def start_router(self
, router
=None):
431 Call the router startRouter method.
432 If no router is specified it is called for all registered routers.
435 # pylint: disable=r1704
436 # XXX should be hosts?
437 for _
, router
in self
.routers().items():
440 if isinstance(router
, str):
441 router
= self
.gears
[router
]
445 def stop_topology(self
):
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.
453 pause
= bool(self
.net
.cfgopt
.get_option("--pause-at-end"))
454 pause
= pause
or bool(self
.net
.cfgopt
.get_option("--pause"))
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
)
463 logger
.info("stopping topology: {}".format(self
.modname
))
466 for gear
in self
.gears
.values():
467 errors
+= gear
.stop()
470 "Errors found post shutdown - details follow: {}".format(errors
)
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
482 Interrupt the test and call the command line interface for manual
483 inspection. Should be only used on non production code.
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():
495 memleak_file
= os
.environ
.get("TOPOTESTS_CHECK_MEMLEAK") or self
.config
.get(
496 self
.CONFIG_SECTION
, "memleak_path"
498 if memleak_file
== "" or memleak_file
is None:
502 def report_memory_leaks(self
, testname
=None):
503 "Run memory leak test and reports."
504 if not self
.is_memleak_enabled():
507 # If no name was specified, use the test module name
509 testname
= self
.modname
511 router_list
= self
.routers().values()
512 for router
in router_list
:
513 router
.report_memory_leaks(self
.modname
)
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
)
519 # If no code is defined use a sequential number
521 code
= len(self
.errorsd
)
523 self
.errorsd
[code
] = message
524 self
.errors
+= "\n{}: {}".format(code
, message
)
526 def has_errors(self
):
527 "Returns whether errors exist or not."
528 return len(self
.errorsd
) > 0
530 def routers_have_failure(self
):
531 "Runs an assertion to make sure that all routers are running."
532 if self
.has_errors():
536 router_list
= self
.routers().values()
537 for router
in router_list
:
538 result
= router
.check_router_running()
540 errors
+= result
+ "\n"
543 self
.set_error(errors
, "router_error")
550 # Topology gears (equipment)
554 class TopoGear(object):
555 "Abstract class for type checking"
557 def __init__(self
, tgen
, name
, **params
):
564 # Would be nice for this to point at the gears log directory rather than the
566 self
.logdir
= tgen
.logdir
567 self
.gearlogdir
= None
571 for myif
, dest
in self
.links
.items():
575 links
+= '"{}"<->"{}"'.format(myif
, destif
)
577 return 'TopoGear<name="{}",links=[{}]>'.format(self
.name
, links
)
581 return self
.tgen
.net
[self
.name
]
584 "Basic start function that just reports equipment start"
585 logger
.info('starting "{}"'.format(self
.name
))
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
))
592 def cmd(self
, command
, **kwargs
):
594 Runs the provided command string in the router and returns a string
597 return self
.net
.cmd_legacy(command
, **kwargs
)
599 def cmd_raises(self
, command
, **kwargs
):
601 Runs the provided command string in the router and returns a string
602 with the response. Raise an exception on any error.
604 return self
.net
.cmd_raises(command
, **kwargs
)
608 def popen(self
, *params
, **kwargs
):
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.
614 return self
.net
.popen(*params
, **kwargs
)
616 def add_link(self
, node
, myif
=None, nodeif
=None):
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.
623 self
.tgen
.add_link(self
, node
, myif
, nodeif
)
625 def link_enable(self
, myif
, enabled
=True, netns
=None):
627 Set this node interface administrative state.
628 myif: this node interface name
629 enabled: whether we should enable or disable the interface
631 if myif
not in self
.links
.keys():
632 raise KeyError("interface doesn't exists")
640 'setting node "{}" link "{}" to state "{}"'.format(
641 self
.name
, myif
, operation
645 if netns
is not None:
646 extract
= "ip netns exec {} ".format(netns
)
648 return self
.run("{}ip link set dev {} {}".format(extract
, myif
, operation
))
650 def peer_link_enable(self
, myif
, enabled
=True, netns
=None):
652 Set the peer interface administrative state.
653 myif: this node interface name
654 enabled: whether we should enable or disable the interface
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.
659 if myif
not in self
.links
.keys():
660 raise KeyError("interface doesn't exists")
662 node
, nodeif
= self
.links
[myif
]
663 node
.link_enable(nodeif
, enabled
, netns
)
667 Generates a new unique link name.
669 NOTE: This function should only be called by Topogen.
671 ifname
= "{}-eth{}".format(self
.name
, self
.linkn
)
675 def register_link(self
, myif
, node
, nodeif
):
677 Register link between this node interface and outside node.
679 NOTE: This function should only be called by Topogen.
681 if myif
in self
.links
.keys():
682 raise KeyError("interface already exists")
684 self
.links
[myif
] = (node
, nodeif
)
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
)
692 class TopoRouter(TopoGear
):
697 # The default required directories by FRR
705 # Router Daemon enumeration definition.
706 RD_FRR
= 0 # not a daemon, but use to setup unified configs
741 RD_STATIC
: "staticd",
751 def __init__(self
, tgen
, cls
, name
, **params
):
753 The constructor has the following parameters:
754 * tgen: Topogen object
755 * cls: router class that will be used to instantiate
757 * daemondir: daemon binary directory
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
765 # Propagate the router log directory
766 logfile
= self
._setup
_tmpdir
()
767 params
["logdir"] = self
.logdir
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
])
774 # Mount gear log directory on a common path
775 self
.net
.bind_mount(self
.gearlogdir
, "/tmp/gearlogdir")
778 with
open(os
.path
.join(self
.logdir
, self
.name
+ ".pid"), "w") as f
:
779 f
.write(str(self
.net
.pid
) + "\n")
782 gear
= super(TopoRouter
, self
).__str
__()
783 gear
+= " TopoRouter<>"
786 def check_capability(self
, daemon
, param
):
788 Checks a capability daemon against an argument option
789 Return True if capability available. False otherwise
791 daemonstr
= self
.RD
.get(daemon
)
792 self
.logger
.info('check capability {} for "{}"'.format(param
, daemonstr
))
793 return self
.net
.checkCapability(daemonstr
, param
)
795 def load_frr_config(self
, source
, daemons
=None):
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
801 self
.load_config(self
.RD_FRR
, source
)
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
)
811 grep_cmd
= "grep 'router {}' {}".format(daemonstr
, source
)
812 result
= self
.run(grep_cmd
, warn
=False).strip()
814 self
.load_config(daemon
)
816 for daemon
in daemons
:
817 self
.load_config(daemon
)
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.
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.
831 This API unfortunately allows for source to not exist for any and
834 daemonstr
= self
.RD
.get(daemon
)
835 self
.logger
.debug('loading "{}" configuration: {}'.format(daemonstr
, source
))
836 self
.net
.loadConf(daemonstr
, source
, param
)
838 def check_router_running(self
):
840 Run a series of checks and returns a status string.
842 self
.logger
.info("checking if daemons are running")
843 return self
.net
.checkRouterRunning()
850 * Configure interfaces
851 * Start daemons (e.g. FRR)
852 * Configure daemon logging files
856 result
= nrouter
.startRouter(self
.tgen
)
858 # Enable command logging
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":
867 "clear log cmdline-targets",
869 "log file {}.log debug".format(daemon
),
871 "log timestamp precision 6",
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
891 * Signal daemons twice, once with SIGTERM, then with SIGKILL.
893 self
.logger
.debug("stopping (no assert)")
894 return self
.net
.stopRouter(False)
896 def startDaemons(self
, daemons
):
898 Start Daemons: to start specific daemon(user defined daemon only)
899 * Start daemons (e.g. FRR)
900 * Configure daemon logging files
902 self
.logger
.debug("starting")
904 result
= nrouter
.startRouterDaemons(daemons
)
907 daemons
= nrouter
.daemons
.keys()
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":
917 "clear log cmdline-targets",
919 "log file {}.log debug".format(daemon
),
921 "log timestamp precision 6",
928 self
.tgen
.set_error(result
)
932 def killDaemons(self
, daemons
, wait
=True, assertOnError
=True):
934 Kill specific daemon(user defined daemon only)
935 forcefully using SIGKILL
937 self
.logger
.debug("Killing daemons using SIGKILL..")
938 return self
.net
.killRouterDaemons(daemons
, wait
, assertOnError
)
940 def vtysh_cmd(self
, command
, isjson
=False, daemon
=None):
942 Runs the provided command string in the vty shell and returns a string
945 This function also accepts multiple commands, but this mode does not
946 return output for each command. See vtysh_multicmd() for more details.
948 # Detect multi line commands
949 if command
.find("\n") != -1:
950 return self
.vtysh_multicmd(command
, daemon
=daemon
)
953 if daemon
is not None:
954 dparam
+= "-d {}".format(daemon
)
956 vtysh_command
= "vtysh {} -c {} 2>/dev/null".format(
957 dparam
, shlex
.quote(command
)
960 self
.logger
.debug("vtysh command => {}".format(shlex
.quote(command
)))
961 output
= self
.run(vtysh_command
)
963 dbgout
= output
.strip()
966 dbgout
= dbgout
.replace("\n", "\n\t")
967 self
.logger
.debug("vtysh result:\n\t{}".format(dbgout
))
969 self
.logger
.debug('vtysh result: "{}"'.format(dbgout
))
975 return json
.loads(output
)
976 except ValueError as error
:
978 "vtysh_cmd: %s: failed to convert json output: %s: %s",
985 def vtysh_multicmd(self
, commands
, pretty_output
=True, daemon
=None):
987 Runs the provided commands in the vty shell and return the result of
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.
994 # Prepare the temporary file that will hold the commands
995 fname
= topotest
.get_file(commands
)
998 if daemon
is not None:
999 dparam
+= "-d {}".format(daemon
)
1001 # Run the commands and delete the temporary file
1003 vtysh_command
= "vtysh {} < {}".format(dparam
, fname
)
1005 vtysh_command
= "vtysh {} -f {}".format(dparam
, fname
)
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
))
1011 res
= self
.run(vtysh_command
)
1014 dbgres
= res
.strip()
1017 dbgres
= dbgres
.replace("\n", "\n\t")
1018 self
.logger
.debug("vtysh result:\n\t{}".format(dbgres
))
1020 self
.logger
.debug('vtysh result: "{}"'.format(dbgres
))
1023 def report_memory_leaks(self
, testname
):
1025 Runs the router memory leak check test. Has the following parameter:
1026 testname: the test file name for identification
1028 NOTE: to run this you must have the environment variable
1029 TOPOTESTS_CHECK_MEMLEAK set or memleak_path configured in `pytest.ini`.
1032 os
.environ
.get("TOPOTESTS_CHECK_MEMLEAK") or self
.params
["memleak_path"]
1034 if memleak_file
== "" or memleak_file
is None:
1039 self
.logger
.info("running memory leak report")
1040 self
.net
.report_memory_leaks(memleak_file
, testname
)
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(" ")
1049 "version": columns
[1],
1057 def has_version(self
, cmpop
, version
):
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
1067 Usage example: router.has_version('>', '1.0')
1069 return self
.net
.checkRouterVersion(cmpop
, version
)
1071 def has_type(self
, rtype
):
1073 Compares router type with `rtype`. Returns `True` if the type matches,
1076 curtype
= self
.version_info()["type"]
1077 return rtype
== curtype
1080 return self
.net
.hasmpls
1083 class TopoSwitch(TopoGear
):
1085 Switch abstraction. Has the following properties:
1086 * cls: switch class that will be used to instantiate
1090 # pylint: disable=too-few-public-methods
1092 def __init__(self
, tgen
, name
, **params
):
1093 super(TopoSwitch
, self
).__init
__(tgen
, name
, **params
)
1094 tgen
.net
.add_switch(name
)
1097 gear
= super(TopoSwitch
, self
).__str
__()
1098 gear
+= " TopoSwitch<>"
1102 class TopoHost(TopoGear
):
1104 # pylint: disable=too-few-public-methods
1106 def __init__(self
, tgen
, name
, **params
):
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').
1115 super(TopoHost
, self
).__init
__(tgen
, name
, **params
)
1117 # Propagate the router log directory
1118 logfile
= self
._setup
_tmpdir
()
1119 params
["logdir"] = self
.logdir
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
])
1127 # Mount gear log directory on a common path
1128 self
.net
.bind_mount(self
.gearlogdir
, "/tmp/gearlogdir")
1131 gear
= super(TopoHost
, self
).__str
__()
1132 gear
+= ' TopoHost<ip="{}",defaultRoute="{}",private_mounts="{}">'.format(
1134 self
.params
["defaultRoute"],
1135 str(self
.params
["private_mounts"]),
1140 class TopoExaBGP(TopoHost
):
1141 "ExaBGP peer abstraction."
1142 # pylint: disable=too-few-public-methods
1150 def __init__(self
, tgen
, name
, **params
):
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')
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
1161 params
["private_mounts"] = self
.PRIVATE_DIRS
1162 super(TopoExaBGP
, self
).__init
__(tgen
, name
, **params
)
1165 gear
= super(TopoExaBGP
, self
).__str
__()
1166 gear
+= " TopoExaBGP<>".format()
1169 def start(self
, peer_dir
, env_file
=None):
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
1177 exacmd
= self
.tgen
.get_exabgp_cmd()
1178 assert exacmd
, "Can't find a usabel ExaBGP (must be < version 4)"
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")
1190 output
= self
.run(exacmd
+ " -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg")
1191 if output
is None or len(output
) == 0:
1194 logger
.info("{} exabgp started, output={}".format(self
.name
, output
))
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`")
1203 # Diagnostic function
1207 # Disable linter branch warning. It is expected to have these here.
1208 # pylint: disable=R0912
1209 def diagnose_env_linux(rundir
):
1211 Run diagnostics in the running environment. Returns `True` when everything
1212 is ok, otherwise `False`.
1216 # Load configuration
1217 config
= configparser
.ConfigParser(defaults
=tgen_defaults
)
1218 pytestini_path
= os
.path
.join(CWD
, "../pytest.ini")
1219 config
.read(pytestini_path
)
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
)
1229 logger
.info("Running environment diagnostics")
1231 # Assert that we are running as root
1232 if os
.getuid() != 0:
1233 logger
.error("you must run topotest as root")
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)")
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)")
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)")
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
))
1258 pwd
.getpwnam("frr")[2]
1260 logger
.warning('could not find "frr" user')
1263 grp
.getgrnam("frr")[2]
1265 logger
.warning('could not find "frr" group')
1268 if "frr" not in grp
.getgrnam("frrvty").gr_mem
:
1270 '"frr" user and group exist, but user is not under "frrvty"'
1273 logger
.warning('could not find "frrvty" group')
1289 path
= os
.path
.join(frrdir
, fname
)
1290 if not os
.path
.isfile(path
):
1291 # LDPd is an exception
1294 "could not find {} in {}".format(fname
, frrdir
)
1295 + "(LDPd tests will not run)"
1299 logger
.error("could not find {} in {}".format(fname
, frrdir
))
1302 if fname
!= "zebra" or fname
!= "mgmtd":
1305 os
.system("{} -v 2>&1 >{}/frr_mgmtd.txt".format(path
, rundir
))
1306 os
.system("{} -v 2>&1 >{}/frr_zebra.txt".format(path
, rundir
))
1308 # Test MPLS availability
1309 krel
= platform
.release()
1310 if topotest
.version_cmp(krel
, "4.5") < 0:
1312 'LDPd tests will not run (have kernel "{}", but it requires 4.5)'.format(
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)")
1323 if not get_exabgp_cmd():
1324 logger
.warning("Failed to find exabgp < 4")
1326 logger
.removeHandler(fhandler
)
1332 def diagnose_env_freebsd():
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()