]>
git.proxmox.com Git - mirror_frr.git/blob - tests/topotests/lib/topogen.py
3 # Library of helper functions for NetDEF Topology Tests
5 # Copyright (c) 2017 by
6 # Network Device Education Foundation, Inc. ("NetDEF")
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
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
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
28 Basic usage instructions:
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()
47 from mininet
.net
import Mininet
48 from mininet
.log
import setLogLevel
49 from mininet
.cli
import CLI
51 from lib
import topotest
52 from lib
.topolog
import logger
, logger_config
54 CWD
= os
.path
.dirname(os
.path
.realpath(__file__
))
56 # pylint: disable=C0103
57 # Global Topogen variable. This is being used to keep the Topogen available on
58 # all test functions without declaring a test local variable.
61 def get_topogen(topo
=None):
63 Helper function to retrieve Topogen. Must be called with `topo` when called
64 inside the build() method of Topology class.
67 global_tgen
.topo
= topo
70 def set_topogen(tgen
):
71 "Helper function to set Topogen"
72 # pylint: disable=W0603
77 # Main class: topology builder
80 class Topogen(object):
81 "A topology test builder helper."
83 CONFIG_SECTION
= 'topogen'
85 def __init__(self
, cls
, modname
='unnamed'):
87 Topogen initialization function, takes the following arguments:
88 * `cls`: the topology class that is child of mininet.topo
89 * `modname`: module name must be a unique name to identify logs later.
97 self
.modname
= modname
99 logger
.info('loading topology: {}'.format(self
.modname
))
102 def _mininet_reset():
103 "Reset the mininet environment"
104 # Clean up the mininet environment
105 os
.system('mn -c > /dev/null 2>&1')
107 def _init_topo(self
, cls
):
109 Initialize the topogily provided by the user. The user topology class
110 must call get_topogen() during build() to get the topogen object.
112 # Set the global variable so the test cases can access it anywhere
115 # Load the default topology configurations
119 self
._mininet
_reset
()
121 self
.net
= Mininet(controller
=None, topo
=self
.topo
)
122 for gear
in self
.gears
.values():
125 def _load_config(self
):
127 Loads the configuration file `pytest.ini` located at the root dir of
132 'frrdir': '/usr/lib/frr',
133 'quaggadir': '/usr/lib/quagga',
135 'memleak_path': None,
137 self
.config
= ConfigParser
.ConfigParser(defaults
)
138 pytestini_path
= os
.path
.join(CWD
, '../pytest.ini')
139 self
.config
.read(pytestini_path
)
141 def add_router(self
, name
=None, cls
=topotest
.Router
, **params
):
143 Adds a new router to the topology. This function has the following
145 * `name`: (optional) select the router name
146 * `daemondir`: (optional) custom daemon binary directory
147 * `routertype`: (optional) `quagga` or `frr`
148 Returns a TopoRouter.
151 name
= 'r{}'.format(self
.routern
)
152 if name
in self
.gears
:
153 raise KeyError('router already exists')
155 params
['frrdir'] = self
.config
.get(self
.CONFIG_SECTION
, 'frrdir')
156 params
['quaggadir'] = self
.config
.get(self
.CONFIG_SECTION
, 'quaggadir')
157 params
['memleak_path'] = self
.config
.get(self
.CONFIG_SECTION
, 'memleak_path')
158 if not params
.has_key('routertype'):
159 params
['routertype'] = self
.config
.get(self
.CONFIG_SECTION
, 'routertype')
161 self
.gears
[name
] = TopoRouter(self
, cls
, name
, **params
)
163 return self
.gears
[name
]
165 def add_switch(self
, name
=None, cls
=topotest
.LegacySwitch
):
167 Adds a new switch to the topology. This function has the following
169 name: (optional) select the switch name
170 Returns the switch name and number.
173 name
= 's{}'.format(self
.switchn
)
174 if name
in self
.gears
:
175 raise KeyError('switch already exists')
177 self
.gears
[name
] = TopoSwitch(self
, cls
, name
)
179 return self
.gears
[name
]
181 def add_link(self
, node1
, node2
, ifname1
=None, ifname2
=None):
183 Creates a connection between node1 and node2. The nodes can be the
189 if not isinstance(node1
, TopoGear
):
190 raise ValueError('invalid node1 type')
191 if not isinstance(node2
, TopoGear
):
192 raise ValueError('invalid node2 type')
195 ifname1
= node1
.new_link()
197 ifname2
= node2
.new_link()
199 node1
.register_link(ifname1
, node2
, ifname2
)
200 node2
.register_link(ifname2
, node1
, ifname1
)
201 self
.topo
.addLink(node1
.name
, node2
.name
,
202 intfName1
=ifname1
, intfName2
=ifname2
)
206 Returns the router dictionary (key is the router name and value is the
207 router object itself).
209 return dict((rname
, gear
) for rname
, gear
in self
.gears
.iteritems()
210 if isinstance(gear
, TopoRouter
))
212 def start_topology(self
, log_level
=None):
214 Starts the topology class. Possible `log_level`s are:
215 'debug': all information possible
216 'info': informational messages
217 'output': default logging level defined by Mininet
218 'warning': only warning, error and critical messages
219 'error': only error and critical messages
220 'critical': only critical messages
222 # If log_level is not specified use the configuration.
223 if log_level
is None:
224 log_level
= self
.config
.get(self
.CONFIG_SECTION
, 'verbosity')
226 # Set python logger level
227 logger_config
.set_log_level(log_level
)
230 if log_level
== 'debug':
231 setLogLevel(log_level
)
233 logger
.info('starting topology: {}'.format(self
.modname
))
236 def start_router(self
, router
=None):
238 Call the router startRouter method.
239 If no router is specified it is called for all registred routers.
242 # pylint: disable=r1704
243 for _
, router
in self
.routers().iteritems():
246 if isinstance(router
, str):
247 router
= self
.gears
[router
]
251 def stop_topology(self
):
252 "Stops the network topology"
253 logger
.info('stopping topology: {}'.format(self
.modname
))
256 def mininet_cli(self
):
258 Interrupt the test and call the command line interface for manual
259 inspection. Should be only used on non production code.
261 if not sys
.stdin
.isatty():
262 raise EnvironmentError(
263 'you must run pytest with \'-s\' in order to use mininet CLI')
267 def is_memleak_enabled(self
):
268 "Returns `True` if memory leak report is enable, otherwise `False`."
269 memleak_file
= os
.environ
.get('TOPOTESTS_CHECK_MEMLEAK')
270 if memleak_file
is None:
274 def report_memory_leaks(self
, testname
=None):
275 "Run memory leak test and reports."
276 if not self
.is_memleak_enabled():
279 # If no name was specified, use the test module name
281 testname
= self
.modname
283 router_list
= self
.routers().values()
284 for router
in router_list
:
285 router
.report_memory_leaks(self
.modname
)
289 # Topology gears (equipment)
292 class TopoGear(object):
293 "Abstract class for type checking"
304 for myif
, dest
in self
.links
.iteritems():
308 links
+= '"{}"<->"{}"'.format(myif
, destif
)
310 return 'TopoGear<name="{}",links=[{}]>'.format(self
.name
, links
)
312 def run(self
, command
):
314 Runs the provided command string in the router and returns a string
317 return self
.tgen
.net
[self
.name
].cmd(command
)
319 def add_link(self
, node
, myif
=None, nodeif
=None):
321 Creates a link (connection) between myself and the specified node.
322 Interfaces name can be speficied with:
323 myif: the interface name that will be created in this node
324 nodeif: the target interface name that will be created on the remote node.
326 self
.tgen
.add_link(self
, node
, myif
, nodeif
)
328 def link_enable(self
, myif
, enabled
=True):
330 Set this node interface administrative state.
331 myif: this node interface name
332 enabled: whether we should enable or disable the interface
334 if myif
not in self
.links
.keys():
335 raise KeyError('interface doesn\'t exists')
342 logger
.info('setting node "{}" link "{}" to state "{}"'.format(
343 self
.name
, myif
, operation
345 return self
.run('ip link set dev {} {}'.format(myif
, operation
))
347 def peer_link_enable(self
, myif
, enabled
=True):
349 Set the peer interface administrative state.
350 myif: this node interface name
351 enabled: whether we should enable or disable the interface
353 NOTE: this is used to simulate a link down on this node, since when the
354 peer disables their interface our interface status changes to no link.
356 if myif
not in self
.links
.keys():
357 raise KeyError('interface doesn\'t exists')
359 node
, nodeif
= self
.links
[myif
]
360 node
.link_enable(nodeif
, enabled
)
364 Generates a new unique link name.
366 NOTE: This function should only be called by Topogen.
368 ifname
= '{}-eth{}'.format(self
.name
, self
.linkn
)
372 def register_link(self
, myif
, node
, nodeif
):
374 Register link between this node interface and outside node.
376 NOTE: This function should only be called by Topogen.
378 if myif
in self
.links
.keys():
379 raise KeyError('interface already exists')
381 self
.links
[myif
] = (node
, nodeif
)
383 class TopoRouter(TopoGear
):
388 # The default required directories by Quagga/FRR
397 # Router Daemon enumeration definition.
419 def __init__(self
, tgen
, cls
, name
, **params
):
421 The constructor has the following parameters:
422 * tgen: Topogen object
423 * cls: router class that will be used to instantiate
425 * daemondir: daemon binary directory
426 * routertype: 'quagga' or 'frr'
428 super(TopoRouter
, self
).__init
__()
434 if not params
.has_key('privateDirs'):
435 params
['privateDirs'] = self
.PRIVATE_DIRS
437 self
.options
['memleak_path'] = params
.get('memleak_path', None)
439 # Create new log directory
440 self
.logdir
= '/tmp/topotests/{}'.format(self
.tgen
.modname
)
441 # Clean up before starting new log files: avoids removing just created
443 self
._prepare
_tmpfiles
()
444 # Propagate the router log directory
445 params
['logdir'] = self
.logdir
447 # Open router log file
448 logfile
= '{}/{}.log'.format(self
.logdir
, name
)
449 self
.logger
= logger_config
.get_logger(name
=name
, target
=logfile
)
450 self
.tgen
.topo
.addNode(self
.name
, cls
=self
.cls
, **params
)
453 gear
= super(TopoRouter
, self
).__str
__()
454 gear
+= ' TopoRouter<>'
457 def _prepare_tmpfiles(self
):
458 # Create directories if they don't exist
460 os
.makedirs(self
.logdir
, 0755)
464 # Try to find relevant old logfiles in /tmp and delete them
465 map(os
.remove
, glob
.glob('{}/*{}*.log'.format(self
.logdir
, self
.name
)))
466 # Remove old core files
467 map(os
.remove
, glob
.glob('{}/{}*.dmp'.format(self
.logdir
, self
.name
)))
469 def load_config(self
, daemon
, source
=None):
471 Loads daemon configuration from the specified source
472 Possible daemon values are: TopoRouter.RD_ZEBRA, TopoRouter.RD_RIP,
473 TopoRouter.RD_RIPNG, TopoRouter.RD_OSPF, TopoRouter.RD_OSPF6,
474 TopoRouter.RD_ISIS, TopoRouter.RD_BGP, TopoRouter.RD_LDP,
477 daemonstr
= self
.RD
.get(daemon
)
478 self
.logger
.info('loading "{}" configuration: {}'.format(daemonstr
, source
))
479 self
.tgen
.net
[self
.name
].loadConf(daemonstr
, source
)
481 def check_router_running(self
):
483 Run a series of checks and returns a status string.
485 self
.logger
.info('checking if daemons are running')
486 return self
.tgen
.net
[self
.name
].checkRouterRunning()
493 * Configure interfaces
494 * Start daemons (e.g. FRR/Quagga)
496 self
.logger
.debug('starting')
497 return self
.tgen
.net
[self
.name
].startRouter()
504 self
.logger
.debug('stopping')
505 return self
.tgen
.net
[self
.name
].stopRouter()
507 def vtysh_cmd(self
, command
, isjson
=False):
509 Runs the provided command string in the vty shell and returns a string
512 This function also accepts multiple commands, but this mode does not
513 return output for each command. See vtysh_multicmd() for more details.
515 # Detect multi line commands
516 if command
.find('\n') != -1:
517 return self
.vtysh_multicmd(command
)
519 vtysh_command
= 'vtysh -c "{}" 2>/dev/null'.format(command
)
520 output
= self
.run(vtysh_command
)
521 self
.logger
.info('\nvtysh command => {}\nvtysh output <= {}'.format(
526 return json
.loads(output
)
528 def vtysh_multicmd(self
, commands
, pretty_output
=True):
530 Runs the provided commands in the vty shell and return the result of
533 pretty_output: defines how the return value will be presented. When
534 True it will show the command as they were executed in the vty shell,
535 otherwise it will only show lines that failed.
537 # Prepare the temporary file that will hold the commands
538 fname
= topotest
.get_file(commands
)
540 # Run the commands and delete the temporary file
542 vtysh_command
= 'vtysh < {}'.format(fname
)
544 vtysh_command
= 'vtysh -f {}'.format(fname
)
546 res
= self
.run(vtysh_command
)
549 self
.logger
.info('\nvtysh command => "{}"\nvtysh output <= "{}"'.format(
554 def report_memory_leaks(self
, testname
):
556 Runs the router memory leak check test. Has the following parameter:
557 testname: the test file name for identification
559 NOTE: to run this you must have the environment variable
560 TOPOTESTS_CHECK_MEMLEAK set or memleak_path configured in `pytest.ini`.
562 memleak_file
= os
.environ
.get('TOPOTESTS_CHECK_MEMLEAK') or self
.options
['memleak_path']
563 if memleak_file
is None:
567 self
.logger
.info('running memory leak report')
568 self
.tgen
.net
[self
.name
].report_memory_leaks(memleak_file
, testname
)
570 class TopoSwitch(TopoGear
):
572 Switch abstraction. Has the following properties:
573 * cls: switch class that will be used to instantiate
576 # pylint: disable=too-few-public-methods
578 def __init__(self
, tgen
, cls
, name
):
579 super(TopoSwitch
, self
).__init
__()
584 self
.tgen
.topo
.addSwitch(name
, cls
=self
.cls
)
587 gear
= super(TopoSwitch
, self
).__str
__()
588 gear
+= ' TopoSwitch<>'