]> git.proxmox.com Git - mirror_frr.git/blame - tests/topotests/lib/topogen.py
pbrd, lib, doc: fix new `show` json key semantics
[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
a0764a36 558 RD_BABEL = 15
1fca63c1 559 RD = {
787e7624 560 RD_ZEBRA: "zebra",
561 RD_RIP: "ripd",
562 RD_RIPNG: "ripngd",
563 RD_OSPF: "ospfd",
564 RD_OSPF6: "ospf6d",
565 RD_ISIS: "isisd",
566 RD_BGP: "bgpd",
567 RD_PIM: "pimd",
568 RD_LDP: "ldpd",
569 RD_EIGRP: "eigrpd",
570 RD_NHRP: "nhrpd",
571 RD_STATIC: "staticd",
572 RD_BFD: "bfdd",
573 RD_SHARP: "sharpd",
a0764a36 574 RD_BABEL: "babeld",
1fca63c1
RZ
575 }
576
2ab85530 577 def __init__(self, tgen, cls, name, **params):
d9ea1cda
RZ
578 """
579 The constructor has the following parameters:
580 * tgen: Topogen object
581 * cls: router class that will be used to instantiate
582 * name: router name
583 * daemondir: daemon binary directory
584 * routertype: 'quagga' or 'frr'
585 """
1fca63c1
RZ
586 super(TopoRouter, self).__init__()
587 self.tgen = tgen
588 self.net = None
589 self.name = name
590 self.cls = cls
c540096e 591 self.options = {}
787e7624 592 self.routertype = params.get("routertype", "frr")
593 if not params.has_key("privateDirs"):
594 params["privateDirs"] = self.PRIVATE_DIRS
c540096e 595
787e7624 596 self.options["memleak_path"] = params.get("memleak_path", None)
13e1fc49
RZ
597
598 # Create new log directory
787e7624 599 self.logdir = "/tmp/topotests/{}".format(self.tgen.modname)
13e1fc49
RZ
600 # Clean up before starting new log files: avoids removing just created
601 # log files.
602 self._prepare_tmpfiles()
603 # Propagate the router log directory
787e7624 604 params["logdir"] = self.logdir
13e1fc49 605
787e7624 606 # setup the per node directory
607 dir = "{}/{}".format(self.logdir, self.name)
608 os.system("mkdir -p " + dir)
609 os.system("chmod -R go+rw /tmp/topotests")
e1dfa45e 610
13e1fc49 611 # Open router log file
787e7624 612 logfile = "{0}/{1}.log".format(self.logdir, name)
13e1fc49 613 self.logger = logger_config.get_logger(name=name, target=logfile)
87ba6e1e 614
2ab85530 615 self.tgen.topo.addNode(self.name, cls=self.cls, **params)
1fca63c1 616
7326ea11
RZ
617 def __str__(self):
618 gear = super(TopoRouter, self).__str__()
787e7624 619 gear += " TopoRouter<>"
7326ea11
RZ
620 return gear
621
13e1fc49
RZ
622 def _prepare_tmpfiles(self):
623 # Create directories if they don't exist
624 try:
a07e17d5 625 os.makedirs(self.logdir, 0o755)
13e1fc49
RZ
626 except OSError:
627 pass
628
f6899d4d
RZ
629 # Allow unprivileged daemon user (frr/quagga) to create log files
630 try:
631 # Only allow group, if it exist.
632 gid = grp.getgrnam(self.routertype)[2]
633 os.chown(self.logdir, 0, gid)
a07e17d5 634 os.chmod(self.logdir, 0o775)
f6899d4d
RZ
635 except KeyError:
636 # Allow anyone, but set the sticky bit to avoid file deletions
a07e17d5 637 os.chmod(self.logdir, 0o1777)
f6899d4d 638
13e1fc49 639 # Try to find relevant old logfiles in /tmp and delete them
787e7624 640 map(os.remove, glob.glob("{}/{}/*.log".format(self.logdir, self.name)))
13e1fc49 641 # Remove old core files
787e7624 642 map(os.remove, glob.glob("{}/{}/*.dmp".format(self.logdir, self.name)))
13e1fc49 643
8dd5077d
PG
644 def check_capability(self, daemon, param):
645 """
646 Checks a capability daemon against an argument option
647 Return True if capability available. False otherwise
648 """
649 daemonstr = self.RD.get(daemon)
650 self.logger.info('check capability {} for "{}"'.format(param, daemonstr))
651 return self.tgen.net[self.name].checkCapability(daemonstr, param)
652
653 def load_config(self, daemon, source=None, param=None):
1fca63c1
RZ
654 """
655 Loads daemon configuration from the specified source
656 Possible daemon values are: TopoRouter.RD_ZEBRA, TopoRouter.RD_RIP,
657 TopoRouter.RD_RIPNG, TopoRouter.RD_OSPF, TopoRouter.RD_OSPF6,
658 TopoRouter.RD_ISIS, TopoRouter.RD_BGP, TopoRouter.RD_LDP,
659 TopoRouter.RD_PIM.
660 """
661 daemonstr = self.RD.get(daemon)
13e1fc49 662 self.logger.info('loading "{}" configuration: {}'.format(daemonstr, source))
8dd5077d 663 self.tgen.net[self.name].loadConf(daemonstr, source, param)
1fca63c1
RZ
664
665 def check_router_running(self):
666 """
667 Run a series of checks and returns a status string.
668 """
787e7624 669 self.logger.info("checking if daemons are running")
1fca63c1
RZ
670 return self.tgen.net[self.name].checkRouterRunning()
671
672 def start(self):
673 """
674 Start router:
675 * Load modules
676 * Clean up files
677 * Configure interfaces
678 * Start daemons (e.g. FRR/Quagga)
f6899d4d 679 * Configure daemon logging files
1fca63c1 680 """
787e7624 681 self.logger.debug("starting")
f6899d4d 682 nrouter = self.tgen.net[self.name]
9711fc7e 683 result = nrouter.startRouter(self.tgen)
f6899d4d 684
deb4cef0
LB
685 # Enable all daemon command logging, logging files
686 # and set them to the start dir.
f6899d4d
RZ
687 for daemon, enabled in nrouter.daemons.iteritems():
688 if enabled == 0:
689 continue
787e7624 690 self.vtysh_cmd(
691 "configure terminal\nlog commands\nlog file {}.log".format(daemon),
692 daemon=daemon,
693 )
f6899d4d 694
787e7624 695 if result != "":
57c5075b 696 self.tgen.set_error(result)
a89241b4
RW
697 else:
698 # Enable MPLS processing on all interfaces.
699 for interface in self.links.keys():
787e7624 700 set_sysctl(nrouter, "net.mpls.conf.{}.input".format(interface), 1)
57c5075b 701
f6899d4d 702 return result
1fca63c1 703
95460a6b 704 def stop(self, wait=True, assertOnError=True):
13e1fc49
RZ
705 """
706 Stop router:
707 * Kill daemons
708 """
787e7624 709 self.logger.debug("stopping")
95460a6b 710 return self.tgen.net[self.name].stopRouter(wait, assertOnError)
13e1fc49 711
c65a7e26
KK
712 def startDaemons(self, daemons):
713 """
714 Start Daemons: to start specific daemon(user defined daemon only)
715 * Start daemons (e.g. FRR/Quagga)
716 * Configure daemon logging files
717 """
718 self.logger.debug('starting')
719 nrouter = self.tgen.net[self.name]
720 result = nrouter.startRouterDaemons(daemons)
721
722 # Enable all daemon command logging, logging files
723 # and set them to the start dir.
724 for daemon, enabled in nrouter.daemons.iteritems():
725 for d in daemons:
726 if enabled == 0:
727 continue
728 self.vtysh_cmd('configure terminal\nlog commands\nlog file {}.log'.\
729 format(daemon), daemon=daemon)
730
731 if result != '':
732 self.tgen.set_error(result)
733
734 return result
735
736 def killDaemons(self, daemons, wait=True, assertOnError=True):
737 """
738 Kill specific daemon(user defined daemon only)
739 forcefully using SIGKILL
740 """
741 self.logger.debug('Killing daemons using SIGKILL..')
742 return self.tgen.net[self.name].killRouterDaemons(daemons, wait, assertOnError)
743
f9b48d8b 744 def vtysh_cmd(self, command, isjson=False, daemon=None):
1fca63c1
RZ
745 """
746 Runs the provided command string in the vty shell and returns a string
747 with the response.
748
749 This function also accepts multiple commands, but this mode does not
750 return output for each command. See vtysh_multicmd() for more details.
751 """
752 # Detect multi line commands
787e7624 753 if command.find("\n") != -1:
f9b48d8b
RZ
754 return self.vtysh_multicmd(command, daemon=daemon)
755
787e7624 756 dparam = ""
f9b48d8b 757 if daemon is not None:
787e7624 758 dparam += "-d {}".format(daemon)
f9b48d8b
RZ
759
760 vtysh_command = 'vtysh {} -c "{}" 2>/dev/null'.format(dparam, command)
1fca63c1 761
a40daddc 762 output = self.run(vtysh_command)
787e7624 763 self.logger.info(
764 "\nvtysh command => {}\nvtysh output <= {}".format(command, output)
765 )
a40daddc
RZ
766 if isjson is False:
767 return output
768
7b093d84
RZ
769 try:
770 return json.loads(output)
771 except ValueError:
787e7624 772 logger.warning("vtysh_cmd: failed to convert json output")
7b093d84 773 return {}
1fca63c1 774
f9b48d8b 775 def vtysh_multicmd(self, commands, pretty_output=True, daemon=None):
1fca63c1
RZ
776 """
777 Runs the provided commands in the vty shell and return the result of
778 execution.
779
780 pretty_output: defines how the return value will be presented. When
781 True it will show the command as they were executed in the vty shell,
782 otherwise it will only show lines that failed.
783 """
784 # Prepare the temporary file that will hold the commands
785 fname = topotest.get_file(commands)
786
787e7624 787 dparam = ""
f9b48d8b 788 if daemon is not None:
787e7624 789 dparam += "-d {}".format(daemon)
f9b48d8b 790
1fca63c1
RZ
791 # Run the commands and delete the temporary file
792 if pretty_output:
787e7624 793 vtysh_command = "vtysh {} < {}".format(dparam, fname)
1fca63c1 794 else:
787e7624 795 vtysh_command = "vtysh {} -f {}".format(dparam, fname)
1fca63c1
RZ
796
797 res = self.run(vtysh_command)
798 os.unlink(fname)
799
787e7624 800 self.logger.info(
801 '\nvtysh command => "{}"\nvtysh output <= "{}"'.format(vtysh_command, res)
802 )
13e1fc49 803
1fca63c1
RZ
804 return res
805
38c39932
RZ
806 def report_memory_leaks(self, testname):
807 """
808 Runs the router memory leak check test. Has the following parameter:
809 testname: the test file name for identification
810
811 NOTE: to run this you must have the environment variable
c540096e 812 TOPOTESTS_CHECK_MEMLEAK set or memleak_path configured in `pytest.ini`.
38c39932 813 """
787e7624 814 memleak_file = (
815 os.environ.get("TOPOTESTS_CHECK_MEMLEAK") or self.options["memleak_path"]
816 )
38c39932 817 if memleak_file is None:
38c39932
RZ
818 return
819
13e1fc49 820 self.stop()
787e7624 821 self.logger.info("running memory leak report")
38c39932
RZ
822 self.tgen.net[self.name].report_memory_leaks(memleak_file, testname)
823
6ca2411e
RZ
824 def version_info(self):
825 "Get equipment information from 'show version'."
787e7624 826 output = self.vtysh_cmd("show version").split("\n")[0]
827 columns = topotest.normalize_text(output).split(" ")
b3b1b1d1
RZ
828 try:
829 return {
787e7624 830 "type": columns[0],
831 "version": columns[1],
b3b1b1d1
RZ
832 }
833 except IndexError:
834 return {
787e7624 835 "type": None,
836 "version": None,
b3b1b1d1 837 }
6ca2411e
RZ
838
839 def has_version(self, cmpop, version):
840 """
841 Compares router version using operation `cmpop` with `version`.
842 Valid `cmpop` values:
843 * `>=`: has the same version or greater
844 * '>': has greater version
845 * '=': has the same version
846 * '<': has a lesser version
847 * '<=': has the same version or lesser
848
849 Usage example: router.has_version('>', '1.0')
850 """
fb80b81b 851 return self.tgen.net[self.name].checkRouterVersion(cmpop, version)
6ca2411e
RZ
852
853 def has_type(self, rtype):
854 """
855 Compares router type with `rtype`. Returns `True` if the type matches,
856 otherwise `false`.
857 """
787e7624 858 curtype = self.version_info()["type"]
6ca2411e
RZ
859 return rtype == curtype
860
447f2d5a
LB
861 def has_mpls(self):
862 nrouter = self.tgen.net[self.name]
863 return nrouter.hasmpls
864
787e7624 865
1fca63c1
RZ
866class TopoSwitch(TopoGear):
867 """
868 Switch abstraction. Has the following properties:
869 * cls: switch class that will be used to instantiate
870 * name: switch name
871 """
787e7624 872
1fca63c1
RZ
873 # pylint: disable=too-few-public-methods
874
875 def __init__(self, tgen, cls, name):
876 super(TopoSwitch, self).__init__()
877 self.tgen = tgen
878 self.net = None
879 self.name = name
880 self.cls = cls
881 self.tgen.topo.addSwitch(name, cls=self.cls)
7326ea11
RZ
882
883 def __str__(self):
884 gear = super(TopoSwitch, self).__str__()
787e7624 885 gear += " TopoSwitch<>"
7326ea11 886 return gear
19ccab57 887
787e7624 888
19ccab57
RZ
889class TopoHost(TopoGear):
890 "Host abstraction."
891 # pylint: disable=too-few-public-methods
892
893 def __init__(self, tgen, name, **params):
894 """
895 Mininet has the following known `params` for hosts:
896 * `ip`: the IP address (string) for the host interface
897 * `defaultRoute`: the default route that will be installed
898 (e.g. 'via 10.0.0.1')
899 * `privateDirs`: directories that will be mounted on a different domain
900 (e.g. '/etc/important_dir').
901 """
902 super(TopoHost, self).__init__()
903 self.tgen = tgen
904 self.net = None
905 self.name = name
906 self.options = params
907 self.tgen.topo.addHost(name, **params)
908
909 def __str__(self):
910 gear = super(TopoHost, self).__str__()
911 gear += ' TopoHost<ip="{}",defaultRoute="{}",privateDirs="{}">'.format(
787e7624 912 self.options["ip"],
913 self.options["defaultRoute"],
914 str(self.options["privateDirs"]),
915 )
19ccab57
RZ
916 return gear
917
787e7624 918
19ccab57
RZ
919class TopoExaBGP(TopoHost):
920 "ExaBGP peer abstraction."
921 # pylint: disable=too-few-public-methods
922
923 PRIVATE_DIRS = [
787e7624 924 "/etc/exabgp",
925 "/var/run/exabgp",
926 "/var/log",
19ccab57
RZ
927 ]
928
929 def __init__(self, tgen, name, **params):
930 """
931 ExaBGP usually uses the following parameters:
932 * `ip`: the IP address (string) for the host interface
933 * `defaultRoute`: the default route that will be installed
934 (e.g. 'via 10.0.0.1')
935
936 Note: the different between a host and a ExaBGP peer is that this class
937 has a privateDirs already defined and contains functions to handle ExaBGP
938 things.
939 """
787e7624 940 params["privateDirs"] = self.PRIVATE_DIRS
19ccab57
RZ
941 super(TopoExaBGP, self).__init__(tgen, name, **params)
942 self.tgen.topo.addHost(name, **params)
943
944 def __str__(self):
945 gear = super(TopoExaBGP, self).__str__()
787e7624 946 gear += " TopoExaBGP<>".format()
19ccab57
RZ
947 return gear
948
949 def start(self, peer_dir, env_file=None):
950 """
951 Start running ExaBGP daemon:
952 * Copy all peer* folder contents into /etc/exabgp
953 * Copy exabgp env file if specified
954 * Make all python files runnable
955 * Run ExaBGP with env file `env_file` and configuration peer*/exabgp.cfg
956 """
787e7624 957 self.run("mkdir /etc/exabgp")
958 self.run("chmod 755 /etc/exabgp")
959 self.run("cp {}/* /etc/exabgp/".format(peer_dir))
19ccab57 960 if env_file is not None:
787e7624 961 self.run("cp {} /etc/exabgp/exabgp.env".format(env_file))
962 self.run("chmod 644 /etc/exabgp/*")
963 self.run("chmod a+x /etc/exabgp/*.py")
964 self.run("chown -R exabgp:exabgp /etc/exabgp")
965 output = self.run("exabgp -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg")
87d5e16a 966 if output == None or len(output) == 0:
787e7624 967 output = "<none>"
968 logger.info("{} exabgp started, output={}".format(self.name, output))
19ccab57 969
95460a6b 970 def stop(self, wait=True, assertOnError=True):
19ccab57 971 "Stop ExaBGP peer and kill the daemon"
787e7624 972 self.run("kill `cat /var/run/exabgp/exabgp.pid`")
95460a6b 973 return ""
007e7313
RZ
974
975
976#
977# Diagnostic function
978#
979
980# Disable linter branch warning. It is expected to have these here.
981# pylint: disable=R0912
af99f19e 982def diagnose_env_linux():
007e7313
RZ
983 """
984 Run diagnostics in the running environment. Returns `True` when everything
985 is ok, otherwise `False`.
986 """
987 ret = True
7547ebd8 988
fcfbc769 989 # Test log path exists before installing handler.
787e7624 990 if not os.path.isdir("/tmp"):
991 logger.warning("could not find /tmp for logs")
fcfbc769 992 else:
787e7624 993 os.system("mkdir /tmp/topotests")
fcfbc769 994 # Log diagnostics to file so it can be examined later.
787e7624 995 fhandler = logging.FileHandler(filename="/tmp/topotests/diagnostics.txt")
fcfbc769
RZ
996 fhandler.setLevel(logging.DEBUG)
997 fhandler.setFormatter(
787e7624 998 logging.Formatter(fmt="%(asctime)s %(levelname)s: %(message)s")
fcfbc769
RZ
999 )
1000 logger.addHandler(fhandler)
7547ebd8 1001
787e7624 1002 logger.info("Running environment diagnostics")
007e7313
RZ
1003
1004 # Load configuration
a07e17d5 1005 config = configparser.ConfigParser(tgen_defaults)
787e7624 1006 pytestini_path = os.path.join(CWD, "../pytest.ini")
007e7313
RZ
1007 config.read(pytestini_path)
1008
1009 # Assert that we are running as root
1010 if os.getuid() != 0:
787e7624 1011 logger.error("you must run topotest as root")
007e7313
RZ
1012 ret = False
1013
1014 # Assert that we have mininet
787e7624 1015 if os.system("which mn >/dev/null 2>/dev/null") != 0:
1016 logger.error("could not find mininet binary (mininet is not installed)")
007e7313
RZ
1017 ret = False
1018
1019 # Assert that we have iproute installed
787e7624 1020 if os.system("which ip >/dev/null 2>/dev/null") != 0:
1021 logger.error("could not find ip binary (iproute is not installed)")
007e7313
RZ
1022 ret = False
1023
1024 # Assert that we have gdb installed
787e7624 1025 if os.system("which gdb >/dev/null 2>/dev/null") != 0:
1026 logger.error("could not find gdb binary (gdb is not installed)")
007e7313
RZ
1027 ret = False
1028
1029 # Assert that FRR utilities exist
787e7624 1030 frrdir = config.get("topogen", "frrdir")
812e38a9 1031 hasfrr = False
007e7313 1032 if not os.path.isdir(frrdir):
787e7624 1033 logger.error("could not find {} directory".format(frrdir))
007e7313
RZ
1034 ret = False
1035 else:
812e38a9 1036 hasfrr = True
007e7313 1037 try:
787e7624 1038 pwd.getpwnam("frr")[2]
007e7313
RZ
1039 except KeyError:
1040 logger.warning('could not find "frr" user')
1041
1042 try:
787e7624 1043 grp.getgrnam("frr")[2]
007e7313
RZ
1044 except KeyError:
1045 logger.warning('could not find "frr" group')
1046
1047 try:
787e7624 1048 if "frr" not in grp.getgrnam("frrvty").gr_mem:
1049 logger.error(
1050 '"frr" user and group exist, but user is not under "frrvty"'
1051 )
007e7313
RZ
1052 except KeyError:
1053 logger.warning('could not find "frrvty" group')
1054
787e7624 1055 for fname in [
1056 "zebra",
1057 "ospfd",
1058 "ospf6d",
1059 "bgpd",
1060 "ripd",
1061 "ripngd",
1062 "isisd",
1063 "pimd",
1064 "ldpd",
1065 ]:
007e7313
RZ
1066 path = os.path.join(frrdir, fname)
1067 if not os.path.isfile(path):
1068 # LDPd is an exception
787e7624 1069 if fname == "ldpd":
1070 logger.info(
1071 "could not find {} in {}".format(fname, frrdir)
1072 + "(LDPd tests will not run)"
1073 )
007e7313
RZ
1074 continue
1075
787e7624 1076 logger.warning("could not find {} in {}".format(fname, frrdir))
007e7313 1077 ret = False
d34f6134 1078 else:
787e7624 1079 if fname != "zebra":
d34f6134
RZ
1080 continue
1081
787e7624 1082 os.system("{} -v 2>&1 >/tmp/topotests/frr_zebra.txt".format(path))
007e7313
RZ
1083
1084 # Assert that Quagga utilities exist
787e7624 1085 quaggadir = config.get("topogen", "quaggadir")
812e38a9
RZ
1086 if hasfrr:
1087 # if we have frr, don't check for quagga
1088 pass
1089 elif not os.path.isdir(quaggadir):
787e7624 1090 logger.info(
1091 "could not find {} directory (quagga tests will not run)".format(quaggadir)
1092 )
007e7313 1093 else:
812e38a9 1094 ret = True
007e7313 1095 try:
787e7624 1096 pwd.getpwnam("quagga")[2]
007e7313
RZ
1097 except KeyError:
1098 logger.info('could not find "quagga" user')
1099
1100 try:
787e7624 1101 grp.getgrnam("quagga")[2]
007e7313
RZ
1102 except KeyError:
1103 logger.info('could not find "quagga" group')
1104
1105 try:
787e7624 1106 if "quagga" not in grp.getgrnam("quaggavty").gr_mem:
1107 logger.error(
1108 '"quagga" user and group exist, but user is not under "quaggavty"'
1109 )
007e7313 1110 except KeyError:
812e38a9 1111 logger.warning('could not find "quaggavty" group')
007e7313 1112
787e7624 1113 for fname in [
1114 "zebra",
1115 "ospfd",
1116 "ospf6d",
1117 "bgpd",
1118 "ripd",
1119 "ripngd",
1120 "isisd",
1121 "pimd",
1122 ]:
007e7313
RZ
1123 path = os.path.join(quaggadir, fname)
1124 if not os.path.isfile(path):
787e7624 1125 logger.warning("could not find {} in {}".format(fname, quaggadir))
007e7313 1126 ret = False
d34f6134 1127 else:
787e7624 1128 if fname != "zebra":
d34f6134
RZ
1129 continue
1130
787e7624 1131 os.system("{} -v 2>&1 >/tmp/topotests/quagga_zebra.txt".format(path))
007e7313 1132
007e7313
RZ
1133 # Test MPLS availability
1134 krel = platform.release()
787e7624 1135 if topotest.version_cmp(krel, "4.5") < 0:
1136 logger.info(
1137 'LDPd tests will not run (have kernel "{}", but it requires 4.5)'.format(
1138 krel
1139 )
1140 )
007e7313 1141
c11c4cc7 1142 # Test for MPLS Kernel modules available
787e7624 1143 if not topotest.module_present("mpls-router", load=False) != 0:
1144 logger.info("LDPd tests will not run (missing mpls-router kernel module)")
1145 if not topotest.module_present("mpls-iptunnel", load=False) != 0:
1146 logger.info("LDPd tests will not run (missing mpls-iptunnel kernel module)")
c11c4cc7 1147
007e7313
RZ
1148 # TODO remove me when we start supporting exabgp >= 4
1149 try:
787e7624 1150 output = subprocess.check_output(["exabgp", "-v"])
1151 line = output.split("\n")[0]
1152 version = line.split(" ")[2]
1153 if topotest.version_cmp(version, "4") >= 0:
1154 logger.warning(
1155 "BGP topologies are still using exabgp version 3, expect failures"
1156 )
007e7313
RZ
1157
1158 # We want to catch all exceptions
1159 # pylint: disable=W0702
1160 except:
787e7624 1161 logger.warning("failed to find exabgp or returned error")
007e7313 1162
7547ebd8
RZ
1163 # After we logged the output to file, remove the handler.
1164 logger.removeHandler(fhandler)
1165
007e7313 1166 return ret
af99f19e 1167
787e7624 1168
af99f19e
DS
1169def diagnose_env_freebsd():
1170 return True
1171
787e7624 1172
af99f19e
DS
1173def diagnose_env():
1174 if sys.platform.startswith("linux"):
1175 return diagnose_env_linux()
1176 elif sys.platform.startswith("freebsd"):
1177 return diagnose_env_freebsd()
1178
1179 return False