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