]>
Commit | Line | Data |
---|---|---|
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 | """ | |
24 | Topogen (Topology Generator) is an abstraction around Topotest and Mininet to | |
25 | help reduce boilerplate code and provide a stable interface to build topology | |
26 | tests on. | |
27 | ||
28 | Basic 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 | ||
41 | import os | |
42 | import sys | |
11761ab0 | 43 | import io |
7547ebd8 | 44 | import logging |
a40daddc | 45 | import json |
a07e17d5 MS |
46 | |
47 | if sys.version_info[0] > 2: | |
48 | import configparser | |
49 | else: | |
50 | import ConfigParser as configparser | |
51 | ||
13e1fc49 | 52 | import glob |
f6899d4d | 53 | import grp |
007e7313 RZ |
54 | import platform |
55 | import pwd | |
007e7313 | 56 | import subprocess |
0e17ee9e | 57 | import pytest |
1fca63c1 RZ |
58 | |
59 | from mininet.net import Mininet | |
60 | from mininet.log import setLogLevel | |
61 | from mininet.cli import CLI | |
62 | ||
63 | from lib import topotest | |
36d1dc45 | 64 | from lib.topolog import logger, logger_config |
a89241b4 | 65 | from lib.topotest import set_sysctl |
1fca63c1 | 66 | |
edd2bdf6 RZ |
67 | CWD = 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. | |
72 | global_tgen = None | |
73 | ||
787e7624 | 74 | |
1fca63c1 RZ |
75 | def 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 |
85 | def 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 |
97 | tgen_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 |
105 | class 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 |
438 | class 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 |
544 | class 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 |
900 | class 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 |
923 | class 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 |
953 | class 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 | 1016 | def 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 |
1155 | def diagnose_env_freebsd(): |
1156 | return True | |
1157 | ||
787e7624 | 1158 | |
af99f19e DS |
1159 | def 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 |