]> git.proxmox.com Git - mirror_frr.git/blame - tests/topotests/lib/topogen.py
Merge pull request #6330 from sworleys/No-NHG-Install-With-VRFns
[mirror_frr.git] / tests / topotests / lib / topogen.py
CommitLineData
1fca63c1
RZ
1#
2# topogen.py
3# Library of helper functions for NetDEF Topology Tests
4#
5# Copyright (c) 2017 by
6# Network Device Education Foundation, Inc. ("NetDEF")
7#
8# Permission to use, copy, modify, and/or distribute this software
9# for any purpose with or without fee is hereby granted, provided
10# that the above copyright notice and this permission notice appear
11# in all copies.
12#
13# THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES
14# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
15# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR
16# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
17# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
18# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
19# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
20# OF THIS SOFTWARE.
21#
22
23"""
24Topogen (Topology Generator) is an abstraction around Topotest and Mininet to
25help reduce boilerplate code and provide a stable interface to build topology
26tests on.
27
28Basic usage instructions:
29
30* Define a Topology class with a build method using mininet.topo.Topo.
31 See examples/test_template.py.
32* Use Topogen inside the build() method with get_topogen.
33 e.g. get_topogen(self).
34* Start up your topology with: Topogen(YourTopology)
35* Initialize the Mininet with your topology with: tgen.start_topology()
36* Configure your routers/hosts and start them
37* Run your tests / mininet cli.
38* After running stop Mininet with: tgen.stop_topology()
39"""
40
41import os
42import sys
7547ebd8 43import logging
a40daddc 44import json
a07e17d5
MS
45
46if sys.version_info[0] > 2:
47 import configparser
48else:
49 import ConfigParser as configparser
50
13e1fc49 51import glob
f6899d4d 52import grp
007e7313
RZ
53import platform
54import pwd
007e7313 55import subprocess
0e17ee9e 56import pytest
1fca63c1
RZ
57
58from mininet.net import Mininet
59from mininet.log import setLogLevel
60from mininet.cli import CLI
61
62from lib import topotest
36d1dc45 63from lib.topolog import logger, logger_config
a89241b4 64from lib.topotest import set_sysctl
1fca63c1 65
edd2bdf6
RZ
66CWD = os.path.dirname(os.path.realpath(__file__))
67
1fca63c1
RZ
68# pylint: disable=C0103
69# Global Topogen variable. This is being used to keep the Topogen available on
70# all test functions without declaring a test local variable.
71global_tgen = None
72
787e7624 73
1fca63c1
RZ
74def get_topogen(topo=None):
75 """
76 Helper function to retrieve Topogen. Must be called with `topo` when called
77 inside the build() method of Topology class.
78 """
79 if topo is not None:
80 global_tgen.topo = topo
81 return global_tgen
82
787e7624 83
1fca63c1
RZ
84def set_topogen(tgen):
85 "Helper function to set Topogen"
86 # pylint: disable=W0603
87 global global_tgen
88 global_tgen = tgen
89
787e7624 90
1fca63c1
RZ
91#
92# Main class: topology builder
93#
94
007e7313
RZ
95# Topogen configuration defaults
96tgen_defaults = {
787e7624 97 "verbosity": "info",
98 "frrdir": "/usr/lib/frr",
99 "quaggadir": "/usr/lib/quagga",
100 "routertype": "frr",
101 "memleak_path": None,
007e7313
RZ
102}
103
787e7624 104
1fca63c1
RZ
105class Topogen(object):
106 "A topology test builder helper."
107
787e7624 108 CONFIG_SECTION = "topogen"
edd2bdf6 109
787e7624 110 def __init__(self, cls, modname="unnamed"):
13e1fc49
RZ
111 """
112 Topogen initialization function, takes the following arguments:
113 * `cls`: the topology class that is child of mininet.topo
114 * `modname`: module name must be a unique name to identify logs later.
115 """
edd2bdf6 116 self.config = None
1fca63c1
RZ
117 self.topo = None
118 self.net = None
119 self.gears = {}
120 self.routern = 1
121 self.switchn = 1
13e1fc49 122 self.modname = modname
1eb633c0 123 self.errorsd = {}
787e7624 124 self.errors = ""
19ccab57 125 self.peern = 1
1fca63c1 126 self._init_topo(cls)
787e7624 127 logger.info("loading topology: {}".format(self.modname))
1fca63c1
RZ
128
129 @staticmethod
130 def _mininet_reset():
131 "Reset the mininet environment"
132 # Clean up the mininet environment
787e7624 133 os.system("mn -c > /dev/null 2>&1")
1fca63c1
RZ
134
135 def _init_topo(self, cls):
136 """
137 Initialize the topogily provided by the user. The user topology class
138 must call get_topogen() during build() to get the topogen object.
139 """
140 # Set the global variable so the test cases can access it anywhere
141 set_topogen(self)
142
9711fc7e
LB
143 # Test for MPLS Kernel modules available
144 self.hasmpls = False
787e7624 145 if not topotest.module_present("mpls-router"):
146 logger.info("MPLS tests will not run (missing mpls-router kernel module)")
147 elif not topotest.module_present("mpls-iptunnel"):
148 logger.info("MPLS tests will not run (missing mpls-iptunnel kernel module)")
9711fc7e
LB
149 else:
150 self.hasmpls = True
edd2bdf6
RZ
151 # Load the default topology configurations
152 self._load_config()
153
1fca63c1
RZ
154 # Initialize the API
155 self._mininet_reset()
156 cls()
157 self.net = Mininet(controller=None, topo=self.topo)
158 for gear in self.gears.values():
159 gear.net = self.net
160
edd2bdf6
RZ
161 def _load_config(self):
162 """
163 Loads the configuration file `pytest.ini` located at the root dir of
164 topotests.
165 """
a07e17d5 166 self.config = configparser.ConfigParser(tgen_defaults)
787e7624 167 pytestini_path = os.path.join(CWD, "../pytest.ini")
edd2bdf6
RZ
168 self.config.read(pytestini_path)
169
2ab85530 170 def add_router(self, name=None, cls=topotest.Router, **params):
1fca63c1
RZ
171 """
172 Adds a new router to the topology. This function has the following
173 options:
edd2bdf6
RZ
174 * `name`: (optional) select the router name
175 * `daemondir`: (optional) custom daemon binary directory
176 * `routertype`: (optional) `quagga` or `frr`
1fca63c1
RZ
177 Returns a TopoRouter.
178 """
179 if name is None:
787e7624 180 name = "r{}".format(self.routern)
1fca63c1 181 if name in self.gears:
787e7624 182 raise KeyError("router already exists")
1fca63c1 183
787e7624 184 params["frrdir"] = self.config.get(self.CONFIG_SECTION, "frrdir")
185 params["quaggadir"] = self.config.get(self.CONFIG_SECTION, "quaggadir")
186 params["memleak_path"] = self.config.get(self.CONFIG_SECTION, "memleak_path")
187 if not params.has_key("routertype"):
188 params["routertype"] = self.config.get(self.CONFIG_SECTION, "routertype")
edd2bdf6 189
2ab85530 190 self.gears[name] = TopoRouter(self, cls, name, **params)
1fca63c1
RZ
191 self.routern += 1
192 return self.gears[name]
193
194 def add_switch(self, name=None, cls=topotest.LegacySwitch):
195 """
196 Adds a new switch to the topology. This function has the following
197 options:
198 name: (optional) select the switch name
199 Returns the switch name and number.
200 """
201 if name is None:
787e7624 202 name = "s{}".format(self.switchn)
1fca63c1 203 if name in self.gears:
787e7624 204 raise KeyError("switch already exists")
1fca63c1
RZ
205
206 self.gears[name] = TopoSwitch(self, cls, name)
207 self.switchn += 1
208 return self.gears[name]
209
19ccab57
RZ
210 def add_exabgp_peer(self, name, ip, defaultRoute):
211 """
212 Adds a new ExaBGP peer to the topology. This function has the following
213 parameters:
214 * `ip`: the peer address (e.g. '1.2.3.4/24')
215 * `defaultRoute`: the peer default route (e.g. 'via 1.2.3.1')
216 """
217 if name is None:
787e7624 218 name = "peer{}".format(self.peern)
19ccab57 219 if name in self.gears:
787e7624 220 raise KeyError("exabgp peer already exists")
19ccab57
RZ
221
222 self.gears[name] = TopoExaBGP(self, name, ip=ip, defaultRoute=defaultRoute)
223 self.peern += 1
224 return self.gears[name]
225
1fca63c1
RZ
226 def add_link(self, node1, node2, ifname1=None, ifname2=None):
227 """
228 Creates a connection between node1 and node2. The nodes can be the
229 following:
230 * TopoGear
231 * TopoRouter
232 * TopoSwitch
233 """
234 if not isinstance(node1, TopoGear):
787e7624 235 raise ValueError("invalid node1 type")
1fca63c1 236 if not isinstance(node2, TopoGear):
787e7624 237 raise ValueError("invalid node2 type")
1fca63c1
RZ
238
239 if ifname1 is None:
8c3fdf62 240 ifname1 = node1.new_link()
1fca63c1 241 if ifname2 is None:
8c3fdf62
RZ
242 ifname2 = node2.new_link()
243
244 node1.register_link(ifname1, node2, ifname2)
245 node2.register_link(ifname2, node1, ifname1)
787e7624 246 self.topo.addLink(node1.name, node2.name, intfName1=ifname1, intfName2=ifname2)
1fca63c1 247
19ccab57
RZ
248 def get_gears(self, geartype):
249 """
250 Returns a dictionary of all gears of type `geartype`.
251
252 Normal usage:
253 * Dictionary iteration:
254 ```py
255 tgen = get_topogen()
256 router_dict = tgen.get_gears(TopoRouter)
257 for router_name, router in router_dict.iteritems():
258 # Do stuff
259 ```
260 * List iteration:
261 ```py
262 tgen = get_topogen()
263 peer_list = tgen.get_gears(TopoExaBGP).values()
264 for peer in peer_list:
265 # Do stuff
266 ```
267 """
787e7624 268 return dict(
269 (name, gear)
270 for name, gear in self.gears.iteritems()
271 if isinstance(gear, geartype)
272 )
19ccab57 273
1fca63c1
RZ
274 def routers(self):
275 """
276 Returns the router dictionary (key is the router name and value is the
277 router object itself).
278 """
19ccab57
RZ
279 return self.get_gears(TopoRouter)
280
281 def exabgp_peers(self):
282 """
283 Returns the exabgp peer dictionary (key is the peer name and value is
284 the peer object itself).
285 """
286 return self.get_gears(TopoExaBGP)
1fca63c1 287
edd2bdf6 288 def start_topology(self, log_level=None):
1fca63c1
RZ
289 """
290 Starts the topology class. Possible `log_level`s are:
291 'debug': all information possible
292 'info': informational messages
293 'output': default logging level defined by Mininet
294 'warning': only warning, error and critical messages
295 'error': only error and critical messages
296 'critical': only critical messages
297 """
edd2bdf6
RZ
298 # If log_level is not specified use the configuration.
299 if log_level is None:
787e7624 300 log_level = self.config.get(self.CONFIG_SECTION, "verbosity")
edd2bdf6 301
36d1dc45
RZ
302 # Set python logger level
303 logger_config.set_log_level(log_level)
304
1fca63c1 305 # Run mininet
787e7624 306 if log_level == "debug":
13e1fc49
RZ
307 setLogLevel(log_level)
308
787e7624 309 logger.info("starting topology: {}".format(self.modname))
1fca63c1
RZ
310 self.net.start()
311
312 def start_router(self, router=None):
313 """
314 Call the router startRouter method.
315 If no router is specified it is called for all registred routers.
316 """
317 if router is None:
318 # pylint: disable=r1704
319 for _, router in self.routers().iteritems():
320 router.start()
321 else:
322 if isinstance(router, str):
323 router = self.gears[router]
324
325 router.start()
326
327 def stop_topology(self):
4cfdff1a
RZ
328 """
329 Stops the network topology. This function will call the stop() function
330 of all gears before calling the mininet stop function, so they can have
3a568b9c
LB
331 their oportunity to do a graceful shutdown. stop() is called twice. The
332 first is a simple kill with no sleep, the second will sleep if not
333 killed and try with a different signal.
4cfdff1a 334 """
787e7624 335 logger.info("stopping topology: {}".format(self.modname))
95460a6b 336 errors = ""
4cfdff1a 337 for gear in self.gears.values():
95460a6b 338 gear.stop(False, False)
3a568b9c 339 for gear in self.gears.values():
95460a6b
LB
340 errors += gear.stop(True, False)
341 if len(errors) > 0:
342 assert "Errors found post shutdown - details follow:" == 0, errors
4cfdff1a 343
1fca63c1
RZ
344 self.net.stop()
345
346 def mininet_cli(self):
347 """
348 Interrupt the test and call the command line interface for manual
349 inspection. Should be only used on non production code.
350 """
351 if not sys.stdin.isatty():
352 raise EnvironmentError(
787e7624 353 "you must run pytest with '-s' in order to use mininet CLI"
354 )
1fca63c1
RZ
355
356 CLI(self.net)
357
13e1fc49
RZ
358 def is_memleak_enabled(self):
359 "Returns `True` if memory leak report is enable, otherwise `False`."
78ed6123
RZ
360 # On router failure we can't run the memory leak test
361 if self.routers_have_failure():
362 return False
363
787e7624 364 memleak_file = os.environ.get("TOPOTESTS_CHECK_MEMLEAK") or self.config.get(
365 self.CONFIG_SECTION, "memleak_path"
366 )
13e1fc49
RZ
367 if memleak_file is None:
368 return False
369 return True
370
371 def report_memory_leaks(self, testname=None):
372 "Run memory leak test and reports."
373 if not self.is_memleak_enabled():
374 return
375
376 # If no name was specified, use the test module name
377 if testname is None:
378 testname = self.modname
379
380 router_list = self.routers().values()
381 for router in router_list:
382 router.report_memory_leaks(self.modname)
383
393ca0fa
RZ
384 def set_error(self, message, code=None):
385 "Sets an error message and signal other tests to skip."
222ea88b 386 logger.info(message)
393ca0fa
RZ
387
388 # If no code is defined use a sequential number
389 if code is None:
1eb633c0 390 code = len(self.errorsd)
393ca0fa 391
1eb633c0 392 self.errorsd[code] = message
787e7624 393 self.errors += "\n{}: {}".format(code, message)
393ca0fa
RZ
394
395 def has_errors(self):
396 "Returns whether errors exist or not."
1eb633c0 397 return len(self.errorsd) > 0
13e1fc49 398
78ed6123
RZ
399 def routers_have_failure(self):
400 "Runs an assertion to make sure that all routers are running."
401 if self.has_errors():
402 return True
403
787e7624 404 errors = ""
78ed6123
RZ
405 router_list = self.routers().values()
406 for router in router_list:
407 result = router.check_router_running()
787e7624 408 if result != "":
409 errors += result + "\n"
78ed6123 410
787e7624 411 if errors != "":
412 self.set_error(errors, "router_error")
46325763 413 assert False, errors
78ed6123
RZ
414 return True
415 return False
416
787e7624 417
1fca63c1
RZ
418#
419# Topology gears (equipment)
420#
421
787e7624 422
1fca63c1
RZ
423class TopoGear(object):
424 "Abstract class for type checking"
425
426 def __init__(self):
427 self.tgen = None
428 self.name = None
429 self.cls = None
8c3fdf62 430 self.links = {}
1fca63c1
RZ
431 self.linkn = 0
432
7326ea11 433 def __str__(self):
787e7624 434 links = ""
7326ea11
RZ
435 for myif, dest in self.links.iteritems():
436 _, destif = dest
787e7624 437 if links != "":
438 links += ","
7326ea11
RZ
439 links += '"{}"<->"{}"'.format(myif, destif)
440
441 return 'TopoGear<name="{}",links=[{}]>'.format(self.name, links)
442
4cfdff1a
RZ
443 def start(self):
444 "Basic start function that just reports equipment start"
445 logger.info('starting "{}"'.format(self.name))
446
95460a6b 447 def stop(self, wait=True, assertOnError=True):
4cfdff1a
RZ
448 "Basic start function that just reports equipment stop"
449 logger.info('stopping "{}"'.format(self.name))
95460a6b 450 return ""
4cfdff1a 451
8c3fdf62
RZ
452 def run(self, command):
453 """
454 Runs the provided command string in the router and returns a string
455 with the response.
456 """
457 return self.tgen.net[self.name].cmd(command)
458
1fca63c1
RZ
459 def add_link(self, node, myif=None, nodeif=None):
460 """
461 Creates a link (connection) between myself and the specified node.
462 Interfaces name can be speficied with:
463 myif: the interface name that will be created in this node
464 nodeif: the target interface name that will be created on the remote node.
465 """
466 self.tgen.add_link(self, node, myif, nodeif)
467
0ab7733f 468 def link_enable(self, myif, enabled=True, netns=None):
1fca63c1 469 """
8c3fdf62
RZ
470 Set this node interface administrative state.
471 myif: this node interface name
472 enabled: whether we should enable or disable the interface
1fca63c1 473 """
8c3fdf62 474 if myif not in self.links.keys():
787e7624 475 raise KeyError("interface doesn't exists")
8c3fdf62
RZ
476
477 if enabled is True:
787e7624 478 operation = "up"
8c3fdf62 479 else:
787e7624 480 operation = "down"
8c3fdf62 481
787e7624 482 logger.info(
483 'setting node "{}" link "{}" to state "{}"'.format(
484 self.name, myif, operation
485 )
486 )
487 extract = ""
0ab7733f 488 if netns is not None:
787e7624 489 extract = "ip netns exec {} ".format(netns)
490 return self.run("{}ip link set dev {} {}".format(extract, myif, operation))
8c3fdf62 491
0ab7733f 492 def peer_link_enable(self, myif, enabled=True, netns=None):
8c3fdf62
RZ
493 """
494 Set the peer interface administrative state.
495 myif: this node interface name
496 enabled: whether we should enable or disable the interface
497
498 NOTE: this is used to simulate a link down on this node, since when the
499 peer disables their interface our interface status changes to no link.
500 """
501 if myif not in self.links.keys():
787e7624 502 raise KeyError("interface doesn't exists")
8c3fdf62
RZ
503
504 node, nodeif = self.links[myif]
0ab7733f 505 node.link_enable(nodeif, enabled, netns)
1fca63c1 506
8c3fdf62
RZ
507 def new_link(self):
508 """
509 Generates a new unique link name.
510
511 NOTE: This function should only be called by Topogen.
512 """
787e7624 513 ifname = "{}-eth{}".format(self.name, self.linkn)
1fca63c1 514 self.linkn += 1
1fca63c1
RZ
515 return ifname
516
8c3fdf62
RZ
517 def register_link(self, myif, node, nodeif):
518 """
519 Register link between this node interface and outside node.
520
521 NOTE: This function should only be called by Topogen.
522 """
523 if myif in self.links.keys():
787e7624 524 raise KeyError("interface already exists")
8c3fdf62
RZ
525
526 self.links[myif] = (node, nodeif)
527
787e7624 528
1fca63c1
RZ
529class TopoRouter(TopoGear):
530 """
d9ea1cda 531 Router abstraction.
1fca63c1
RZ
532 """
533
534 # The default required directories by Quagga/FRR
535 PRIVATE_DIRS = [
787e7624 536 "/etc/frr",
537 "/etc/quagga",
538 "/var/run/frr",
539 "/var/run/quagga",
540 "/var/log",
1fca63c1
RZ
541 ]
542
543 # Router Daemon enumeration definition.
7326ea11 544 RD_ZEBRA = 1
1fca63c1
RZ
545 RD_RIP = 2
546 RD_RIPNG = 3
547 RD_OSPF = 4
548 RD_OSPF6 = 5
549 RD_ISIS = 6
550 RD_BGP = 7
551 RD_LDP = 8
552 RD_PIM = 9
c267e5b1
RZ
553 RD_EIGRP = 10
554 RD_NHRP = 11
a2a1134c 555 RD_STATIC = 12
4d45d6d3 556 RD_BFD = 13
a38f0083 557 RD_SHARP = 14
1fca63c1 558 RD = {
787e7624 559 RD_ZEBRA: "zebra",
560 RD_RIP: "ripd",
561 RD_RIPNG: "ripngd",
562 RD_OSPF: "ospfd",
563 RD_OSPF6: "ospf6d",
564 RD_ISIS: "isisd",
565 RD_BGP: "bgpd",
566 RD_PIM: "pimd",
567 RD_LDP: "ldpd",
568 RD_EIGRP: "eigrpd",
569 RD_NHRP: "nhrpd",
570 RD_STATIC: "staticd",
571 RD_BFD: "bfdd",
572 RD_SHARP: "sharpd",
1fca63c1
RZ
573 }
574
2ab85530 575 def __init__(self, tgen, cls, name, **params):
d9ea1cda
RZ
576 """
577 The constructor has the following parameters:
578 * tgen: Topogen object
579 * cls: router class that will be used to instantiate
580 * name: router name
581 * daemondir: daemon binary directory
582 * routertype: 'quagga' or 'frr'
583 """
1fca63c1
RZ
584 super(TopoRouter, self).__init__()
585 self.tgen = tgen
586 self.net = None
587 self.name = name
588 self.cls = cls
c540096e 589 self.options = {}
787e7624 590 self.routertype = params.get("routertype", "frr")
591 if not params.has_key("privateDirs"):
592 params["privateDirs"] = self.PRIVATE_DIRS
c540096e 593
787e7624 594 self.options["memleak_path"] = params.get("memleak_path", None)
13e1fc49
RZ
595
596 # Create new log directory
787e7624 597 self.logdir = "/tmp/topotests/{}".format(self.tgen.modname)
13e1fc49
RZ
598 # Clean up before starting new log files: avoids removing just created
599 # log files.
600 self._prepare_tmpfiles()
601 # Propagate the router log directory
787e7624 602 params["logdir"] = self.logdir
13e1fc49 603
787e7624 604 # setup the per node directory
605 dir = "{}/{}".format(self.logdir, self.name)
606 os.system("mkdir -p " + dir)
607 os.system("chmod -R go+rw /tmp/topotests")
e1dfa45e 608
13e1fc49 609 # Open router log file
787e7624 610 logfile = "{0}/{1}.log".format(self.logdir, name)
13e1fc49 611 self.logger = logger_config.get_logger(name=name, target=logfile)
87ba6e1e 612
2ab85530 613 self.tgen.topo.addNode(self.name, cls=self.cls, **params)
1fca63c1 614
7326ea11
RZ
615 def __str__(self):
616 gear = super(TopoRouter, self).__str__()
787e7624 617 gear += " TopoRouter<>"
7326ea11
RZ
618 return gear
619
13e1fc49
RZ
620 def _prepare_tmpfiles(self):
621 # Create directories if they don't exist
622 try:
a07e17d5 623 os.makedirs(self.logdir, 0o755)
13e1fc49
RZ
624 except OSError:
625 pass
626
f6899d4d
RZ
627 # Allow unprivileged daemon user (frr/quagga) to create log files
628 try:
629 # Only allow group, if it exist.
630 gid = grp.getgrnam(self.routertype)[2]
631 os.chown(self.logdir, 0, gid)
a07e17d5 632 os.chmod(self.logdir, 0o775)
f6899d4d
RZ
633 except KeyError:
634 # Allow anyone, but set the sticky bit to avoid file deletions
a07e17d5 635 os.chmod(self.logdir, 0o1777)
f6899d4d 636
13e1fc49 637 # Try to find relevant old logfiles in /tmp and delete them
787e7624 638 map(os.remove, glob.glob("{}/{}/*.log".format(self.logdir, self.name)))
13e1fc49 639 # Remove old core files
787e7624 640 map(os.remove, glob.glob("{}/{}/*.dmp".format(self.logdir, self.name)))
13e1fc49 641
8dd5077d
PG
642 def check_capability(self, daemon, param):
643 """
644 Checks a capability daemon against an argument option
645 Return True if capability available. False otherwise
646 """
647 daemonstr = self.RD.get(daemon)
648 self.logger.info('check capability {} for "{}"'.format(param, daemonstr))
649 return self.tgen.net[self.name].checkCapability(daemonstr, param)
650
651 def load_config(self, daemon, source=None, param=None):
1fca63c1
RZ
652 """
653 Loads daemon configuration from the specified source
654 Possible daemon values are: TopoRouter.RD_ZEBRA, TopoRouter.RD_RIP,
655 TopoRouter.RD_RIPNG, TopoRouter.RD_OSPF, TopoRouter.RD_OSPF6,
656 TopoRouter.RD_ISIS, TopoRouter.RD_BGP, TopoRouter.RD_LDP,
657 TopoRouter.RD_PIM.
658 """
659 daemonstr = self.RD.get(daemon)
13e1fc49 660 self.logger.info('loading "{}" configuration: {}'.format(daemonstr, source))
8dd5077d 661 self.tgen.net[self.name].loadConf(daemonstr, source, param)
1fca63c1
RZ
662
663 def check_router_running(self):
664 """
665 Run a series of checks and returns a status string.
666 """
787e7624 667 self.logger.info("checking if daemons are running")
1fca63c1
RZ
668 return self.tgen.net[self.name].checkRouterRunning()
669
670 def start(self):
671 """
672 Start router:
673 * Load modules
674 * Clean up files
675 * Configure interfaces
676 * Start daemons (e.g. FRR/Quagga)
f6899d4d 677 * Configure daemon logging files
1fca63c1 678 """
787e7624 679 self.logger.debug("starting")
f6899d4d 680 nrouter = self.tgen.net[self.name]
9711fc7e 681 result = nrouter.startRouter(self.tgen)
f6899d4d 682
deb4cef0
LB
683 # Enable all daemon command logging, logging files
684 # and set them to the start dir.
f6899d4d
RZ
685 for daemon, enabled in nrouter.daemons.iteritems():
686 if enabled == 0:
687 continue
787e7624 688 self.vtysh_cmd(
689 "configure terminal\nlog commands\nlog file {}.log".format(daemon),
690 daemon=daemon,
691 )
f6899d4d 692
787e7624 693 if result != "":
57c5075b 694 self.tgen.set_error(result)
a89241b4
RW
695 else:
696 # Enable MPLS processing on all interfaces.
697 for interface in self.links.keys():
787e7624 698 set_sysctl(nrouter, "net.mpls.conf.{}.input".format(interface), 1)
57c5075b 699
f6899d4d 700 return result
1fca63c1 701
95460a6b 702 def stop(self, wait=True, assertOnError=True):
13e1fc49
RZ
703 """
704 Stop router:
705 * Kill daemons
706 """
787e7624 707 self.logger.debug("stopping")
95460a6b 708 return self.tgen.net[self.name].stopRouter(wait, assertOnError)
13e1fc49 709
f9b48d8b 710 def vtysh_cmd(self, command, isjson=False, daemon=None):
1fca63c1
RZ
711 """
712 Runs the provided command string in the vty shell and returns a string
713 with the response.
714
715 This function also accepts multiple commands, but this mode does not
716 return output for each command. See vtysh_multicmd() for more details.
717 """
718 # Detect multi line commands
787e7624 719 if command.find("\n") != -1:
f9b48d8b
RZ
720 return self.vtysh_multicmd(command, daemon=daemon)
721
787e7624 722 dparam = ""
f9b48d8b 723 if daemon is not None:
787e7624 724 dparam += "-d {}".format(daemon)
f9b48d8b
RZ
725
726 vtysh_command = 'vtysh {} -c "{}" 2>/dev/null'.format(dparam, command)
1fca63c1 727
a40daddc 728 output = self.run(vtysh_command)
787e7624 729 self.logger.info(
730 "\nvtysh command => {}\nvtysh output <= {}".format(command, output)
731 )
a40daddc
RZ
732 if isjson is False:
733 return output
734
7b093d84
RZ
735 try:
736 return json.loads(output)
737 except ValueError:
787e7624 738 logger.warning("vtysh_cmd: failed to convert json output")
7b093d84 739 return {}
1fca63c1 740
f9b48d8b 741 def vtysh_multicmd(self, commands, pretty_output=True, daemon=None):
1fca63c1
RZ
742 """
743 Runs the provided commands in the vty shell and return the result of
744 execution.
745
746 pretty_output: defines how the return value will be presented. When
747 True it will show the command as they were executed in the vty shell,
748 otherwise it will only show lines that failed.
749 """
750 # Prepare the temporary file that will hold the commands
751 fname = topotest.get_file(commands)
752
787e7624 753 dparam = ""
f9b48d8b 754 if daemon is not None:
787e7624 755 dparam += "-d {}".format(daemon)
f9b48d8b 756
1fca63c1
RZ
757 # Run the commands and delete the temporary file
758 if pretty_output:
787e7624 759 vtysh_command = "vtysh {} < {}".format(dparam, fname)
1fca63c1 760 else:
787e7624 761 vtysh_command = "vtysh {} -f {}".format(dparam, fname)
1fca63c1
RZ
762
763 res = self.run(vtysh_command)
764 os.unlink(fname)
765
787e7624 766 self.logger.info(
767 '\nvtysh command => "{}"\nvtysh output <= "{}"'.format(vtysh_command, res)
768 )
13e1fc49 769
1fca63c1
RZ
770 return res
771
38c39932
RZ
772 def report_memory_leaks(self, testname):
773 """
774 Runs the router memory leak check test. Has the following parameter:
775 testname: the test file name for identification
776
777 NOTE: to run this you must have the environment variable
c540096e 778 TOPOTESTS_CHECK_MEMLEAK set or memleak_path configured in `pytest.ini`.
38c39932 779 """
787e7624 780 memleak_file = (
781 os.environ.get("TOPOTESTS_CHECK_MEMLEAK") or self.options["memleak_path"]
782 )
38c39932 783 if memleak_file is None:
38c39932
RZ
784 return
785
13e1fc49 786 self.stop()
787e7624 787 self.logger.info("running memory leak report")
38c39932
RZ
788 self.tgen.net[self.name].report_memory_leaks(memleak_file, testname)
789
6ca2411e
RZ
790 def version_info(self):
791 "Get equipment information from 'show version'."
787e7624 792 output = self.vtysh_cmd("show version").split("\n")[0]
793 columns = topotest.normalize_text(output).split(" ")
b3b1b1d1
RZ
794 try:
795 return {
787e7624 796 "type": columns[0],
797 "version": columns[1],
b3b1b1d1
RZ
798 }
799 except IndexError:
800 return {
787e7624 801 "type": None,
802 "version": None,
b3b1b1d1 803 }
6ca2411e
RZ
804
805 def has_version(self, cmpop, version):
806 """
807 Compares router version using operation `cmpop` with `version`.
808 Valid `cmpop` values:
809 * `>=`: has the same version or greater
810 * '>': has greater version
811 * '=': has the same version
812 * '<': has a lesser version
813 * '<=': has the same version or lesser
814
815 Usage example: router.has_version('>', '1.0')
816 """
fb80b81b 817 return self.tgen.net[self.name].checkRouterVersion(cmpop, version)
6ca2411e
RZ
818
819 def has_type(self, rtype):
820 """
821 Compares router type with `rtype`. Returns `True` if the type matches,
822 otherwise `false`.
823 """
787e7624 824 curtype = self.version_info()["type"]
6ca2411e
RZ
825 return rtype == curtype
826
447f2d5a
LB
827 def has_mpls(self):
828 nrouter = self.tgen.net[self.name]
829 return nrouter.hasmpls
830
787e7624 831
1fca63c1
RZ
832class TopoSwitch(TopoGear):
833 """
834 Switch abstraction. Has the following properties:
835 * cls: switch class that will be used to instantiate
836 * name: switch name
837 """
787e7624 838
1fca63c1
RZ
839 # pylint: disable=too-few-public-methods
840
841 def __init__(self, tgen, cls, name):
842 super(TopoSwitch, self).__init__()
843 self.tgen = tgen
844 self.net = None
845 self.name = name
846 self.cls = cls
847 self.tgen.topo.addSwitch(name, cls=self.cls)
7326ea11
RZ
848
849 def __str__(self):
850 gear = super(TopoSwitch, self).__str__()
787e7624 851 gear += " TopoSwitch<>"
7326ea11 852 return gear
19ccab57 853
787e7624 854
19ccab57
RZ
855class TopoHost(TopoGear):
856 "Host abstraction."
857 # pylint: disable=too-few-public-methods
858
859 def __init__(self, tgen, name, **params):
860 """
861 Mininet has the following known `params` for hosts:
862 * `ip`: the IP address (string) for the host interface
863 * `defaultRoute`: the default route that will be installed
864 (e.g. 'via 10.0.0.1')
865 * `privateDirs`: directories that will be mounted on a different domain
866 (e.g. '/etc/important_dir').
867 """
868 super(TopoHost, self).__init__()
869 self.tgen = tgen
870 self.net = None
871 self.name = name
872 self.options = params
873 self.tgen.topo.addHost(name, **params)
874
875 def __str__(self):
876 gear = super(TopoHost, self).__str__()
877 gear += ' TopoHost<ip="{}",defaultRoute="{}",privateDirs="{}">'.format(
787e7624 878 self.options["ip"],
879 self.options["defaultRoute"],
880 str(self.options["privateDirs"]),
881 )
19ccab57
RZ
882 return gear
883
787e7624 884
19ccab57
RZ
885class TopoExaBGP(TopoHost):
886 "ExaBGP peer abstraction."
887 # pylint: disable=too-few-public-methods
888
889 PRIVATE_DIRS = [
787e7624 890 "/etc/exabgp",
891 "/var/run/exabgp",
892 "/var/log",
19ccab57
RZ
893 ]
894
895 def __init__(self, tgen, name, **params):
896 """
897 ExaBGP usually uses the following parameters:
898 * `ip`: the IP address (string) for the host interface
899 * `defaultRoute`: the default route that will be installed
900 (e.g. 'via 10.0.0.1')
901
902 Note: the different between a host and a ExaBGP peer is that this class
903 has a privateDirs already defined and contains functions to handle ExaBGP
904 things.
905 """
787e7624 906 params["privateDirs"] = self.PRIVATE_DIRS
19ccab57
RZ
907 super(TopoExaBGP, self).__init__(tgen, name, **params)
908 self.tgen.topo.addHost(name, **params)
909
910 def __str__(self):
911 gear = super(TopoExaBGP, self).__str__()
787e7624 912 gear += " TopoExaBGP<>".format()
19ccab57
RZ
913 return gear
914
915 def start(self, peer_dir, env_file=None):
916 """
917 Start running ExaBGP daemon:
918 * Copy all peer* folder contents into /etc/exabgp
919 * Copy exabgp env file if specified
920 * Make all python files runnable
921 * Run ExaBGP with env file `env_file` and configuration peer*/exabgp.cfg
922 """
787e7624 923 self.run("mkdir /etc/exabgp")
924 self.run("chmod 755 /etc/exabgp")
925 self.run("cp {}/* /etc/exabgp/".format(peer_dir))
19ccab57 926 if env_file is not None:
787e7624 927 self.run("cp {} /etc/exabgp/exabgp.env".format(env_file))
928 self.run("chmod 644 /etc/exabgp/*")
929 self.run("chmod a+x /etc/exabgp/*.py")
930 self.run("chown -R exabgp:exabgp /etc/exabgp")
931 output = self.run("exabgp -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg")
87d5e16a 932 if output == None or len(output) == 0:
787e7624 933 output = "<none>"
934 logger.info("{} exabgp started, output={}".format(self.name, output))
19ccab57 935
95460a6b 936 def stop(self, wait=True, assertOnError=True):
19ccab57 937 "Stop ExaBGP peer and kill the daemon"
787e7624 938 self.run("kill `cat /var/run/exabgp/exabgp.pid`")
95460a6b 939 return ""
007e7313
RZ
940
941
942#
943# Diagnostic function
944#
945
946# Disable linter branch warning. It is expected to have these here.
947# pylint: disable=R0912
af99f19e 948def diagnose_env_linux():
007e7313
RZ
949 """
950 Run diagnostics in the running environment. Returns `True` when everything
951 is ok, otherwise `False`.
952 """
953 ret = True
7547ebd8 954
fcfbc769 955 # Test log path exists before installing handler.
787e7624 956 if not os.path.isdir("/tmp"):
957 logger.warning("could not find /tmp for logs")
fcfbc769 958 else:
787e7624 959 os.system("mkdir /tmp/topotests")
fcfbc769 960 # Log diagnostics to file so it can be examined later.
787e7624 961 fhandler = logging.FileHandler(filename="/tmp/topotests/diagnostics.txt")
fcfbc769
RZ
962 fhandler.setLevel(logging.DEBUG)
963 fhandler.setFormatter(
787e7624 964 logging.Formatter(fmt="%(asctime)s %(levelname)s: %(message)s")
fcfbc769
RZ
965 )
966 logger.addHandler(fhandler)
7547ebd8 967
787e7624 968 logger.info("Running environment diagnostics")
007e7313
RZ
969
970 # Load configuration
a07e17d5 971 config = configparser.ConfigParser(tgen_defaults)
787e7624 972 pytestini_path = os.path.join(CWD, "../pytest.ini")
007e7313
RZ
973 config.read(pytestini_path)
974
975 # Assert that we are running as root
976 if os.getuid() != 0:
787e7624 977 logger.error("you must run topotest as root")
007e7313
RZ
978 ret = False
979
980 # Assert that we have mininet
787e7624 981 if os.system("which mn >/dev/null 2>/dev/null") != 0:
982 logger.error("could not find mininet binary (mininet is not installed)")
007e7313
RZ
983 ret = False
984
985 # Assert that we have iproute installed
787e7624 986 if os.system("which ip >/dev/null 2>/dev/null") != 0:
987 logger.error("could not find ip binary (iproute is not installed)")
007e7313
RZ
988 ret = False
989
990 # Assert that we have gdb installed
787e7624 991 if os.system("which gdb >/dev/null 2>/dev/null") != 0:
992 logger.error("could not find gdb binary (gdb is not installed)")
007e7313
RZ
993 ret = False
994
995 # Assert that FRR utilities exist
787e7624 996 frrdir = config.get("topogen", "frrdir")
812e38a9 997 hasfrr = False
007e7313 998 if not os.path.isdir(frrdir):
787e7624 999 logger.error("could not find {} directory".format(frrdir))
007e7313
RZ
1000 ret = False
1001 else:
812e38a9 1002 hasfrr = True
007e7313 1003 try:
787e7624 1004 pwd.getpwnam("frr")[2]
007e7313
RZ
1005 except KeyError:
1006 logger.warning('could not find "frr" user')
1007
1008 try:
787e7624 1009 grp.getgrnam("frr")[2]
007e7313
RZ
1010 except KeyError:
1011 logger.warning('could not find "frr" group')
1012
1013 try:
787e7624 1014 if "frr" not in grp.getgrnam("frrvty").gr_mem:
1015 logger.error(
1016 '"frr" user and group exist, but user is not under "frrvty"'
1017 )
007e7313
RZ
1018 except KeyError:
1019 logger.warning('could not find "frrvty" group')
1020
787e7624 1021 for fname in [
1022 "zebra",
1023 "ospfd",
1024 "ospf6d",
1025 "bgpd",
1026 "ripd",
1027 "ripngd",
1028 "isisd",
1029 "pimd",
1030 "ldpd",
1031 ]:
007e7313
RZ
1032 path = os.path.join(frrdir, fname)
1033 if not os.path.isfile(path):
1034 # LDPd is an exception
787e7624 1035 if fname == "ldpd":
1036 logger.info(
1037 "could not find {} in {}".format(fname, frrdir)
1038 + "(LDPd tests will not run)"
1039 )
007e7313
RZ
1040 continue
1041
787e7624 1042 logger.warning("could not find {} in {}".format(fname, frrdir))
007e7313 1043 ret = False
d34f6134 1044 else:
787e7624 1045 if fname != "zebra":
d34f6134
RZ
1046 continue
1047
787e7624 1048 os.system("{} -v 2>&1 >/tmp/topotests/frr_zebra.txt".format(path))
007e7313
RZ
1049
1050 # Assert that Quagga utilities exist
787e7624 1051 quaggadir = config.get("topogen", "quaggadir")
812e38a9
RZ
1052 if hasfrr:
1053 # if we have frr, don't check for quagga
1054 pass
1055 elif not os.path.isdir(quaggadir):
787e7624 1056 logger.info(
1057 "could not find {} directory (quagga tests will not run)".format(quaggadir)
1058 )
007e7313 1059 else:
812e38a9 1060 ret = True
007e7313 1061 try:
787e7624 1062 pwd.getpwnam("quagga")[2]
007e7313
RZ
1063 except KeyError:
1064 logger.info('could not find "quagga" user')
1065
1066 try:
787e7624 1067 grp.getgrnam("quagga")[2]
007e7313
RZ
1068 except KeyError:
1069 logger.info('could not find "quagga" group')
1070
1071 try:
787e7624 1072 if "quagga" not in grp.getgrnam("quaggavty").gr_mem:
1073 logger.error(
1074 '"quagga" user and group exist, but user is not under "quaggavty"'
1075 )
007e7313 1076 except KeyError:
812e38a9 1077 logger.warning('could not find "quaggavty" group')
007e7313 1078
787e7624 1079 for fname in [
1080 "zebra",
1081 "ospfd",
1082 "ospf6d",
1083 "bgpd",
1084 "ripd",
1085 "ripngd",
1086 "isisd",
1087 "pimd",
1088 ]:
007e7313
RZ
1089 path = os.path.join(quaggadir, fname)
1090 if not os.path.isfile(path):
787e7624 1091 logger.warning("could not find {} in {}".format(fname, quaggadir))
007e7313 1092 ret = False
d34f6134 1093 else:
787e7624 1094 if fname != "zebra":
d34f6134
RZ
1095 continue
1096
787e7624 1097 os.system("{} -v 2>&1 >/tmp/topotests/quagga_zebra.txt".format(path))
007e7313 1098
007e7313
RZ
1099 # Test MPLS availability
1100 krel = platform.release()
787e7624 1101 if topotest.version_cmp(krel, "4.5") < 0:
1102 logger.info(
1103 'LDPd tests will not run (have kernel "{}", but it requires 4.5)'.format(
1104 krel
1105 )
1106 )
007e7313 1107
c11c4cc7 1108 # Test for MPLS Kernel modules available
787e7624 1109 if not topotest.module_present("mpls-router", load=False) != 0:
1110 logger.info("LDPd tests will not run (missing mpls-router kernel module)")
1111 if not topotest.module_present("mpls-iptunnel", load=False) != 0:
1112 logger.info("LDPd tests will not run (missing mpls-iptunnel kernel module)")
c11c4cc7 1113
007e7313
RZ
1114 # TODO remove me when we start supporting exabgp >= 4
1115 try:
787e7624 1116 output = subprocess.check_output(["exabgp", "-v"])
1117 line = output.split("\n")[0]
1118 version = line.split(" ")[2]
1119 if topotest.version_cmp(version, "4") >= 0:
1120 logger.warning(
1121 "BGP topologies are still using exabgp version 3, expect failures"
1122 )
007e7313
RZ
1123
1124 # We want to catch all exceptions
1125 # pylint: disable=W0702
1126 except:
787e7624 1127 logger.warning("failed to find exabgp or returned error")
007e7313 1128
7547ebd8
RZ
1129 # After we logged the output to file, remove the handler.
1130 logger.removeHandler(fhandler)
1131
007e7313 1132 return ret
af99f19e 1133
787e7624 1134
af99f19e
DS
1135def diagnose_env_freebsd():
1136 return True
1137
787e7624 1138
af99f19e
DS
1139def diagnose_env():
1140 if sys.platform.startswith("linux"):
1141 return diagnose_env_linux()
1142 elif sys.platform.startswith("freebsd"):
1143 return diagnose_env_freebsd()
1144
1145 return False