]>
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
== 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
730 RD_STATIC
: "staticd",
739 def __init__(self
, tgen
, cls
, name
, **params
):
741 The constructor has the following parameters:
742 * tgen: Topogen object
743 * cls: router class that will be used to instantiate
745 * daemondir: daemon binary directory
748 super(TopoRouter
, self
).__init
__(tgen
, name
, **params
)
749 self
.routertype
= params
.get("routertype", "frr")
750 if "privateDirs" not in params
:
751 params
["privateDirs"] = self
.PRIVATE_DIRS
753 # Propagate the router log directory
754 logfile
= self
._setup
_tmpdir
()
755 params
["logdir"] = self
.logdir
757 self
.logger
= topolog
.get_logger(name
, log_level
="debug", target
=logfile
)
758 params
["logger"] = self
.logger
759 tgen
.net
.add_host(self
.name
, cls
=cls
, **params
)
760 topotest
.fix_netns_limits(tgen
.net
[name
])
762 # Mount gear log directory on a common path
763 self
.net
.bind_mount(self
.gearlogdir
, "/tmp/gearlogdir")
766 with
open(os
.path
.join(self
.logdir
, self
.name
+ ".pid"), "w") as f
:
767 f
.write(str(self
.net
.pid
) + "\n")
770 gear
= super(TopoRouter
, self
).__str
__()
771 gear
+= " TopoRouter<>"
774 def check_capability(self
, daemon
, param
):
776 Checks a capability daemon against an argument option
777 Return True if capability available. False otherwise
779 daemonstr
= self
.RD
.get(daemon
)
780 self
.logger
.info('check capability {} for "{}"'.format(param
, daemonstr
))
781 return self
.net
.checkCapability(daemonstr
, param
)
783 def load_frr_config(self
, source
, daemons
=None):
785 Loads the unified configuration file source
786 Start the daemons in the list
787 If daemons is None, try to infer daemons from the config file
789 self
.load_config(self
.RD_FRR
, source
)
792 self
.load_config(self
.RD_ZEBRA
)
793 for daemon
in self
.RD
:
794 # This will not work for all daemons
795 daemonstr
= self
.RD
.get(daemon
).rstrip("d")
796 if daemonstr
== "pim":
797 grep_cmd
= "grep 'ip {}' {}".format(daemonstr
, source
)
799 grep_cmd
= "grep 'router {}' {}".format(daemonstr
, source
)
800 result
= self
.run(grep_cmd
).strip()
802 self
.load_config(daemon
)
804 for daemon
in daemons
:
805 self
.load_config(daemon
)
807 def load_config(self
, daemon
, source
=None, param
=None):
808 """Loads daemon configuration from the specified source
809 Possible daemon values are: TopoRouter.RD_ZEBRA, TopoRouter.RD_RIP,
810 TopoRouter.RD_RIPNG, TopoRouter.RD_OSPF, TopoRouter.RD_OSPF6,
811 TopoRouter.RD_ISIS, TopoRouter.RD_BGP, TopoRouter.RD_LDP,
812 TopoRouter.RD_PIM, TopoRouter.RD_PIM6, TopoRouter.RD_PBR,
815 Possible `source` values are `None` for an empty config file, a path name which is
816 used directly, or a file name with no path components which is first looked for
817 directly and then looked for under a sub-directory named after router.
819 This API unfortunately allows for source to not exist for any and
822 daemonstr
= self
.RD
.get(daemon
)
823 self
.logger
.info('loading "{}" configuration: {}'.format(daemonstr
, source
))
824 self
.net
.loadConf(daemonstr
, source
, param
)
826 def check_router_running(self
):
828 Run a series of checks and returns a status string.
830 self
.logger
.info("checking if daemons are running")
831 return self
.net
.checkRouterRunning()
838 * Configure interfaces
839 * Start daemons (e.g. FRR)
840 * Configure daemon logging files
844 result
= nrouter
.startRouter(self
.tgen
)
846 # Enable command logging
848 # Enable all daemon command logging, logging files
849 # and set them to the start dir.
850 for daemon
, enabled
in nrouter
.daemons
.items():
851 if enabled
and daemon
!= "snmpd":
855 "clear log cmdline-targets",
857 "log file {}.log debug".format(daemon
),
859 "log timestamp precision 3",
866 self
.tgen
.set_error(result
)
867 elif nrouter
.daemons
["ldpd"] == 1 or nrouter
.daemons
["pathd"] == 1:
868 # Enable MPLS processing on all interfaces.
869 for interface
in self
.links
:
870 topotest
.sysctl_assure(
871 nrouter
, "net.mpls.conf.{}.input".format(interface
), 1
879 * Signal daemons twice, once with SIGTERM, then with SIGKILL.
881 self
.logger
.debug("stopping (no assert)")
882 return self
.net
.stopRouter(False)
884 def startDaemons(self
, daemons
):
886 Start Daemons: to start specific daemon(user defined daemon only)
887 * Start daemons (e.g. FRR)
888 * Configure daemon logging files
890 self
.logger
.debug("starting")
892 result
= nrouter
.startRouterDaemons(daemons
)
895 daemons
= nrouter
.daemons
.keys()
897 # Enable all daemon command logging, logging files
898 # and set them to the start dir.
899 for daemon
in daemons
:
900 enabled
= nrouter
.daemons
[daemon
]
901 if enabled
and daemon
!= "snmpd":
905 "clear log cmdline-targets",
907 "log file {}.log debug".format(daemon
),
909 "log timestamp precision 3",
916 self
.tgen
.set_error(result
)
920 def killDaemons(self
, daemons
, wait
=True, assertOnError
=True):
922 Kill specific daemon(user defined daemon only)
923 forcefully using SIGKILL
925 self
.logger
.debug("Killing daemons using SIGKILL..")
926 return self
.net
.killRouterDaemons(daemons
, wait
, assertOnError
)
928 def vtysh_cmd(self
, command
, isjson
=False, daemon
=None):
930 Runs the provided command string in the vty shell and returns a string
933 This function also accepts multiple commands, but this mode does not
934 return output for each command. See vtysh_multicmd() for more details.
936 # Detect multi line commands
937 if command
.find("\n") != -1:
938 return self
.vtysh_multicmd(command
, daemon
=daemon
)
941 if daemon
is not None:
942 dparam
+= "-d {}".format(daemon
)
944 vtysh_command
= 'vtysh {} -c "{}" 2>/dev/null'.format(dparam
, command
)
946 self
.logger
.info('vtysh command => "{}"'.format(command
))
947 output
= self
.run(vtysh_command
)
949 dbgout
= output
.strip()
952 dbgout
= dbgout
.replace("\n", "\n\t")
953 self
.logger
.info("vtysh result:\n\t{}".format(dbgout
))
955 self
.logger
.info('vtysh result: "{}"'.format(dbgout
))
961 return json
.loads(output
)
962 except ValueError as error
:
964 "vtysh_cmd: %s: failed to convert json output: %s: %s",
971 def vtysh_multicmd(self
, commands
, pretty_output
=True, daemon
=None):
973 Runs the provided commands in the vty shell and return the result of
976 pretty_output: defines how the return value will be presented. When
977 True it will show the command as they were executed in the vty shell,
978 otherwise it will only show lines that failed.
980 # Prepare the temporary file that will hold the commands
981 fname
= topotest
.get_file(commands
)
984 if daemon
is not None:
985 dparam
+= "-d {}".format(daemon
)
987 # Run the commands and delete the temporary file
989 vtysh_command
= "vtysh {} < {}".format(dparam
, fname
)
991 vtysh_command
= "vtysh {} -f {}".format(dparam
, fname
)
993 dbgcmds
= commands
if is_string(commands
) else "\n".join(commands
)
994 dbgcmds
= "\t" + dbgcmds
.replace("\n", "\n\t")
995 self
.logger
.info("vtysh command => FILE:\n{}".format(dbgcmds
))
997 res
= self
.run(vtysh_command
)
1000 dbgres
= res
.strip()
1003 dbgres
= dbgres
.replace("\n", "\n\t")
1004 self
.logger
.info("vtysh result:\n\t{}".format(dbgres
))
1006 self
.logger
.info('vtysh result: "{}"'.format(dbgres
))
1009 def report_memory_leaks(self
, testname
):
1011 Runs the router memory leak check test. Has the following parameter:
1012 testname: the test file name for identification
1014 NOTE: to run this you must have the environment variable
1015 TOPOTESTS_CHECK_MEMLEAK set or memleak_path configured in `pytest.ini`.
1018 os
.environ
.get("TOPOTESTS_CHECK_MEMLEAK") or self
.params
["memleak_path"]
1020 if memleak_file
== "" or memleak_file
== None:
1025 self
.logger
.info("running memory leak report")
1026 self
.net
.report_memory_leaks(memleak_file
, testname
)
1028 def version_info(self
):
1029 "Get equipment information from 'show version'."
1030 output
= self
.vtysh_cmd("show version").split("\n")[0]
1031 columns
= topotest
.normalize_text(output
).split(" ")
1035 "version": columns
[1],
1043 def has_version(self
, cmpop
, version
):
1045 Compares router version using operation `cmpop` with `version`.
1046 Valid `cmpop` values:
1047 * `>=`: has the same version or greater
1048 * '>': has greater version
1049 * '=': has the same version
1050 * '<': has a lesser version
1051 * '<=': has the same version or lesser
1053 Usage example: router.has_version('>', '1.0')
1055 return self
.net
.checkRouterVersion(cmpop
, version
)
1057 def has_type(self
, rtype
):
1059 Compares router type with `rtype`. Returns `True` if the type matches,
1062 curtype
= self
.version_info()["type"]
1063 return rtype
== curtype
1066 return self
.net
.hasmpls
1069 class TopoSwitch(TopoGear
):
1071 Switch abstraction. Has the following properties:
1072 * cls: switch class that will be used to instantiate
1076 # pylint: disable=too-few-public-methods
1078 def __init__(self
, tgen
, name
, **params
):
1079 super(TopoSwitch
, self
).__init
__(tgen
, name
, **params
)
1080 tgen
.net
.add_switch(name
)
1083 gear
= super(TopoSwitch
, self
).__str
__()
1084 gear
+= " TopoSwitch<>"
1088 class TopoHost(TopoGear
):
1090 # pylint: disable=too-few-public-methods
1092 def __init__(self
, tgen
, name
, **params
):
1094 Mininet has the following known `params` for hosts:
1095 * `ip`: the IP address (string) for the host interface
1096 * `defaultRoute`: the default route that will be installed
1097 (e.g. 'via 10.0.0.1')
1098 * `privateDirs`: directories that will be mounted on a different domain
1099 (e.g. '/etc/important_dir').
1101 super(TopoHost
, self
).__init
__(tgen
, name
, **params
)
1103 # Propagate the router log directory
1104 logfile
= self
._setup
_tmpdir
()
1105 params
["logdir"] = self
.logdir
1107 # Odd to have 2 logfiles for each host
1108 self
.logger
= topolog
.get_logger(name
, log_level
="debug", target
=logfile
)
1109 params
["logger"] = self
.logger
1110 tgen
.net
.add_host(name
, **params
)
1111 topotest
.fix_netns_limits(tgen
.net
[name
])
1113 # Mount gear log directory on a common path
1114 self
.net
.bind_mount(self
.gearlogdir
, "/tmp/gearlogdir")
1117 gear
= super(TopoHost
, self
).__str
__()
1118 gear
+= ' TopoHost<ip="{}",defaultRoute="{}",privateDirs="{}">'.format(
1120 self
.params
["defaultRoute"],
1121 str(self
.params
["privateDirs"]),
1126 class TopoExaBGP(TopoHost
):
1127 "ExaBGP peer abstraction."
1128 # pylint: disable=too-few-public-methods
1136 def __init__(self
, tgen
, name
, **params
):
1138 ExaBGP usually uses the following parameters:
1139 * `ip`: the IP address (string) for the host interface
1140 * `defaultRoute`: the default route that will be installed
1141 (e.g. 'via 10.0.0.1')
1143 Note: the different between a host and a ExaBGP peer is that this class
1144 has a privateDirs already defined and contains functions to handle ExaBGP
1147 params
["privateDirs"] = self
.PRIVATE_DIRS
1148 super(TopoExaBGP
, self
).__init
__(tgen
, name
, **params
)
1151 gear
= super(TopoExaBGP
, self
).__str
__()
1152 gear
+= " TopoExaBGP<>".format()
1155 def start(self
, peer_dir
, env_file
=None):
1157 Start running ExaBGP daemon:
1158 * Copy all peer* folder contents into /etc/exabgp
1159 * Copy exabgp env file if specified
1160 * Make all python files runnable
1161 * Run ExaBGP with env file `env_file` and configuration peer*/exabgp.cfg
1163 exacmd
= self
.tgen
.get_exabgp_cmd()
1164 assert exacmd
, "Can't find a usabel ExaBGP (must be < version 4)"
1166 self
.run("mkdir -p /etc/exabgp")
1167 self
.run("chmod 755 /etc/exabgp")
1168 self
.run("cp {}/exa-* /etc/exabgp/".format(CWD
))
1169 self
.run("cp {}/* /etc/exabgp/".format(peer_dir
))
1170 if env_file
is not None:
1171 self
.run("cp {} /etc/exabgp/exabgp.env".format(env_file
))
1172 self
.run("chmod 644 /etc/exabgp/*")
1173 self
.run("chmod a+x /etc/exabgp/*.py")
1174 self
.run("chown -R exabgp:exabgp /etc/exabgp")
1176 output
= self
.run(exacmd
+ " -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg")
1177 if output
== None or len(output
) == 0:
1180 logger
.info("{} exabgp started, output={}".format(self
.name
, output
))
1182 def stop(self
, wait
=True, assertOnError
=True):
1183 "Stop ExaBGP peer and kill the daemon"
1184 self
.run("kill `cat /var/run/exabgp/exabgp.pid`")
1189 # Diagnostic function
1192 # Disable linter branch warning. It is expected to have these here.
1193 # pylint: disable=R0912
1194 def diagnose_env_linux(rundir
):
1196 Run diagnostics in the running environment. Returns `True` when everything
1197 is ok, otherwise `False`.
1201 # Load configuration
1202 config
= configparser
.ConfigParser(defaults
=tgen_defaults
)
1203 pytestini_path
= os
.path
.join(CWD
, "../pytest.ini")
1204 config
.read(pytestini_path
)
1206 # Test log path exists before installing handler.
1207 os
.system("mkdir -p " + rundir
)
1208 # Log diagnostics to file so it can be examined later.
1209 fhandler
= logging
.FileHandler(filename
="{}/diagnostics.txt".format(rundir
))
1210 fhandler
.setLevel(logging
.DEBUG
)
1211 fhandler
.setFormatter(logging
.Formatter(fmt
=topolog
.FORMAT
))
1212 logger
.addHandler(fhandler
)
1214 logger
.info("Running environment diagnostics")
1216 # Assert that we are running as root
1217 if os
.getuid() != 0:
1218 logger
.error("you must run topotest as root")
1221 # Assert that we have mininet
1222 # if os.system("which mn >/dev/null 2>/dev/null") != 0:
1223 # logger.error("could not find mininet binary (mininet is not installed)")
1226 # Assert that we have iproute installed
1227 if os
.system("which ip >/dev/null 2>/dev/null") != 0:
1228 logger
.error("could not find ip binary (iproute is not installed)")
1231 # Assert that we have gdb installed
1232 if os
.system("which gdb >/dev/null 2>/dev/null") != 0:
1233 logger
.error("could not find gdb binary (gdb is not installed)")
1236 # Assert that FRR utilities exist
1237 frrdir
= config
.get("topogen", "frrdir")
1238 if not os
.path
.isdir(frrdir
):
1239 logger
.error("could not find {} directory".format(frrdir
))
1243 pwd
.getpwnam("frr")[2]
1245 logger
.warning('could not find "frr" user')
1248 grp
.getgrnam("frr")[2]
1250 logger
.warning('could not find "frr" group')
1253 if "frr" not in grp
.getgrnam("frrvty").gr_mem
:
1255 '"frr" user and group exist, but user is not under "frrvty"'
1258 logger
.warning('could not find "frrvty" group')
1273 path
= os
.path
.join(frrdir
, fname
)
1274 if not os
.path
.isfile(path
):
1275 # LDPd is an exception
1278 "could not find {} in {}".format(fname
, frrdir
)
1279 + "(LDPd tests will not run)"
1283 logger
.error("could not find {} in {}".format(fname
, frrdir
))
1286 if fname
!= "zebra":
1289 os
.system("{} -v 2>&1 >{}/frr_zebra.txt".format(path
, rundir
))
1291 # Test MPLS availability
1292 krel
= platform
.release()
1293 if topotest
.version_cmp(krel
, "4.5") < 0:
1295 'LDPd tests will not run (have kernel "{}", but it requires 4.5)'.format(
1300 # Test for MPLS Kernel modules available
1301 if not topotest
.module_present("mpls-router", load
=False) != 0:
1302 logger
.info("LDPd tests will not run (missing mpls-router kernel module)")
1303 if not topotest
.module_present("mpls-iptunnel", load
=False) != 0:
1304 logger
.info("LDPd tests will not run (missing mpls-iptunnel kernel module)")
1306 if not get_exabgp_cmd():
1307 logger
.warning("Failed to find exabgp < 4")
1309 logger
.removeHandler(fhandler
)
1315 def diagnose_env_freebsd():
1319 def diagnose_env(rundir
):
1320 if sys
.platform
.startswith("linux"):
1321 return diagnose_env_linux(rundir
)
1322 elif sys
.platform
.startswith("freebsd"):
1323 return diagnose_env_freebsd()