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