]>
git.proxmox.com Git - mirror_frr.git/blob - tests/topotests/lib/topogen.py
3 # Library of helper functions for NetDEF Topology Tests
5 # Copyright (c) 2017 by
6 # Network Device Education Foundation, Inc. ("NetDEF")
8 # Permission to use, copy, modify, and/or distribute this software
9 # for any purpose with or without fee is hereby granted, provided
10 # that the above copyright notice and this permission notice appear
13 # THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES
14 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
15 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR
16 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
17 # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
18 # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
19 # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
24 Topogen (Topology Generator) is an abstraction around Topotest and Mininet to
25 help reduce boilerplate code and provide a stable interface to build topology
28 Basic usage instructions:
30 * Define a Topology class with a build method using mininet.topo.Topo.
31 See examples/test_template.py.
32 * Use Topogen inside the build() method with get_topogen.
33 e.g. get_topogen(self).
34 * Start up your topology with: Topogen(YourTopology)
35 * Initialize the Mininet with your topology with: tgen.start_topology()
36 * Configure your routers/hosts and start them
37 * Run your tests / mininet cli.
38 * After running stop Mininet with: tgen.stop_topology()
51 from collections
import OrderedDict
53 if sys
.version_info
[0] > 2:
56 import ConfigParser
as configparser
58 import lib
.topolog
as topolog
59 from lib
.micronet
import Commander
60 from lib
.micronet_compat
import Mininet
61 from lib
.topolog
import logger
62 from lib
.topotest
import g_extra_config
64 from lib
import topotest
66 CWD
= os
.path
.dirname(os
.path
.realpath(__file__
))
68 # pylint: disable=C0103
69 # Global Topogen variable. This is being used to keep the Topogen available on
70 # all test functions without declaring a test local variable.
74 def get_topogen(topo
=None):
76 Helper function to retrieve Topogen. Must be called with `topo` when called
77 inside the build() method of Topology class.
80 global_tgen
.topo
= topo
84 def set_topogen(tgen
):
85 "Helper function to set Topogen"
86 # pylint: disable=W0603
92 """Return True if value is a string."""
94 return isinstance(value
, basestring
) # type: ignore
96 return isinstance(value
, str)
99 def get_exabgp_cmd(commander
=None):
100 """Return the command to use for ExaBGP version < 4."""
102 if commander
is None:
103 commander
= Commander("topogen")
105 def exacmd_version_ok(exacmd
):
106 logger
.debug("checking %s for exabgp < version 4", exacmd
)
107 _
, stdout
, _
= commander
.cmd_status(exacmd
+ " -v", warn
=False)
108 m
= re
.search(r
"ExaBGP\s*:\s*((\d+)\.(\d+)(?:\.(\d+))?)", stdout
)
112 if topotest
.version_cmp(version
, "4") >= 0:
113 logging
.debug("found exabgp version >= 4 in %s will keep looking", exacmd
)
115 logger
.info("Using ExaBGP version %s in %s", version
, exacmd
)
118 exacmd
= commander
.get_exec_path("exabgp")
119 if exacmd
and exacmd_version_ok(exacmd
):
121 py2_path
= commander
.get_exec_path("python2")
123 exacmd
= py2_path
+ " -m exabgp"
124 if exacmd_version_ok(exacmd
):
126 py2_path
= commander
.get_exec_path("python")
128 exacmd
= py2_path
+ " -m exabgp"
129 if exacmd_version_ok(exacmd
):
135 # Main class: topology builder
138 # Topogen configuration defaults
141 "frrdir": "/usr/lib/frr",
147 class Topogen(object):
148 "A topology test builder helper."
150 CONFIG_SECTION
= "topogen"
152 def __init__(self
, topodef
, modname
="unnamed"):
154 Topogen initialization function, takes the following arguments:
155 * `cls`: OLD:uthe topology class that is child of mininet.topo or a build function.
156 * `topodef`: A dictionary defining the topology, a filename of a json file, or a
157 function that will do the same
158 * `modname`: module name must be a unique name to identify logs later.
165 self
.modname
= modname
170 self
.exabgp_cmd
= None
171 self
._init
_topo
(topodef
)
173 logger
.info("loading topology: {}".format(self
.modname
))
176 # def _mininet_reset():
177 # "Reset the mininet environment"
178 # # Clean up the mininet environment
179 # os.system("mn -c > /dev/null 2>&1")
184 def _init_topo(self
, topodef
):
186 Initialize the topogily provided by the user. The user topology class
187 must call get_topogen() during build() to get the topogen object.
189 # Set the global variable so the test cases can access it anywhere
192 # Increase host based limits
193 topotest
.fix_host_limits()
195 # Test for MPLS Kernel modules available
197 if not topotest
.module_present("mpls-router"):
198 logger
.info("MPLS tests will not run (missing mpls-router kernel module)")
199 elif not topotest
.module_present("mpls-iptunnel"):
200 logger
.info("MPLS tests will not run (missing mpls-iptunnel kernel module)")
204 # Load the default topology configurations
207 # Create new log directory
208 self
.logdir
= topotest
.get_logs_path(g_extra_config
["rundir"])
209 subprocess
.check_call(
210 "mkdir -p {0} && chmod 1777 {0}".format(self
.logdir
), shell
=True
213 routertype
= self
.config
.get(self
.CONFIG_SECTION
, "routertype")
214 # Only allow group, if it exist.
215 gid
= grp
.getgrnam(routertype
)[2]
216 os
.chown(self
.logdir
, 0, gid
)
217 os
.chmod(self
.logdir
, 0o775)
219 # Allow anyone, but set the sticky bit to avoid file deletions
220 os
.chmod(self
.logdir
, 0o1777)
222 # Remove old twisty way of creating sub-classed topology object which has it's
223 # build method invoked which calls Topogen methods which then call Topo methods
224 # to create a topology within the Topo object, which is then used by
225 # Mininet(Micronet) to build the actual topology.
226 assert not inspect
.isclass(topodef
)
228 self
.net
= Mininet(controller
=None)
230 # New direct way: Either a dictionary defines the topology or a build function
231 # is supplied, or a json filename all of which build the topology by calling
232 # Topogen methods which call Mininet(Micronet) methods to create the actual
234 if not inspect
.isclass(topodef
):
235 if callable(topodef
):
237 self
.net
.configure_hosts()
238 elif is_string(topodef
):
239 # topojson imports topogen in one function too,
240 # switch away from this use here to the topojson
241 # fixutre and remove this case
242 from lib
.topojson
import build_topo_from_json
244 with
open(topodef
, "r") as topof
:
245 self
.json_topo
= json
.load(topof
)
246 build_topo_from_json(self
, self
.json_topo
)
247 self
.net
.configure_hosts()
249 self
.add_topology_from_dict(topodef
)
251 def add_topology_from_dict(self
, topodef
):
255 if isinstance(topodef
, OrderedDict
)
256 else sorted(topodef
.keys())
258 # ---------------------------
259 # Create all referenced hosts
260 # ---------------------------
261 for oname
in keylist
:
262 tup
= (topodef
[oname
],) if is_string(topodef
[oname
]) else topodef
[oname
]
266 if name
not in self
.gears
:
267 logging
.debug("Adding router: %s", name
)
268 self
.add_router(name
)
270 # ------------------------------
271 # Create all referenced switches
272 # ------------------------------
273 for oname
in keylist
:
274 if oname
is not None and oname
not in self
.gears
:
275 logging
.debug("Adding switch: %s", oname
)
276 self
.add_switch(oname
)
281 for oname
in keylist
:
284 tup
= (topodef
[oname
],) if is_string(topodef
[oname
]) else topodef
[oname
]
288 ifname
= desc
[1] if len(desc
) > 1 else None
289 sifname
= desc
[2] if len(desc
) > 2 else None
290 self
.add_link(self
.gears
[oname
], self
.gears
[name
], sifname
, ifname
)
292 self
.net
.configure_hosts()
294 def _load_config(self
):
296 Loads the configuration file `pytest.ini` located at the root dir of
299 self
.config
= configparser
.ConfigParser(tgen_defaults
)
300 pytestini_path
= os
.path
.join(CWD
, "../pytest.ini")
301 self
.config
.read(pytestini_path
)
303 def add_router(self
, name
=None, cls
=None, **params
):
305 Adds a new router to the topology. This function has the following
307 * `name`: (optional) select the router name
308 * `daemondir`: (optional) custom daemon binary directory
309 * `routertype`: (optional) `frr`
310 Returns a TopoRouter.
313 cls
= topotest
.Router
315 name
= "r{}".format(self
.routern
)
316 if name
in self
.gears
:
317 raise KeyError("router already exists")
319 params
["frrdir"] = self
.config
.get(self
.CONFIG_SECTION
, "frrdir")
320 params
["memleak_path"] = self
.config
.get(self
.CONFIG_SECTION
, "memleak_path")
321 if "routertype" not in params
:
322 params
["routertype"] = self
.config
.get(self
.CONFIG_SECTION
, "routertype")
324 self
.gears
[name
] = TopoRouter(self
, cls
, name
, **params
)
326 return self
.gears
[name
]
328 def add_switch(self
, name
=None):
330 Adds a new switch to the topology. This function has the following
332 name: (optional) select the switch name
333 Returns the switch name and number.
336 name
= "s{}".format(self
.switchn
)
337 if name
in self
.gears
:
338 raise KeyError("switch already exists")
340 self
.gears
[name
] = TopoSwitch(self
, name
)
342 return self
.gears
[name
]
344 def add_exabgp_peer(self
, name
, ip
, defaultRoute
):
346 Adds a new ExaBGP peer to the topology. This function has the following
348 * `ip`: the peer address (e.g. '1.2.3.4/24')
349 * `defaultRoute`: the peer default route (e.g. 'via 1.2.3.1')
352 name
= "peer{}".format(self
.peern
)
353 if name
in self
.gears
:
354 raise KeyError("exabgp peer already exists")
356 self
.gears
[name
] = TopoExaBGP(self
, name
, ip
=ip
, defaultRoute
=defaultRoute
)
358 return self
.gears
[name
]
360 def add_host(self
, name
, ip
, defaultRoute
):
362 Adds a new host to the topology. This function has the following
364 * `ip`: the peer address (e.g. '1.2.3.4/24')
365 * `defaultRoute`: the peer default route (e.g. 'via 1.2.3.1')
368 name
= "host{}".format(self
.peern
)
369 if name
in self
.gears
:
370 raise KeyError("host already exists")
372 self
.gears
[name
] = TopoHost(self
, name
, ip
=ip
, defaultRoute
=defaultRoute
)
374 return self
.gears
[name
]
376 def add_link(self
, node1
, node2
, ifname1
=None, ifname2
=None):
378 Creates a connection between node1 and node2. The nodes can be the
384 if not isinstance(node1
, TopoGear
):
385 raise ValueError("invalid node1 type")
386 if not isinstance(node2
, TopoGear
):
387 raise ValueError("invalid node2 type")
390 ifname1
= node1
.new_link()
392 ifname2
= node2
.new_link()
394 node1
.register_link(ifname1
, node2
, ifname2
)
395 node2
.register_link(ifname2
, node1
, ifname1
)
396 self
.net
.add_link(node1
.name
, node2
.name
, ifname1
, ifname2
)
398 def get_gears(self
, geartype
):
400 Returns a dictionary of all gears of type `geartype`.
403 * Dictionary iteration:
406 router_dict = tgen.get_gears(TopoRouter)
407 for router_name, router in router_dict.items():
413 peer_list = tgen.get_gears(TopoExaBGP).values()
414 for peer in peer_list:
420 for name
, gear
in self
.gears
.items()
421 if isinstance(gear
, geartype
)
426 Returns the router dictionary (key is the router name and value is the
427 router object itself).
429 return self
.get_gears(TopoRouter
)
431 def exabgp_peers(self
):
433 Returns the exabgp peer dictionary (key is the peer name and value is
434 the peer object itself).
436 return self
.get_gears(TopoExaBGP
)
438 def start_topology(self
):
439 """Starts the topology class."""
440 logger
.info("starting topology: {}".format(self
.modname
))
443 def start_router(self
, router
=None):
445 Call the router startRouter method.
446 If no router is specified it is called for all registred routers.
449 # pylint: disable=r1704
450 # XXX should be hosts?
451 for _
, router
in self
.routers().items():
454 if isinstance(router
, str):
455 router
= self
.gears
[router
]
459 def stop_topology(self
):
461 Stops the network topology. This function will call the stop() function
462 of all gears before calling the mininet stop function, so they can have
463 their oportunity to do a graceful shutdown. stop() is called twice. The
464 first is a simple kill with no sleep, the second will sleep if not
465 killed and try with a different signal.
467 logger
.info("stopping topology: {}".format(self
.modname
))
469 for gear
in self
.gears
.values():
470 errors
+= gear
.stop()
473 "Errors found post shutdown - details follow: {}".format(errors
)
478 def get_exabgp_cmd(self
):
479 if not self
.exabgp_cmd
:
480 self
.exabgp_cmd
= get_exabgp_cmd(self
.net
)
481 return self
.exabgp_cmd
485 Interrupt the test and call the command line interface for manual
486 inspection. Should be only used on non production code.
492 def is_memleak_enabled(self
):
493 "Returns `True` if memory leak report is enable, otherwise `False`."
494 # On router failure we can't run the memory leak test
495 if self
.routers_have_failure():
498 memleak_file
= os
.environ
.get("TOPOTESTS_CHECK_MEMLEAK") or self
.config
.get(
499 self
.CONFIG_SECTION
, "memleak_path"
501 if memleak_file
== "" or memleak_file
== None:
505 def report_memory_leaks(self
, testname
=None):
506 "Run memory leak test and reports."
507 if not self
.is_memleak_enabled():
510 # If no name was specified, use the test module name
512 testname
= self
.modname
514 router_list
= self
.routers().values()
515 for router
in router_list
:
516 router
.report_memory_leaks(self
.modname
)
518 def set_error(self
, message
, code
=None):
519 "Sets an error message and signal other tests to skip."
522 # If no code is defined use a sequential number
524 code
= len(self
.errorsd
)
526 self
.errorsd
[code
] = message
527 self
.errors
+= "\n{}: {}".format(code
, message
)
529 def has_errors(self
):
530 "Returns whether errors exist or not."
531 return len(self
.errorsd
) > 0
533 def routers_have_failure(self
):
534 "Runs an assertion to make sure that all routers are running."
535 if self
.has_errors():
539 router_list
= self
.routers().values()
540 for router
in router_list
:
541 result
= router
.check_router_running()
543 errors
+= result
+ "\n"
546 self
.set_error(errors
, "router_error")
553 # Topology gears (equipment)
557 class TopoGear(object):
558 "Abstract class for type checking"
560 def __init__(self
, tgen
, name
, **params
):
567 # Would be nice for this to point at the gears log directory rather than the
569 self
.logdir
= tgen
.logdir
570 self
.gearlogdir
= None
574 for myif
, dest
in self
.links
.items():
578 links
+= '"{}"<->"{}"'.format(myif
, destif
)
580 return 'TopoGear<name="{}",links=[{}]>'.format(self
.name
, links
)
584 return self
.tgen
.net
[self
.name
]
587 "Basic start function that just reports equipment start"
588 logger
.info('starting "{}"'.format(self
.name
))
590 def stop(self
, wait
=True, assertOnError
=True):
591 "Basic stop function that just reports equipment stop"
592 logger
.info('"{}" base stop called'.format(self
.name
))
595 def cmd(self
, command
, **kwargs
):
597 Runs the provided command string in the router and returns a string
600 return self
.net
.cmd_legacy(command
, **kwargs
)
602 def cmd_raises(self
, command
, **kwargs
):
604 Runs the provided command string in the router and returns a string
605 with the response. Raise an exception on any error.
607 return self
.net
.cmd_raises(command
, **kwargs
)
611 def popen(self
, *params
, **kwargs
):
613 Creates a pipe with the given command. Same args as python Popen.
614 If `command` is a string then will be invoked with shell, otherwise
615 `command` is a list and will be invoked w/o shell. Returns a popen object.
617 return self
.net
.popen(*params
, **kwargs
)
619 def add_link(self
, node
, myif
=None, nodeif
=None):
621 Creates a link (connection) between myself and the specified node.
622 Interfaces name can be speficied with:
623 myif: the interface name that will be created in this node
624 nodeif: the target interface name that will be created on the remote node.
626 self
.tgen
.add_link(self
, node
, myif
, nodeif
)
628 def link_enable(self
, myif
, enabled
=True, netns
=None):
630 Set this node interface administrative state.
631 myif: this node interface name
632 enabled: whether we should enable or disable the interface
634 if myif
not in self
.links
.keys():
635 raise KeyError("interface doesn't exists")
643 'setting node "{}" link "{}" to state "{}"'.format(
644 self
.name
, myif
, operation
648 if netns
is not None:
649 extract
= "ip netns exec {} ".format(netns
)
651 return self
.run("{}ip link set dev {} {}".format(extract
, myif
, operation
))
653 def peer_link_enable(self
, myif
, enabled
=True, netns
=None):
655 Set the peer interface administrative state.
656 myif: this node interface name
657 enabled: whether we should enable or disable the interface
659 NOTE: this is used to simulate a link down on this node, since when the
660 peer disables their interface our interface status changes to no link.
662 if myif
not in self
.links
.keys():
663 raise KeyError("interface doesn't exists")
665 node
, nodeif
= self
.links
[myif
]
666 node
.link_enable(nodeif
, enabled
, netns
)
670 Generates a new unique link name.
672 NOTE: This function should only be called by Topogen.
674 ifname
= "{}-eth{}".format(self
.name
, self
.linkn
)
678 def register_link(self
, myif
, node
, nodeif
):
680 Register link between this node interface and outside node.
682 NOTE: This function should only be called by Topogen.
684 if myif
in self
.links
.keys():
685 raise KeyError("interface already exists")
687 self
.links
[myif
] = (node
, nodeif
)
689 def _setup_tmpdir(self
):
690 topotest
.setup_node_tmpdir(self
.logdir
, self
.name
)
691 self
.gearlogdir
= "{}/{}".format(self
.logdir
, self
.name
)
692 return "{}/{}.log".format(self
.logdir
, self
.name
)
695 class TopoRouter(TopoGear
):
700 # The default required directories by FRR
708 # Router Daemon enumeration definition.
709 RD_FRR
= 0 # not a daemon, but use to setup unified configs
741 RD_STATIC
: "staticd",
750 def __init__(self
, tgen
, cls
, name
, **params
):
752 The constructor has the following parameters:
753 * tgen: Topogen object
754 * cls: router class that will be used to instantiate
756 * daemondir: daemon binary directory
759 super(TopoRouter
, self
).__init
__(tgen
, name
, **params
)
760 self
.routertype
= params
.get("routertype", "frr")
761 if "privateDirs" not in params
:
762 params
["privateDirs"] = self
.PRIVATE_DIRS
764 # Propagate the router log directory
765 logfile
= self
._setup
_tmpdir
()
766 params
["logdir"] = self
.logdir
768 self
.logger
= topolog
.get_logger(name
, log_level
="debug", target
=logfile
)
769 params
["logger"] = self
.logger
770 tgen
.net
.add_host(self
.name
, cls
=cls
, **params
)
771 topotest
.fix_netns_limits(tgen
.net
[name
])
773 # Mount gear log directory on a common path
774 self
.net
.bind_mount(self
.gearlogdir
, "/tmp/gearlogdir")
777 with
open(os
.path
.join(self
.logdir
, self
.name
+ ".pid"), "w") as f
:
778 f
.write(str(self
.net
.pid
) + "\n")
781 gear
= super(TopoRouter
, self
).__str
__()
782 gear
+= " TopoRouter<>"
785 def check_capability(self
, daemon
, param
):
787 Checks a capability daemon against an argument option
788 Return True if capability available. False otherwise
790 daemonstr
= self
.RD
.get(daemon
)
791 self
.logger
.info('check capability {} for "{}"'.format(param
, daemonstr
))
792 return self
.net
.checkCapability(daemonstr
, param
)
794 def load_frr_config(self
, source
, daemons
=None):
796 Loads the unified configuration file source
797 Start the daemons in the list
798 If daemons is None, try to infer daemons from the config file
800 self
.load_config(self
.RD_FRR
, source
)
803 self
.load_config(self
.RD_ZEBRA
)
804 for daemon
in self
.RD
:
805 # This will not work for all daemons
806 daemonstr
= self
.RD
.get(daemon
).rstrip("d")
808 "grep 'router {}' {}".format(daemonstr
, source
)
811 self
.load_config(daemon
)
813 for daemon
in daemons
:
814 self
.load_config(daemon
)
816 def load_config(self
, daemon
, source
=None, param
=None):
817 """Loads daemon configuration from the specified source
818 Possible daemon values are: TopoRouter.RD_ZEBRA, TopoRouter.RD_RIP,
819 TopoRouter.RD_RIPNG, TopoRouter.RD_OSPF, TopoRouter.RD_OSPF6,
820 TopoRouter.RD_ISIS, TopoRouter.RD_BGP, TopoRouter.RD_LDP,
821 TopoRouter.RD_PIM, TopoRouter.RD_PBR, TopoRouter.RD_SNMP.
823 Possible `source` values are `None` for an empty config file, a path name which is
824 used directly, or a file name with no path components which is first looked for
825 directly and then looked for under a sub-directory named after router.
827 This API unfortunately allows for source to not exist for any and
830 daemonstr
= self
.RD
.get(daemon
)
831 self
.logger
.info('loading "{}" configuration: {}'.format(daemonstr
, source
))
832 self
.net
.loadConf(daemonstr
, source
, param
)
834 def check_router_running(self
):
836 Run a series of checks and returns a status string.
838 self
.logger
.info("checking if daemons are running")
839 return self
.net
.checkRouterRunning()
846 * Configure interfaces
847 * Start daemons (e.g. FRR)
848 * Configure daemon logging files
852 result
= nrouter
.startRouter(self
.tgen
)
854 # Enable command logging
856 # Enable all daemon command logging, logging files
857 # and set them to the start dir.
858 for daemon
, enabled
in nrouter
.daemons
.items():
859 if enabled
and daemon
!= "snmpd":
863 "clear log cmdline-targets",
865 "log file {}.log debug".format(daemon
),
867 "log timestamp precision 3",
874 self
.tgen
.set_error(result
)
875 elif nrouter
.daemons
["ldpd"] == 1 or nrouter
.daemons
["pathd"] == 1:
876 # Enable MPLS processing on all interfaces.
877 for interface
in self
.links
:
878 topotest
.sysctl_assure(
879 nrouter
, "net.mpls.conf.{}.input".format(interface
), 1
887 * Signal daemons twice, once with SIGTERM, then with SIGKILL.
889 self
.logger
.debug("stopping (no assert)")
890 return self
.net
.stopRouter(False)
892 def startDaemons(self
, daemons
):
894 Start Daemons: to start specific daemon(user defined daemon only)
895 * Start daemons (e.g. FRR)
896 * Configure daemon logging files
898 self
.logger
.debug("starting")
900 result
= nrouter
.startRouterDaemons(daemons
)
903 daemons
= nrouter
.daemons
.keys()
905 # Enable all daemon command logging, logging files
906 # and set them to the start dir.
907 for daemon
in daemons
:
908 enabled
= nrouter
.daemons
[daemon
]
909 if enabled
and daemon
!= "snmpd":
913 "clear log cmdline-targets",
915 "log file {}.log debug".format(daemon
),
917 "log timestamp precision 3",
924 self
.tgen
.set_error(result
)
928 def killDaemons(self
, daemons
, wait
=True, assertOnError
=True):
930 Kill specific daemon(user defined daemon only)
931 forcefully using SIGKILL
933 self
.logger
.debug("Killing daemons using SIGKILL..")
934 return self
.net
.killRouterDaemons(daemons
, wait
, assertOnError
)
936 def vtysh_cmd(self
, command
, isjson
=False, daemon
=None):
938 Runs the provided command string in the vty shell and returns a string
941 This function also accepts multiple commands, but this mode does not
942 return output for each command. See vtysh_multicmd() for more details.
944 # Detect multi line commands
945 if command
.find("\n") != -1:
946 return self
.vtysh_multicmd(command
, daemon
=daemon
)
949 if daemon
is not None:
950 dparam
+= "-d {}".format(daemon
)
952 vtysh_command
= 'vtysh {} -c "{}" 2>/dev/null'.format(dparam
, command
)
954 self
.logger
.info('vtysh command => "{}"'.format(command
))
955 output
= self
.run(vtysh_command
)
957 dbgout
= output
.strip()
960 dbgout
= dbgout
.replace("\n", "\n\t")
961 self
.logger
.info("vtysh result:\n\t{}".format(dbgout
))
963 self
.logger
.info('vtysh result: "{}"'.format(dbgout
))
969 return json
.loads(output
)
970 except ValueError as error
:
972 "vtysh_cmd: %s: failed to convert json output: %s: %s",
979 def vtysh_multicmd(self
, commands
, pretty_output
=True, daemon
=None):
981 Runs the provided commands in the vty shell and return the result of
984 pretty_output: defines how the return value will be presented. When
985 True it will show the command as they were executed in the vty shell,
986 otherwise it will only show lines that failed.
988 # Prepare the temporary file that will hold the commands
989 fname
= topotest
.get_file(commands
)
992 if daemon
is not None:
993 dparam
+= "-d {}".format(daemon
)
995 # Run the commands and delete the temporary file
997 vtysh_command
= "vtysh {} < {}".format(dparam
, fname
)
999 vtysh_command
= "vtysh {} -f {}".format(dparam
, fname
)
1001 dbgcmds
= commands
if is_string(commands
) else "\n".join(commands
)
1002 dbgcmds
= "\t" + dbgcmds
.replace("\n", "\n\t")
1003 self
.logger
.info("vtysh command => FILE:\n{}".format(dbgcmds
))
1005 res
= self
.run(vtysh_command
)
1008 dbgres
= res
.strip()
1011 dbgres
= dbgres
.replace("\n", "\n\t")
1012 self
.logger
.info("vtysh result:\n\t{}".format(dbgres
))
1014 self
.logger
.info('vtysh result: "{}"'.format(dbgres
))
1017 def report_memory_leaks(self
, testname
):
1019 Runs the router memory leak check test. Has the following parameter:
1020 testname: the test file name for identification
1022 NOTE: to run this you must have the environment variable
1023 TOPOTESTS_CHECK_MEMLEAK set or memleak_path configured in `pytest.ini`.
1026 os
.environ
.get("TOPOTESTS_CHECK_MEMLEAK") or self
.params
["memleak_path"]
1028 if memleak_file
== "" or memleak_file
== None:
1033 self
.logger
.info("running memory leak report")
1034 self
.net
.report_memory_leaks(memleak_file
, testname
)
1036 def version_info(self
):
1037 "Get equipment information from 'show version'."
1038 output
= self
.vtysh_cmd("show version").split("\n")[0]
1039 columns
= topotest
.normalize_text(output
).split(" ")
1043 "version": columns
[1],
1051 def has_version(self
, cmpop
, version
):
1053 Compares router version using operation `cmpop` with `version`.
1054 Valid `cmpop` values:
1055 * `>=`: has the same version or greater
1056 * '>': has greater version
1057 * '=': has the same version
1058 * '<': has a lesser version
1059 * '<=': has the same version or lesser
1061 Usage example: router.has_version('>', '1.0')
1063 return self
.net
.checkRouterVersion(cmpop
, version
)
1065 def has_type(self
, rtype
):
1067 Compares router type with `rtype`. Returns `True` if the type matches,
1070 curtype
= self
.version_info()["type"]
1071 return rtype
== curtype
1074 return self
.net
.hasmpls
1077 class TopoSwitch(TopoGear
):
1079 Switch abstraction. Has the following properties:
1080 * cls: switch class that will be used to instantiate
1084 # pylint: disable=too-few-public-methods
1086 def __init__(self
, tgen
, name
, **params
):
1087 super(TopoSwitch
, self
).__init
__(tgen
, name
, **params
)
1088 tgen
.net
.add_switch(name
)
1091 gear
= super(TopoSwitch
, self
).__str
__()
1092 gear
+= " TopoSwitch<>"
1096 class TopoHost(TopoGear
):
1098 # pylint: disable=too-few-public-methods
1100 def __init__(self
, tgen
, name
, **params
):
1102 Mininet has the following known `params` for hosts:
1103 * `ip`: the IP address (string) for the host interface
1104 * `defaultRoute`: the default route that will be installed
1105 (e.g. 'via 10.0.0.1')
1106 * `privateDirs`: directories that will be mounted on a different domain
1107 (e.g. '/etc/important_dir').
1109 super(TopoHost
, self
).__init
__(tgen
, name
, **params
)
1111 # Propagate the router log directory
1112 logfile
= self
._setup
_tmpdir
()
1113 params
["logdir"] = self
.logdir
1115 # Odd to have 2 logfiles for each host
1116 self
.logger
= topolog
.get_logger(name
, log_level
="debug", target
=logfile
)
1117 params
["logger"] = self
.logger
1118 tgen
.net
.add_host(name
, **params
)
1119 topotest
.fix_netns_limits(tgen
.net
[name
])
1121 # Mount gear log directory on a common path
1122 self
.net
.bind_mount(self
.gearlogdir
, "/tmp/gearlogdir")
1125 gear
= super(TopoHost
, self
).__str
__()
1126 gear
+= ' TopoHost<ip="{}",defaultRoute="{}",privateDirs="{}">'.format(
1128 self
.params
["defaultRoute"],
1129 str(self
.params
["privateDirs"]),
1134 class TopoExaBGP(TopoHost
):
1135 "ExaBGP peer abstraction."
1136 # pylint: disable=too-few-public-methods
1144 def __init__(self
, tgen
, name
, **params
):
1146 ExaBGP usually uses the following parameters:
1147 * `ip`: the IP address (string) for the host interface
1148 * `defaultRoute`: the default route that will be installed
1149 (e.g. 'via 10.0.0.1')
1151 Note: the different between a host and a ExaBGP peer is that this class
1152 has a privateDirs already defined and contains functions to handle ExaBGP
1155 params
["privateDirs"] = self
.PRIVATE_DIRS
1156 super(TopoExaBGP
, self
).__init
__(tgen
, name
, **params
)
1159 gear
= super(TopoExaBGP
, self
).__str
__()
1160 gear
+= " TopoExaBGP<>".format()
1163 def start(self
, peer_dir
, env_file
=None):
1165 Start running ExaBGP daemon:
1166 * Copy all peer* folder contents into /etc/exabgp
1167 * Copy exabgp env file if specified
1168 * Make all python files runnable
1169 * Run ExaBGP with env file `env_file` and configuration peer*/exabgp.cfg
1171 exacmd
= self
.tgen
.get_exabgp_cmd()
1172 assert exacmd
, "Can't find a usabel ExaBGP (must be < version 4)"
1174 self
.run("mkdir -p /etc/exabgp")
1175 self
.run("chmod 755 /etc/exabgp")
1176 self
.run("cp {}/exa-* /etc/exabgp/".format(CWD
))
1177 self
.run("cp {}/* /etc/exabgp/".format(peer_dir
))
1178 if env_file
is not None:
1179 self
.run("cp {} /etc/exabgp/exabgp.env".format(env_file
))
1180 self
.run("chmod 644 /etc/exabgp/*")
1181 self
.run("chmod a+x /etc/exabgp/*.py")
1182 self
.run("chown -R exabgp:exabgp /etc/exabgp")
1184 output
= self
.run(exacmd
+ " -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg")
1185 if output
== None or len(output
) == 0:
1188 logger
.info("{} exabgp started, output={}".format(self
.name
, output
))
1190 def stop(self
, wait
=True, assertOnError
=True):
1191 "Stop ExaBGP peer and kill the daemon"
1192 self
.run("kill `cat /var/run/exabgp/exabgp.pid`")
1197 # Diagnostic function
1200 # Disable linter branch warning. It is expected to have these here.
1201 # pylint: disable=R0912
1202 def diagnose_env_linux(rundir
):
1204 Run diagnostics in the running environment. Returns `True` when everything
1205 is ok, otherwise `False`.
1209 # Load configuration
1210 config
= configparser
.ConfigParser(defaults
=tgen_defaults
)
1211 pytestini_path
= os
.path
.join(CWD
, "../pytest.ini")
1212 config
.read(pytestini_path
)
1214 # Test log path exists before installing handler.
1215 os
.system("mkdir -p " + rundir
)
1216 # Log diagnostics to file so it can be examined later.
1217 fhandler
= logging
.FileHandler(filename
="{}/diagnostics.txt".format(rundir
))
1218 fhandler
.setLevel(logging
.DEBUG
)
1219 fhandler
.setFormatter(logging
.Formatter(fmt
=topolog
.FORMAT
))
1220 logger
.addHandler(fhandler
)
1222 logger
.info("Running environment diagnostics")
1224 # Assert that we are running as root
1225 if os
.getuid() != 0:
1226 logger
.error("you must run topotest as root")
1229 # Assert that we have mininet
1230 # if os.system("which mn >/dev/null 2>/dev/null") != 0:
1231 # logger.error("could not find mininet binary (mininet is not installed)")
1234 # Assert that we have iproute installed
1235 if os
.system("which ip >/dev/null 2>/dev/null") != 0:
1236 logger
.error("could not find ip binary (iproute is not installed)")
1239 # Assert that we have gdb installed
1240 if os
.system("which gdb >/dev/null 2>/dev/null") != 0:
1241 logger
.error("could not find gdb binary (gdb is not installed)")
1244 # Assert that FRR utilities exist
1245 frrdir
= config
.get("topogen", "frrdir")
1246 if not os
.path
.isdir(frrdir
):
1247 logger
.error("could not find {} directory".format(frrdir
))
1251 pwd
.getpwnam("frr")[2]
1253 logger
.warning('could not find "frr" user')
1256 grp
.getgrnam("frr")[2]
1258 logger
.warning('could not find "frr" group')
1261 if "frr" not in grp
.getgrnam("frrvty").gr_mem
:
1263 '"frr" user and group exist, but user is not under "frrvty"'
1266 logger
.warning('could not find "frrvty" group')
1280 path
= os
.path
.join(frrdir
, fname
)
1281 if not os
.path
.isfile(path
):
1282 # LDPd is an exception
1285 "could not find {} in {}".format(fname
, frrdir
)
1286 + "(LDPd tests will not run)"
1290 logger
.warning("could not find {} in {}".format(fname
, frrdir
))
1293 if fname
!= "zebra":
1296 os
.system("{} -v 2>&1 >{}/frr_zebra.txt".format(path
, rundir
))
1298 # Test MPLS availability
1299 krel
= platform
.release()
1300 if topotest
.version_cmp(krel
, "4.5") < 0:
1302 'LDPd tests will not run (have kernel "{}", but it requires 4.5)'.format(
1307 # Test for MPLS Kernel modules available
1308 if not topotest
.module_present("mpls-router", load
=False) != 0:
1309 logger
.info("LDPd tests will not run (missing mpls-router kernel module)")
1310 if not topotest
.module_present("mpls-iptunnel", load
=False) != 0:
1311 logger
.info("LDPd tests will not run (missing mpls-iptunnel kernel module)")
1313 if not get_exabgp_cmd():
1314 logger
.warning("Failed to find exabgp < 4")
1316 logger
.removeHandler(fhandler
)
1322 def diagnose_env_freebsd():
1326 def diagnose_env(rundir
):
1327 if sys
.platform
.startswith("linux"):
1328 return diagnose_env_linux(rundir
)
1329 elif sys
.platform
.startswith("freebsd"):
1330 return diagnose_env_freebsd()