Merge pull request #3983 from opensourcerouting/pim-tests

topotests: pim-basic: add topology to test PIM
This commit is contained in:
Lou Berger 2019-03-26 17:05:13 +01:00 committed by GitHub
commit d5737d2baa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 340 additions and 0 deletions

View File

@ -0,0 +1,83 @@
#!/usr/bin/env python
#
# mcast-rx.py
#
# Copyright (c) 2018 Cumulus Networks, Inc.
#
# 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 Cumulus Networks 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.
#
"""
Subscribe to a multicast group so that the kernel sends an IGMP JOIN
for the multicast group we subscribed to.
"""
import argparse
import logging
import re
import os
import socket
import subprocess
import struct
import sys
import time
def ifname_to_ifindex(ifname):
output = subprocess.check_output("ip link show %s" % ifname, shell=True)
first_line = output.split('\n')[0]
re_index = re.search('^(\d+):', first_line)
if re_index:
return int(re_index.group(1))
log.error("Could not parse the ifindex for %s out of\n%s" % (ifname, first_line))
return None
# Thou shalt be root
if os.geteuid() != 0:
sys.stderr.write('ERROR: You must have root privileges\n')
sys.exit(1)
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)5s: %(message)s')
# Color the errors and warnings in red
logging.addLevelName(logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR))
logging.addLevelName(logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING))
log = logging.getLogger(__name__)
parser = argparse.ArgumentParser(description='Multicast RX utility',
version='1.0.0')
parser.add_argument('group', help='Multicast IP')
parser.add_argument('ifname', help='Interface name')
parser.add_argument('--port', help='UDP port', default=1000)
parser.add_argument('--sleep', help='Time to sleep before we stop waiting',
default = 5)
args = parser.parse_args()
# Create the datagram socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((args.group, args.port))
newpid = os.fork()
if newpid == 0:
ifindex = ifname_to_ifindex(args.ifname)
mreq = struct.pack("=4sLL", socket.inet_aton(args.group), socket.INADDR_ANY, ifindex)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
time.sleep(float(args.sleep))
sock.close()

View File

@ -0,0 +1,72 @@
#!/usr/bin/env python
#
# mcast-tx.py
#
# Copyright (c) 2018 Cumulus Networks, Inc.
#
# 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 Cumulus Networks 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.
#
import argparse
import logging
import socket
import struct
import time
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)5s: %(message)s')
# Color the errors and warnings in red
logging.addLevelName(logging.ERROR, "\033[91m %s\033[0m" % logging.getLevelName(logging.ERROR))
logging.addLevelName(logging.WARNING, "\033[91m%s\033[0m" % logging.getLevelName(logging.WARNING))
log = logging.getLogger(__name__)
parser = argparse.ArgumentParser(description='Multicast packet generator', version='1.0.0')
parser.add_argument('group', help='Multicast IP')
parser.add_argument('ifname', help='Interface name')
parser.add_argument('--port', type=int, help='UDP port number', default=1000)
parser.add_argument('--ttl', type=int, help='time-to-live', default=20)
parser.add_argument('--count', type=int, help='Packets to send', default=1)
parser.add_argument('--interval', type=int, help='ms between packets', default=100)
args = parser.parse_args()
# Create the datagram socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# IN.SO_BINDTODEVICE is not defined in some releases of python but it is 25
# https://github.com/sivel/bonding/issues/10
#
# Bind our socket to ifname
sock.setsockopt(socket.SOL_SOCKET,
25,
struct.pack("%ds" % len(args.ifname), args.ifname))
# We need to make sure our sendto() finishes before we close the socket
sock.setblocking(1)
# Set the time-to-live
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, struct.pack('b', args.ttl))
ms = args.interval / 1000.0
# Send data to the multicast group
for x in xrange(args.count):
log.info('TX multicast UDP packet to %s:%d on %s' % (args.group, args.port, args.ifname))
sent = sock.sendto('foobar %d' % x, (args.group, args.port))
if args.count > 1 and ms:
time.sleep(ms)
sock.close()

View File

@ -0,0 +1,10 @@
hostname r1
!
interface r1-eth0
ip igmp
ip pim sm
!
interface lo
ip pim sm
!
ip pim rp 10.254.0.1

View File

@ -0,0 +1,8 @@
hostname r1
!
interface r1-eth0
ip address 10.0.20.1/24
!
interface lo
ip address 10.254.0.1/32
!

