summaryrefslogtreecommitdiff
path: root/tests/topotests/lib/topogen.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/topotests/lib/topogen.py')
-rw-r--r--tests/topotests/lib/topogen.py1068
1 files changed, 1068 insertions, 0 deletions
diff --git a/tests/topotests/lib/topogen.py b/tests/topotests/lib/topogen.py
new file mode 100644
index 0000000000..25edfe0324
--- /dev/null
+++ b/tests/topotests/lib/topogen.py
@@ -0,0 +1,1068 @@
+#
+# topogen.py
+# Library of helper functions for NetDEF Topology Tests
+#
+# Copyright (c) 2017 by
+# Network Device Education Foundation, Inc. ("NetDEF")
+#
+# Permission to use, copy, modify, and/or distribute this software
+# for any purpose with or without fee is hereby granted, provided
+# that the above copyright notice and this permission notice appear
+# in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
+# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
+# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
+# OF THIS SOFTWARE.
+#
+
+"""
+Topogen (Topology Generator) is an abstraction around Topotest and Mininet to
+help reduce boilerplate code and provide a stable interface to build topology
+tests on.
+
+Basic usage instructions:
+
+* Define a Topology class with a build method using mininet.topo.Topo.
+ See examples/test_template.py.
+* Use Topogen inside the build() method with get_topogen.
+ e.g. get_topogen(self).
+* Start up your topology with: Topogen(YourTopology)
+* Initialize the Mininet with your topology with: tgen.start_topology()
+* Configure your routers/hosts and start them
+* Run your tests / mininet cli.
+* After running stop Mininet with: tgen.stop_topology()
+"""
+
+import os
+import sys
+import logging
+import json
+import ConfigParser
+import glob
+import grp
+import platform
+import pwd
+import subprocess
+import pytest
+
+from mininet.net import Mininet
+from mininet.log import setLogLevel
+from mininet.cli import CLI
+
+from lib import topotest
+from lib.topolog import logger, logger_config
+
+CWD = os.path.dirname(os.path.realpath(__file__))
+
+# pylint: disable=C0103
+# Global Topogen variable. This is being used to keep the Topogen available on
+# all test functions without declaring a test local variable.
+global_tgen = None
+
+def get_topogen(topo=None):
+ """
+ Helper function to retrieve Topogen. Must be called with `topo` when called
+ inside the build() method of Topology class.
+ """
+ if topo is not None:
+ global_tgen.topo = topo
+ return global_tgen
+
+def set_topogen(tgen):
+ "Helper function to set Topogen"
+ # pylint: disable=W0603
+ global global_tgen
+ global_tgen = tgen
+
+#
+# Main class: topology builder
+#
+
+# Topogen configuration defaults
+tgen_defaults = {
+ 'verbosity': 'info',
+ 'frrdir': '/usr/lib/frr',
+ 'quaggadir': '/usr/lib/quagga',
+ 'routertype': 'frr',
+ 'memleak_path': None,
+}
+
+class Topogen(object):
+ "A topology test builder helper."
+
+ CONFIG_SECTION = 'topogen'
+
+ def __init__(self, cls, modname='unnamed'):
+ """
+ Topogen initialization function, takes the following arguments:
+ * `cls`: the topology class that is child of mininet.topo
+ * `modname`: module name must be a unique name to identify logs later.
+ """
+ self.config = None
+ self.topo = None
+ self.net = None
+ self.gears = {}
+ self.routern = 1
+ self.switchn = 1
+ self.modname = modname
+ self.errorsd = {}
+ self.errors = ''
+ self.peern = 1
+ self._init_topo(cls)
+ logger.info('loading topology: {}'.format(self.modname))
+
+ @staticmethod
+ def _mininet_reset():
+ "Reset the mininet environment"
+ # Clean up the mininet environment
+ os.system('mn -c > /dev/null 2>&1')
+
+ def _init_topo(self, cls):
+ """
+ Initialize the topogily provided by the user. The user topology class
+ must call get_topogen() during build() to get the topogen object.
+ """
+ # Set the global variable so the test cases can access it anywhere
+ set_topogen(self)
+
+ # Test for MPLS Kernel modules available
+ self.hasmpls = False
+ if not topotest.module_present('mpls-router'):
+ logger.info('MPLS tests will not run (missing mpls-router kernel module)')
+ elif not topotest.module_present('mpls-iptunnel'):
+ logger.info('MPLS tests will not run (missing mpls-iptunnel kernel module)')
+ else:
+ self.hasmpls = True
+ # Load the default topology configurations
+ self._load_config()
+
+ # Initialize the API
+ self._mininet_reset()
+ cls()
+ self.net = Mininet(controller=None, topo=self.topo)
+ for gear in self.gears.values():
+ gear.net = self.net
+
+ def _load_config(self):
+ """
+ Loads the configuration file `pytest.ini` located at the root dir of
+ topotests.
+ """
+ self.config = ConfigParser.ConfigParser(tgen_defaults)
+ pytestini_path = os.path.join(CWD, '../pytest.ini')
+ self.config.read(pytestini_path)
+
+ def add_router(self, name=None, cls=topotest.Router, **params):
+ """
+ Adds a new router to the topology. This function has the following
+ options:
+ * `name`: (optional) select the router name
+ * `daemondir`: (optional) custom daemon binary directory
+ * `routertype`: (optional) `quagga` or `frr`
+ Returns a TopoRouter.
+ """
+ if name is None:
+ name = 'r{}'.format(self.routern)
+ if name in self.gears:
+ raise KeyError('router already exists')
+
+ params['frrdir'] = self.config.get(self.CONFIG_SECTION, 'frrdir')
+ params['quaggadir'] = self.config.get(self.CONFIG_SECTION, 'quaggadir')
+ params['memleak_path'] = self.config.get(self.CONFIG_SECTION, 'memleak_path')
+ if not params.has_key('routertype'):
+ params['routertype'] = self.config.get(self.CONFIG_SECTION, 'routertype')
+
+ self.gears[name] = TopoRouter(self, cls, name, **params)
+ self.routern += 1
+ return self.gears[name]
+
+ def add_switch(self, name=None, cls=topotest.LegacySwitch):
+ """
+ Adds a new switch to the topology. This function has the following
+ options:
+ name: (optional) select the switch name
+ Returns the switch name and number.
+ """
+ if name is None:
+ name = 's{}'.format(self.switchn)
+ if name in self.gears:
+ raise KeyError('switch already exists')
+
+ self.gears[name] = TopoSwitch(self, cls, name)
+ self.switchn += 1
+ return self.gears[name]
+
+ def add_exabgp_peer(self, name, ip, defaultRoute):
+ """
+ Adds a new ExaBGP peer to the topology. This function has the following
+ parameters:
+ * `ip`: the peer address (e.g. '1.2.3.4/24')
+ * `defaultRoute`: the peer default route (e.g. 'via 1.2.3.1')
+ """
+ if name is None:
+ name = 'peer{}'.format(self.peern)
+ if name in self.gears:
+ raise KeyError('exabgp peer already exists')
+
+ self.gears[name] = TopoExaBGP(self, name, ip=ip, defaultRoute=defaultRoute)
+ self.peern += 1
+ return self.gears[name]
+
+ def add_link(self, node1, node2, ifname1=None, ifname2=None):
+ """
+ Creates a connection between node1 and node2. The nodes can be the
+ following:
+ * TopoGear
+ * TopoRouter
+ * TopoSwitch
+ """
+ if not isinstance(node1, TopoGear):
+ raise ValueError('invalid node1 type')
+ if not isinstance(node2, TopoGear):
+ raise ValueError('invalid node2 type')
+
+ if ifname1 is None:
+ ifname1 = node1.new_link()
+ if ifname2 is None:
+ ifname2 = node2.new_link()
+
+ node1.register_link(ifname1, node2, ifname2)
+ node2.register_link(ifname2, node1, ifname1)
+ self.topo.addLink(node1.name, node2.name,
+ intfName1=ifname1, intfName2=ifname2)
+
+ def get_gears(self, geartype):
+ """
+ Returns a dictionary of all gears of type `geartype`.
+
+ Normal usage:
+ * Dictionary iteration:
+ ```py
+ tgen = get_topogen()
+ router_dict = tgen.get_gears(TopoRouter)
+ for router_name, router in router_dict.iteritems():
+ # Do stuff
+ ```
+ * List iteration:
+ ```py
+ tgen = get_topogen()
+ peer_list = tgen.get_gears(TopoExaBGP).values()
+ for peer in peer_list:
+ # Do stuff
+ ```
+ """
+ return dict((name, gear) for name, gear in self.gears.iteritems()
+ if isinstance(gear, geartype))
+
+ def routers(self):
+ """
+ Returns the router dictionary (key is the router name and value is the
+ router object itself).
+ """
+ return self.get_gears(TopoRouter)
+
+ def exabgp_peers(self):
+ """
+ Returns the exabgp peer dictionary (key is the peer name and value is
+ the peer object itself).
+ """
+ return self.get_gears(TopoExaBGP)
+
+ def start_topology(self, log_level=None):
+ """
+ Starts the topology class. Possible `log_level`s are:
+ 'debug': all information possible
+ 'info': informational messages
+ 'output': default logging level defined by Mininet
+ 'warning': only warning, error and critical messages
+ 'error': only error and critical messages
+ 'critical': only critical messages
+ """
+ # If log_level is not specified use the configuration.
+ if log_level is None:
+ log_level = self.config.get(self.CONFIG_SECTION, 'verbosity')
+
+ # Set python logger level
+ logger_config.set_log_level(log_level)
+
+ # Run mininet
+ if log_level == 'debug':
+ setLogLevel(log_level)
+
+ logger.info('starting topology: {}'.format(self.modname))
+ self.net.start()
+
+ def start_router(self, router=None):
+ """
+ Call the router startRouter method.
+ If no router is specified it is called for all registred routers.
+ """
+ if router is None:
+ # pylint: disable=r1704
+ for _, router in self.routers().iteritems():
+ router.start()
+ else:
+ if isinstance(router, str):
+ router = self.gears[router]
+
+ router.start()
+
+ def stop_topology(self):
+ """
+ Stops the network topology. This function will call the stop() function
+ of all gears before calling the mininet stop function, so they can have
+ their oportunity to do a graceful shutdown. stop() is called twice. The
+ first is a simple kill with no sleep, the second will sleep if not
+ killed and try with a different signal.
+ """
+ logger.info('stopping topology: {}'.format(self.modname))
+ errors = ""
+ for gear in self.gears.values():
+ gear.stop(False, False)
+ for gear in self.gears.values():
+ errors += gear.stop(True, False)
+ if len(errors) > 0:
+ assert "Errors found post shutdown - details follow:" == 0, errors
+
+ self.net.stop()
+
+ def mininet_cli(self):
+ """
+ Interrupt the test and call the command line interface for manual
+ inspection. Should be only used on non production code.
+ """
+ if not sys.stdin.isatty():
+ raise EnvironmentError(
+ 'you must run pytest with \'-s\' in order to use mininet CLI')
+
+ CLI(self.net)
+
+ def is_memleak_enabled(self):
+ "Returns `True` if memory leak report is enable, otherwise `False`."
+ # On router failure we can't run the memory leak test
+ if self.routers_have_failure():
+ return False
+
+ memleak_file = (os.environ.get('TOPOTESTS_CHECK_MEMLEAK') or
+ self.config.get(self.CONFIG_SECTION, 'memleak_path'))
+ if memleak_file is None:
+ return False
+ return True
+
+ def report_memory_leaks(self, testname=None):
+ "Run memory leak test and reports."
+ if not self.is_memleak_enabled():
+ return
+
+ # If no name was specified, use the test module name
+ if testname is None:
+ testname = self.modname
+
+ router_list = self.routers().values()
+ for router in router_list:
+ router.report_memory_leaks(self.modname)
+
+ def set_error(self, message, code=None):
+ "Sets an error message and signal other tests to skip."
+ logger.info(message)
+
+ # If no code is defined use a sequential number
+ if code is None:
+ code = len(self.errorsd)
+
+ self.errorsd[code] = message
+ self.errors += '\n{}: {}'.format(code, message)
+
+ def has_errors(self):
+ "Returns whether errors exist or not."
+ return len(self.errorsd) > 0
+
+ def routers_have_failure(self):
+ "Runs an assertion to make sure that all routers are running."
+ if self.has_errors():
+ return True
+
+ errors = ''
+ router_list = self.routers().values()
+ for router in router_list:
+ result = router.check_router_running()
+ if result != '':
+ errors += result + '\n'
+
+ if errors != '':
+ self.set_error(errors, 'router_error')
+ assert False, errors
+ return True
+ return False
+
+#
+# Topology gears (equipment)
+#
+
+class TopoGear(object):
+ "Abstract class for type checking"
+
+ def __init__(self):
+ self.tgen = None
+ self.name = None
+ self.cls = None
+ self.links = {}
+ self.linkn = 0
+
+ def __str__(self):
+ links = ''
+ for myif, dest in self.links.iteritems():
+ _, destif = dest
+ if links != '':
+ links += ','
+ links += '"{}"<->"{}"'.format(myif, destif)
+
+ return 'TopoGear<name="{}",links=[{}]>'.format(self.name, links)
+
+ def start(self):
+ "Basic start function that just reports equipment start"
+ logger.info('starting "{}"'.format(self.name))
+
+ def stop(self, wait=True, assertOnError=True):
+ "Basic start function that just reports equipment stop"
+ logger.info('stopping "{}"'.format(self.name))
+ return ""
+
+ def run(self, command):
+ """
+ Runs the provided command string in the router and returns a string
+ with the response.
+ """
+ return self.tgen.net[self.name].cmd(command)
+
+ def add_link(self, node, myif=None, nodeif=None):
+ """
+ Creates a link (connection) between myself and the specified node.
+ Interfaces name can be speficied with:
+ myif: the interface name that will be created in this node
+ nodeif: the target interface name that will be created on the remote node.
+ """
+ self.tgen.add_link(self, node, myif, nodeif)
+
+ def link_enable(self, myif, enabled=True, netns=None):
+ """
+ Set this node interface administrative state.
+ myif: this node interface name
+ enabled: whether we should enable or disable the interface
+ """
+ if myif not in self.links.keys():
+ raise KeyError('interface doesn\'t exists')
+
+ if enabled is True:
+ operation = 'up'
+ else:
+ operation = 'down'
+
+ logger.info('setting node "{}" link "{}" to state "{}"'.format(
+ self.name, myif, operation
+ ))
+ extract=''
+ if netns is not None:
+ extract = 'ip netns exec {} '.format(netns)
+ return self.run('{}ip link set dev {} {}'.format(extract, myif, operation))
+
+ def peer_link_enable(self, myif, enabled=True, netns=None):
+ """
+ Set the peer interface administrative state.
+ myif: this node interface name
+ enabled: whether we should enable or disable the interface
+
+ NOTE: this is used to simulate a link down on this node, since when the
+ peer disables their interface our interface status changes to no link.
+ """
+ if myif not in self.links.keys():
+ raise KeyError('interface doesn\'t exists')
+
+ node, nodeif = self.links[myif]
+ node.link_enable(nodeif, enabled, netns)
+
+ def new_link(self):
+ """
+ Generates a new unique link name.
+
+ NOTE: This function should only be called by Topogen.
+ """
+ ifname = '{}-eth{}'.format(self.name, self.linkn)
+ self.linkn += 1
+ return ifname
+
+ def register_link(self, myif, node, nodeif):
+ """
+ Register link between this node interface and outside node.
+
+ NOTE: This function should only be called by Topogen.
+ """
+ if myif in self.links.keys():
+ raise KeyError('interface already exists')
+
+ self.links[myif] = (node, nodeif)
+
+class TopoRouter(TopoGear):
+ """
+ Router abstraction.
+ """
+
+ # The default required directories by Quagga/FRR
+ PRIVATE_DIRS = [
+ '/etc/frr',
+ '/etc/quagga',
+ '/var/run/frr',
+ '/var/run/quagga',
+ '/var/log'
+ ]
+
+ # Router Daemon enumeration definition.
+ RD_ZEBRA = 1
+ RD_RIP = 2
+ RD_RIPNG = 3
+ RD_OSPF = 4
+ RD_OSPF6 = 5
+ RD_ISIS = 6
+ RD_BGP = 7
+ RD_LDP = 8
+ RD_PIM = 9
+ RD_EIGRP = 10
+ RD_NHRP = 11
+ RD_STATIC = 12
+ RD_BFD = 13
+ RD = {
+ RD_ZEBRA: 'zebra',
+ RD_RIP: 'ripd',
+ RD_RIPNG: 'ripngd',
+ RD_OSPF: 'ospfd',
+ RD_OSPF6: 'ospf6d',
+ RD_ISIS: 'isisd',
+ RD_BGP: 'bgpd',
+ RD_PIM: 'pimd',
+ RD_LDP: 'ldpd',
+ RD_EIGRP: 'eigrpd',
+ RD_NHRP: 'nhrpd',
+ RD_STATIC: 'staticd',
+ RD_BFD: 'bfdd',
+ }
+
+ def __init__(self, tgen, cls, name, **params):
+ """
+ The constructor has the following parameters:
+ * tgen: Topogen object
+ * cls: router class that will be used to instantiate
+ * name: router name
+ * daemondir: daemon binary directory
+ * routertype: 'quagga' or 'frr'
+ """
+ super(TopoRouter, self).__init__()
+ self.tgen = tgen
+ self.net = None
+ self.name = name
+ self.cls = cls
+ self.options = {}
+ self.routertype = params.get('routertype', 'frr')
+ if not params.has_key('privateDirs'):
+ params['privateDirs'] = self.PRIVATE_DIRS
+
+ self.options['memleak_path'] = params.get('memleak_path', None)
+
+ # Create new log directory
+ self.logdir = '/tmp/topotests/{}'.format(self.tgen.modname)
+ # Clean up before starting new log files: avoids removing just created
+ # log files.
+ self._prepare_tmpfiles()
+ # Propagate the router log directory
+ params['logdir'] = self.logdir
+
+ #setup the per node directory
+ dir = '{}/{}'.format(self.logdir, self.name)
+ os.system('mkdir -p ' + dir)
+ os.system('chmod -R go+rw /tmp/topotests')
+
+ # Open router log file
+ logfile = '{0}/{1}.log'.format(dir, name)
+
+ self.logger = logger_config.get_logger(name=name, target=logfile)
+ self.tgen.topo.addNode(self.name, cls=self.cls, **params)
+
+ def __str__(self):
+ gear = super(TopoRouter, self).__str__()
+ gear += ' TopoRouter<>'
+ return gear
+
+ def _prepare_tmpfiles(self):
+ # Create directories if they don't exist
+ try:
+ os.makedirs(self.logdir, 0755)
+ except OSError:
+ pass
+
+ # Allow unprivileged daemon user (frr/quagga) to create log files
+ try:
+ # Only allow group, if it exist.
+ gid = grp.getgrnam(self.routertype)[2]
+ os.chown(self.logdir, 0, gid)
+ os.chmod(self.logdir, 0775)
+ except KeyError:
+ # Allow anyone, but set the sticky bit to avoid file deletions
+ os.chmod(self.logdir, 01777)
+
+ # Try to find relevant old logfiles in /tmp and delete them
+ map(os.remove, glob.glob('{}/{}/*.log'.format(self.logdir, self.name)))
+ # Remove old core files
+ map(os.remove, glob.glob('{}/{}/*.dmp'.format(self.logdir, self.name)))
+
+ def check_capability(self, daemon, param):
+ """
+ Checks a capability daemon against an argument option
+ Return True if capability available. False otherwise
+ """
+ daemonstr = self.RD.get(daemon)
+ self.logger.info('check capability {} for "{}"'.format(param, daemonstr))
+ return self.tgen.net[self.name].checkCapability(daemonstr, param)
+
+ def load_config(self, daemon, source=None, param=None):
+ """
+ Loads daemon configuration from the specified source
+ Possible daemon values are: TopoRouter.RD_ZEBRA, TopoRouter.RD_RIP,
+ TopoRouter.RD_RIPNG, TopoRouter.RD_OSPF, TopoRouter.RD_OSPF6,
+ TopoRouter.RD_ISIS, TopoRouter.RD_BGP, TopoRouter.RD_LDP,
+ TopoRouter.RD_PIM.
+ """
+ daemonstr = self.RD.get(daemon)
+ self.logger.info('loading "{}" configuration: {}'.format(daemonstr, source))
+ self.tgen.net[self.name].loadConf(daemonstr, source, param)
+
+ def check_router_running(self):
+ """
+ Run a series of checks and returns a status string.
+ """
+ self.logger.info('checking if daemons are running')
+ return self.tgen.net[self.name].checkRouterRunning()
+
+ def start(self):
+ """
+ Start router:
+ * Load modules
+ * Clean up files
+ * Configure interfaces
+ * Start daemons (e.g. FRR/Quagga)
+ * Configure daemon logging files
+ """
+ self.logger.debug('starting')
+ nrouter = self.tgen.net[self.name]
+ result = nrouter.startRouter(self.tgen)
+
+ # Enable all daemon command logging, logging files
+ # and set them to the start dir.
+ for daemon, enabled in nrouter.daemons.iteritems():
+ if enabled == 0:
+ continue
+ self.vtysh_cmd('configure terminal\nlog commands\nlog file {}.log'.format(
+ daemon), daemon=daemon)
+
+ if result != '':
+ self.tgen.set_error(result)
+
+ return result
+
+ def stop(self, wait=True, assertOnError=True):
+ """
+ Stop router:
+ * Kill daemons
+ """
+ self.logger.debug('stopping')
+ return self.tgen.net[self.name].stopRouter(wait, assertOnError)
+
+ def vtysh_cmd(self, command, isjson=False, daemon=None):
+ """
+ Runs the provided command string in the vty shell and returns a string
+ with the response.
+
+ This function also accepts multiple commands, but this mode does not
+ return output for each command. See vtysh_multicmd() for more details.
+ """
+ # Detect multi line commands
+ if command.find('\n') != -1:
+ return self.vtysh_multicmd(command, daemon=daemon)
+
+ dparam = ''
+ if daemon is not None:
+ dparam += '-d {}'.format(daemon)
+
+ vtysh_command = 'vtysh {} -c "{}" 2>/dev/null'.format(dparam, command)
+
+ output = self.run(vtysh_command)
+ self.logger.info('\nvtysh command => {}\nvtysh output <= {}'.format(
+ command, output))
+ if isjson is False:
+ return output
+
+ try:
+ return json.loads(output)
+ except ValueError:
+ logger.warning('vtysh_cmd: failed to convert json output')
+ return {}
+
+ def vtysh_multicmd(self, commands, pretty_output=True, daemon=None):
+ """
+ Runs the provided commands in the vty shell and return the result of
+ execution.
+
+ pretty_output: defines how the return value will be presented. When
+ True it will show the command as they were executed in the vty shell,
+ otherwise it will only show lines that failed.
+ """
+ # Prepare the temporary file that will hold the commands
+ fname = topotest.get_file(commands)
+
+ dparam = ''
+ if daemon is not None:
+ dparam += '-d {}'.format(daemon)
+
+ # Run the commands and delete the temporary file
+ if pretty_output:
+ vtysh_command = 'vtysh {} < {}'.format(dparam, fname)
+ else:
+ vtysh_command = 'vtysh {} -f {}'.format(dparam, fname)
+
+ res = self.run(vtysh_command)
+ os.unlink(fname)
+
+ self.logger.info('\nvtysh command => "{}"\nvtysh output <= "{}"'.format(
+ vtysh_command, res))
+
+ return res
+
+ def report_memory_leaks(self, testname):
+ """
+ Runs the router memory leak check test. Has the following parameter:
+ testname: the test file name for identification
+
+ NOTE: to run this you must have the environment variable
+ TOPOTESTS_CHECK_MEMLEAK set or memleak_path configured in `pytest.ini`.
+ """
+ memleak_file = os.environ.get('TOPOTESTS_CHECK_MEMLEAK') or self.options['memleak_path']
+ if memleak_file is None:
+ return
+
+ self.stop()
+ self.logger.info('running memory leak report')
+ self.tgen.net[self.name].report_memory_leaks(memleak_file, testname)
+
+ def version_info(self):
+ "Get equipment information from 'show version'."
+ output = self.vtysh_cmd('show version').split('\n')[0]
+ columns = topotest.normalize_text(output).split(' ')
+ try:
+ return {
+ 'type': columns[0],
+ 'version': columns[1],
+ }
+ except IndexError:
+ return {
+ 'type': None,
+ 'version': None,
+ }
+
+ def has_version(self, cmpop, version):
+ """
+ Compares router version using operation `cmpop` with `version`.
+ Valid `cmpop` values:
+ * `>=`: has the same version or greater
+ * '>': has greater version
+ * '=': has the same version
+ * '<': has a lesser version
+ * '<=': has the same version or lesser
+
+ Usage example: router.has_version('>', '1.0')
+ """
+ return self.tgen.net[self.name].checkRouterVersion(cmpop, version)
+
+ def has_type(self, rtype):
+ """
+ Compares router type with `rtype`. Returns `True` if the type matches,
+ otherwise `false`.
+ """
+ curtype = self.version_info()['type']
+ return rtype == curtype
+
+ def has_mpls(self):
+ nrouter = self.tgen.net[self.name]
+ return nrouter.hasmpls
+
+class TopoSwitch(TopoGear):
+ """
+ Switch abstraction. Has the following properties:
+ * cls: switch class that will be used to instantiate
+ * name: switch name
+ """
+ # pylint: disable=too-few-public-methods
+
+ def __init__(self, tgen, cls, name):
+ super(TopoSwitch, self).__init__()
+ self.tgen = tgen
+ self.net = None
+ self.name = name
+ self.cls = cls
+ self.tgen.topo.addSwitch(name, cls=self.cls)
+
+ def __str__(self):
+ gear = super(TopoSwitch, self).__str__()
+ gear += ' TopoSwitch<>'
+ return gear
+
+class TopoHost(TopoGear):
+ "Host abstraction."
+ # pylint: disable=too-few-public-methods
+
+ def __init__(self, tgen, name, **params):
+ """
+ Mininet has the following known `params` for hosts:
+ * `ip`: the IP address (string) for the host interface
+ * `defaultRoute`: the default route that will be installed
+ (e.g. 'via 10.0.0.1')
+ * `privateDirs`: directories that will be mounted on a different domain
+ (e.g. '/etc/important_dir').
+ """
+ super(TopoHost, self).__init__()
+ self.tgen = tgen
+ self.net = None
+ self.name = name
+ self.options = params
+ self.tgen.topo.addHost(name, **params)
+
+ def __str__(self):
+ gear = super(TopoHost, self).__str__()
+ gear += ' TopoHost<ip="{}",defaultRoute="{}",privateDirs="{}">'.format(
+ self.options['ip'], self.options['defaultRoute'],
+ str(self.options['privateDirs']))
+ return gear
+
+class TopoExaBGP(TopoHost):
+ "ExaBGP peer abstraction."
+ # pylint: disable=too-few-public-methods
+
+ PRIVATE_DIRS = [
+ '/etc/exabgp',
+ '/var/run/exabgp',
+ '/var/log',
+ ]
+
+ def __init__(self, tgen, name, **params):
+ """
+ ExaBGP usually uses the following parameters:
+ * `ip`: the IP address (string) for the host interface
+ * `defaultRoute`: the default route that will be installed
+ (e.g. 'via 10.0.0.1')
+
+ Note: the different between a host and a ExaBGP peer is that this class
+ has a privateDirs already defined and contains functions to handle ExaBGP
+ things.
+ """
+ params['privateDirs'] = self.PRIVATE_DIRS
+ super(TopoExaBGP, self).__init__(tgen, name, **params)
+ self.tgen.topo.addHost(name, **params)
+
+ def __str__(self):
+ gear = super(TopoExaBGP, self).__str__()
+ gear += ' TopoExaBGP<>'.format()
+ return gear
+
+ def start(self, peer_dir, env_file=None):
+ """
+ Start running ExaBGP daemon:
+ * Copy all peer* folder contents into /etc/exabgp
+ * Copy exabgp env file if specified
+ * Make all python files runnable
+ * Run ExaBGP with env file `env_file` and configuration peer*/exabgp.cfg
+ """
+ self.run('mkdir /etc/exabgp')
+ self.run('chmod 755 /etc/exabgp')
+ self.run('cp {}/* /etc/exabgp/'.format(peer_dir))
+ if env_file is not None:
+ self.run('cp {} /etc/exabgp/exabgp.env'.format(env_file))
+ self.run('chmod 644 /etc/exabgp/*')
+ self.run('chmod a+x /etc/exabgp/*.py')
+ self.run('chown -R exabgp:exabgp /etc/exabgp')
+ output = self.run('exabgp -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg')
+ if output == None or len(output) == 0:
+ output = '<none>'
+ logger.info('{} exabgp started, output={}'.format(self.name, output))
+
+ def stop(self, wait=True, assertOnError=True):
+ "Stop ExaBGP peer and kill the daemon"
+ self.run('kill `cat /var/run/exabgp/exabgp.pid`')
+ return ""
+
+
+#
+# Diagnostic function
+#
+
+# Disable linter branch warning. It is expected to have these here.
+# pylint: disable=R0912
+def diagnose_env():
+ """
+ Run diagnostics in the running environment. Returns `True` when everything
+ is ok, otherwise `False`.
+ """
+ ret = True
+
+ # Test log path exists before installing handler.
+ if not os.path.isdir('/tmp'):
+ logger.warning('could not find /tmp for logs')
+ else:
+ os.system('mkdir /tmp/topotests')
+ # Log diagnostics to file so it can be examined later.
+ fhandler = logging.FileHandler(filename='/tmp/topotests/diagnostics.txt')
+ fhandler.setLevel(logging.DEBUG)
+ fhandler.setFormatter(
+ logging.Formatter(fmt='%(asctime)s %(levelname)s: %(message)s')
+ )
+ logger.addHandler(fhandler)
+
+ logger.info('Running environment diagnostics')
+
+ # Load configuration
+ config = ConfigParser.ConfigParser(tgen_defaults)
+ pytestini_path = os.path.join(CWD, '../pytest.ini')
+ config.read(pytestini_path)
+
+ # Assert that we are running as root
+ if os.getuid() != 0:
+ logger.error('you must run topotest as root')
+ ret = False
+
+ # Assert that we have mininet
+ if os.system('which mn >/dev/null 2>/dev/null') != 0:
+ logger.error('could not find mininet binary (mininet is not installed)')
+ ret = False
+
+ # Assert that we have iproute installed
+ if os.system('which ip >/dev/null 2>/dev/null') != 0:
+ logger.error('could not find ip binary (iproute is not installed)')
+ ret = False
+
+ # Assert that we have gdb installed
+ if os.system('which gdb >/dev/null 2>/dev/null') != 0:
+ logger.error('could not find gdb binary (gdb is not installed)')
+ ret = False
+
+ # Assert that FRR utilities exist
+ frrdir = config.get('topogen', 'frrdir')
+ hasfrr = False
+ if not os.path.isdir(frrdir):
+ logger.error('could not find {} directory'.format(frrdir))
+ ret = False
+ else:
+ hasfrr = True
+ try:
+ pwd.getpwnam('frr')[2]
+ except KeyError:
+ logger.warning('could not find "frr" user')
+
+ try:
+ grp.getgrnam('frr')[2]
+ except KeyError:
+ logger.warning('could not find "frr" group')
+
+ try:
+ if 'frr' not in grp.getgrnam('frrvty').gr_mem:
+ logger.error('"frr" user and group exist, but user is not under "frrvty"')
+ except KeyError:
+ logger.warning('could not find "frrvty" group')
+
+ for fname in ['zebra', 'ospfd', 'ospf6d', 'bgpd', 'ripd', 'ripngd',
+ 'isisd', 'pimd', 'ldpd']:
+ path = os.path.join(frrdir, fname)
+ if not os.path.isfile(path):
+ # LDPd is an exception
+ if fname == 'ldpd':
+ logger.info('could not find {} in {}'.format(fname, frrdir) +
+ '(LDPd tests will not run)')
+ continue
+
+ logger.warning('could not find {} in {}'.format(fname, frrdir))
+ ret = False
+ else:
+ if fname != 'zebra':
+ continue
+
+ os.system(
+ '{} -v 2>&1 >/tmp/topotests/frr_zebra.txt'.format(path)
+ )
+
+ # Assert that Quagga utilities exist
+ quaggadir = config.get('topogen', 'quaggadir')
+ if hasfrr:
+ # if we have frr, don't check for quagga
+ pass
+ elif not os.path.isdir(quaggadir):
+ logger.info('could not find {} directory (quagga tests will not run)'.format(quaggadir))
+ else:
+ ret = True
+ try:
+ pwd.getpwnam('quagga')[2]
+ except KeyError:
+ logger.info('could not find "quagga" user')
+
+ try:
+ grp.getgrnam('quagga')[2]
+ except KeyError:
+ logger.info('could not find "quagga" group')
+
+ try:
+ if 'quagga' not in grp.getgrnam('quaggavty').gr_mem:
+ logger.error('"quagga" user and group exist, but user is not under "quaggavty"')
+ except KeyError:
+ logger.warning('could not find "quaggavty" group')
+
+ for fname in ['zebra', 'ospfd', 'ospf6d', 'bgpd', 'ripd', 'ripngd',
+ 'isisd', 'pimd']:
+ path = os.path.join(quaggadir, fname)
+ if not os.path.isfile(path):
+ logger.warning('could not find {} in {}'.format(fname, quaggadir))
+ ret = False
+ else:
+ if fname != 'zebra':
+ continue
+
+ os.system(
+ '{} -v 2>&1 >/tmp/topotests/quagga_zebra.txt'.format(path)
+ )
+
+ # Test MPLS availability
+ krel = platform.release()
+ if topotest.version_cmp(krel, '4.5') < 0:
+ logger.info('LDPd tests will not run (have kernel "{}", but it requires 4.5)'.format(krel))
+
+ # Test for MPLS Kernel modules available
+ if not topotest.module_present('mpls-router', load=False) != 0:
+ logger.info('LDPd tests will not run (missing mpls-router kernel module)')
+ if not topotest.module_present('mpls-iptunnel', load=False) != 0:
+ logger.info('LDPd tests will not run (missing mpls-iptunnel kernel module)')
+
+ # TODO remove me when we start supporting exabgp >= 4
+ try:
+ output = subprocess.check_output(['exabgp', '-v'])
+ line = output.split('\n')[0]
+ version = line.split(' ')[2]
+ if topotest.version_cmp(version, '4') >= 0:
+ logger.warning('BGP topologies are still using exabgp version 3, expect failures')
+
+ # We want to catch all exceptions
+ # pylint: disable=W0702
+ except:
+ logger.warning('failed to find exabgp or returned error')
+
+ # After we logged the output to file, remove the handler.
+ logger.removeHandler(fhandler)
+
+ return ret