]>
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()
38 from collections
import OrderedDict
40 if sys
.version_info
[0] > 2:
43 import ConfigParser
as configparser
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
51 from lib
import topotest
53 CWD
= os
.path
.dirname(os
.path
.realpath(__file__
))
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.
61 def get_topogen(topo
=None):
63 Helper function to retrieve Topogen. Must be called with `topo` when called
64 inside the build() method of Topology class.
67 global_tgen
.topo
= topo
71 def set_topogen(tgen
):
72 "Helper function to set Topogen"
73 # pylint: disable=W0603
79 """Return True if value is a string."""
81 return isinstance(value
, basestring
) # type: ignore
83 return isinstance(value
, str)
86 def get_exabgp_cmd(commander
=None):
87 """Return the command to use for ExaBGP version < 4."""
90 commander
= Commander("topogen")
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
)
99 if topotest
.version_cmp(version
, "4") >= 0:
100 logging
.debug("found exabgp version >= 4 in %s will keep looking", exacmd
)
102 logger
.info("Using ExaBGP version %s in %s", version
, exacmd
)
105 exacmd
= commander
.get_exec_path("exabgp")
106 if exacmd
and exacmd_version_ok(exacmd
):
108 py2_path
= commander
.get_exec_path("python2")
110 exacmd
= py2_path
+ " -m exabgp"
111 if exacmd_version_ok(exacmd
):
113 py2_path
= commander
.get_exec_path("python")
115 exacmd
= py2_path
+ " -m exabgp"
116 if exacmd_version_ok(exacmd
):
122 # Main class: topology builder
125 # Topogen configuration defaults
128 "frrdir": "/usr/lib/frr",
134 class Topogen(object):
135 "A topology test builder helper."
137 CONFIG_SECTION
= "topogen"
139 def __init__(self
, topodef
, modname
="unnamed"):
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.
152 self
.modname
= modname
157 self
.exabgp_cmd
= None
158 self
._init
_topo
(topodef
)
160 logger
.info("loading topology: {}".format(self
.modname
))
163 # def _mininet_reset():
164 # "Reset the mininet environment"
165 # # Clean up the mininet environment
166 # os.system("mn -c > /dev/null 2>&1")
171 def _init_topo(self
, topodef
):
173 Initialize the topogily provided by the user. The user topology class
174 must call get_topogen() during build() to get the topogen object.
176 # Set the global variable so the test cases can access it anywhere
179 # Increase host based limits
180 topotest
.fix_host_limits()
182 # Test for MPLS Kernel modules available
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)")
191 # Load the default topology configurations
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
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)
206 # Allow anyone, but set the sticky bit to avoid file deletions
207 os
.chmod(self
.logdir
, 0o1777)
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
)
215 self
.net
= Mininet(controller
=None)
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
):
242 if isinstance(topodef
, OrderedDict
)
243 else sorted(topodef
.keys())
245 # ---------------------------
246 # Create all referenced hosts
247 # ---------------------------
248 for oname
in keylist
:
249 tup
= (topodef
[oname
],) if is_string(topodef
[oname
]) else topodef
[oname
]
253 if name
not in self
.gears
:
254 logging
.debug("Adding router: %s", name
)
255 self
.add_router(name
)
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
)
268 for oname
in keylist
:
271 tup
= (topodef
[oname
],) if is_string(topodef
[oname
]) else topodef
[oname
]
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
)
279 self
.net
.configure_hosts()
281 def _load_config(self
):
283 Loads the configuration file `pytest.ini` located at the root dir of
286 self
.config
= configparser
.ConfigParser(tgen_defaults
)
287 pytestini_path
= os
.path
.join(CWD
, "../pytest.ini")
288 self
.config
.read(pytestini_path
)
290 def add_router(self
, name
=None, cls
=None, **params
):
292 Adds a new router to the topology. This function has the following
294 * `name`: (optional) select the router name
295 * `daemondir`: (optional) custom daemon binary directory
296 * `routertype`: (optional) `frr`
297 Returns a TopoRouter.
300 cls
= topotest
.Router
302 name
= "r{}".format(self
.routern
)
303 if name
in self
.gears
:
304 raise KeyError("router already exists")
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")
311 self
.gears
[name
] = TopoRouter(self
, cls
, name
, **params
)
313 return self
.gears
[name
]
315 def add_switch(self
, name
=None):
317 Adds a new switch to the topology. This function has the following
319 name: (optional) select the switch name
320 Returns the switch name and number.
323 name
= "s{}".format(self
.switchn
)
324 if name
in self
.gears
:
325 raise KeyError("switch already exists")
327 self
.gears
[name
] = TopoSwitch(self
, name
)
329 return self
.gears
[name
]
331 def add_exabgp_peer(self
, name
, ip
, defaultRoute
):
333 Adds a new ExaBGP peer to the topology. This function has the following
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')
339 name
= "peer{}".format(self
.peern
)
340 if name
in self
.gears
:
341 raise KeyError("exabgp peer already exists")
343 self
.gears
[name
] = TopoExaBGP(self
, name
, ip
=ip
, defaultRoute
=defaultRoute
)
345 return self
.gears
[name
]
347 def add_host(self
, name
, ip
, defaultRoute
):
349 Adds a new host to the topology. This function has the following
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')
355 name
= "host{}".format(self
.peern
)
356 if name
in self
.gears
:
357 raise KeyError("host already exists")
359 self
.gears
[name
] = TopoHost(self
, name
, ip
=ip
, defaultRoute
=defaultRoute
)
361 return self
.gears
[name
]
363 def add_link(self
, node1
, node2
, ifname1
=None, ifname2
=None):
365 Creates a connection between node1 and node2. The nodes can be the
371 if not isinstance(node1
, TopoGear
):
372 raise ValueError("invalid node1 type")
373 if not isinstance(node2
, TopoGear
):
374 raise ValueError("invalid node2 type")
377 ifname1
= node1
.new_link()
379 ifname2
= node2
.new_link()
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
)
385 def get_gears(self
, geartype
):
387 Returns a dictionary of all gears of type `geartype`.
390 * Dictionary iteration:
393 router_dict = tgen.get_gears(TopoRouter)
394 for router_name, router in router_dict.items():
400 peer_list = tgen.get_gears(TopoExaBGP).values()
401 for peer in peer_list:
407 for name
, gear
in self
.gears
.items()
408 if isinstance(gear
, geartype
)
413 Returns the router dictionary (key is the router name and value is the
414 router object itself).
416 return self
.get_gears(TopoRouter
)
418 def exabgp_peers(self
):
420 Returns the exabgp peer dictionary (key is the peer name and value is
421 the peer object itself).
423 return self
.get_gears(TopoExaBGP
)
425 def start_topology(self
):
426 """Starts the topology class."""
427 logger
.info("starting topology: {}".format(self
.modname
))
430 def start_router(self
, router
=None):
432 Call the router startRouter method.
433 If no router is specified it is called for all registered routers.
436 # pylint: disable=r1704
437 # XXX should be hosts?
438 for _
, router
in self
.routers().items():
441 if isinstance(router
, str):
442 router
= self
.gears
[router
]
446 def stop_topology(self
):
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.
454 logger
.info("stopping topology: {}".format(self
.modname
))
456 for gear
in self
.gears
.values():
457 errors
+= gear
.stop()
460 "Errors found post shutdown - details follow: {}".format(errors
)
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
472 Interrupt the test and call the command line interface for manual
473 inspection. Should be only used on non production code.
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():
485 memleak_file
= os
.environ
.get("TOPOTESTS_CHECK_MEMLEAK") or self
.config
.get(
486 self
.CONFIG_SECTION
, "memleak_path"
488 if memleak_file
== "" or memleak_file
is None:
492 def report_memory_leaks(self
, testname
=None):
493 "Run memory leak test and reports."
494 if not self
.is_memleak_enabled():
497 # If no name was specified, use the test module name
499 testname
= self
.modname
501 router_list
= self
.routers().values()
502 for router
in router_list
:
503 router
.report_memory_leaks(self
.modname
)
505 def set_error(self
, message
, code
=None):
506 "Sets an error message and signal other tests to skip."
509 # If no code is defined use a sequential number
511 code
= len(self
.errorsd
)
513 self
.errorsd
[code
] = message
514 self
.errors
+= "\n{}: {}".format(code
, message
)
516 def has_errors(self
):
517 "Returns whether errors exist or not."
518 return len(self
.errorsd
) > 0
520 def routers_have_failure(self
):
521 "Runs an assertion to make sure that all routers are running."
522 if self
.has_errors():
526 router_list
= self
.routers().values()
527 for router
in router_list
:
528 result
= router
.check_router_running()
530 errors
+= result
+ "\n"
533 self
.set_error(errors
, "router_error")
540 # Topology gears (equipment)
544 class TopoGear(object):
545 "Abstract class for type checking"
547 def __init__(self
, tgen
, name
, **params
):
554 # Would be nice for this to point at the gears log directory rather than the
556 self
.logdir
= tgen
.logdir
557 self
.gearlogdir
= None
561 for myif
, dest
in self
.links
.items():
565 links
+= '"{}"<->"{}"'.format(myif
, destif
)
567 return 'TopoGear<name="{}",links=[{}]>'.format(self
.name
, links
)
571 return self
.tgen
.net
[self
.name
]
574 "Basic start function that just reports equipment start"
575 logger
.info('starting "{}"'.format(self
.name
))
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
))
582 def cmd(self
, command
, **kwargs
):
584 Runs the provided command string in the router and returns a string
587 return self
.net
.cmd_legacy(command
, **kwargs
)
589 def cmd_raises(self
, command
, **kwargs
):
591 Runs the provided command string in the router and returns a string
592 with the response. Raise an exception on any error.
594 return self
.net
.cmd_raises(command
, **kwargs
)
598 def popen(self
, *params
, **kwargs
):
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.
604 return self
.net
.popen(*params
, **kwargs
)
606 def add_link(self
, node
, myif
=None, nodeif
=None):
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.
613 self
.tgen
.add_link(self
, node
, myif
, nodeif
)
615 def link_enable(self
, myif
, enabled
=True, netns
=None):
617 Set this node interface administrative state.
618 myif: this node interface name
619 enabled: whether we should enable or disable the interface
621 if myif
not in self
.links
.keys():
622 raise KeyError("interface doesn't exists")
630 'setting node "{}" link "{}" to state "{}"'.format(
631 self
.name
, myif
, operation
635 if netns
is not None:
636 extract
= "ip netns exec {} ".format(netns
)
638 return self
.run("{}ip link set dev {} {}".format(extract
, myif
, operation
))
640 def peer_link_enable(self
, myif
, enabled
=True, netns
=None):
642 Set the peer interface administrative state.
643 myif: this node interface name
644 enabled: whether we should enable or disable the interface
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.
649 if myif
not in self
.links
.keys():
650 raise KeyError("interface doesn't exists")
652 node
, nodeif
= self
.links
[myif
]
653 node
.link_enable(nodeif
, enabled
, netns
)
657 Generates a new unique link name.
659 NOTE: This function should only be called by Topogen.
661 ifname
= "{}-eth{}".format(self
.name
, self
.linkn
)
665 def register_link(self
, myif
, node
, nodeif
):
667 Register link between this node interface and outside node.
669 NOTE: This function should only be called by Topogen.
671 if myif
in self
.links
.keys():
672 raise KeyError("interface already exists")
674 self
.links
[myif
] = (node
, nodeif
)
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
)
682 class TopoRouter(TopoGear
):
687 # The default required directories by FRR
695 # Router Daemon enumeration definition.
696 RD_FRR
= 0 # not a daemon, but use to setup unified configs
731 RD_STATIC
: "staticd",
741 def __init__(self
, tgen
, cls
, name
, **params
):
743 The constructor has the following parameters:
744 * tgen: Topogen object
745 * cls: router class that will be used to instantiate
747 * daemondir: daemon binary directory
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
755 # Propagate the router log directory
756 logfile
= self
._setup
_tmpdir
()
757 params
["logdir"] = self
.logdir
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
])
764 # Mount gear log directory on a common path
765 self
.net
.bind_mount(self
.gearlogdir
, "/tmp/gearlogdir")
768 with
open(os
.path
.join(self
.logdir
, self
.name
+ ".pid"), "w") as f
:
769 f
.write(str(self
.net
.pid
) + "\n")
772 gear
= super(TopoRouter
, self
).__str
__()
773 gear
+= " TopoRouter<>"
776 def check_capability(self
, daemon
, param
):
778 Checks a capability daemon against an argument option
779 Return True if capability available. False otherwise
781 daemonstr
= self
.RD
.get(daemon
)
782 self
.logger
.info('check capability {} for "{}"'.format(param
, daemonstr
))
783 return self
.net
.checkCapability(daemonstr
, param
)
785 def load_frr_config(self
, source
, daemons
=None):
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
791 self
.load_config(self
.RD_FRR
, source
)
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
)
801 grep_cmd
= "grep 'router {}' {}".format(daemonstr
, source
)
802 result
= self
.run(grep_cmd
).strip()
804 self
.load_config(daemon
)
806 for daemon
in daemons
:
807 self
.load_config(daemon
)
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.
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.
821 This API unfortunately allows for source to not exist for any and
824 daemonstr
= self
.RD
.get(daemon
)
825 self
.logger
.info('loading "{}" configuration: {}'.format(daemonstr
, source
))
826 self
.net
.loadConf(daemonstr
, source
, param
)
828 def check_router_running(self
):
830 Run a series of checks and returns a status string.
832 self
.logger
.info("checking if daemons are running")
833 return self
.net
.checkRouterRunning()
840 * Configure interfaces
841 * Start daemons (e.g. FRR)
842 * Configure daemon logging files
846 result
= nrouter
.startRouter(self
.tgen
)
848 # Enable command logging
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":
857 "clear log cmdline-targets",
859 "log file {}.log debug".format(daemon
),
861 "log timestamp precision 3",
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
881 * Signal daemons twice, once with SIGTERM, then with SIGKILL.
883 self
.logger
.debug("stopping (no assert)")
884 return self
.net
.stopRouter(False)
886 def startDaemons(self
, daemons
):
888 Start Daemons: to start specific daemon(user defined daemon only)
889 * Start daemons (e.g. FRR)
890 * Configure daemon logging files
892 self
.logger
.debug("starting")
894 result
= nrouter
.startRouterDaemons(daemons
)
897 daemons
= nrouter
.daemons
.keys()
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":
907 "clear log cmdline-targets",
909 "log file {}.log debug".format(daemon
),
911 "log timestamp precision 3",
918 self
.tgen
.set_error(result
)
922 def killDaemons(self
, daemons
, wait
=True, assertOnError
=True):
924 Kill specific daemon(user defined daemon only)
925 forcefully using SIGKILL
927 self
.logger
.debug("Killing daemons using SIGKILL..")
928 return self
.net
.killRouterDaemons(daemons
, wait
, assertOnError
)
930 def vtysh_cmd(self
, command
, isjson
=False, daemon
=None):
932 Runs the provided command string in the vty shell and returns a string
935 This function also accepts multiple commands, but this mode does not
936 return output for each command. See vtysh_multicmd() for more details.
938 # Detect multi line commands
939 if command
.find("\n") != -1:
940 return self
.vtysh_multicmd(command
, daemon
=daemon
)
943 if daemon
is not None:
944 dparam
+= "-d {}".format(daemon
)
946 vtysh_command
= 'vtysh {} -c "{}" 2>/dev/null'.format(dparam
, command
)
948 self
.logger
.info('vtysh command => "{}"'.format(command
))
949 output
= self
.run(vtysh_command
)
951 dbgout
= output
.strip()
954 dbgout
= dbgout
.replace("\n", "\n\t")
955 self
.logger
.info("vtysh result:\n\t{}".format(dbgout
))
957 self
.logger
.info('vtysh result: "{}"'.format(dbgout
))
963 return json
.loads(output
)
964 except ValueError as error
:
966 "vtysh_cmd: %s: failed to convert json output: %s: %s",
973 def vtysh_multicmd(self
, commands
, pretty_output
=True, daemon
=None):
975 Runs the provided commands in the vty shell and return the result of
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.
982 # Prepare the temporary file that will hold the commands
983 fname
= topotest
.get_file(commands
)
986 if daemon
is not None:
987 dparam
+= "-d {}".format(daemon
)
989 # Run the commands and delete the temporary file
991 vtysh_command
= "vtysh {} < {}".format(dparam
, fname
)
993 vtysh_command
= "vtysh {} -f {}".format(dparam
, fname
)
995 dbgcmds
= commands
if is_string(commands
) else "\n".join(commands
)
996 dbgcmds
= "\t" + dbgcmds
.replace("\n", "\n\t")
997 self
.logger
.info("vtysh command => FILE:\n{}".format(dbgcmds
))
999 res
= self
.run(vtysh_command
)
1002 dbgres
= res
.strip()
1005 dbgres
= dbgres
.replace("\n", "\n\t")
1006 self
.logger
.info("vtysh result:\n\t{}".format(dbgres
))
1008 self
.logger
.info('vtysh result: "{}"'.format(dbgres
))
1011 def report_memory_leaks(self
, testname
):
1013 Runs the router memory leak check test. Has the following parameter:
1014 testname: the test file name for identification
1016 NOTE: to run this you must have the environment variable
1017 TOPOTESTS_CHECK_MEMLEAK set or memleak_path configured in `pytest.ini`.
1020 os
.environ
.get("TOPOTESTS_CHECK_MEMLEAK") or self
.params
["memleak_path"]
1022 if memleak_file
== "" or memleak_file
is None:
1027 self
.logger
.info("running memory leak report")
1028 self
.net
.report_memory_leaks(memleak_file
, testname
)
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(" ")
1037 "version": columns
[1],
1045 def has_version(self
, cmpop
, version
):
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
1055 Usage example: router.has_version('>', '1.0')
1057 return self
.net
.checkRouterVersion(cmpop
, version
)
1059 def has_type(self
, rtype
):
1061 Compares router type with `rtype`. Returns `True` if the type matches,
1064 curtype
= self
.version_info()["type"]
1065 return rtype
== curtype
1068 return self
.net
.hasmpls
1071 class TopoSwitch(TopoGear
):
1073 Switch abstraction. Has the following properties:
1074 * cls: switch class that will be used to instantiate
1078 # pylint: disable=too-few-public-methods
1080 def __init__(self
, tgen
, name
, **params
):
1081 super(TopoSwitch
, self
).__init
__(tgen
, name
, **params
)
1082 tgen
.net
.add_switch(name
)
1085 gear
= super(TopoSwitch
, self
).__str
__()
1086 gear
+= " TopoSwitch<>"
1090 class TopoHost(TopoGear
):
1092 # pylint: disable=too-few-public-methods
1094 def __init__(self
, tgen
, name
, **params
):
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').
1103 super(TopoHost
, self
).__init
__(tgen
, name
, **params
)
1105 # Propagate the router log directory
1106 logfile
= self
._setup
_tmpdir
()
1107 params
["logdir"] = self
.logdir
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
])
1115 # Mount gear log directory on a common path
1116 self
.net
.bind_mount(self
.gearlogdir
, "/tmp/gearlogdir")
1119 gear
= super(TopoHost
, self
).__str
__()
1120 gear
+= ' TopoHost<ip="{}",defaultRoute="{}",privateDirs="{}">'.format(
1122 self
.params
["defaultRoute"],
1123 str(self
.params
["privateDirs"]),
1128 class TopoExaBGP(TopoHost
):
1129 "ExaBGP peer abstraction."
1130 # pylint: disable=too-few-public-methods
1138 def __init__(self
, tgen
, name
, **params
):
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')
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
1149 params
["privateDirs"] = self
.PRIVATE_DIRS
1150 super(TopoExaBGP
, self
).__init
__(tgen
, name
, **params
)
1153 gear
= super(TopoExaBGP
, self
).__str
__()
1154 gear
+= " TopoExaBGP<>".format()
1157 def start(self
, peer_dir
, env_file
=None):
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
1165 exacmd
= self
.tgen
.get_exabgp_cmd()
1166 assert exacmd
, "Can't find a usabel ExaBGP (must be < version 4)"
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")
1178 output
= self
.run(exacmd
+ " -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg")
1179 if output
is None or len(output
) == 0:
1182 logger
.info("{} exabgp started, output={}".format(self
.name
, output
))
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`")
1191 # Diagnostic function
1194 # Disable linter branch warning. It is expected to have these here.
1195 # pylint: disable=R0912
1196 def diagnose_env_linux(rundir
):
1198 Run diagnostics in the running environment. Returns `True` when everything
1199 is ok, otherwise `False`.
1203 # Load configuration
1204 config
= configparser
.ConfigParser(defaults
=tgen_defaults
)
1205 pytestini_path
= os
.path
.join(CWD
, "../pytest.ini")
1206 config
.read(pytestini_path
)
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
)
1216 logger
.info("Running environment diagnostics")
1218 # Assert that we are running as root
1219 if os
.getuid() != 0:
1220 logger
.error("you must run topotest as root")
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)")
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)")
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)")
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
))
1245 pwd
.getpwnam("frr")[2]
1247 logger
.warning('could not find "frr" user')
1250 grp
.getgrnam("frr")[2]
1252 logger
.warning('could not find "frr" group')
1255 if "frr" not in grp
.getgrnam("frrvty").gr_mem
:
1257 '"frr" user and group exist, but user is not under "frrvty"'
1260 logger
.warning('could not find "frrvty" group')
1276 path
= os
.path
.join(frrdir
, fname
)
1277 if not os
.path
.isfile(path
):
1278 # LDPd is an exception
1281 "could not find {} in {}".format(fname
, frrdir
)
1282 + "(LDPd tests will not run)"
1286 logger
.error("could not find {} in {}".format(fname
, frrdir
))
1289 if fname
!= "zebra" or fname
!= "mgmtd":
1292 os
.system("{} -v 2>&1 >{}/frr_mgmtd.txt".format(path
, rundir
))
1293 os
.system("{} -v 2>&1 >{}/frr_zebra.txt".format(path
, rundir
))
1295 # Test MPLS availability
1296 krel
= platform
.release()
1297 if topotest
.version_cmp(krel
, "4.5") < 0:
1299 'LDPd tests will not run (have kernel "{}", but it requires 4.5)'.format(
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)")
1310 if not get_exabgp_cmd():
1311 logger
.warning("Failed to find exabgp < 4")
1313 logger
.removeHandler(fhandler
)
1319 def diagnose_env_freebsd():
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()