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