]>
Commit | Line | Data |
---|---|---|
acddc0ed | 1 | # SPDX-License-Identifier: ISC |
1fca63c1 RZ |
2 | # |
3 | # topogen.py | |
4 | # Library of helper functions for NetDEF Topology Tests | |
5 | # | |
6 | # Copyright (c) 2017 by | |
7 | # Network Device Education Foundation, Inc. ("NetDEF") | |
8 | # | |
1fca63c1 RZ |
9 | |
10 | """ | |
11 | Topogen (Topology Generator) is an abstraction around Topotest and Mininet to | |
12 | help reduce boilerplate code and provide a stable interface to build topology | |
13 | tests on. | |
14 | ||
15 | Basic usage instructions: | |
16 | ||
17 | * Define a Topology class with a build method using mininet.topo.Topo. | |
18 | See examples/test_template.py. | |
19 | * Use Topogen inside the build() method with get_topogen. | |
20 | e.g. get_topogen(self). | |
21 | * Start up your topology with: Topogen(YourTopology) | |
22 | * Initialize the Mininet with your topology with: tgen.start_topology() | |
23 | * Configure your routers/hosts and start them | |
24 | * Run your tests / mininet cli. | |
25 | * After running stop Mininet with: tgen.stop_topology() | |
26 | """ | |
27 | ||
7592b2cc | 28 | import configparser |
49581587 CH |
29 | import grp |
30 | import inspect | |
31 | import json | |
32 | import logging | |
1fca63c1 | 33 | import os |
49581587 CH |
34 | import platform |
35 | import pwd | |
36 | import re | |
b5c12fb1 | 37 | import shlex |
49581587 | 38 | import subprocess |
1fca63c1 | 39 | import sys |
49581587 | 40 | from collections import OrderedDict |
a07e17d5 | 41 | |
49581587 CH |
42 | import lib.topolog as topolog |
43 | from lib.micronet import Commander | |
44 | from lib.micronet_compat import Mininet | |
45 | from lib.topolog import logger | |
773fd82e | 46 | from munet.testing.util import pause_test |
1fca63c1 RZ |
47 | |
48 | from lib import topotest | |
49 | ||
edd2bdf6 RZ |
50 | CWD = os.path.dirname(os.path.realpath(__file__)) |
51 | ||
1fca63c1 RZ |
52 | # pylint: disable=C0103 |
53 | # Global Topogen variable. This is being used to keep the Topogen available on | |
54 | # all test functions without declaring a test local variable. | |
55 | global_tgen = None | |
56 | ||
787e7624 | 57 | |
1fca63c1 RZ |
58 | def get_topogen(topo=None): |
59 | """ | |
60 | Helper function to retrieve Topogen. Must be called with `topo` when called | |
61 | inside the build() method of Topology class. | |
62 | """ | |
63 | if topo is not None: | |
64 | global_tgen.topo = topo | |
65 | return global_tgen | |
66 | ||
787e7624 | 67 | |
1fca63c1 RZ |
68 | def set_topogen(tgen): |
69 | "Helper function to set Topogen" | |
70 | # pylint: disable=W0603 | |
71 | global global_tgen | |
72 | global_tgen = tgen | |
73 | ||
787e7624 | 74 | |
49581587 CH |
75 | def is_string(value): |
76 | """Return True if value is a string.""" | |
77 | try: | |
78 | return isinstance(value, basestring) # type: ignore | |
79 | except NameError: | |
80 | return isinstance(value, str) | |
81 | ||
82 | ||
83 | def get_exabgp_cmd(commander=None): | |
84 | """Return the command to use for ExaBGP version < 4.""" | |
85 | ||
86 | if commander is None: | |
87 | commander = Commander("topogen") | |
88 | ||
89 | def exacmd_version_ok(exacmd): | |
90 | logger.debug("checking %s for exabgp < version 4", exacmd) | |
91 | _, stdout, _ = commander.cmd_status(exacmd + " -v", warn=False) | |
92 | m = re.search(r"ExaBGP\s*:\s*((\d+)\.(\d+)(?:\.(\d+))?)", stdout) | |
93 | if not m: | |
94 | return False | |
95 | version = m.group(1) | |
96 | if topotest.version_cmp(version, "4") >= 0: | |
a53c08bc | 97 | logging.debug("found exabgp version >= 4 in %s will keep looking", exacmd) |
49581587 CH |
98 | return False |
99 | logger.info("Using ExaBGP version %s in %s", version, exacmd) | |
100 | return True | |
101 | ||
102 | exacmd = commander.get_exec_path("exabgp") | |
103 | if exacmd and exacmd_version_ok(exacmd): | |
104 | return exacmd | |
105 | py2_path = commander.get_exec_path("python2") | |
106 | if py2_path: | |
107 | exacmd = py2_path + " -m exabgp" | |
108 | if exacmd_version_ok(exacmd): | |
109 | return exacmd | |
110 | py2_path = commander.get_exec_path("python") | |
111 | if py2_path: | |
112 | exacmd = py2_path + " -m exabgp" | |
113 | if exacmd_version_ok(exacmd): | |
114 | return exacmd | |
115 | return None | |
116 | ||
117 | ||
1fca63c1 RZ |
118 | # |
119 | # Main class: topology builder | |
120 | # | |
121 | ||
007e7313 RZ |
122 | # Topogen configuration defaults |
123 | tgen_defaults = { | |
787e7624 | 124 | "verbosity": "info", |
125 | "frrdir": "/usr/lib/frr", | |
787e7624 | 126 | "routertype": "frr", |
11761ab0 | 127 | "memleak_path": "", |
007e7313 RZ |
128 | } |
129 | ||
787e7624 | 130 | |
1fca63c1 RZ |
131 | class Topogen(object): |
132 | "A topology test builder helper." | |
133 | ||
787e7624 | 134 | CONFIG_SECTION = "topogen" |
edd2bdf6 | 135 | |
49581587 | 136 | def __init__(self, topodef, modname="unnamed"): |
13e1fc49 RZ |
137 | """ |
138 | Topogen initialization function, takes the following arguments: | |
49581587 CH |
139 | * `cls`: OLD:uthe topology class that is child of mininet.topo or a build function. |
140 | * `topodef`: A dictionary defining the topology, a filename of a json file, or a | |
141 | function that will do the same | |
13e1fc49 RZ |
142 | * `modname`: module name must be a unique name to identify logs later. |
143 | """ | |
edd2bdf6 | 144 | self.config = None |
1fca63c1 RZ |
145 | self.net = None |
146 | self.gears = {} | |
147 | self.routern = 1 | |
148 | self.switchn = 1 | |
13e1fc49 | 149 | self.modname = modname |
1eb633c0 | 150 | self.errorsd = {} |
787e7624 | 151 | self.errors = "" |
19ccab57 | 152 | self.peern = 1 |
49581587 CH |
153 | self.cfg_gen = 0 |
154 | self.exabgp_cmd = None | |
155 | self._init_topo(topodef) | |
156 | ||
787e7624 | 157 | logger.info("loading topology: {}".format(self.modname)) |
1fca63c1 | 158 | |
49581587 CH |
159 | # @staticmethod |
160 | # def _mininet_reset(): | |
161 | # "Reset the mininet environment" | |
162 | # # Clean up the mininet environment | |
163 | # os.system("mn -c > /dev/null 2>&1") | |
1fca63c1 | 164 | |
49581587 CH |
165 | def __str__(self): |
166 | return "Topogen()" | |
167 | ||
168 | def _init_topo(self, topodef): | |
1fca63c1 RZ |
169 | """ |
170 | Initialize the topogily provided by the user. The user topology class | |
171 | must call get_topogen() during build() to get the topogen object. | |
172 | """ | |
173 | # Set the global variable so the test cases can access it anywhere | |
174 | set_topogen(self) | |
175 | ||
49581587 CH |
176 | # Increase host based limits |
177 | topotest.fix_host_limits() | |
178 | ||
9711fc7e LB |
179 | # Test for MPLS Kernel modules available |
180 | self.hasmpls = False | |
787e7624 | 181 | if not topotest.module_present("mpls-router"): |
182 | logger.info("MPLS tests will not run (missing mpls-router kernel module)") | |
183 | elif not topotest.module_present("mpls-iptunnel"): | |
184 | logger.info("MPLS tests will not run (missing mpls-iptunnel kernel module)") | |
9711fc7e LB |
185 | else: |
186 | self.hasmpls = True | |
49581587 | 187 | |
edd2bdf6 RZ |
188 | # Load the default topology configurations |
189 | self._load_config() | |
190 | ||
49581587 | 191 | # Create new log directory |
249ac6f0 | 192 | self.logdir = topotest.get_logs_path(topotest.g_pytest_config.option.rundir) |
a53c08bc CH |
193 | subprocess.check_call( |
194 | "mkdir -p {0} && chmod 1777 {0}".format(self.logdir), shell=True | |
195 | ) | |
49581587 CH |
196 | try: |
197 | routertype = self.config.get(self.CONFIG_SECTION, "routertype") | |
198 | # Only allow group, if it exist. | |
199 | gid = grp.getgrnam(routertype)[2] | |
200 | os.chown(self.logdir, 0, gid) | |
201 | os.chmod(self.logdir, 0o775) | |
202 | except KeyError: | |
203 | # Allow anyone, but set the sticky bit to avoid file deletions | |
204 | os.chmod(self.logdir, 0o1777) | |
205 | ||
fe50239b CH |
206 | # Remove old twisty way of creating sub-classed topology object which has it's |
207 | # build method invoked which calls Topogen methods which then call Topo methods | |
208 | # to create a topology within the Topo object, which is then used by | |
49581587 | 209 | # Mininet(Micronet) to build the actual topology. |
fe50239b | 210 | assert not inspect.isclass(topodef) |
49581587 | 211 | |
249ac6f0 | 212 | self.net = Mininet(rundir=self.logdir, pytestconfig=topotest.g_pytest_config) |
49581587 | 213 | |
fa773d11 CH |
214 | # Adjust the parent namespace |
215 | topotest.fix_netns_limits(self.net) | |
216 | ||
49581587 CH |
217 | # New direct way: Either a dictionary defines the topology or a build function |
218 | # is supplied, or a json filename all of which build the topology by calling | |
219 | # Topogen methods which call Mininet(Micronet) methods to create the actual | |
220 | # topology. | |
221 | if not inspect.isclass(topodef): | |
222 | if callable(topodef): | |
223 | topodef(self) | |
224 | self.net.configure_hosts() | |
225 | elif is_string(topodef): | |
226 | # topojson imports topogen in one function too, | |
227 | # switch away from this use here to the topojson | |
228 | # fixutre and remove this case | |
229 | from lib.topojson import build_topo_from_json | |
a53c08bc | 230 | |
49581587 CH |
231 | with open(topodef, "r") as topof: |
232 | self.json_topo = json.load(topof) | |
233 | build_topo_from_json(self, self.json_topo) | |
234 | self.net.configure_hosts() | |
235 | elif topodef: | |
236 | self.add_topology_from_dict(topodef) | |
237 | ||
238 | def add_topology_from_dict(self, topodef): | |
a53c08bc CH |
239 | keylist = ( |
240 | topodef.keys() | |
241 | if isinstance(topodef, OrderedDict) | |
242 | else sorted(topodef.keys()) | |
243 | ) | |
49581587 CH |
244 | # --------------------------- |
245 | # Create all referenced hosts | |
246 | # --------------------------- | |
247 | for oname in keylist: | |
248 | tup = (topodef[oname],) if is_string(topodef[oname]) else topodef[oname] | |
249 | for e in tup: | |
250 | desc = e.split(":") | |
251 | name = desc[0] | |
252 | if name not in self.gears: | |
253 | logging.debug("Adding router: %s", name) | |
254 | self.add_router(name) | |
255 | ||
256 | # ------------------------------ | |
257 | # Create all referenced switches | |
258 | # ------------------------------ | |
259 | for oname in keylist: | |
260 | if oname is not None and oname not in self.gears: | |
261 | logging.debug("Adding switch: %s", oname) | |
262 | self.add_switch(oname) | |
263 | ||
264 | # ---------------- | |
265 | # Create all links | |
266 | # ---------------- | |
267 | for oname in keylist: | |
268 | if oname is None: | |
269 | continue | |
270 | tup = (topodef[oname],) if is_string(topodef[oname]) else topodef[oname] | |
271 | for e in tup: | |
272 | desc = e.split(":") | |
273 | name = desc[0] | |
274 | ifname = desc[1] if len(desc) > 1 else None | |
275 | sifname = desc[2] if len(desc) > 2 else None | |
276 | self.add_link(self.gears[oname], self.gears[name], sifname, ifname) | |
277 | ||
278 | self.net.configure_hosts() | |
1fca63c1 | 279 | |
edd2bdf6 RZ |
280 | def _load_config(self): |
281 | """ | |
282 | Loads the configuration file `pytest.ini` located at the root dir of | |
283 | topotests. | |
284 | """ | |
a07e17d5 | 285 | self.config = configparser.ConfigParser(tgen_defaults) |
787e7624 | 286 | pytestini_path = os.path.join(CWD, "../pytest.ini") |
edd2bdf6 RZ |
287 | self.config.read(pytestini_path) |
288 | ||
49581587 | 289 | def add_router(self, name=None, cls=None, **params): |
1fca63c1 RZ |
290 | """ |
291 | Adds a new router to the topology. This function has the following | |
292 | options: | |
edd2bdf6 RZ |
293 | * `name`: (optional) select the router name |
294 | * `daemondir`: (optional) custom daemon binary directory | |
622c4996 | 295 | * `routertype`: (optional) `frr` |
1fca63c1 RZ |
296 | Returns a TopoRouter. |
297 | """ | |
49581587 CH |
298 | if cls is None: |
299 | cls = topotest.Router | |
1fca63c1 | 300 | if name is None: |
787e7624 | 301 | name = "r{}".format(self.routern) |
1fca63c1 | 302 | if name in self.gears: |
787e7624 | 303 | raise KeyError("router already exists") |
1fca63c1 | 304 | |
787e7624 | 305 | params["frrdir"] = self.config.get(self.CONFIG_SECTION, "frrdir") |
787e7624 | 306 | params["memleak_path"] = self.config.get(self.CONFIG_SECTION, "memleak_path") |
11761ab0 | 307 | if "routertype" not in params: |
787e7624 | 308 | params["routertype"] = self.config.get(self.CONFIG_SECTION, "routertype") |
edd2bdf6 | 309 | |
2ab85530 | 310 | self.gears[name] = TopoRouter(self, cls, name, **params) |
1fca63c1 RZ |
311 | self.routern += 1 |
312 | return self.gears[name] | |
313 | ||
49581587 | 314 | def add_switch(self, name=None): |
1fca63c1 RZ |
315 | """ |
316 | Adds a new switch to the topology. This function has the following | |
317 | options: | |
318 | name: (optional) select the switch name | |
319 | Returns the switch name and number. | |
320 | """ | |
321 | if name is None: | |
787e7624 | 322 | name = "s{}".format(self.switchn) |
1fca63c1 | 323 | if name in self.gears: |
787e7624 | 324 | raise KeyError("switch already exists") |
1fca63c1 | 325 | |
49581587 | 326 | self.gears[name] = TopoSwitch(self, name) |
1fca63c1 RZ |
327 | self.switchn += 1 |
328 | return self.gears[name] | |
329 | ||
19ccab57 RZ |
330 | def add_exabgp_peer(self, name, ip, defaultRoute): |
331 | """ | |
332 | Adds a new ExaBGP peer to the topology. This function has the following | |
333 | parameters: | |
334 | * `ip`: the peer address (e.g. '1.2.3.4/24') | |
335 | * `defaultRoute`: the peer default route (e.g. 'via 1.2.3.1') | |
336 | """ | |
337 | if name is None: | |
787e7624 | 338 | name = "peer{}".format(self.peern) |
19ccab57 | 339 | if name in self.gears: |
787e7624 | 340 | raise KeyError("exabgp peer already exists") |
19ccab57 RZ |
341 | |
342 | self.gears[name] = TopoExaBGP(self, name, ip=ip, defaultRoute=defaultRoute) | |
343 | self.peern += 1 | |
344 | return self.gears[name] | |
345 | ||
ab59579a RZ |
346 | def add_host(self, name, ip, defaultRoute): |
347 | """ | |
348 | Adds a new host to the topology. This function has the following | |
349 | parameters: | |
350 | * `ip`: the peer address (e.g. '1.2.3.4/24') | |
351 | * `defaultRoute`: the peer default route (e.g. 'via 1.2.3.1') | |
352 | """ | |
353 | if name is None: | |
354 | name = "host{}".format(self.peern) | |
355 | if name in self.gears: | |
356 | raise KeyError("host already exists") | |
357 | ||
358 | self.gears[name] = TopoHost(self, name, ip=ip, defaultRoute=defaultRoute) | |
359 | self.peern += 1 | |
360 | return self.gears[name] | |
361 | ||
1fca63c1 RZ |
362 | def add_link(self, node1, node2, ifname1=None, ifname2=None): |
363 | """ | |
364 | Creates a connection between node1 and node2. The nodes can be the | |
365 | following: | |
366 | * TopoGear | |
367 | * TopoRouter | |
368 | * TopoSwitch | |
369 | """ | |
370 | if not isinstance(node1, TopoGear): | |
787e7624 | 371 | raise ValueError("invalid node1 type") |
1fca63c1 | 372 | if not isinstance(node2, TopoGear): |
787e7624 | 373 | raise ValueError("invalid node2 type") |
1fca63c1 RZ |
374 | |
375 | if ifname1 is None: | |
8c3fdf62 | 376 | ifname1 = node1.new_link() |
1fca63c1 | 377 | if ifname2 is None: |
8c3fdf62 RZ |
378 | ifname2 = node2.new_link() |
379 | ||
380 | node1.register_link(ifname1, node2, ifname2) | |
381 | node2.register_link(ifname2, node1, ifname1) | |
fe50239b | 382 | self.net.add_link(node1.name, node2.name, ifname1, ifname2) |
1fca63c1 | 383 | |
19ccab57 RZ |
384 | def get_gears(self, geartype): |
385 | """ | |
386 | Returns a dictionary of all gears of type `geartype`. | |
387 | ||
388 | Normal usage: | |
389 | * Dictionary iteration: | |
390 | ```py | |
391 | tgen = get_topogen() | |
392 | router_dict = tgen.get_gears(TopoRouter) | |
e5f0ed14 | 393 | for router_name, router in router_dict.items(): |
19ccab57 RZ |
394 | # Do stuff |
395 | ``` | |
396 | * List iteration: | |
397 | ```py | |
398 | tgen = get_topogen() | |
399 | peer_list = tgen.get_gears(TopoExaBGP).values() | |
400 | for peer in peer_list: | |
401 | # Do stuff | |
402 | ``` | |
403 | """ | |
787e7624 | 404 | return dict( |
405 | (name, gear) | |
e5f0ed14 | 406 | for name, gear in self.gears.items() |
787e7624 | 407 | if isinstance(gear, geartype) |
408 | ) | |
19ccab57 | 409 | |
1fca63c1 RZ |
410 | def routers(self): |
411 | """ | |
412 | Returns the router dictionary (key is the router name and value is the | |
413 | router object itself). | |
414 | """ | |
19ccab57 RZ |
415 | return self.get_gears(TopoRouter) |
416 | ||
417 | def exabgp_peers(self): | |
418 | """ | |
419 | Returns the exabgp peer dictionary (key is the peer name and value is | |
420 | the peer object itself). | |
421 | """ | |
422 | return self.get_gears(TopoExaBGP) | |
1fca63c1 | 423 | |
49581587 CH |
424 | def start_topology(self): |
425 | """Starts the topology class.""" | |
787e7624 | 426 | logger.info("starting topology: {}".format(self.modname)) |
1fca63c1 RZ |
427 | self.net.start() |
428 | ||
429 | def start_router(self, router=None): | |
430 | """ | |
431 | Call the router startRouter method. | |
b515c81a | 432 | If no router is specified it is called for all registered routers. |
1fca63c1 RZ |
433 | """ |
434 | if router is None: | |
435 | # pylint: disable=r1704 | |
49581587 | 436 | # XXX should be hosts? |
e5f0ed14 | 437 | for _, router in self.routers().items(): |
1fca63c1 RZ |
438 | router.start() |
439 | else: | |
440 | if isinstance(router, str): | |
441 | router = self.gears[router] | |
442 | ||
443 | router.start() | |
444 | ||
445 | def stop_topology(self): | |
4cfdff1a RZ |
446 | """ |
447 | Stops the network topology. This function will call the stop() function | |
448 | of all gears before calling the mininet stop function, so they can have | |
3a568b9c LB |
449 | their oportunity to do a graceful shutdown. stop() is called twice. The |
450 | first is a simple kill with no sleep, the second will sleep if not | |
451 | killed and try with a different signal. | |
4cfdff1a | 452 | """ |
773fd82e CH |
453 | pause = bool(self.net.cfgopt.get_option("--pause-at-end")) |
454 | pause = pause or bool(self.net.cfgopt.get_option("--pause")) | |
455 | if pause: | |
456 | try: | |
457 | pause_test("Before MUNET delete") | |
458 | except KeyboardInterrupt: | |
459 | print("^C...continuing") | |
460 | except Exception as error: | |
461 | self.logger.error("\n...continuing after error: %s", error) | |
462 | ||
787e7624 | 463 | logger.info("stopping topology: {}".format(self.modname)) |
773fd82e | 464 | |
95460a6b | 465 | errors = "" |
4cfdff1a | 466 | for gear in self.gears.values(): |
942a224e | 467 | errors += gear.stop() |
95460a6b | 468 | if len(errors) > 0: |
9fa6ec14 | 469 | logger.error( |
470 | "Errors found post shutdown - details follow: {}".format(errors) | |
471 | ) | |
4cfdff1a | 472 | |
1fca63c1 RZ |
473 | self.net.stop() |
474 | ||
49581587 CH |
475 | def get_exabgp_cmd(self): |
476 | if not self.exabgp_cmd: | |
477 | self.exabgp_cmd = get_exabgp_cmd(self.net) | |
478 | return self.exabgp_cmd | |
479 | ||
480 | def cli(self): | |
1fca63c1 RZ |
481 | """ |
482 | Interrupt the test and call the command line interface for manual | |
483 | inspection. Should be only used on non production code. | |
484 | """ | |
49581587 | 485 | self.net.cli() |
1fca63c1 | 486 | |
49581587 | 487 | mininet_cli = cli |
1fca63c1 | 488 | |
13e1fc49 RZ |
489 | def is_memleak_enabled(self): |
490 | "Returns `True` if memory leak report is enable, otherwise `False`." | |
78ed6123 RZ |
491 | # On router failure we can't run the memory leak test |
492 | if self.routers_have_failure(): | |
493 | return False | |
494 | ||
787e7624 | 495 | memleak_file = os.environ.get("TOPOTESTS_CHECK_MEMLEAK") or self.config.get( |
496 | self.CONFIG_SECTION, "memleak_path" | |
497 | ) | |
f637ac01 | 498 | if memleak_file == "" or memleak_file is None: |
13e1fc49 RZ |
499 | return False |
500 | return True | |
501 | ||
502 | def report_memory_leaks(self, testname=None): | |
503 | "Run memory leak test and reports." | |
504 | if not self.is_memleak_enabled(): | |
505 | return | |
506 | ||
507 | # If no name was specified, use the test module name | |
508 | if testname is None: | |
509 | testname = self.modname | |
510 | ||
511 | router_list = self.routers().values() | |
512 | for router in router_list: | |
513 | router.report_memory_leaks(self.modname) | |
514 | ||
393ca0fa RZ |
515 | def set_error(self, message, code=None): |
516 | "Sets an error message and signal other tests to skip." | |
e8f7a22f | 517 | logger.info("setting error msg: %s", message) |
393ca0fa RZ |
518 | |
519 | # If no code is defined use a sequential number | |
520 | if code is None: | |
1eb633c0 | 521 | code = len(self.errorsd) |
393ca0fa | 522 | |
1eb633c0 | 523 | self.errorsd[code] = message |
787e7624 | 524 | self.errors += "\n{}: {}".format(code, message) |
393ca0fa RZ |
525 | |
526 | def has_errors(self): | |
527 | "Returns whether errors exist or not." | |
1eb633c0 | 528 | return len(self.errorsd) > 0 |
13e1fc49 | 529 | |
78ed6123 RZ |
530 | def routers_have_failure(self): |
531 | "Runs an assertion to make sure that all routers are running." | |
532 | if self.has_errors(): | |
533 | return True | |
534 | ||
787e7624 | 535 | errors = "" |
78ed6123 RZ |
536 | router_list = self.routers().values() |
537 | for router in router_list: | |
538 | result = router.check_router_running() | |
787e7624 | 539 | if result != "": |
540 | errors += result + "\n" | |
78ed6123 | 541 | |
787e7624 | 542 | if errors != "": |
543 | self.set_error(errors, "router_error") | |
46325763 | 544 | assert False, errors |
78ed6123 RZ |
545 | return True |
546 | return False | |
547 | ||
787e7624 | 548 | |
1fca63c1 RZ |
549 | # |
550 | # Topology gears (equipment) | |
551 | # | |
552 | ||
787e7624 | 553 | |
1fca63c1 RZ |
554 | class TopoGear(object): |
555 | "Abstract class for type checking" | |
556 | ||
49581587 CH |
557 | def __init__(self, tgen, name, **params): |
558 | self.tgen = tgen | |
559 | self.name = name | |
560 | self.params = params | |
8c3fdf62 | 561 | self.links = {} |
1fca63c1 RZ |
562 | self.linkn = 0 |
563 | ||
49581587 CH |
564 | # Would be nice for this to point at the gears log directory rather than the |
565 | # test's. | |
566 | self.logdir = tgen.logdir | |
567 | self.gearlogdir = None | |
568 | ||
7326ea11 | 569 | def __str__(self): |
787e7624 | 570 | links = "" |
e5f0ed14 | 571 | for myif, dest in self.links.items(): |
7326ea11 | 572 | _, destif = dest |
787e7624 | 573 | if links != "": |
574 | links += "," | |
7326ea11 RZ |
575 | links += '"{}"<->"{}"'.format(myif, destif) |
576 | ||
577 | return 'TopoGear<name="{}",links=[{}]>'.format(self.name, links) | |
578 | ||
49581587 CH |
579 | @property |
580 | def net(self): | |
581 | return self.tgen.net[self.name] | |
582 | ||
4cfdff1a RZ |
583 | def start(self): |
584 | "Basic start function that just reports equipment start" | |
585 | logger.info('starting "{}"'.format(self.name)) | |
586 | ||
95460a6b | 587 | def stop(self, wait=True, assertOnError=True): |
49581587 CH |
588 | "Basic stop function that just reports equipment stop" |
589 | logger.info('"{}" base stop called'.format(self.name)) | |
95460a6b | 590 | return "" |
4cfdff1a | 591 | |
49581587 | 592 | def cmd(self, command, **kwargs): |
8c3fdf62 RZ |
593 | """ |
594 | Runs the provided command string in the router and returns a string | |
595 | with the response. | |
596 | """ | |
49581587 CH |
597 | return self.net.cmd_legacy(command, **kwargs) |
598 | ||
599 | def cmd_raises(self, command, **kwargs): | |
600 | """ | |
601 | Runs the provided command string in the router and returns a string | |
602 | with the response. Raise an exception on any error. | |
603 | """ | |
604 | return self.net.cmd_raises(command, **kwargs) | |
605 | ||
606 | run = cmd | |
8c3fdf62 | 607 | |
35c4c991 CH |
608 | def popen(self, *params, **kwargs): |
609 | """ | |
49581587 CH |
610 | Creates a pipe with the given command. Same args as python Popen. |
611 | If `command` is a string then will be invoked with shell, otherwise | |
612 | `command` is a list and will be invoked w/o shell. Returns a popen object. | |
35c4c991 | 613 | """ |
49581587 | 614 | return self.net.popen(*params, **kwargs) |
35c4c991 | 615 | |
1fca63c1 RZ |
616 | def add_link(self, node, myif=None, nodeif=None): |
617 | """ | |
618 | Creates a link (connection) between myself and the specified node. | |
619 | Interfaces name can be speficied with: | |
620 | myif: the interface name that will be created in this node | |
621 | nodeif: the target interface name that will be created on the remote node. | |
622 | """ | |
623 | self.tgen.add_link(self, node, myif, nodeif) | |
624 | ||
0ab7733f | 625 | def link_enable(self, myif, enabled=True, netns=None): |
1fca63c1 | 626 | """ |
8c3fdf62 RZ |
627 | Set this node interface administrative state. |
628 | myif: this node interface name | |
629 | enabled: whether we should enable or disable the interface | |
1fca63c1 | 630 | """ |
8c3fdf62 | 631 | if myif not in self.links.keys(): |
787e7624 | 632 | raise KeyError("interface doesn't exists") |
8c3fdf62 RZ |
633 | |
634 | if enabled is True: | |
787e7624 | 635 | operation = "up" |
8c3fdf62 | 636 | else: |
787e7624 | 637 | operation = "down" |
8c3fdf62 | 638 | |
787e7624 | 639 | logger.info( |
640 | 'setting node "{}" link "{}" to state "{}"'.format( | |
641 | self.name, myif, operation | |
642 | ) | |
643 | ) | |
644 | extract = "" | |
0ab7733f | 645 | if netns is not None: |
787e7624 | 646 | extract = "ip netns exec {} ".format(netns) |
49581587 | 647 | |
787e7624 | 648 | return self.run("{}ip link set dev {} {}".format(extract, myif, operation)) |
8c3fdf62 | 649 | |
0ab7733f | 650 | def peer_link_enable(self, myif, enabled=True, netns=None): |
8c3fdf62 RZ |
651 | """ |
652 | Set the peer interface administrative state. | |
653 | myif: this node interface name | |
654 | enabled: whether we should enable or disable the interface | |
655 | ||
656 | NOTE: this is used to simulate a link down on this node, since when the | |
657 | peer disables their interface our interface status changes to no link. | |
658 | """ | |
659 | if myif not in self.links.keys(): | |
787e7624 | 660 | raise KeyError("interface doesn't exists") |
8c3fdf62 RZ |
661 | |
662 | node, nodeif = self.links[myif] | |
0ab7733f | 663 | node.link_enable(nodeif, enabled, netns) |
1fca63c1 | 664 | |
8c3fdf62 RZ |
665 | def new_link(self): |
666 | """ | |
667 | Generates a new unique link name. | |
668 | ||
669 | NOTE: This function should only be called by Topogen. | |
670 | """ | |
787e7624 | 671 | ifname = "{}-eth{}".format(self.name, self.linkn) |
1fca63c1 | 672 | self.linkn += 1 |
1fca63c1 RZ |
673 | return ifname |
674 | ||
8c3fdf62 RZ |
675 | def register_link(self, myif, node, nodeif): |
676 | """ | |
677 | Register link between this node interface and outside node. | |
678 | ||
679 | NOTE: This function should only be called by Topogen. | |
680 | """ | |
681 | if myif in self.links.keys(): | |
787e7624 | 682 | raise KeyError("interface already exists") |
8c3fdf62 RZ |
683 | |
684 | self.links[myif] = (node, nodeif) | |
685 | ||
49581587 CH |
686 | def _setup_tmpdir(self): |
687 | topotest.setup_node_tmpdir(self.logdir, self.name) | |
688 | self.gearlogdir = "{}/{}".format(self.logdir, self.name) | |
689 | return "{}/{}.log".format(self.logdir, self.name) | |
690 | ||
787e7624 | 691 | |
1fca63c1 RZ |
692 | class TopoRouter(TopoGear): |
693 | """ | |
d9ea1cda | 694 | Router abstraction. |
1fca63c1 RZ |
695 | """ |
696 | ||
622c4996 | 697 | # The default required directories by FRR |
1fca63c1 | 698 | PRIVATE_DIRS = [ |
787e7624 | 699 | "/etc/frr", |
49581587 | 700 | "/etc/snmp", |
787e7624 | 701 | "/var/run/frr", |
787e7624 | 702 | "/var/log", |
1fca63c1 RZ |
703 | ] |
704 | ||
705 | # Router Daemon enumeration definition. | |
a4b4bb50 | 706 | RD_FRR = 0 # not a daemon, but use to setup unified configs |
7326ea11 | 707 | RD_ZEBRA = 1 |
1fca63c1 RZ |
708 | RD_RIP = 2 |
709 | RD_RIPNG = 3 | |
710 | RD_OSPF = 4 | |
711 | RD_OSPF6 = 5 | |
712 | RD_ISIS = 6 | |
713 | RD_BGP = 7 | |
714 | RD_LDP = 8 | |
715 | RD_PIM = 9 | |
c267e5b1 RZ |
716 | RD_EIGRP = 10 |
717 | RD_NHRP = 11 | |
a2a1134c | 718 | RD_STATIC = 12 |
4d45d6d3 | 719 | RD_BFD = 13 |
a38f0083 | 720 | RD_SHARP = 14 |
a0764a36 | 721 | RD_BABEL = 15 |
223f87f4 | 722 | RD_PBRD = 16 |
4d7b695d | 723 | RD_PATH = 17 |
92be50e6 | 724 | RD_SNMP = 18 |
e13f9c4f | 725 | RD_PIM6 = 19 |
f637ac01 | 726 | RD_MGMTD = 20 |
1fca63c1 | 727 | RD = { |
a4b4bb50 | 728 | RD_FRR: "frr", |
787e7624 | 729 | RD_ZEBRA: "zebra", |
730 | RD_RIP: "ripd", | |
731 | RD_RIPNG: "ripngd", | |
732 | RD_OSPF: "ospfd", | |
733 | RD_OSPF6: "ospf6d", | |
734 | RD_ISIS: "isisd", | |
735 | RD_BGP: "bgpd", | |
736 | RD_PIM: "pimd", | |
e13f9c4f | 737 | RD_PIM6: "pim6d", |
787e7624 | 738 | RD_LDP: "ldpd", |
739 | RD_EIGRP: "eigrpd", | |
740 | RD_NHRP: "nhrpd", | |
741 | RD_STATIC: "staticd", | |
742 | RD_BFD: "bfdd", | |
743 | RD_SHARP: "sharpd", | |
a0764a36 | 744 | RD_BABEL: "babeld", |
223f87f4 | 745 | RD_PBRD: "pbrd", |
92be50e6 BC |
746 | RD_PATH: "pathd", |
747 | RD_SNMP: "snmpd", | |
f637ac01 | 748 | RD_MGMTD: "mgmtd", |
1fca63c1 RZ |
749 | } |
750 | ||
2ab85530 | 751 | def __init__(self, tgen, cls, name, **params): |
d9ea1cda RZ |
752 | """ |
753 | The constructor has the following parameters: | |
754 | * tgen: Topogen object | |
755 | * cls: router class that will be used to instantiate | |
756 | * name: router name | |
757 | * daemondir: daemon binary directory | |
622c4996 | 758 | * routertype: 'frr' |
d9ea1cda | 759 | """ |
49581587 | 760 | super(TopoRouter, self).__init__(tgen, name, **params) |
787e7624 | 761 | self.routertype = params.get("routertype", "frr") |
60e03778 CH |
762 | if "private_mounts" not in params: |
763 | params["private_mounts"] = self.PRIVATE_DIRS | |
c540096e | 764 | |
13e1fc49 | 765 | # Propagate the router log directory |
49581587 | 766 | logfile = self._setup_tmpdir() |
787e7624 | 767 | params["logdir"] = self.logdir |
13e1fc49 | 768 | |
49581587 CH |
769 | self.logger = topolog.get_logger(name, log_level="debug", target=logfile) |
770 | params["logger"] = self.logger | |
771 | tgen.net.add_host(self.name, cls=cls, **params) | |
772 | topotest.fix_netns_limits(tgen.net[name]) | |
87ba6e1e | 773 | |
49581587 CH |
774 | # Mount gear log directory on a common path |
775 | self.net.bind_mount(self.gearlogdir, "/tmp/gearlogdir") | |
1fca63c1 | 776 | |
e03862c3 HS |
777 | # Ensure pid file |
778 | with open(os.path.join(self.logdir, self.name + ".pid"), "w") as f: | |
c84159a9 | 779 | f.write(str(self.net.pid) + "\n") |
e03862c3 | 780 | |
7326ea11 RZ |
781 | def __str__(self): |
782 | gear = super(TopoRouter, self).__str__() | |
787e7624 | 783 | gear += " TopoRouter<>" |
7326ea11 RZ |
784 | return gear |
785 | ||
8dd5077d PG |
786 | def check_capability(self, daemon, param): |
787 | """ | |
788 | Checks a capability daemon against an argument option | |
789 | Return True if capability available. False otherwise | |
790 | """ | |
791 | daemonstr = self.RD.get(daemon) | |
792 | self.logger.info('check capability {} for "{}"'.format(param, daemonstr)) | |
49581587 | 793 | return self.net.checkCapability(daemonstr, param) |
8dd5077d | 794 | |
a4b4bb50 JAG |
795 | def load_frr_config(self, source, daemons=None): |
796 | """ | |
797 | Loads the unified configuration file source | |
798 | Start the daemons in the list | |
799 | If daemons is None, try to infer daemons from the config file | |
800 | """ | |
27c6bfc2 | 801 | source_path = self.load_config(self.RD_FRR, source) |
a4b4bb50 JAG |
802 | if not daemons: |
803 | # Always add zebra | |
27c6bfc2 | 804 | self.load_config(self.RD_ZEBRA, "") |
a4b4bb50 JAG |
805 | for daemon in self.RD: |
806 | # This will not work for all daemons | |
807 | daemonstr = self.RD.get(daemon).rstrip("d") | |
93a7f236 | 808 | if daemonstr == "pim": |
27c6bfc2 | 809 | grep_cmd = "grep 'ip {}' {}".format(daemonstr, source_path) |
93a7f236 | 810 | else: |
27c6bfc2 | 811 | grep_cmd = "grep 'router {}' {}".format(daemonstr, source_path) |
19003d6e | 812 | result = self.run(grep_cmd, warn=False).strip() |
a4b4bb50 | 813 | if result: |
27c6bfc2 | 814 | self.load_config(daemon, "") |
a4b4bb50 JAG |
815 | else: |
816 | for daemon in daemons: | |
27c6bfc2 | 817 | self.load_config(daemon, "") |
a4b4bb50 | 818 | |
8dd5077d | 819 | def load_config(self, daemon, source=None, param=None): |
02547745 | 820 | """Loads daemon configuration from the specified source |
1fca63c1 RZ |
821 | Possible daemon values are: TopoRouter.RD_ZEBRA, TopoRouter.RD_RIP, |
822 | TopoRouter.RD_RIPNG, TopoRouter.RD_OSPF, TopoRouter.RD_OSPF6, | |
823 | TopoRouter.RD_ISIS, TopoRouter.RD_BGP, TopoRouter.RD_LDP, | |
e13f9c4f | 824 | TopoRouter.RD_PIM, TopoRouter.RD_PIM6, TopoRouter.RD_PBR, |
f637ac01 | 825 | TopoRouter.RD_SNMP, TopoRouter.RD_MGMTD. |
49581587 | 826 | |
02547745 CH |
827 | Possible `source` values are `None` for an empty config file, a path name which is |
828 | used directly, or a file name with no path components which is first looked for | |
829 | directly and then looked for under a sub-directory named after router. | |
830 | ||
49581587 CH |
831 | This API unfortunately allows for source to not exist for any and |
832 | all routers. | |
1fca63c1 RZ |
833 | """ |
834 | daemonstr = self.RD.get(daemon) | |
e8f7a22f | 835 | self.logger.debug('loading "{}" configuration: {}'.format(daemonstr, source)) |
27c6bfc2 | 836 | return self.net.loadConf(daemonstr, source, param) |
1fca63c1 RZ |
837 | |
838 | def check_router_running(self): | |
839 | """ | |
840 | Run a series of checks and returns a status string. | |
841 | """ | |
787e7624 | 842 | self.logger.info("checking if daemons are running") |
49581587 | 843 | return self.net.checkRouterRunning() |
1fca63c1 RZ |
844 | |
845 | def start(self): | |
846 | """ | |
847 | Start router: | |
848 | * Load modules | |
849 | * Clean up files | |
850 | * Configure interfaces | |
622c4996 | 851 | * Start daemons (e.g. FRR) |
f6899d4d | 852 | * Configure daemon logging files |
1fca63c1 | 853 | """ |
49581587 CH |
854 | |
855 | nrouter = self.net | |
9711fc7e | 856 | result = nrouter.startRouter(self.tgen) |
f6899d4d | 857 | |
49581587 CH |
858 | # Enable command logging |
859 | ||
deb4cef0 LB |
860 | # Enable all daemon command logging, logging files |
861 | # and set them to the start dir. | |
e5f0ed14 | 862 | for daemon, enabled in nrouter.daemons.items(): |
49581587 CH |
863 | if enabled and daemon != "snmpd": |
864 | self.vtysh_cmd( | |
a53c08bc CH |
865 | "\n".join( |
866 | [ | |
867 | "clear log cmdline-targets", | |
868 | "conf t", | |
869 | "log file {}.log debug".format(daemon), | |
870 | "log commands", | |
3e097918 | 871 | "log timestamp precision 6", |
a53c08bc CH |
872 | ] |
873 | ), | |
49581587 CH |
874 | daemon=daemon, |
875 | ) | |
f6899d4d | 876 | |
787e7624 | 877 | if result != "": |
57c5075b | 878 | self.tgen.set_error(result) |
49581587 | 879 | elif nrouter.daemons["ldpd"] == 1 or nrouter.daemons["pathd"] == 1: |
a89241b4 | 880 | # Enable MPLS processing on all interfaces. |
49581587 | 881 | for interface in self.links: |
a53c08bc CH |
882 | topotest.sysctl_assure( |
883 | nrouter, "net.mpls.conf.{}.input".format(interface), 1 | |
884 | ) | |
57c5075b | 885 | |
f6899d4d | 886 | return result |
1fca63c1 | 887 | |
942a224e MS |
888 | def stop(self): |
889 | """ | |
890 | Stop router cleanly: | |
49581587 | 891 | * Signal daemons twice, once with SIGTERM, then with SIGKILL. |
942a224e | 892 | """ |
49581587 CH |
893 | self.logger.debug("stopping (no assert)") |
894 | return self.net.stopRouter(False) | |
942a224e | 895 | |
c65a7e26 KK |
896 | def startDaemons(self, daemons): |
897 | """ | |
898 | Start Daemons: to start specific daemon(user defined daemon only) | |
622c4996 | 899 | * Start daemons (e.g. FRR) |
c65a7e26 KK |
900 | * Configure daemon logging files |
901 | """ | |
701a0192 | 902 | self.logger.debug("starting") |
49581587 | 903 | nrouter = self.net |
c65a7e26 KK |
904 | result = nrouter.startRouterDaemons(daemons) |
905 | ||
49581587 CH |
906 | if daemons is None: |
907 | daemons = nrouter.daemons.keys() | |
908 | ||
c65a7e26 KK |
909 | # Enable all daemon command logging, logging files |
910 | # and set them to the start dir. | |
49581587 CH |
911 | for daemon in daemons: |
912 | enabled = nrouter.daemons[daemon] | |
913 | if enabled and daemon != "snmpd": | |
701a0192 | 914 | self.vtysh_cmd( |
a53c08bc CH |
915 | "\n".join( |
916 | [ | |
917 | "clear log cmdline-targets", | |
918 | "conf t", | |
919 | "log file {}.log debug".format(daemon), | |
920 | "log commands", | |
3e097918 | 921 | "log timestamp precision 6", |
a53c08bc CH |
922 | ] |
923 | ), | |
701a0192 | 924 | daemon=daemon, |
925 | ) | |
c65a7e26 | 926 | |
701a0192 | 927 | if result != "": |
c65a7e26 KK |
928 | self.tgen.set_error(result) |
929 | ||
930 | return result | |
931 | ||
932 | def killDaemons(self, daemons, wait=True, assertOnError=True): | |
933 | """ | |
934 | Kill specific daemon(user defined daemon only) | |
935 | forcefully using SIGKILL | |
936 | """ | |
701a0192 | 937 | self.logger.debug("Killing daemons using SIGKILL..") |
49581587 | 938 | return self.net.killRouterDaemons(daemons, wait, assertOnError) |
c65a7e26 | 939 | |
f9b48d8b | 940 | def vtysh_cmd(self, command, isjson=False, daemon=None): |
1fca63c1 RZ |
941 | """ |
942 | Runs the provided command string in the vty shell and returns a string | |
943 | with the response. | |
944 | ||
945 | This function also accepts multiple commands, but this mode does not | |
946 | return output for each command. See vtysh_multicmd() for more details. | |
947 | """ | |
948 | # Detect multi line commands | |
787e7624 | 949 | if command.find("\n") != -1: |
f9b48d8b RZ |
950 | return self.vtysh_multicmd(command, daemon=daemon) |
951 | ||
787e7624 | 952 | dparam = "" |
f9b48d8b | 953 | if daemon is not None: |
787e7624 | 954 | dparam += "-d {}".format(daemon) |
f9b48d8b | 955 | |
b5c12fb1 CH |
956 | vtysh_command = "vtysh {} -c {} 2>/dev/null".format( |
957 | dparam, shlex.quote(command) | |
958 | ) | |
1fca63c1 | 959 | |
b5c12fb1 | 960 | self.logger.debug("vtysh command => {}".format(shlex.quote(command))) |
a40daddc | 961 | output = self.run(vtysh_command) |
49581587 CH |
962 | |
963 | dbgout = output.strip() | |
964 | if dbgout: | |
965 | if "\n" in dbgout: | |
966 | dbgout = dbgout.replace("\n", "\n\t") | |
e8f7a22f | 967 | self.logger.debug("vtysh result:\n\t{}".format(dbgout)) |
49581587 | 968 | else: |
e8f7a22f | 969 | self.logger.debug('vtysh result: "{}"'.format(dbgout)) |
49581587 | 970 | |
a40daddc RZ |
971 | if isjson is False: |
972 | return output | |
973 | ||
7b093d84 RZ |
974 | try: |
975 | return json.loads(output) | |
0ba1d257 | 976 | except ValueError as error: |
a53c08bc CH |
977 | logger.warning( |
978 | "vtysh_cmd: %s: failed to convert json output: %s: %s", | |
979 | self.name, | |
980 | str(output), | |
981 | str(error), | |
982 | ) | |
7b093d84 | 983 | return {} |
1fca63c1 | 984 | |
f9b48d8b | 985 | def vtysh_multicmd(self, commands, pretty_output=True, daemon=None): |
1fca63c1 RZ |
986 | """ |
987 | Runs the provided commands in the vty shell and return the result of | |
988 | execution. | |
989 | ||
990 | pretty_output: defines how the return value will be presented. When | |
991 | True it will show the command as they were executed in the vty shell, | |
992 | otherwise it will only show lines that failed. | |
993 | """ | |
994 | # Prepare the temporary file that will hold the commands | |
995 | fname = topotest.get_file(commands) | |
996 | ||
787e7624 | 997 | dparam = "" |
f9b48d8b | 998 | if daemon is not None: |
787e7624 | 999 | dparam += "-d {}".format(daemon) |
f9b48d8b | 1000 | |
1fca63c1 RZ |
1001 | # Run the commands and delete the temporary file |
1002 | if pretty_output: | |
787e7624 | 1003 | vtysh_command = "vtysh {} < {}".format(dparam, fname) |
1fca63c1 | 1004 | else: |
787e7624 | 1005 | vtysh_command = "vtysh {} -f {}".format(dparam, fname) |
1fca63c1 | 1006 | |
49581587 CH |
1007 | dbgcmds = commands if is_string(commands) else "\n".join(commands) |
1008 | dbgcmds = "\t" + dbgcmds.replace("\n", "\n\t") | |
e8f7a22f | 1009 | self.logger.debug("vtysh command => FILE:\n{}".format(dbgcmds)) |
49581587 | 1010 | |
1fca63c1 RZ |
1011 | res = self.run(vtysh_command) |
1012 | os.unlink(fname) | |
1013 | ||
49581587 CH |
1014 | dbgres = res.strip() |
1015 | if dbgres: | |
1016 | if "\n" in dbgres: | |
1017 | dbgres = dbgres.replace("\n", "\n\t") | |
e8f7a22f | 1018 | self.logger.debug("vtysh result:\n\t{}".format(dbgres)) |
49581587 | 1019 | else: |
e8f7a22f | 1020 | self.logger.debug('vtysh result: "{}"'.format(dbgres)) |
1fca63c1 RZ |
1021 | return res |
1022 | ||
38c39932 RZ |
1023 | def report_memory_leaks(self, testname): |
1024 | """ | |
1025 | Runs the router memory leak check test. Has the following parameter: | |
1026 | testname: the test file name for identification | |
1027 | ||
1028 | NOTE: to run this you must have the environment variable | |
c540096e | 1029 | TOPOTESTS_CHECK_MEMLEAK set or memleak_path configured in `pytest.ini`. |
38c39932 | 1030 | """ |
787e7624 | 1031 | memleak_file = ( |
49581587 | 1032 | os.environ.get("TOPOTESTS_CHECK_MEMLEAK") or self.params["memleak_path"] |
787e7624 | 1033 | ) |
f637ac01 | 1034 | if memleak_file == "" or memleak_file is None: |
38c39932 RZ |
1035 | return |
1036 | ||
942a224e | 1037 | self.stop() |
d2bdb82f | 1038 | |
787e7624 | 1039 | self.logger.info("running memory leak report") |
49581587 | 1040 | self.net.report_memory_leaks(memleak_file, testname) |
38c39932 | 1041 | |
6ca2411e RZ |
1042 | def version_info(self): |
1043 | "Get equipment information from 'show version'." | |
787e7624 | 1044 | output = self.vtysh_cmd("show version").split("\n")[0] |
1045 | columns = topotest.normalize_text(output).split(" ") | |
b3b1b1d1 RZ |
1046 | try: |
1047 | return { | |
787e7624 | 1048 | "type": columns[0], |
1049 | "version": columns[1], | |
b3b1b1d1 RZ |
1050 | } |
1051 | except IndexError: | |
1052 | return { | |
787e7624 | 1053 | "type": None, |
1054 | "version": None, | |
b3b1b1d1 | 1055 | } |
6ca2411e RZ |
1056 | |
1057 | def has_version(self, cmpop, version): | |
1058 | """ | |
1059 | Compares router version using operation `cmpop` with `version`. | |
1060 | Valid `cmpop` values: | |
1061 | * `>=`: has the same version or greater | |
1062 | * '>': has greater version | |
1063 | * '=': has the same version | |
1064 | * '<': has a lesser version | |
1065 | * '<=': has the same version or lesser | |
1066 | ||
1067 | Usage example: router.has_version('>', '1.0') | |
1068 | """ | |
49581587 | 1069 | return self.net.checkRouterVersion(cmpop, version) |
6ca2411e RZ |
1070 | |
1071 | def has_type(self, rtype): | |
1072 | """ | |
1073 | Compares router type with `rtype`. Returns `True` if the type matches, | |
1074 | otherwise `false`. | |
1075 | """ | |
787e7624 | 1076 | curtype = self.version_info()["type"] |
6ca2411e RZ |
1077 | return rtype == curtype |
1078 | ||
447f2d5a | 1079 | def has_mpls(self): |
49581587 | 1080 | return self.net.hasmpls |
447f2d5a | 1081 | |
787e7624 | 1082 | |
1fca63c1 RZ |
1083 | class TopoSwitch(TopoGear): |
1084 | """ | |
1085 | Switch abstraction. Has the following properties: | |
1086 | * cls: switch class that will be used to instantiate | |
1087 | * name: switch name | |
1088 | """ | |
787e7624 | 1089 | |
1fca63c1 RZ |
1090 | # pylint: disable=too-few-public-methods |
1091 | ||
49581587 CH |
1092 | def __init__(self, tgen, name, **params): |
1093 | super(TopoSwitch, self).__init__(tgen, name, **params) | |
1094 | tgen.net.add_switch(name) | |
7326ea11 RZ |
1095 | |
1096 | def __str__(self): | |
1097 | gear = super(TopoSwitch, self).__str__() | |
787e7624 | 1098 | gear += " TopoSwitch<>" |
7326ea11 | 1099 | return gear |
19ccab57 | 1100 | |
787e7624 | 1101 | |
19ccab57 RZ |
1102 | class TopoHost(TopoGear): |
1103 | "Host abstraction." | |
1104 | # pylint: disable=too-few-public-methods | |
1105 | ||
1106 | def __init__(self, tgen, name, **params): | |
1107 | """ | |
1108 | Mininet has the following known `params` for hosts: | |
1109 | * `ip`: the IP address (string) for the host interface | |
1110 | * `defaultRoute`: the default route that will be installed | |
1111 | (e.g. 'via 10.0.0.1') | |
60e03778 | 1112 | * `private_mounts`: directories that will be mounted on a different domain |
19ccab57 RZ |
1113 | (e.g. '/etc/important_dir'). |
1114 | """ | |
49581587 CH |
1115 | super(TopoHost, self).__init__(tgen, name, **params) |
1116 | ||
1117 | # Propagate the router log directory | |
1118 | logfile = self._setup_tmpdir() | |
1119 | params["logdir"] = self.logdir | |
1120 | ||
1121 | # Odd to have 2 logfiles for each host | |
1122 | self.logger = topolog.get_logger(name, log_level="debug", target=logfile) | |
1123 | params["logger"] = self.logger | |
1124 | tgen.net.add_host(name, **params) | |
1125 | topotest.fix_netns_limits(tgen.net[name]) | |
1126 | ||
1127 | # Mount gear log directory on a common path | |
1128 | self.net.bind_mount(self.gearlogdir, "/tmp/gearlogdir") | |
19ccab57 RZ |
1129 | |
1130 | def __str__(self): | |
1131 | gear = super(TopoHost, self).__str__() | |
60e03778 | 1132 | gear += ' TopoHost<ip="{}",defaultRoute="{}",private_mounts="{}">'.format( |
49581587 CH |
1133 | self.params["ip"], |
1134 | self.params["defaultRoute"], | |
60e03778 | 1135 | str(self.params["private_mounts"]), |
787e7624 | 1136 | ) |
19ccab57 RZ |
1137 | return gear |
1138 | ||
787e7624 | 1139 | |
19ccab57 RZ |
1140 | class TopoExaBGP(TopoHost): |
1141 | "ExaBGP peer abstraction." | |
1142 | # pylint: disable=too-few-public-methods | |
1143 | ||
1144 | PRIVATE_DIRS = [ | |
787e7624 | 1145 | "/etc/exabgp", |
1146 | "/var/run/exabgp", | |
1147 | "/var/log", | |
19ccab57 RZ |
1148 | ] |
1149 | ||
1150 | def __init__(self, tgen, name, **params): | |
1151 | """ | |
1152 | ExaBGP usually uses the following parameters: | |
1153 | * `ip`: the IP address (string) for the host interface | |
1154 | * `defaultRoute`: the default route that will be installed | |
1155 | (e.g. 'via 10.0.0.1') | |
1156 | ||
1157 | Note: the different between a host and a ExaBGP peer is that this class | |
60e03778 CH |
1158 | has a private_mounts already defined and contains functions to handle |
1159 | ExaBGP things. | |
19ccab57 | 1160 | """ |
60e03778 | 1161 | params["private_mounts"] = self.PRIVATE_DIRS |
19ccab57 | 1162 | super(TopoExaBGP, self).__init__(tgen, name, **params) |
19ccab57 RZ |
1163 | |
1164 | def __str__(self): | |
1165 | gear = super(TopoExaBGP, self).__str__() | |
787e7624 | 1166 | gear += " TopoExaBGP<>".format() |
19ccab57 RZ |
1167 | return gear |
1168 | ||
1169 | def start(self, peer_dir, env_file=None): | |
1170 | """ | |
1171 | Start running ExaBGP daemon: | |
1172 | * Copy all peer* folder contents into /etc/exabgp | |
1173 | * Copy exabgp env file if specified | |
1174 | * Make all python files runnable | |
1175 | * Run ExaBGP with env file `env_file` and configuration peer*/exabgp.cfg | |
1176 | """ | |
49581587 CH |
1177 | exacmd = self.tgen.get_exabgp_cmd() |
1178 | assert exacmd, "Can't find a usabel ExaBGP (must be < version 4)" | |
1179 | ||
1180 | self.run("mkdir -p /etc/exabgp") | |
787e7624 | 1181 | self.run("chmod 755 /etc/exabgp") |
29053988 | 1182 | self.run("cp {}/exa-* /etc/exabgp/".format(CWD)) |
787e7624 | 1183 | self.run("cp {}/* /etc/exabgp/".format(peer_dir)) |
19ccab57 | 1184 | if env_file is not None: |
787e7624 | 1185 | self.run("cp {} /etc/exabgp/exabgp.env".format(env_file)) |
1186 | self.run("chmod 644 /etc/exabgp/*") | |
1187 | self.run("chmod a+x /etc/exabgp/*.py") | |
1188 | self.run("chown -R exabgp:exabgp /etc/exabgp") | |
49581587 CH |
1189 | |
1190 | output = self.run(exacmd + " -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg") | |
f637ac01 | 1191 | if output is None or len(output) == 0: |
787e7624 | 1192 | output = "<none>" |
49581587 | 1193 | |
787e7624 | 1194 | logger.info("{} exabgp started, output={}".format(self.name, output)) |
19ccab57 | 1195 | |
95460a6b | 1196 | def stop(self, wait=True, assertOnError=True): |
19ccab57 | 1197 | "Stop ExaBGP peer and kill the daemon" |
787e7624 | 1198 | self.run("kill `cat /var/run/exabgp/exabgp.pid`") |
95460a6b | 1199 | return "" |
007e7313 RZ |
1200 | |
1201 | ||
1202 | # | |
1203 | # Diagnostic function | |
1204 | # | |
1205 | ||
3e097918 | 1206 | |
007e7313 RZ |
1207 | # Disable linter branch warning. It is expected to have these here. |
1208 | # pylint: disable=R0912 | |
49581587 | 1209 | def diagnose_env_linux(rundir): |
007e7313 RZ |
1210 | """ |
1211 | Run diagnostics in the running environment. Returns `True` when everything | |
1212 | is ok, otherwise `False`. | |
1213 | """ | |
1214 | ret = True | |
7547ebd8 | 1215 | |
007e7313 | 1216 | # Load configuration |
11761ab0 | 1217 | config = configparser.ConfigParser(defaults=tgen_defaults) |
787e7624 | 1218 | pytestini_path = os.path.join(CWD, "../pytest.ini") |
007e7313 RZ |
1219 | config.read(pytestini_path) |
1220 | ||
49581587 CH |
1221 | # Test log path exists before installing handler. |
1222 | os.system("mkdir -p " + rundir) | |
1223 | # Log diagnostics to file so it can be examined later. | |
1224 | fhandler = logging.FileHandler(filename="{}/diagnostics.txt".format(rundir)) | |
1225 | fhandler.setLevel(logging.DEBUG) | |
1226 | fhandler.setFormatter(logging.Formatter(fmt=topolog.FORMAT)) | |
1227 | logger.addHandler(fhandler) | |
1228 | ||
1229 | logger.info("Running environment diagnostics") | |
1230 | ||
007e7313 RZ |
1231 | # Assert that we are running as root |
1232 | if os.getuid() != 0: | |
787e7624 | 1233 | logger.error("you must run topotest as root") |
007e7313 RZ |
1234 | ret = False |
1235 | ||
1236 | # Assert that we have mininet | |
49581587 CH |
1237 | # if os.system("which mn >/dev/null 2>/dev/null") != 0: |
1238 | # logger.error("could not find mininet binary (mininet is not installed)") | |
1239 | # ret = False | |
007e7313 RZ |
1240 | |
1241 | # Assert that we have iproute installed | |
787e7624 | 1242 | if os.system("which ip >/dev/null 2>/dev/null") != 0: |
1243 | logger.error("could not find ip binary (iproute is not installed)") | |
007e7313 RZ |
1244 | ret = False |
1245 | ||
1246 | # Assert that we have gdb installed | |
787e7624 | 1247 | if os.system("which gdb >/dev/null 2>/dev/null") != 0: |
1248 | logger.error("could not find gdb binary (gdb is not installed)") | |
007e7313 RZ |
1249 | ret = False |
1250 | ||
1251 | # Assert that FRR utilities exist | |
787e7624 | 1252 | frrdir = config.get("topogen", "frrdir") |
007e7313 | 1253 | if not os.path.isdir(frrdir): |
787e7624 | 1254 | logger.error("could not find {} directory".format(frrdir)) |
007e7313 RZ |
1255 | ret = False |
1256 | else: | |
1257 | try: | |
787e7624 | 1258 | pwd.getpwnam("frr")[2] |
007e7313 RZ |
1259 | except KeyError: |
1260 | logger.warning('could not find "frr" user') | |
1261 | ||
1262 | try: | |
787e7624 | 1263 | grp.getgrnam("frr")[2] |
007e7313 RZ |
1264 | except KeyError: |
1265 | logger.warning('could not find "frr" group') | |
1266 | ||
1267 | try: | |
787e7624 | 1268 | if "frr" not in grp.getgrnam("frrvty").gr_mem: |
1269 | logger.error( | |
1270 | '"frr" user and group exist, but user is not under "frrvty"' | |
1271 | ) | |
007e7313 RZ |
1272 | except KeyError: |
1273 | logger.warning('could not find "frrvty" group') | |
1274 | ||
787e7624 | 1275 | for fname in [ |
1276 | "zebra", | |
1277 | "ospfd", | |
1278 | "ospf6d", | |
1279 | "bgpd", | |
1280 | "ripd", | |
1281 | "ripngd", | |
1282 | "isisd", | |
1283 | "pimd", | |
e13f9c4f | 1284 | "pim6d", |
787e7624 | 1285 | "ldpd", |
701a0192 | 1286 | "pbrd", |
f637ac01 | 1287 | "mgmtd", |
787e7624 | 1288 | ]: |
007e7313 RZ |
1289 | path = os.path.join(frrdir, fname) |
1290 | if not os.path.isfile(path): | |
1291 | # LDPd is an exception | |
787e7624 | 1292 | if fname == "ldpd": |
1293 | logger.info( | |
1294 | "could not find {} in {}".format(fname, frrdir) | |
1295 | + "(LDPd tests will not run)" | |
1296 | ) | |
007e7313 RZ |
1297 | continue |
1298 | ||
e40b7130 | 1299 | logger.error("could not find {} in {}".format(fname, frrdir)) |
007e7313 | 1300 | ret = False |
d34f6134 | 1301 | else: |
f637ac01 | 1302 | if fname != "zebra" or fname != "mgmtd": |
d34f6134 RZ |
1303 | continue |
1304 | ||
f637ac01 | 1305 | os.system("{} -v 2>&1 >{}/frr_mgmtd.txt".format(path, rundir)) |
49581587 | 1306 | os.system("{} -v 2>&1 >{}/frr_zebra.txt".format(path, rundir)) |
007e7313 | 1307 | |
007e7313 RZ |
1308 | # Test MPLS availability |
1309 | krel = platform.release() | |
787e7624 | 1310 | if topotest.version_cmp(krel, "4.5") < 0: |
1311 | logger.info( | |
1312 | 'LDPd tests will not run (have kernel "{}", but it requires 4.5)'.format( | |
1313 | krel | |
1314 | ) | |
1315 | ) | |
007e7313 | 1316 | |
c11c4cc7 | 1317 | # Test for MPLS Kernel modules available |
787e7624 | 1318 | if not topotest.module_present("mpls-router", load=False) != 0: |
1319 | logger.info("LDPd tests will not run (missing mpls-router kernel module)") | |
1320 | if not topotest.module_present("mpls-iptunnel", load=False) != 0: | |
1321 | logger.info("LDPd tests will not run (missing mpls-iptunnel kernel module)") | |
c11c4cc7 | 1322 | |
49581587 CH |
1323 | if not get_exabgp_cmd(): |
1324 | logger.warning("Failed to find exabgp < 4") | |
007e7313 | 1325 | |
7547ebd8 | 1326 | logger.removeHandler(fhandler) |
46a0656f | 1327 | fhandler.close() |
7547ebd8 | 1328 | |
007e7313 | 1329 | return ret |
af99f19e | 1330 | |
787e7624 | 1331 | |
af99f19e DS |
1332 | def diagnose_env_freebsd(): |
1333 | return True | |
1334 | ||
787e7624 | 1335 | |
49581587 | 1336 | def diagnose_env(rundir): |
af99f19e | 1337 | if sys.platform.startswith("linux"): |
49581587 | 1338 | return diagnose_env_linux(rundir) |
af99f19e DS |
1339 | elif sys.platform.startswith("freebsd"): |
1340 | return diagnose_env_freebsd() | |
1341 | ||
1342 | return False |