]> git.proxmox.com Git - mirror_frr.git/blame - tests/topotests/lib/topogen.py
Merge pull request #13649 from donaldsharp/unlock_the_node_or_else
[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):
a53c08bc
CH
239 keylist = (
240 topodef.keys()
241 if isinstance(topodef, OrderedDict)
242 else sorted(topodef.keys())
243 )
49581587
CH
244 # ---------------------------
245 # Create all referenced hosts
246 # ---------------------------
247 for oname in keylist:
248 tup = (topodef[oname],) if is_string(topodef[oname]) else topodef[oname]
249 for e in tup:
250 desc = e.split(":")
251 name = desc[0]
252 if name not in self.gears:
253 logging.debug("Adding router: %s", name)
254 self.add_router(name)
255
256 # ------------------------------
257 # Create all referenced switches
258 # ------------------------------
259 for oname in keylist:
260 if oname is not None and oname not in self.gears:
261 logging.debug("Adding switch: %s", oname)
262 self.add_switch(oname)
263
264 # ----------------
265 # Create all links
266 # ----------------
267 for oname in keylist:
268 if oname is None:
269 continue
270 tup = (topodef[oname],) if is_string(topodef[oname]) else topodef[oname]
271 for e in tup:
272 desc = e.split(":")
273 name = desc[0]
274 ifname = desc[1] if len(desc) > 1 else None
275 sifname = desc[2] if len(desc) > 2 else None
276 self.add_link(self.gears[oname], self.gears[name], sifname, ifname)
277
278 self.net.configure_hosts()
1fca63c1 279
edd2bdf6
RZ
280 def _load_config(self):
281 """
282 Loads the configuration file `pytest.ini` located at the root dir of
283 topotests.
284 """
a07e17d5 285 self.config = configparser.ConfigParser(tgen_defaults)
787e7624 286 pytestini_path = os.path.join(CWD, "../pytest.ini")
edd2bdf6
RZ
287 self.config.read(pytestini_path)
288
49581587 289 def add_router(self, name=None, cls=None, **params):
1fca63c1
RZ
290 """
291 Adds a new router to the topology. This function has the following
292 options:
edd2bdf6
RZ
293 * `name`: (optional) select the router name
294 * `daemondir`: (optional) custom daemon binary directory
622c4996 295 * `routertype`: (optional) `frr`
1fca63c1
RZ
296 Returns a TopoRouter.
297 """
49581587
CH
298 if cls is None:
299 cls = topotest.Router
1fca63c1 300 if name is None:
787e7624 301 name = "r{}".format(self.routern)
1fca63c1 302 if name in self.gears:
787e7624 303 raise KeyError("router already exists")
1fca63c1 304
787e7624 305 params["frrdir"] = self.config.get(self.CONFIG_SECTION, "frrdir")
787e7624 306 params["memleak_path"] = self.config.get(self.CONFIG_SECTION, "memleak_path")
11761ab0 307 if "routertype" not in params:
787e7624 308 params["routertype"] = self.config.get(self.CONFIG_SECTION, "routertype")
edd2bdf6 309
2ab85530 310 self.gears[name] = TopoRouter(self, cls, name, **params)
1fca63c1
RZ
311 self.routern += 1
312 return self.gears[name]
313
49581587 314 def add_switch(self, name=None):
1fca63c1
RZ
315 """
316 Adds a new switch to the topology. This function has the following
317 options:
318 name: (optional) select the switch name
319 Returns the switch name and number.
320 """
321 if name is None:
787e7624 322 name = "s{}".format(self.switchn)
1fca63c1 323 if name in self.gears:
787e7624 324 raise KeyError("switch already exists")
1fca63c1 325
49581587 326 self.gears[name] = TopoSwitch(self, name)
1fca63c1
RZ
327 self.switchn += 1
328 return self.gears[name]
329
19ccab57
RZ
330 def add_exabgp_peer(self, name, ip, defaultRoute):
331 """
332 Adds a new ExaBGP peer to the topology. This function has the following
333 parameters:
334 * `ip`: the peer address (e.g. '1.2.3.4/24')
335 * `defaultRoute`: the peer default route (e.g. 'via 1.2.3.1')
336 """
337 if name is None:
787e7624 338 name = "peer{}".format(self.peern)
19ccab57 339 if name in self.gears:
787e7624 340 raise KeyError("exabgp peer already exists")
19ccab57
RZ
341
342 self.gears[name] = TopoExaBGP(self, name, ip=ip, defaultRoute=defaultRoute)
343 self.peern += 1
344 return self.gears[name]
345
ab59579a
RZ
346 def add_host(self, name, ip, defaultRoute):
347 """
348 Adds a new host to the topology. This function has the following
349 parameters:
350 * `ip`: the peer address (e.g. '1.2.3.4/24')
351 * `defaultRoute`: the peer default route (e.g. 'via 1.2.3.1')
352 """
353 if name is None:
354 name = "host{}".format(self.peern)
355 if name in self.gears:
356 raise KeyError("host already exists")
357
358 self.gears[name] = TopoHost(self, name, ip=ip, defaultRoute=defaultRoute)
359 self.peern += 1
360 return self.gears[name]
361
1fca63c1
RZ
362 def add_link(self, node1, node2, ifname1=None, ifname2=None):
363 """
364 Creates a connection between node1 and node2. The nodes can be the
365 following:
366 * TopoGear
367 * TopoRouter
368 * TopoSwitch
369 """
370 if not isinstance(node1, TopoGear):
787e7624 371 raise ValueError("invalid node1 type")
1fca63c1 372 if not isinstance(node2, TopoGear):
787e7624 373 raise ValueError("invalid node2 type")
1fca63c1
RZ
374
375 if ifname1 is None:
8c3fdf62 376 ifname1 = node1.new_link()
1fca63c1 377 if ifname2 is None:
8c3fdf62
RZ
378 ifname2 = node2.new_link()
379
380 node1.register_link(ifname1, node2, ifname2)
381 node2.register_link(ifname2, node1, ifname1)
fe50239b 382 self.net.add_link(node1.name, node2.name, ifname1, ifname2)
1fca63c1 383
19ccab57
RZ
384 def get_gears(self, geartype):
385 """
386 Returns a dictionary of all gears of type `geartype`.
387
388 Normal usage:
389 * Dictionary iteration:
390 ```py
391 tgen = get_topogen()
392 router_dict = tgen.get_gears(TopoRouter)
e5f0ed14 393 for router_name, router in router_dict.items():
19ccab57
RZ
394 # Do stuff
395 ```
396 * List iteration:
397 ```py
398 tgen = get_topogen()
399 peer_list = tgen.get_gears(TopoExaBGP).values()
400 for peer in peer_list:
401 # Do stuff
402 ```
403 """
787e7624 404 return dict(
405 (name, gear)
e5f0ed14 406 for name, gear in self.gears.items()
787e7624 407 if isinstance(gear, geartype)
408 )
19ccab57 409
1fca63c1
RZ
410 def routers(self):
411 """
412 Returns the router dictionary (key is the router name and value is the
413 router object itself).
414 """
19ccab57
RZ
415 return self.get_gears(TopoRouter)
416
417 def exabgp_peers(self):
418 """
419 Returns the exabgp peer dictionary (key is the peer name and value is
420 the peer object itself).
421 """
422 return self.get_gears(TopoExaBGP)
1fca63c1 423
49581587
CH
424 def start_topology(self):
425 """Starts the topology class."""
787e7624 426 logger.info("starting topology: {}".format(self.modname))
1fca63c1
RZ
427 self.net.start()
428
429 def start_router(self, router=None):
430 """
431 Call the router startRouter method.
b515c81a 432 If no router is specified it is called for all registered routers.
1fca63c1
RZ
433 """
434 if router is None:
435 # pylint: disable=r1704
49581587 436 # XXX should be hosts?
e5f0ed14 437 for _, router in self.routers().items():
1fca63c1
RZ
438 router.start()
439 else:
440 if isinstance(router, str):
441 router = self.gears[router]
442
443 router.start()
444
445 def stop_topology(self):
4cfdff1a
RZ
446 """
447 Stops the network topology. This function will call the stop() function
448 of all gears before calling the mininet stop function, so they can have
3a568b9c
LB
449 their oportunity to do a graceful shutdown. stop() is called twice. The
450 first is a simple kill with no sleep, the second will sleep if not
451 killed and try with a different signal.
4cfdff1a 452 """
773fd82e
CH
453 pause = bool(self.net.cfgopt.get_option("--pause-at-end"))
454 pause = pause or bool(self.net.cfgopt.get_option("--pause"))
455 if pause:
456 try:
457 pause_test("Before MUNET delete")
458 except KeyboardInterrupt:
459 print("^C...continuing")
460 except Exception as error:
461 self.logger.error("\n...continuing after error: %s", error)
462
787e7624 463 logger.info("stopping topology: {}".format(self.modname))
773fd82e 464
95460a6b 465 errors = ""
4cfdff1a 466 for gear in self.gears.values():
942a224e 467 errors += gear.stop()
95460a6b 468 if len(errors) > 0:
9fa6ec14 469 logger.error(
470 "Errors found post shutdown - details follow: {}".format(errors)
471 )
4cfdff1a 472
1fca63c1
RZ
473 self.net.stop()
474
49581587
CH
475 def get_exabgp_cmd(self):
476 if not self.exabgp_cmd:
477 self.exabgp_cmd = get_exabgp_cmd(self.net)
478 return self.exabgp_cmd
479
480 def cli(self):
1fca63c1
RZ
481 """
482 Interrupt the test and call the command line interface for manual
483 inspection. Should be only used on non production code.
484 """
49581587 485 self.net.cli()
1fca63c1 486
49581587 487 mininet_cli = cli
1fca63c1 488
13e1fc49
RZ
489 def is_memleak_enabled(self):
490 "Returns `True` if memory leak report is enable, otherwise `False`."
78ed6123
RZ
491 # On router failure we can't run the memory leak test
492 if self.routers_have_failure():
493 return False
494
787e7624 495 memleak_file = os.environ.get("TOPOTESTS_CHECK_MEMLEAK") or self.config.get(
496 self.CONFIG_SECTION, "memleak_path"
497 )
f637ac01 498 if memleak_file == "" or memleak_file is None:
13e1fc49
RZ
499 return False
500 return True
501
502 def report_memory_leaks(self, testname=None):
503 "Run memory leak test and reports."
504 if not self.is_memleak_enabled():
505 return
506
507 # If no name was specified, use the test module name
508 if testname is None:
509 testname = self.modname
510
511 router_list = self.routers().values()
512 for router in router_list:
513 router.report_memory_leaks(self.modname)
514
393ca0fa
RZ
515 def set_error(self, message, code=None):
516 "Sets an error message and signal other tests to skip."
e8f7a22f 517 logger.info("setting error msg: %s", message)
393ca0fa
RZ
518
519 # If no code is defined use a sequential number
520 if code is None:
1eb633c0 521 code = len(self.errorsd)
393ca0fa 522
1eb633c0 523 self.errorsd[code] = message
787e7624 524 self.errors += "\n{}: {}".format(code, message)
393ca0fa
RZ
525
526 def has_errors(self):
527 "Returns whether errors exist or not."
1eb633c0 528 return len(self.errorsd) > 0
13e1fc49 529
78ed6123
RZ
530 def routers_have_failure(self):
531 "Runs an assertion to make sure that all routers are running."
532 if self.has_errors():
533 return True
534
787e7624 535 errors = ""
78ed6123
RZ
536 router_list = self.routers().values()
537 for router in router_list:
538 result = router.check_router_running()
787e7624 539 if result != "":
540 errors += result + "\n"
78ed6123 541
787e7624 542 if errors != "":
543 self.set_error(errors, "router_error")
46325763 544 assert False, errors
78ed6123
RZ
545 return True
546 return False
547
787e7624 548
1fca63c1
RZ
549#
550# Topology gears (equipment)
551#
552
787e7624 553
1fca63c1
RZ
554class TopoGear(object):
555 "Abstract class for type checking"
556
49581587
CH
557 def __init__(self, tgen, name, **params):
558 self.tgen = tgen
559 self.name = name
560 self.params = params
8c3fdf62 561 self.links = {}
1fca63c1
RZ
562 self.linkn = 0
563
49581587
CH
564 # Would be nice for this to point at the gears log directory rather than the
565 # test's.
566 self.logdir = tgen.logdir
567 self.gearlogdir = None
568
7326ea11 569 def __str__(self):
787e7624 570 links = ""
e5f0ed14 571 for myif, dest in self.links.items():
7326ea11 572 _, destif = dest
787e7624 573 if links != "":
574 links += ","
7326ea11
RZ
575 links += '"{}"<->"{}"'.format(myif, destif)
576
577 return 'TopoGear<name="{}",links=[{}]>'.format(self.name, links)
578
49581587
CH
579 @property
580 def net(self):
581 return self.tgen.net[self.name]
582
4cfdff1a
RZ
583 def start(self):
584 "Basic start function that just reports equipment start"
585 logger.info('starting "{}"'.format(self.name))
586
95460a6b 587 def stop(self, wait=True, assertOnError=True):
49581587
CH
588 "Basic stop function that just reports equipment stop"
589 logger.info('"{}" base stop called'.format(self.name))
95460a6b 590 return ""
4cfdff1a 591
49581587 592 def cmd(self, command, **kwargs):
8c3fdf62
RZ
593 """
594 Runs the provided command string in the router and returns a string
595 with the response.
596 """
49581587
CH
597 return self.net.cmd_legacy(command, **kwargs)
598
599 def cmd_raises(self, command, **kwargs):
600 """
601 Runs the provided command string in the router and returns a string
602 with the response. Raise an exception on any error.
603 """
604 return self.net.cmd_raises(command, **kwargs)
605
606 run = cmd
8c3fdf62 607
35c4c991
CH
608 def popen(self, *params, **kwargs):
609 """
49581587
CH
610 Creates a pipe with the given command. Same args as python Popen.
611 If `command` is a string then will be invoked with shell, otherwise
612 `command` is a list and will be invoked w/o shell. Returns a popen object.
35c4c991 613 """
49581587 614 return self.net.popen(*params, **kwargs)
35c4c991 615
1fca63c1
RZ
616 def add_link(self, node, myif=None, nodeif=None):
617 """
618 Creates a link (connection) between myself and the specified node.
619 Interfaces name can be speficied with:
620 myif: the interface name that will be created in this node
621 nodeif: the target interface name that will be created on the remote node.
622 """
623 self.tgen.add_link(self, node, myif, nodeif)
624
0ab7733f 625 def link_enable(self, myif, enabled=True, netns=None):
1fca63c1 626 """
8c3fdf62
RZ
627 Set this node interface administrative state.
628 myif: this node interface name
629 enabled: whether we should enable or disable the interface
1fca63c1 630 """
8c3fdf62 631 if myif not in self.links.keys():
787e7624 632 raise KeyError("interface doesn't exists")
8c3fdf62
RZ
633
634 if enabled is True:
787e7624 635 operation = "up"
8c3fdf62 636 else:
787e7624 637 operation = "down"
8c3fdf62 638
787e7624 639 logger.info(
640 'setting node "{}" link "{}" to state "{}"'.format(
641 self.name, myif, operation
642 )
643 )
644 extract = ""
0ab7733f 645 if netns is not None:
787e7624 646 extract = "ip netns exec {} ".format(netns)
49581587 647
787e7624 648 return self.run("{}ip link set dev {} {}".format(extract, myif, operation))
8c3fdf62 649
0ab7733f 650 def peer_link_enable(self, myif, enabled=True, netns=None):
8c3fdf62
RZ
651 """
652 Set the peer interface administrative state.
653 myif: this node interface name
654 enabled: whether we should enable or disable the interface
655
656 NOTE: this is used to simulate a link down on this node, since when the
657 peer disables their interface our interface status changes to no link.
658 """
659 if myif not in self.links.keys():
787e7624 660 raise KeyError("interface doesn't exists")
8c3fdf62
RZ
661
662 node, nodeif = self.links[myif]
0ab7733f 663 node.link_enable(nodeif, enabled, netns)
1fca63c1 664
8c3fdf62
RZ
665 def new_link(self):
666 """
667 Generates a new unique link name.
668
669 NOTE: This function should only be called by Topogen.
670 """
787e7624 671 ifname = "{}-eth{}".format(self.name, self.linkn)
1fca63c1 672 self.linkn += 1
1fca63c1
RZ
673 return ifname
674
8c3fdf62
RZ
675 def register_link(self, myif, node, nodeif):
676 """
677 Register link between this node interface and outside node.
678
679 NOTE: This function should only be called by Topogen.
680 """
681 if myif in self.links.keys():
787e7624 682 raise KeyError("interface already exists")
8c3fdf62
RZ
683
684 self.links[myif] = (node, nodeif)
685
49581587
CH
686 def _setup_tmpdir(self):
687 topotest.setup_node_tmpdir(self.logdir, self.name)
688 self.gearlogdir = "{}/{}".format(self.logdir, self.name)
689 return "{}/{}.log".format(self.logdir, self.name)
690
787e7624 691
1fca63c1
RZ
692class TopoRouter(TopoGear):
693 """
d9ea1cda 694 Router abstraction.
1fca63c1
RZ
695 """
696
622c4996 697 # The default required directories by FRR
1fca63c1 698 PRIVATE_DIRS = [
787e7624 699 "/etc/frr",
49581587 700 "/etc/snmp",
787e7624 701 "/var/run/frr",
787e7624 702 "/var/log",
1fca63c1
RZ
703 ]
704
705 # Router Daemon enumeration definition.
a4b4bb50 706 RD_FRR = 0 # not a daemon, but use to setup unified configs
7326ea11 707 RD_ZEBRA = 1
1fca63c1
RZ
708 RD_RIP = 2
709 RD_RIPNG = 3
710 RD_OSPF = 4
711 RD_OSPF6 = 5
712 RD_ISIS = 6
713 RD_BGP = 7
714 RD_LDP = 8
715 RD_PIM = 9
c267e5b1
RZ
716 RD_EIGRP = 10
717 RD_NHRP = 11
a2a1134c 718 RD_STATIC = 12
4d45d6d3 719 RD_BFD = 13
a38f0083 720 RD_SHARP = 14
a0764a36 721 RD_BABEL = 15
223f87f4 722 RD_PBRD = 16
4d7b695d 723 RD_PATH = 17
92be50e6 724 RD_SNMP = 18
e13f9c4f 725 RD_PIM6 = 19
f637ac01 726 RD_MGMTD = 20
1fca63c1 727 RD = {
a4b4bb50 728 RD_FRR: "frr",
787e7624 729 RD_ZEBRA: "zebra",
730 RD_RIP: "ripd",
731 RD_RIPNG: "ripngd",
732 RD_OSPF: "ospfd",
733 RD_OSPF6: "ospf6d",
734 RD_ISIS: "isisd",
735 RD_BGP: "bgpd",
736 RD_PIM: "pimd",
e13f9c4f 737 RD_PIM6: "pim6d",
787e7624 738 RD_LDP: "ldpd",
739 RD_EIGRP: "eigrpd",
740 RD_NHRP: "nhrpd",
741 RD_STATIC: "staticd",
742 RD_BFD: "bfdd",
743 RD_SHARP: "sharpd",
a0764a36 744 RD_BABEL: "babeld",
223f87f4 745 RD_PBRD: "pbrd",
92be50e6
BC
746 RD_PATH: "pathd",
747 RD_SNMP: "snmpd",
f637ac01 748 RD_MGMTD: "mgmtd",
1fca63c1
RZ
749 }
750
2ab85530 751 def __init__(self, tgen, cls, name, **params):
d9ea1cda
RZ
752 """
753 The constructor has the following parameters:
754 * tgen: Topogen object
755 * cls: router class that will be used to instantiate
756 * name: router name
757 * daemondir: daemon binary directory
622c4996 758 * routertype: 'frr'
d9ea1cda 759 """
49581587 760 super(TopoRouter, self).__init__(tgen, name, **params)
787e7624 761 self.routertype = params.get("routertype", "frr")
60e03778
CH
762 if "private_mounts" not in params:
763 params["private_mounts"] = self.PRIVATE_DIRS
c540096e 764
13e1fc49 765 # Propagate the router log directory
49581587 766 logfile = self._setup_tmpdir()
787e7624 767 params["logdir"] = self.logdir
13e1fc49 768
49581587
CH
769 self.logger = topolog.get_logger(name, log_level="debug", target=logfile)
770 params["logger"] = self.logger
771 tgen.net.add_host(self.name, cls=cls, **params)
772 topotest.fix_netns_limits(tgen.net[name])
87ba6e1e 773
49581587
CH
774 # Mount gear log directory on a common path
775 self.net.bind_mount(self.gearlogdir, "/tmp/gearlogdir")
1fca63c1 776
e03862c3
HS
777 # Ensure pid file
778 with open(os.path.join(self.logdir, self.name + ".pid"), "w") as f:
c84159a9 779 f.write(str(self.net.pid) + "\n")
e03862c3 780
7326ea11
RZ
781 def __str__(self):
782 gear = super(TopoRouter, self).__str__()
787e7624 783 gear += " TopoRouter<>"
7326ea11
RZ
784 return gear
785
8dd5077d
PG
786 def check_capability(self, daemon, param):
787 """
788 Checks a capability daemon against an argument option
789 Return True if capability available. False otherwise
790 """
791 daemonstr = self.RD.get(daemon)
792 self.logger.info('check capability {} for "{}"'.format(param, daemonstr))
49581587 793 return self.net.checkCapability(daemonstr, param)
8dd5077d 794
a4b4bb50
JAG
795 def load_frr_config(self, source, daemons=None):
796 """
797 Loads the unified configuration file source
798 Start the daemons in the list
799 If daemons is None, try to infer daemons from the config file
800 """
27c6bfc2 801 source_path = self.load_config(self.RD_FRR, source)
a4b4bb50
JAG
802 if not daemons:
803 # Always add zebra
27c6bfc2 804 self.load_config(self.RD_ZEBRA, "")
a4b4bb50
JAG
805 for daemon in self.RD:
806 # This will not work for all daemons
807 daemonstr = self.RD.get(daemon).rstrip("d")
93a7f236 808 if daemonstr == "pim":
27c6bfc2 809 grep_cmd = "grep 'ip {}' {}".format(daemonstr, source_path)
93a7f236 810 else:
27c6bfc2 811 grep_cmd = "grep 'router {}' {}".format(daemonstr, source_path)
19003d6e 812 result = self.run(grep_cmd, warn=False).strip()
a4b4bb50 813 if result:
27c6bfc2 814 self.load_config(daemon, "")
a4b4bb50
JAG
815 else:
816 for daemon in daemons:
27c6bfc2 817 self.load_config(daemon, "")
a4b4bb50 818
8dd5077d 819 def load_config(self, daemon, source=None, param=None):
02547745 820 """Loads daemon configuration from the specified source
1fca63c1
RZ
821 Possible daemon values are: TopoRouter.RD_ZEBRA, TopoRouter.RD_RIP,
822 TopoRouter.RD_RIPNG, TopoRouter.RD_OSPF, TopoRouter.RD_OSPF6,
823 TopoRouter.RD_ISIS, TopoRouter.RD_BGP, TopoRouter.RD_LDP,
e13f9c4f 824 TopoRouter.RD_PIM, TopoRouter.RD_PIM6, TopoRouter.RD_PBR,
f637ac01 825 TopoRouter.RD_SNMP, TopoRouter.RD_MGMTD.
49581587 826
02547745
CH
827 Possible `source` values are `None` for an empty config file, a path name which is
828 used directly, or a file name with no path components which is first looked for
829 directly and then looked for under a sub-directory named after router.
830
49581587
CH
831 This API unfortunately allows for source to not exist for any and
832 all routers.
1fca63c1
RZ
833 """
834 daemonstr = self.RD.get(daemon)
e8f7a22f 835 self.logger.debug('loading "{}" configuration: {}'.format(daemonstr, source))
27c6bfc2 836 return self.net.loadConf(daemonstr, source, param)
1fca63c1
RZ
837
838 def check_router_running(self):
839 """
840 Run a series of checks and returns a status string.
841 """
787e7624 842 self.logger.info("checking if daemons are running")
49581587 843 return self.net.checkRouterRunning()
1fca63c1
RZ
844
845 def start(self):
846 """
847 Start router:
848 * Load modules
849 * Clean up files
850 * Configure interfaces
622c4996 851 * Start daemons (e.g. FRR)
f6899d4d 852 * Configure daemon logging files
1fca63c1 853 """
49581587
CH
854
855 nrouter = self.net
9711fc7e 856 result = nrouter.startRouter(self.tgen)
f6899d4d 857
49581587
CH
858 # Enable command logging
859
deb4cef0
LB
860 # Enable all daemon command logging, logging files
861 # and set them to the start dir.
e5f0ed14 862 for daemon, enabled in nrouter.daemons.items():
49581587
CH
863 if enabled and daemon != "snmpd":
864 self.vtysh_cmd(
a53c08bc
CH
865 "\n".join(
866 [
867 "clear log cmdline-targets",
868 "conf t",
869 "log file {}.log debug".format(daemon),
870 "log commands",
3e097918 871 "log timestamp precision 6",
a53c08bc
CH
872 ]
873 ),
49581587
CH
874 daemon=daemon,
875 )
f6899d4d 876
787e7624 877 if result != "":
57c5075b 878 self.tgen.set_error(result)
49581587 879 elif nrouter.daemons["ldpd"] == 1 or nrouter.daemons["pathd"] == 1:
a89241b4 880 # Enable MPLS processing on all interfaces.
49581587 881 for interface in self.links:
a53c08bc
CH
882 topotest.sysctl_assure(
883 nrouter, "net.mpls.conf.{}.input".format(interface), 1
884 )
57c5075b 885
f6899d4d 886 return result
1fca63c1 887
942a224e
MS
888 def stop(self):
889 """
890 Stop router cleanly:
49581587 891 * Signal daemons twice, once with SIGTERM, then with SIGKILL.
942a224e 892 """
49581587
CH
893 self.logger.debug("stopping (no assert)")
894 return self.net.stopRouter(False)
942a224e 895
c65a7e26
KK
896 def startDaemons(self, daemons):
897 """
898 Start Daemons: to start specific daemon(user defined daemon only)
622c4996 899 * Start daemons (e.g. FRR)
c65a7e26
KK
900 * Configure daemon logging files
901 """
701a0192 902 self.logger.debug("starting")
49581587 903 nrouter = self.net
c65a7e26
KK
904 result = nrouter.startRouterDaemons(daemons)
905
49581587
CH
906 if daemons is None:
907 daemons = nrouter.daemons.keys()
908
c65a7e26
KK
909 # Enable all daemon command logging, logging files
910 # and set them to the start dir.
49581587
CH
911 for daemon in daemons:
912 enabled = nrouter.daemons[daemon]
913 if enabled and daemon != "snmpd":
701a0192 914 self.vtysh_cmd(
a53c08bc
CH
915 "\n".join(
916 [
917 "clear log cmdline-targets",
918 "conf t",
919 "log file {}.log debug".format(daemon),
920 "log commands",
3e097918 921 "log timestamp precision 6",
a53c08bc
CH
922 ]
923 ),
701a0192 924 daemon=daemon,
925 )
c65a7e26 926
701a0192 927 if result != "":
c65a7e26
KK
928 self.tgen.set_error(result)
929
930 return result
931
932 def killDaemons(self, daemons, wait=True, assertOnError=True):
933 """
934 Kill specific daemon(user defined daemon only)
935 forcefully using SIGKILL
936 """
701a0192 937 self.logger.debug("Killing daemons using SIGKILL..")
49581587 938 return self.net.killRouterDaemons(daemons, wait, assertOnError)
c65a7e26 939
f9b48d8b 940 def vtysh_cmd(self, command, isjson=False, daemon=None):
1fca63c1
RZ
941 """
942 Runs the provided command string in the vty shell and returns a string
943 with the response.
944
945 This function also accepts multiple commands, but this mode does not
946 return output for each command. See vtysh_multicmd() for more details.
947 """
948 # Detect multi line commands
787e7624 949 if command.find("\n") != -1:
f9b48d8b
RZ
950 return self.vtysh_multicmd(command, daemon=daemon)
951
787e7624 952 dparam = ""
f9b48d8b 953 if daemon is not None:
787e7624 954 dparam += "-d {}".format(daemon)
f9b48d8b 955
b5c12fb1
CH
956 vtysh_command = "vtysh {} -c {} 2>/dev/null".format(
957 dparam, shlex.quote(command)
958 )
1fca63c1 959
b5c12fb1 960 self.logger.debug("vtysh command => {}".format(shlex.quote(command)))
a40daddc 961 output = self.run(vtysh_command)
49581587
CH
962
963 dbgout = output.strip()
964 if dbgout:
965 if "\n" in dbgout:
966 dbgout = dbgout.replace("\n", "\n\t")
e8f7a22f 967 self.logger.debug("vtysh result:\n\t{}".format(dbgout))
49581587 968 else:
e8f7a22f 969 self.logger.debug('vtysh result: "{}"'.format(dbgout))
49581587 970
a40daddc
RZ
971 if isjson is False:
972 return output
973
7b093d84
RZ
974 try:
975 return json.loads(output)
0ba1d257 976 except ValueError as error:
a53c08bc
CH
977 logger.warning(
978 "vtysh_cmd: %s: failed to convert json output: %s: %s",
979 self.name,
980 str(output),
981 str(error),
982 )
7b093d84 983 return {}
1fca63c1 984
f9b48d8b 985 def vtysh_multicmd(self, commands, pretty_output=True, daemon=None):
1fca63c1
RZ
986 """
987 Runs the provided commands in the vty shell and return the result of
988 execution.
989
990 pretty_output: defines how the return value will be presented. When
991 True it will show the command as they were executed in the vty shell,
992 otherwise it will only show lines that failed.
993 """
994 # Prepare the temporary file that will hold the commands
995 fname = topotest.get_file(commands)
996
787e7624 997 dparam = ""
f9b48d8b 998 if daemon is not None:
787e7624 999 dparam += "-d {}".format(daemon)
f9b48d8b 1000
1fca63c1
RZ
1001 # Run the commands and delete the temporary file
1002 if pretty_output:
787e7624 1003 vtysh_command = "vtysh {} < {}".format(dparam, fname)
1fca63c1 1004 else:
787e7624 1005 vtysh_command = "vtysh {} -f {}".format(dparam, fname)
1fca63c1 1006
49581587
CH
1007 dbgcmds = commands if is_string(commands) else "\n".join(commands)
1008 dbgcmds = "\t" + dbgcmds.replace("\n", "\n\t")
e8f7a22f 1009 self.logger.debug("vtysh command => FILE:\n{}".format(dbgcmds))
49581587 1010
1fca63c1
RZ
1011 res = self.run(vtysh_command)
1012 os.unlink(fname)
1013
49581587
CH
1014 dbgres = res.strip()
1015 if dbgres:
1016 if "\n" in dbgres:
1017 dbgres = dbgres.replace("\n", "\n\t")
e8f7a22f 1018 self.logger.debug("vtysh result:\n\t{}".format(dbgres))
49581587 1019 else:
e8f7a22f 1020 self.logger.debug('vtysh result: "{}"'.format(dbgres))
1fca63c1
RZ
1021 return res
1022
38c39932
RZ
1023 def report_memory_leaks(self, testname):
1024 """
1025 Runs the router memory leak check test. Has the following parameter:
1026 testname: the test file name for identification
1027
1028 NOTE: to run this you must have the environment variable
c540096e 1029 TOPOTESTS_CHECK_MEMLEAK set or memleak_path configured in `pytest.ini`.
38c39932 1030 """
787e7624 1031 memleak_file = (
49581587 1032 os.environ.get("TOPOTESTS_CHECK_MEMLEAK") or self.params["memleak_path"]
787e7624 1033 )
f637ac01 1034 if memleak_file == "" or memleak_file is None:
38c39932
RZ
1035 return
1036
942a224e 1037 self.stop()
d2bdb82f 1038
787e7624 1039 self.logger.info("running memory leak report")
49581587 1040 self.net.report_memory_leaks(memleak_file, testname)
38c39932 1041
6ca2411e
RZ
1042 def version_info(self):
1043 "Get equipment information from 'show version'."
787e7624 1044 output = self.vtysh_cmd("show version").split("\n")[0]
1045 columns = topotest.normalize_text(output).split(" ")
b3b1b1d1
RZ
1046 try:
1047 return {
787e7624 1048 "type": columns[0],
1049 "version": columns[1],
b3b1b1d1
RZ
1050 }
1051 except IndexError:
1052 return {
787e7624 1053 "type": None,
1054 "version": None,
b3b1b1d1 1055 }
6ca2411e
RZ
1056
1057 def has_version(self, cmpop, version):
1058 """
1059 Compares router version using operation `cmpop` with `version`.
1060 Valid `cmpop` values:
1061 * `>=`: has the same version or greater
1062 * '>': has greater version
1063 * '=': has the same version
1064 * '<': has a lesser version
1065 * '<=': has the same version or lesser
1066
1067 Usage example: router.has_version('>', '1.0')
1068 """
49581587 1069 return self.net.checkRouterVersion(cmpop, version)
6ca2411e
RZ
1070
1071 def has_type(self, rtype):
1072 """
1073 Compares router type with `rtype`. Returns `True` if the type matches,
1074 otherwise `false`.
1075 """
787e7624 1076 curtype = self.version_info()["type"]
6ca2411e
RZ
1077 return rtype == curtype
1078
447f2d5a 1079 def has_mpls(self):
49581587 1080 return self.net.hasmpls
447f2d5a 1081
787e7624 1082
1fca63c1
RZ
1083class TopoSwitch(TopoGear):
1084 """
1085 Switch abstraction. Has the following properties:
1086 * cls: switch class that will be used to instantiate
1087 * name: switch name
1088 """
787e7624 1089
1fca63c1
RZ
1090 # pylint: disable=too-few-public-methods
1091
49581587
CH
1092 def __init__(self, tgen, name, **params):
1093 super(TopoSwitch, self).__init__(tgen, name, **params)
1094 tgen.net.add_switch(name)
7326ea11
RZ
1095
1096 def __str__(self):
1097 gear = super(TopoSwitch, self).__str__()
787e7624 1098 gear += " TopoSwitch<>"
7326ea11 1099 return gear
19ccab57 1100
787e7624 1101
19ccab57
RZ
1102class TopoHost(TopoGear):
1103 "Host abstraction."
1104 # pylint: disable=too-few-public-methods
1105
1106 def __init__(self, tgen, name, **params):
1107 """
1108 Mininet has the following known `params` for hosts:
1109 * `ip`: the IP address (string) for the host interface
1110 * `defaultRoute`: the default route that will be installed
1111 (e.g. 'via 10.0.0.1')
60e03778 1112 * `private_mounts`: directories that will be mounted on a different domain
19ccab57
RZ
1113 (e.g. '/etc/important_dir').
1114 """
49581587
CH
1115 super(TopoHost, self).__init__(tgen, name, **params)
1116
1117 # Propagate the router log directory
1118 logfile = self._setup_tmpdir()
1119 params["logdir"] = self.logdir
1120
1121 # Odd to have 2 logfiles for each host
1122 self.logger = topolog.get_logger(name, log_level="debug", target=logfile)
1123 params["logger"] = self.logger
1124 tgen.net.add_host(name, **params)
1125 topotest.fix_netns_limits(tgen.net[name])
1126
1127 # Mount gear log directory on a common path
1128 self.net.bind_mount(self.gearlogdir, "/tmp/gearlogdir")
19ccab57
RZ
1129
1130 def __str__(self):
1131 gear = super(TopoHost, self).__str__()
60e03778 1132 gear += ' TopoHost<ip="{}",defaultRoute="{}",private_mounts="{}">'.format(
49581587
CH
1133 self.params["ip"],
1134 self.params["defaultRoute"],
60e03778 1135 str(self.params["private_mounts"]),
787e7624 1136 )
19ccab57
RZ
1137 return gear
1138
787e7624 1139
19ccab57
RZ
1140class TopoExaBGP(TopoHost):
1141 "ExaBGP peer abstraction."
1142 # pylint: disable=too-few-public-methods
1143
1144 PRIVATE_DIRS = [
787e7624 1145 "/etc/exabgp",
1146 "/var/run/exabgp",
1147 "/var/log",
19ccab57
RZ
1148 ]
1149
1150 def __init__(self, tgen, name, **params):
1151 """
1152 ExaBGP usually uses the following parameters:
1153 * `ip`: the IP address (string) for the host interface
1154 * `defaultRoute`: the default route that will be installed
1155 (e.g. 'via 10.0.0.1')
1156
1157 Note: the different between a host and a ExaBGP peer is that this class
60e03778
CH
1158 has a private_mounts already defined and contains functions to handle
1159 ExaBGP things.
19ccab57 1160 """
60e03778 1161 params["private_mounts"] = self.PRIVATE_DIRS
19ccab57 1162 super(TopoExaBGP, self).__init__(tgen, name, **params)
19ccab57
RZ
1163
1164 def __str__(self):
1165 gear = super(TopoExaBGP, self).__str__()
787e7624 1166 gear += " TopoExaBGP<>".format()
19ccab57
RZ
1167 return gear
1168
1169 def start(self, peer_dir, env_file=None):
1170 """
1171 Start running ExaBGP daemon:
1172 * Copy all peer* folder contents into /etc/exabgp
1173 * Copy exabgp env file if specified
1174 * Make all python files runnable
1175 * Run ExaBGP with env file `env_file` and configuration peer*/exabgp.cfg
1176 """
49581587
CH
1177 exacmd = self.tgen.get_exabgp_cmd()
1178 assert exacmd, "Can't find a usabel ExaBGP (must be < version 4)"
1179
1180 self.run("mkdir -p /etc/exabgp")
787e7624 1181 self.run("chmod 755 /etc/exabgp")
29053988 1182 self.run("cp {}/exa-* /etc/exabgp/".format(CWD))
787e7624 1183 self.run("cp {}/* /etc/exabgp/".format(peer_dir))
19ccab57 1184 if env_file is not None:
787e7624 1185 self.run("cp {} /etc/exabgp/exabgp.env".format(env_file))
1186 self.run("chmod 644 /etc/exabgp/*")
1187 self.run("chmod a+x /etc/exabgp/*.py")
1188 self.run("chown -R exabgp:exabgp /etc/exabgp")
49581587
CH
1189
1190 output = self.run(exacmd + " -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg")
f637ac01 1191 if output is None or len(output) == 0:
787e7624 1192 output = "<none>"
49581587 1193
787e7624 1194 logger.info("{} exabgp started, output={}".format(self.name, output))
19ccab57 1195
95460a6b 1196 def stop(self, wait=True, assertOnError=True):
19ccab57 1197 "Stop ExaBGP peer and kill the daemon"
787e7624 1198 self.run("kill `cat /var/run/exabgp/exabgp.pid`")
95460a6b 1199 return ""
007e7313
RZ
1200
1201
1202#
1203# Diagnostic function
1204#
1205
3e097918 1206
007e7313
RZ
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