View File

@ -0,0 +1 @@
hostname r2

View File

@ -0,0 +1,8 @@
hostname r2
!
interface r2-eth0
ip address 10.0.20.2/24
!
interface lo
ip address 10.254.0.2/32
!

View File

@ -0,0 +1,158 @@
#!/usr/bin/env python
#
# test_pim.py
#
# Copyright (c) 2018 Cumulus Networks, Inc.
# Donald Sharp
#
# 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 Cumulus Networks 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.
#
"""
test_pim.py: Test pim
"""
import os
import sys
import pytest
CWD = os.path.dirname(os.path.realpath(__file__))
sys.path.append(os.path.join(CWD, '../'))
# pylint: disable=C0413
from lib import topotest
from lib.topogen import Topogen, TopoRouter, get_topogen
from lib.topolog import logger
from mininet.topo import Topo
class PIMTopo(Topo):
def build(self, *_args, **_opts):
"Build function"
tgen = get_topogen(self)
for routern in range(1, 3):
tgen.add_router('r{}'.format(routern))
# r1 <- sw1 -> r2
sw = tgen.add_switch('sw1')
sw.add_link(tgen.gears['r1'])
sw.add_link(tgen.gears['r2'])
def setup_module(mod):
"Sets up the pytest environment"
tgen = Topogen(PIMTopo, mod.__name__)
tgen.start_topology()
# For all registered routers, load the zebra configuration file
for rname, router in tgen.routers().iteritems():
router.load_config(
TopoRouter.RD_ZEBRA,
os.path.join(CWD, '{}/zebra.conf'.format(rname))
)
router.load_config(
TopoRouter.RD_PIM,
os.path.join(CWD, '{}/pimd.conf'.format(rname))
)
# After loading the configurations, this function loads configured daemons.
tgen.start_router()
def teardown_module(mod):
"Teardown the pytest environment"
tgen = get_topogen()
# This function tears down the whole topology.
tgen.stop_topology()
def test_pim_send_mcast_stream():
"Establish a Multicast stream from r2 -> r1 and then ensure S,G is created as appropriate"
logger.info("Establish a Mcast stream from r2->r1 and then ensure S,G created")
tgen = get_topogen()
if tgen.routers_have_failure():
pytest.skip(tgen.errors)
r2 = tgen.gears['r2']
r1 = tgen.gears['r1']
# Let's establish a S,G stream from r2 -> r1
CWD = os.path.dirname(os.path.realpath(__file__))
r2.run("{}/mcast-tx.py --ttl 5 --count 5 --interval 10 229.1.1.1 r2-eth0 > /tmp/bar".format(CWD))
# Let's see that it shows up and we have established some basic state
out = r1.vtysh_cmd("show ip pim upstream json", isjson=True)
expected = {
'229.1.1.1': {
'10.0.20.2': {
'firstHopRouter': 1,
'joinState': 'NotJoined',
'regState': 'RegPrune',
'inboundInterface': 'r1-eth0',
}
}
}
assert topotest.json_cmp(out, expected) is None, 'failed to converge pim'
def test_pim_igmp_report():
"Send a igmp report from r2->r1 and ensure that the *,G state is created on r1"
logger.info("Send a igmp report from r2-r1 and ensure *,G created")
tgen = get_topogen()
if tgen.routers_have_failure():
pytest.skip(tgen.errors)
r2 = tgen.gears['r2']
r1 = tgen.gears['r1']
# Let's send a igmp report from r2->r1
CWD = os.path.dirname(os.path.realpath(__file__))
r2.run("{}/mcast-rx.py 229.1.1.2 r2-eth0 &".format(CWD))
out = r1.vtysh_cmd("show ip pim upstream json", isjson=True)
expected = {
'229.1.1.2': {
'*': {
'sourceIgmp': 1,
'joinState': 'Joined',
'regState': 'RegNoInfo',
'sptBit': 0,
}
}
}
assert topotest.json_cmp(out, expected) is None, 'failed to converge pim'
def test_memory_leak():
"Run the memory leak test and report results."
tgen = get_topogen()
if not tgen.is_memleak_enabled():
pytest.skip('Memory leak test/report is disabled')
tgen.report_memory_leaks()
if __name__ == '__main__':
args = ["-s"] + sys.argv[1:]
sys.exit(pytest.main(args))