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