<filename>src/test/proxyarp/proxyarpTest.py

# Copyright 2017-present Open Networking Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.



# Copyright 2016-present Ciena Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import unittest
from nose.tools import *
from scapy.all import *
from CordTestUtils import get_mac, log_test
from OnosCtrl import OnosCtrl
from OltConfig import OltConfig
from OnosFlowCtrl import OnosFlowCtrl
from onosclidriver import OnosCliDriver
from CordContainer import Container, Onos, Quagga
from CordTestServer import cord_test_onos_restart, cord_test_quagga_restart
from portmaps import g_subscriber_port_map
import threading
from threading import current_thread
import time
import os
import json
log_test.setLevel('INFO')


class proxyarp_exchange(unittest.TestCase):

    apps = ('org.onosproject.vrouter','org.onosproject.proxyarp')
    device_id = 'of:' + get_mac()
    device_dict = { "devices" : {
                "{}".format(device_id) : {
                    "basic" : {
                        "driver" : "softrouter"
                    }
                }
             },
          }
    test_path = os.path.dirname(os.path.realpath(__file__))
    onos_config_path = os.path.join(test_path, '..', 'setup/onos-config')
    GATEWAY = '192.168.10.50'
    INGRESS_PORT = 1
    EGRESS_PORT = 2
    MAX_PORTS = 100
    hosts_list = [ ('192.168.10.1', '00:00:00:00:00:01'), ('192.168.11.1', '00:00:00:00:02:01'), ]

    @classmethod
    def setUpClass(cls):
        cls.olt = OltConfig()
        cls.port_map, _ = cls.olt.olt_port_map()
        if not cls.port_map:
            cls.port_map = g_subscriber_port_map
        time.sleep(3)
        cls.load_device_id()

    @classmethod
    def tearDownClass(cls):
        '''Deactivate the vrouter apps'''
        #cls.vrouter_host_unload()

    @classmethod
    def load_device_id(cls):
        did = OnosCtrl.get_device_id()
        cls.device_id = did
        cls.device_dict = { "devices" : {
                "{}".format(did) : {
                    "basic" : {
                        "driver" : "softrouter"
                    }
                }
            },
        }

    def cliEnter(self):
        retries = 0
        while retries < 3:
            self.cli = OnosCliDriver(connect = True)
            if self.cli.handle:
                break
            else:
                retries += 1
                time.sleep(2)
    def cliExit(self):
        self.cli.disconnect()

    @classmethod
    def proxyarp_host_unload(cls):
        index = 1
        for host,_ in cls.hosts_list:
            iface = cls.port_map[index]
            index += 1
            config_cmds = ('ifconfig {} 0'.format(iface), )
            for cmd in config_cmds:
		log_test.info('host unload command %s' % cmd)
                os.system(cmd)

    @classmethod
    def interface_config_load(cls, interface_cfg = None):
  	if type(interface_cfg) is tuple:
            res = []
            for v in interface_cfg:
		if type(v) == list:
		    pass
		else:
                    res += v.items()
                    config = dict(res)
        else:
            config = interface_cfg
        cfg = json.dumps(config)
        with open('{}/network-cfg.json'.format(cls.onos_config_path), 'w') as f:
            f.write(cfg)
        return cord_test_onos_restart()

    @classmethod
    def host_config_load(cls, host_config = None):
	for host in host_config:
	    status, code = OnosCtrl.host_config(host)
	    if status is False:
                log_test.info('JSON request returned status %d' %code)
                assert_equal(status, True)

    @classmethod
    def generate_interface_config(cls, hosts = 1):
        num = 0
        start_host = ( 192 << 24) | ( 168 << 16)  |  (10 << 8) | 0
        end_host =   ( 200 << 24 ) | (168 << 16)  |  (10 << 8) | 0
        ports_dict = { 'ports' : {} }
        interface_list = []
        hosts_list = []
        for n in xrange(start_host, end_host, 256):
            port_map = ports_dict['ports']
            port = num + 1 if num < cls.MAX_PORTS - 1 else cls.MAX_PORTS - 1
            device_port_key = '{0}/{1}'.format(cls.device_id, port)
            try:
                interfaces = port_map[device_port_key]['interfaces']
            except:
                port_map[device_port_key] = { 'interfaces' : [] }
                interfaces = port_map[device_port_key]['interfaces']
            ip = n + 1
            host_ip = n + 2
            ips = '%d.%d.%d.%d/24'%( (ip >> 24) & 0xff, ( (ip >> 16) & 0xff ), ( (ip >> 8 ) & 0xff ), ip & 0xff)
            host = '%d.%d.%d.%d' % ( (host_ip >> 24) & 0xff, ( ( host_ip >> 16) & 0xff ), ( (host_ip >> 8 ) & 0xff ), host_ip & 0xff )
            mac = RandMAC()._fix()
            hosts_list.append((host, mac))
            if num < cls.MAX_PORTS - 1:
                interface_dict = { 'name' : 'b1-{}'.format(port), 'ips': [ips], 'mac' : mac }
                interfaces.append(interface_dict)
                interface_list.append(interface_dict['name'])
            else:
                interfaces[0]['ips'].append(ips)
            num += 1
            if num == hosts:
                break
        cls.hosts_list = hosts_list
        return (cls.device_dict, ports_dict, hosts_list)

    @classmethod
    def generate_host_config(cls):
        num = 0
        hosts_dict = {}
        for host, mac in cls.hosts_list:
            port = num + 1 if num < cls.MAX_PORTS - 1 else cls.MAX_PORTS - 1
	    hosts_dict[host] = {'mac':mac, 'vlan':'none', 'ipAddresses':[host], 'location':{ 'elementId' : '{}'.format(cls.device_id), 'port': port}}
            num += 1
        return hosts_dict.values()

    @classmethod
    def proxyarp_activate(cls, deactivate = False):
        app = 'org.onosproject.proxyarp'
        onos_ctrl = OnosCtrl(app)
        if deactivate is True:
            onos_ctrl.deactivate()
        else:
            onos_ctrl.activate()
        time.sleep(3)

    @classmethod
    def proxyarp_config(cls, hosts = 1):
        proxyarp_configs = cls.generate_interface_config(hosts = hosts)
	cls.interface_config_load(interface_cfg = proxyarp_configs)
	hostcfg = cls.generate_host_config()
	cls.host_config_load(host_config = hostcfg)
        return proxyarp_configs

    def proxyarp_arpreply_verify(self, ingress, hostip, hostmac, PositiveTest=True):
	log_test.info('verifying arp reply for host ip %s host mac %s on interface %s'%(hostip ,hostmac ,self.port_map[ingress]))
	self.success = False
        def recv_task():
            def recv_cb(pkt):
                log_test.info('Arp Reply seen with source Mac is %s' %(pkt[ARP].hwsrc))
                self.success = True if PositiveTest == True else False
            sniff(count=1, timeout=2, lfilter = lambda p: ARP in p and p[ARP].op == 2 and p[ARP].hwsrc == hostmac,
                  prn = recv_cb, iface = self.port_map[ingress])
        t = threading.Thread(target = recv_task)
        t.start()
        pkt = (Ether(dst = 'ff:ff:ff:ff:ff:ff')/ARP(op=1,pdst=hostip))
        log_test.info('sending arp request  for dest ip %s on interface %s' %
                 (hostip, self.port_map[ingress]))
        sendp( pkt, count = 10, iface = self.port_map[ingress])
        t.join()
	if PositiveTest:
            assert_equal(self.success, True)
	else:
	    assert_equal(self.success, False)

    def __proxyarp_hosts_verify(self, hosts = 1,PositiveTest = True):
        _,_,hosts_config = self.proxyarp_config(hosts = hosts)
	log_test.info('\nhosts_config %s and its type %s'%(hosts_config,type(hosts_config)))
        self.cliEnter()
        connected_hosts = json.loads(self.cli.hosts(jsonFormat = True))
        log_test.info('Discovered hosts: %s' %connected_hosts)
        #We read from cli if we expect less number of routes to avoid cli timeouts
        if hosts <= 10000:
            assert_equal(len(connected_hosts), hosts)
	ingress = hosts+1
	for hostip, hostmac in hosts_config:
	        self.proxyarp_arpreply_verify(ingress,hostip,hostmac,PositiveTest = PositiveTest)
		time.sleep(1)
	self.cliExit()
        return True

    def test_proxyarp_with_1_host(self, hosts=1):
        res = self.__proxyarp_hosts_verify(hosts = hosts)
        assert_equal(res, True)
	#cls.proxyarp_host_unload()
    def test_proxyarp_with_10_hosts(self, hosts=10):
        res = self.__proxyarp_hosts_verify(hosts = hosts)
        assert_equal(res, True)
    def test_proxyarp_with_50_hosts(self, hosts=50):
        res = self.__proxyarp_hosts_verify(hosts = hosts)
        assert_equal(res, True)
    def test_proxyarp_app_with_disabling_and_re_enabling(self,hosts = 3):
	ports_map, egress_map,hosts_config = self.proxyarp_config(hosts = hosts)
	ingress = hosts+1
	for hostip, hostmac in hosts_config:
	    self.proxyarp_arpreply_verify(ingress,hostip,hostmac,PositiveTest = True)
	    time.sleep(1)
	log_test.info('Deactivating proxyarp  app and expecting not to get arp reply from ONOS')
	self.proxyarp_activate(deactivate = True)
	for hostip, hostmac in hosts_config:
	    self.proxyarp_arpreply_verify(ingress,hostip,hostmac,PositiveTest = False)
	    time.sleep(1)
	log_test.info('activating proxyarp  app and expecting to get arp reply from ONOS')
	self.proxyarp_activate(deactivate = False)
	for hostip, hostmac in hosts_config:
            self.proxyarp_arpreply_verify(ingress,hostip,hostmac,PositiveTest = True)
            time.sleep(1)

    def test_proxyarp_nonexisting_host(self,hosts = 1):
    	_,_,hosts_config = self.proxyarp_config(hosts = hosts)
	ingress = hosts + 2
	for host, mac in hosts_config:
	    self.proxyarp_arpreply_verify(ingress,host,mac,PositiveTest = True)
	new_host = hosts_config[-1][0].split('.')
	new_host[2] = str(int(new_host[2])+1)
	new_host = '.'.join(new_host)
	new_mac =  RandMAC()._fix()
	log_test.info('verifying arp reply for host ip %s on interface %s'%(new_host,self.port_map[ingress]))
	res=srp1(Ether(dst='ff:ff:ff:ff:ff:ff')/ARP(op=1,pdst=new_host),timeout=2,iface=self.port_map[ingress])
	assert_equal(res, None)
	log_test.info('arp reply not seen for host ip %s on interface %s as expected'%(new_host,self.port_map[ingress]))
	hosts = hosts + 1
	_,_,hosts_config = self.proxyarp_config(hosts = hosts)
	for host in hosts_config:
	    if host[0] == new_host:
		new_mac = host[1]
	self.proxyarp_arpreply_verify(ingress,new_host,new_mac,PositiveTest = True)

    def test_proxyarp_removing_host(self,hosts = 3):
        ports_map, egress_map,hosts_config = self.proxyarp_config(hosts = hosts)
        ingress = hosts+1
        for hostip, hostmac in hosts_config:
            self.proxyarp_arpreply_verify(ingress,hostip,hostmac,PositiveTest = True)
            time.sleep(1)
	host_mac = hosts_config[0][1]
        log_test.info('removing host entry %s' % host_mac)
        self.cliEnter()
        hostentries = json.loads(self.cli.hosts(jsonFormat = True))
        for host in hostentries:
	    res = host_mac.upper() in host.values()
	    if res:
	 	break
	assert_equal(res, True)
        hostid = host_mac+'/'+'None'
        delete_host  = self.cli.host_remove(hostid)
        hostentries = json.loads(self.cli.hosts(jsonFormat = True))
	for host in hostentries:
            res = host_mac.upper() in host.values()
            if res:
                break
        assert_equal(res, False)
        self.proxyarp_arpreply_verify(ingress,hosts_config[0][0],host_mac,PositiveTest = False)
        time.sleep(1)
        self.cliExit()

    def test_proxyarp_concurrent_requests_with_multiple_host_and_different_interfaces(self,hosts = 10):
	ports_map, egress_map,hosts_config = self.proxyarp_config(hosts = hosts)
	self.success = True
	ingress = hosts+1
	ports = range(ingress,ingress+10)
	hostmac = []
	hostip = []
	for ip,mac in hosts_config:
	    hostmac.append(mac)
	    hostip.append(ip)
	success_dir = {}
	def verify_proxyarp(*r):
            ingress,hostmac,hostip = r[0],r[1],r[2]
            def mac_recv_task():
                def recv_cb(pkt):
		    log_test.info('Arp Reply seen with source Mac is %s' %(pkt[ARP].hwsrc))
                    success_dir[current_thread().name] = True
		sniff(count=1, timeout=5,lfilter = lambda p: ARP in p and p[ARP].op == 2 and p[ARP].hwsrc == hostmac,
                    prn = recv_cb, iface = self.port_map[ingress])
	    t = threading.Thread(target = mac_recv_task)
	    t.start()
	    pkt = (Ether(dst = 'ff:ff:ff:ff:ff:ff')/ARP(op=1,pdst= hostip))
            log_test.info('sending arp request  for dest ip %s on interface %s' %
                 (hostip,self.port_map[ingress]))
            sendp(pkt, count = 10,iface = self.port_map[ingress])
            t.join()
	t = []
	for i in range(10):
	    t.append(threading.Thread(target = verify_proxyarp, args = [ports[i],hostmac[i],hostip[i]]))
        for i in range(10):
	    t[i].start()
	for i in range(10):
            t[i].join()
        if len(success_dir) != 10:
                self.success = False
        assert_equal(self.success, True)

    def test_proxyarp_disabling_enabling_app_initiating_concurrent_requests(self,hosts = 10):
	'''Test sending arp requests to multiple host ips at once from different interfaces by disabling and re-enabling proxyarp app'''
        ports_map, egress_map,hosts_config = self.proxyarp_config(hosts = hosts)
        self.success = True
        ingress = hosts+1
        ports = range(ingress,ingress+10)
        hostmac = []
        hostip = []
        for ip,mac in hosts_config:
            hostmac.append(mac)
            hostip.append(ip)
        success_dir = {}
        def verify_proxyarp(*r):
            ingress,hostmac,hostip = r[0],r[1],r[2]
            def mac_recv_task():
                def recv_cb(pkt):
                    log_test.info('Arp Reply seen with source Mac is %s' %(pkt[ARP].hwsrc))
                    success_dir[current_thread().name] = True
                sniff(count=1, timeout=5,lfilter = lambda p: ARP in p and p[ARP].op == 2 and p[ARP].hwsrc == hostmac,
                    prn = recv_cb, iface = self.port_map[ingress])
            t = threading.Thread(target = mac_recv_task)
            t.start()
            pkt = (Ether(dst = 'ff:ff:ff:ff:ff:ff')/ARP(op=1,pdst= hostip))
            log_test.info('sending arp request  for dest ip %s on interface %s' %
                 (hostip,self.port_map[ingress]))
            sendp(pkt, count = 10,iface = self.port_map[ingress])
            t.join()
        t1 = []
	#starting multi threading before proxyarp disable
        for i in range(10):
            t1.append(threading.Thread(target = verify_proxyarp, args = [ports[i],hostmac[i],hostip[i]]))
        for i in range(10):
            t1[i].start()
        for i in range(10):
            t1[i].join()
        if len(success_dir) != 10:
                self.success = False
        assert_equal(self.success, True)
	self.proxyarp_activate(deactivate = True)
	#starting multi threading after proxyarp disable
	t2 = []
	self.success = False
	for i in range(10):
            t2.append(threading.Thread(target = verify_proxyarp, args = [ports[i],hostmac[i],hostip[i]]))
        for i in range(10):
            t2[i].start()
        for i in range(10):
            t2[i].join()
        if len(success_dir) != 10:
                self.success = True
        assert_equal(self.success, False)
	self.proxyarp_activate(deactivate = False)
	#starting multi threading after proxyarp re-enable
	self.success = True
	t3 = []
	for i in range(10):
            t3.append(threading.Thread(target = verify_proxyarp, args = [ports[i],hostmac[i],hostip[i]]))
        for i in range(10):
            t3[i].start()
        for i in range(10):
            t3[i].join()
        if len(success_dir) != 20:
                self.success = False
	assert_equal(self.success, True)

    def test_proxyarp_with_existing_and_non_existing_hostIPs_initiating_concurrent_requests(self,hosts = 5):
        ports_map, egress_map,hosts_config = self.proxyarp_config(hosts = hosts)
        self.success = True
        ingress = hosts+1
        ports = range(ingress,ingress+10)
        hostmac = []
        hostip = []
        for ip,mac in hosts_config:
            hostmac.append(mac)
            hostip.append(ip)
	#adding 5 non-existing host IPs to hostip list
	for i in range(1,6):
	    ip = hostip[-1].split('.')
	    ip[3] = str(int(ip[3])+int(i))
            ip = '.'.join(ip)
	    hostip.append(ip)
	    hostmac.append(RandMAC()._fix())
        success_dir = {}
	replied_hosts = []
        def verify_proxyarp(*r):
            ingress,hostmac,hostip = r[0],r[1],r[2]
            def mac_recv_task():
                def recv_cb(pkt):
                    log_test.info('Arp Reply seen with source Mac is %s' %(pkt[ARP].hwsrc))
                    success_dir[current_thread().name] = True
		    replied_hosts.append(hostip)
                sniff(count=1, timeout=5,lfilter = lambda p: ARP in p and p[ARP].op == 2 and p[ARP].psrc == hostip,
                    prn = recv_cb, iface = self.port_map[ingress])
            t = threading.Thread(target = mac_recv_task)
            t.start()
            pkt = (Ether(dst = 'ff:ff:ff:ff:ff:ff')/ARP(op=1,pdst= hostip))
            log_test.info('sending arp request  for dest ip %s on interface %s' %
                 (hostip,self.port_map[ingress]))
            sendp(pkt, count = 10,iface = self.port_map[ingress])
            t.join()
        t = []
        for i in range(10):
            t.append(threading.Thread(target = verify_proxyarp, args = [ports[i],hostmac[i],hostip[i]]))
        for i in range(10):
            t[i].start()
        for i in range(10):
            t[i].join()
        if len(success_dir) != 5 and len(replied_hosts) != 5:
                self.success = False
        assert_equal(self.success, True)
	for i in range(5):
	    if hostip[i] not in replied_hosts:
		self.success = False
	assert_equal(self.success, True)
