]> git.proxmox.com Git - mirror_frr.git/blame - tests/topotests/lib/topogen.py
topogen: add support for setting link state
[mirror_frr.git] / tests / topotests / lib / topogen.py
CommitLineData
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"""
24Topogen (Topology Generator) is an abstraction around Topotest and Mininet to
25help reduce boilerplate code and provide a stable interface to build topology
26tests on.
27
28Basic 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
41import os
42import sys
43
44from mininet.net import Mininet
45from mininet.log import setLogLevel
46from mininet.cli import CLI
47
48from lib import topotest
49
50# pylint: disable=C0103
51# Global Topogen variable. This is being used to keep the Topogen available on
52# all test functions without declaring a test local variable.
53global_tgen = None
54
55def get_topogen(topo=None):
56 """
57 Helper function to retrieve Topogen. Must be called with `topo` when called
58 inside the build() method of Topology class.
59 """
60 if topo is not None:
61 global_tgen.topo = topo
62 return global_tgen
63
64def set_topogen(tgen):
65 "Helper function to set Topogen"
66 # pylint: disable=W0603
67 global global_tgen
68 global_tgen = tgen
69
70#
71# Main class: topology builder
72#
73
74class Topogen(object):
75 "A topology test builder helper."
76
77 def __init__(self, cls):
78 self.topo = None
79 self.net = None
80 self.gears = {}
81 self.routern = 1
82 self.switchn = 1
83 self._init_topo(cls)
84
85 @staticmethod
86 def _mininet_reset():
87 "Reset the mininet environment"
88 # Clean up the mininet environment
89 os.system('mn -c > /dev/null 2>&1')
90
91 def _init_topo(self, cls):
92 """
93 Initialize the topogily provided by the user. The user topology class
94 must call get_topogen() during build() to get the topogen object.
95 """
96 # Set the global variable so the test cases can access it anywhere
97 set_topogen(self)
98
99 # Initialize the API
100 self._mininet_reset()
101 cls()
102 self.net = Mininet(controller=None, topo=self.topo)
103 for gear in self.gears.values():
104 gear.net = self.net
105
2ab85530 106 def add_router(self, name=None, cls=topotest.Router, **params):
1fca63c1
RZ
107 """
108 Adds a new router to the topology. This function has the following
109 options:
110 name: (optional) select the router name
111 Returns a TopoRouter.
112 """
113 if name is None:
114 name = 'router{}'.format(self.routern)
115 if name in self.gears:
116 raise KeyError('router already exists')
117
2ab85530 118 self.gears[name] = TopoRouter(self, cls, name, **params)
1fca63c1
RZ
119 self.routern += 1
120 return self.gears[name]
121
122 def add_switch(self, name=None, cls=topotest.LegacySwitch):
123 """
124 Adds a new switch to the topology. This function has the following
125 options:
126 name: (optional) select the switch name
127 Returns the switch name and number.
128 """
129 if name is None:
130 name = 'switch{}'.format(self.switchn)
131 if name in self.gears:
132 raise KeyError('switch already exists')
133
134 self.gears[name] = TopoSwitch(self, cls, name)
135 self.switchn += 1
136 return self.gears[name]
137
138 def add_link(self, node1, node2, ifname1=None, ifname2=None):
139 """
140 Creates a connection between node1 and node2. The nodes can be the
141 following:
142 * TopoGear
143 * TopoRouter
144 * TopoSwitch
145 """
146 if not isinstance(node1, TopoGear):
147 raise ValueError('invalid node1 type')
148 if not isinstance(node2, TopoGear):
149 raise ValueError('invalid node2 type')
150
151 if ifname1 is None:
8c3fdf62 152 ifname1 = node1.new_link()
1fca63c1 153 if ifname2 is None:
8c3fdf62
RZ
154 ifname2 = node2.new_link()
155
156 node1.register_link(ifname1, node2, ifname2)
157 node2.register_link(ifname2, node1, ifname1)
1fca63c1
RZ
158 self.topo.addLink(node1.name, node2.name,
159 intfName1=ifname1, intfName2=ifname2)
160
161 def routers(self):
162 """
163 Returns the router dictionary (key is the router name and value is the
164 router object itself).
165 """
166 return dict((rname, gear) for rname, gear in self.gears.iteritems()
167 if isinstance(gear, TopoRouter))
168
169 def start_topology(self, log_level='info'):
170 """
171 Starts the topology class. Possible `log_level`s are:
172 'debug': all information possible
173 'info': informational messages
174 'output': default logging level defined by Mininet
175 'warning': only warning, error and critical messages
176 'error': only error and critical messages
177 'critical': only critical messages
178 """
179 # Run mininet
180 setLogLevel(log_level)
181 self.net.start()
182
183 def start_router(self, router=None):
184 """
185 Call the router startRouter method.
186 If no router is specified it is called for all registred routers.
187 """
188 if router is None:
189 # pylint: disable=r1704
190 for _, router in self.routers().iteritems():
191 router.start()
192 else:
193 if isinstance(router, str):
194 router = self.gears[router]
195
196 router.start()
197
198 def stop_topology(self):
199 "Stops the network topology"
200 self.net.stop()
201
202 def mininet_cli(self):
203 """
204 Interrupt the test and call the command line interface for manual
205 inspection. Should be only used on non production code.
206 """
207 if not sys.stdin.isatty():
208 raise EnvironmentError(
209 'you must run pytest with \'-s\' in order to use mininet CLI')
210
211 CLI(self.net)
212
213#
214# Topology gears (equipment)
215#
216
217class TopoGear(object):
218 "Abstract class for type checking"
219
220 def __init__(self):
221 self.tgen = None
222 self.name = None
223 self.cls = None
8c3fdf62 224 self.links = {}
1fca63c1
RZ
225 self.linkn = 0
226
8c3fdf62
RZ
227 def run(self, command):
228 """
229 Runs the provided command string in the router and returns a string
230 with the response.
231 """
232 return self.tgen.net[self.name].cmd(command)
233
1fca63c1
RZ
234 def add_link(self, node, myif=None, nodeif=None):
235 """
236 Creates a link (connection) between myself and the specified node.
237 Interfaces name can be speficied with:
238 myif: the interface name that will be created in this node
239 nodeif: the target interface name that will be created on the remote node.
240 """
241 self.tgen.add_link(self, node, myif, nodeif)
242
8c3fdf62 243 def link_enable(self, myif, enabled=True):
1fca63c1 244 """
8c3fdf62
RZ
245 Set this node interface administrative state.
246 myif: this node interface name
247 enabled: whether we should enable or disable the interface
1fca63c1 248 """
8c3fdf62
RZ
249 if myif not in self.links.keys():
250 raise KeyError('interface doesn\'t exists')
251
252 if enabled is True:
253 operation = 'up'
254 else:
255 operation = 'down'
256
257 return self.run('ip link set dev {} {}'.format(myif, operation))
258
259 def peer_link_enable(self, myif, enabled=True):
260 """
261 Set the peer interface administrative state.
262 myif: this node interface name
263 enabled: whether we should enable or disable the interface
264
265 NOTE: this is used to simulate a link down on this node, since when the
266 peer disables their interface our interface status changes to no link.
267 """
268 if myif not in self.links.keys():
269 raise KeyError('interface doesn\'t exists')
270
271 node, nodeif = self.links[myif]
272 node.link_enable(nodeif, enabled)
1fca63c1 273
8c3fdf62
RZ
274 def new_link(self):
275 """
276 Generates a new unique link name.
277
278 NOTE: This function should only be called by Topogen.
279 """
280 ifname = '{}-eth{}'.format(self.name, self.linkn)
1fca63c1 281 self.linkn += 1
1fca63c1
RZ
282 return ifname
283
8c3fdf62
RZ
284 def register_link(self, myif, node, nodeif):
285 """
286 Register link between this node interface and outside node.
287
288 NOTE: This function should only be called by Topogen.
289 """
290 if myif in self.links.keys():
291 raise KeyError('interface already exists')
292
293 self.links[myif] = (node, nodeif)
294
1fca63c1
RZ
295class TopoRouter(TopoGear):
296 """
d9ea1cda 297 Router abstraction.
1fca63c1
RZ
298 """
299
300 # The default required directories by Quagga/FRR
301 PRIVATE_DIRS = [
302 '/etc/frr',
303 '/etc/quagga',
304 '/var/run/frr',
305 '/var/run/quagga',
306 '/var/log'
307 ]
308
309 # Router Daemon enumeration definition.
310 RD_ZEBRA = 1,
311 RD_RIP = 2
312 RD_RIPNG = 3
313 RD_OSPF = 4
314 RD_OSPF6 = 5
315 RD_ISIS = 6
316 RD_BGP = 7
317 RD_LDP = 8
318 RD_PIM = 9
319 RD = {
320 RD_ZEBRA: 'zebra',
321 RD_RIP: 'ripd',
322 RD_RIPNG: 'ripngd',
323 RD_OSPF: 'ospfd',
324 RD_OSPF6: 'ospf6d',
325 RD_ISIS: 'isisd',
326 RD_BGP: 'bgpd',
327 RD_PIM: 'pimd',
328 RD_LDP: 'ldpd',
329 }
330
2ab85530 331 def __init__(self, tgen, cls, name, **params):
d9ea1cda
RZ
332 """
333 The constructor has the following parameters:
334 * tgen: Topogen object
335 * cls: router class that will be used to instantiate
336 * name: router name
337 * daemondir: daemon binary directory
338 * routertype: 'quagga' or 'frr'
339 """
1fca63c1
RZ
340 super(TopoRouter, self).__init__()
341 self.tgen = tgen
342 self.net = None
343 self.name = name
344 self.cls = cls
2ab85530
RZ
345 if not params.has_key('privateDirs'):
346 params['privateDirs'] = self.PRIVATE_DIRS
347 self.tgen.topo.addNode(self.name, cls=self.cls, **params)
1fca63c1
RZ
348
349 def load_config(self, daemon, source=None):
350 """
351 Loads daemon configuration from the specified source
352 Possible daemon values are: TopoRouter.RD_ZEBRA, TopoRouter.RD_RIP,
353 TopoRouter.RD_RIPNG, TopoRouter.RD_OSPF, TopoRouter.RD_OSPF6,
354 TopoRouter.RD_ISIS, TopoRouter.RD_BGP, TopoRouter.RD_LDP,
355 TopoRouter.RD_PIM.
356 """
357 daemonstr = self.RD.get(daemon)
358 self.tgen.net[self.name].loadConf(daemonstr, source)
359
360 def check_router_running(self):
361 """
362 Run a series of checks and returns a status string.
363 """
364 return self.tgen.net[self.name].checkRouterRunning()
365
366 def start(self):
367 """
368 Start router:
369 * Load modules
370 * Clean up files
371 * Configure interfaces
372 * Start daemons (e.g. FRR/Quagga)
373 """
374 return self.tgen.net[self.name].startRouter()
375
1fca63c1
RZ
376 def vtysh_cmd(self, command):
377 """
378 Runs the provided command string in the vty shell and returns a string
379 with the response.
380
381 This function also accepts multiple commands, but this mode does not
382 return output for each command. See vtysh_multicmd() for more details.
383 """
384 # Detect multi line commands
385 if command.find('\n') != -1:
386 return self.vtysh_multicmd(command)
387
388 vtysh_command = 'vtysh -c "{}" 2>/dev/null'.format(command)
389 return self.run(vtysh_command)
390
391 def vtysh_multicmd(self, commands, pretty_output=True):
392 """
393 Runs the provided commands in the vty shell and return the result of
394 execution.
395
396 pretty_output: defines how the return value will be presented. When
397 True it will show the command as they were executed in the vty shell,
398 otherwise it will only show lines that failed.
399 """
400 # Prepare the temporary file that will hold the commands
401 fname = topotest.get_file(commands)
402
403 # Run the commands and delete the temporary file
404 if pretty_output:
405 vtysh_command = 'vtysh < {}'.format(fname)
406 else:
407 vtysh_command = 'vtysh -f {}'.format(fname)
408
409 res = self.run(vtysh_command)
410 os.unlink(fname)
411
412 return res
413
414class TopoSwitch(TopoGear):
415 """
416 Switch abstraction. Has the following properties:
417 * cls: switch class that will be used to instantiate
418 * name: switch name
419 """
420 # pylint: disable=too-few-public-methods
421
422 def __init__(self, tgen, cls, name):
423 super(TopoSwitch, self).__init__()
424 self.tgen = tgen
425 self.net = None
426 self.name = name
427 self.cls = cls
428 self.tgen.topo.addSwitch(name, cls=self.cls)