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